<commit_before>Promise = require('bluebird')
_ = require('lodash')
which = require('which')
spawn = require('child_process').spawn
path = require('path')
semver = require('semver')
shellEnv = require('shell-env')

parentConfigKey = "atom-beautify.executables"

module.exports = class Executable

  name: null
  cmd: null
  key: null
  homepage: null
  installation: null
  versionArgs: ['--version']
  versionParse: (text) -> semver.clean(text)
  versionRunOptions: {}
  versionsSupported: '>= 0.0.0'
  required: true

  constructor: (options) ->
    # Validation
    if !options.cmd?
      throw new Error("The command (i.e. cmd property) is required for an Executable.")
    @name = options.name
    @cmd = options.cmd
    @key = @cmd
    @homepage = options.homepage
    @installation = options.installation
    @required = not options.optional
    if options.version?
      versionOptions = options.version
      @versionArgs = versionOptions.args if versionOptions.args
      @versionParse = versionOptions.parse if versionOptions.parse
      @versionRunOptions = versionOptions.runOptions if versionOptions.runOptions
      @versionsSupported = versionOptions.supported if versionOptions.supported
    @setupLogger()

  init: () ->
    Promise.all([
      @loadVersion()
    ])
      .then(() => @verbose("Done init of #{@name}"))
      .then(() => @)
      .catch((error) =>
        if not @.required
          @
        else
          Promise.reject(error)
      )

  ###
  Logger instance
  ###
  logger: null
  ###
  Initialize and configure Logger
  ###
  setupLogger: ->
    @logger = require('../logger')("#{@name} Executable")
    for key, method of @logger
      @[key] = method
    @verbose("#{@name} executable logger has been initialized.")

  isInstalled = null
  version = null
  loadVersion: (force = false) ->
    @verbose("loadVersion", @version, force)
    if force or !@version?
      @verbose("Loading version without cache")
      @runVersion()
        .then((text) => @versionParse(text))
        .then((version) ->
          valid = Boolean(semver.valid(version))
          if not valid
            throw new Error("Version is not valid: "+version)
          version
        )
        .then((version) =>
          @isInstalled = true
          @version = version
        )
        .then((version) =>
          @verbose("#{@cmd} version: #{version}")
          version
        )
        .catch((error) =>
          @isInstalled = false
          @error(error)
          help = {
            program: @cmd
            link: @installation or @homepage
            pathOption: "Executable - #{@name or @cmd} - Path"
          }
          Promise.reject(@commandNotFoundError(@name or @cmd, help))
        )
    else
      @verbose("Loading cached version")
      Promise.resolve(@version)

  runVersion: () ->
    @run(@versionArgs, @versionRunOptions)
      .then((version) =>
        @verbose("Version: " + version)
        version
      )

  isSupported: () ->
    @isVersion(@versionsSupported)

  isVersion: (range) ->
    semver.satisfies(@version, range)

  getConfig: () ->
    atom?.config.get("#{parentConfigKey}.#{@key}") or {}

  ###
  Run command-line interface command
  ###
  run: (args, options = {}) ->
    @debug("Run: ", @cmd, args, options)
    { cwd, ignoreReturnCode, help, onStdin, returnStderr } = options
    # Flatten args first
    args = _.flatten(args)
    exeName = @cmd
    config = @getConfig()

    # Resolve executable and all args
    Promise.all([@shellEnv(), Promise.all(args)])
      .then(([env, args]) =>
        @debug('exeName, args:', exeName, args)

        # Get PATH and other environment variables
        if config and config.path
          exePath = config.path
        else
          exePath = @which(exeName)
        Promise.all([exeName, args, env, exePath])
      )
      .then(([exeName, args, env, exePath]) =>
        @debug('exePath:', exePath)
        @debug('env:', env)
        @debug('args', args)

        exe = exePath ? exeName
        spawnOptions = {
          cwd: cwd
          env: env
        }

        @spawn(exe, args, spawnOptions, onStdin)
          .then(({returnCode, stdout, stderr}) =>
            @verbose('spawn result, returnCode', returnCode)
            @verbose('spawn result, stdout', stdout)
            @verbose('spawn result, stderr', stderr)

            # If return code is not 0 then error occured
            if not ignoreReturnCode and returnCode isnt 0
              # operable program or batch file
              windowsProgramNotFoundMsg = "is not recognized as an internal or external command"

              @verbose(stderr, windowsProgramNotFoundMsg)

              if @isWindows() and returnCode is 1 and stderr.indexOf(windowsProgramNotFoundMsg) isnt -1
                throw @commandNotFoundError(exeName, help)
              else
                throw new Error(stderr)
            else
              if returnStderr
                stderr
              else
                stdout
          )
          .catch((err) =>
            @debug('error', err)

            # Check if error is ENOENT (command could not be found)
            if err.code is 'ENOENT' or err.errno is 'ENOENT'
              throw @commandNotFoundError(exeName, help)
            else
              # continue as normal error
              throw err
          )
      )

  ###
  Spawn
  ###
  spawn: (exe, args, options, onStdin) ->
    # Remove undefined/null values
    args = _.without(args, undefined)
    args = _.without(args, null)

    return new Promise((resolve, reject) =>
      @debug('spawn', exe, args)

      cmd = spawn(exe, args, options)
      stdout = ""
      stderr = ""

      cmd.stdout.on('data', (data) ->
        stdout += data
      )
      cmd.stderr.on('data', (data) ->
        stderr += data
      )
      cmd.on('close', (returnCode) =>
        @debug('spawn done', returnCode, stderr, stdout)
        resolve({returnCode, stdout, stderr})
      )
      cmd.on('error', (err) =>
        @debug('error', err)<commit_msg>Add support for Whalebrew installed executables<commit_after>Promise = require('bluebird')
_ = require('lodash')
which = require('which')
spawn = require('child_process').spawn
path = require('path')
semver = require('semver')
shellEnv = require('shell-env')
os = require('os')

parentConfigKey = "atom-beautify.executables"

module.exports = class Executable

  name: null
  cmd: null
  key: null
  homepage: null
  installation: null
  versionArgs: ['--version']
  versionParse: (text) -> semver.clean(text)
  versionRunOptions: {}
  versionsSupported: '>= 0.0.0'
  required: true

  constructor: (options) ->
    # Validation
    if !options.cmd?
      throw new Error("The command (i.e. cmd property) is required for an Executable.")
    @name = options.name
    @cmd = options.cmd
    @key = @cmd
    @homepage = options.homepage
    @installation = options.installation
    @required = not options.optional
    if options.version?
      versionOptions = options.version
      @versionArgs = versionOptions.args if versionOptions.args
      @versionParse = versionOptions.parse if versionOptions.parse
      @versionRunOptions = versionOptions.runOptions if versionOptions.runOptions
      @versionsSupported = versionOptions.supported if versionOptions.supported
    @setupLogger()

  init: () ->
    Promise.all([
      @loadVersion()
    ])
      .then(() => @verbose("Done init of #{@name}"))
      .then(() => @)
      .catch((error) =>
        if not @.required
          @
        else
          Promise.reject(error)
      )

  ###
  Logger instance
  ###
  logger: null
  ###
  Initialize and configure Logger
  ###
  setupLogger: ->
    @logger = require('../logger')("#{@name} Executable")
    for key, method of @logger
      @[key] = method
    @verbose("#{@name} executable logger has been initialized.")

  isInstalled = null
  version = null
  loadVersion: (force = false) ->
    @verbose("loadVersion", @version, force)
    if force or !@version?
      @verbose("Loading version without cache")
      @runVersion()
        .then((text) => @versionParse(text))
        .then((version) ->
          valid = Boolean(semver.valid(version))
          if not valid
            throw new Error("Version is not valid: "+version)
          version
        )
        .then((version) =>
          @isInstalled = true
          @version = version
        )
        .then((version) =>
          @verbose("#{@cmd} version: #{version}")
          version
        )
        .catch((error) =>
          @isInstalled = false
          @error(error)
          help = {
            program: @cmd
            link: @installation or @homepage
            pathOption: "Executable - #{@name or @cmd} - Path"
          }
          Promise.reject(@commandNotFoundError(@name or @cmd, help))
        )
    else
      @verbose("Loading cached version")
      Promise.resolve(@version)

  runVersion: () ->
    @run(@versionArgs, @versionRunOptions)
      .then((version) =>
        @verbose("Version: " + version)
        version
      )

  isSupported: () ->
    @isVersion(@versionsSupported)

  isVersion: (range) ->
    semver.satisfies(@version, range)

  getConfig: () ->
    atom?.config.get("#{parentConfigKey}.#{@key}") or {}

  ###
  Run command-line interface command
  ###
  run: (args, options = {}) ->
    @debug("Run: ", @cmd, args, options)
    { cwd, ignoreReturnCode, help, onStdin, returnStderr } = options
    # Flatten args first
    args = _.flatten(args)
    exeName = @cmd
    config = @getConfig()
    cwd ?= os.tmpDir()

    # Resolve executable and all args
    Promise.all([@shellEnv(), Promise.all(args)])
      .then(([env, args]) =>
        @debug('exeName, args:', exeName, args)

        # Get PATH and other environment variables
        if config and config.path
          exePath = config.path
        else
          exePath = @which(exeName)
        Promise.all([exeName, args, env, exePath])
      )
      .then(([exeName, args, env, exePath]) =>
        @debug('exePath:', exePath)
        @debug('env:', env)
        @debug('PATH:', env.PATH)
        @debug('args', args)
        args = this.relativizePaths(args)
        @debug('relativized args', args)

        exe = exePath ? exeName
        spawnOptions = {
          cwd: cwd
          env: env
        }
        @debug('spawnOptions', spawnOptions)

        @spawn(exe, args, spawnOptions, onStdin)
          .then(({returnCode, stdout, stderr}) =>
            @verbose('spawn result, returnCode', returnCode)
            @verbose('spawn result, stdout', stdout)
            @verbose('spawn result, stderr', stderr)

            # If return code is not 0 then error occured
            if not ignoreReturnCode and returnCode isnt 0
              # operable program or batch file
              windowsProgramNotFoundMsg = "is not recognized as an internal or external command"

              @verbose(stderr, windowsProgramNotFoundMsg)

              if @isWindows() and returnCode is 1 and stderr.indexOf(windowsProgramNotFoundMsg) isnt -1
                throw @commandNotFoundError(exeName, help)
              else
                throw new Error(stderr)
            else
              if returnStderr
                stderr
              else
                stdout
          )
          .catch((err) =>
            @debug('error', err)

            # Check if error is ENOENT (command could not be found)
            if err.code is 'ENOENT' or err.errno is 'ENOENT'
              throw @commandNotFoundError(exeName, help)
            else
              # continue as normal error
              throw err
          )
      )

  relativizePaths: (args) ->
    tmpDir = os.tmpDir()
    newArgs = args.map((arg) ->
      isTmpFile = typeof arg is 'string' and path.isAbsolute(arg) and path.dirname(arg).startsWith(tmpDir)
      if isTmpFile
        return path.relative(tmpDir, arg)
      return arg
    )
    newArgs

  ###
  Spawn
  ###
  spawn: (exe, args, options, onStdin) ->
    # Remove undefined/null values
    args = _.without(args, undefined)
    args = _.without(args, null)

    return new Promise((resolve, reject) =>
      @debug('spawn', exe, args)

      cmd = spawn(exe, args, options)
      stdout = ""
      stderr = ""

      cmd.stdout.on('data', (data) ->
        stdout += data
      )
      cmd.stderr.on('data', (data) ->
        stderr += data
      )
      cmd.on('close', (returnCode) =>
        @debug('spawn done', returnCode, stderr, stdout)
        resolve({returnCode, stdout, stderr})
      )
      cmd.on('error', (err) =>
        @debug('error', err)
{'fraction_non_alphanumeric': 0.12721659278024067, 'fraction_numerical': 0.0011082963901203294, 'mean_word_length': 2.154307116104869, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 65, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1666227', 'n_tokens_mistral': 3691, 'n_tokens_neox': 3600, 'n_words': 936}