repo_description: :shell: Portable Unix shell commands for Node.js
repo_topics: bash javascript node nodejs shell shelljs unix
code: // Ignore warning about 'new String()' and use of the Buffer constructor
  /* eslint no-new-wrappers: "off",
     no-buffer-constructor: "off" */
  
  'use strict';
  
  var os = require('os');
  var fs = require('fs');
  var glob = require('fast-glob');
  
  var shell = {};
  exports.shell = shell;
  
  var shellMethods = Object.create(shell);
  
  exports.extend = Object.assign;
  
  // Check if we're running under electron
  var isElectron = Boolean(process.versions.electron);
  
  // Module globals (assume no execPath by default)
  var DEFAULT_CONFIG = {
    fatal: false,
    globOptions: {},
    maxdepth: 255,
    noglob: false,
    silent: false,
    verbose: false,
    execPath: null,
    bufLength: 64 * 1024, // 64KB
  };
  
  var config = {
    reset() {
      Object.assign(this, DEFAULT_CONFIG);
      if (!isElectron) {
        this.execPath = process.execPath;
      }
    },
    resetForTesting() {
      this.reset();
      this.silent = true;
    },
  };
  
  config.reset();
  exports.config = config;
  
  // Note: commands should generally consider these as read-only values.
  var state = {
    error: null,
    errorCode: 0,
    currentCmd: 'shell.js',
  };
  exports.state = state;
  
  delete process.env.OLDPWD; // initially, there's no previous directory
  
  // Reliably test if something is any sort of javascript object
  function isObject(a) {
    return typeof a === 'object' && a !== null;
  }
  exports.isObject = isObject;
  
  function log() {
    /* istanbul ignore next */
    if (!config.silent) {
      console.error.apply(console, arguments);
    }
  }
  exports.log = log;
  
  // Converts strings to be equivalent across all platforms. Primarily responsible
  // for making sure we use '/' instead of '\' as path separators, but this may be
  // expanded in the future if necessary
  function convertErrorOutput(msg) {
    if (typeof msg !== 'string') {
      throw new TypeError('input must be a string');
    }
    return msg.replace(/\\/g, '/');
  }
  exports.convertErrorOutput = convertErrorOutput;
  
  // An exception class to help propagate command errors (e.g., non-zero exit
  // status) up to the top-level. {@param value} should be a ShellString.
  class CommandError extends Error {
    constructor(value) {
      super(value.toString());
      this.returnValue = value;
    }
  }
  exports.CommandError = CommandError; // visible for testing
  
  // Shows error message. Throws if fatal is true (defaults to config.fatal, overridable with options.fatal)
  function error(msg, _code, options) {
    // Validate input
    if (typeof msg !== 'string') throw new Error('msg must be a string');
  
    var DEFAULT_OPTIONS = {
      continue: false,
      code: 1,
      prefix: state.currentCmd + ': ',
      silent: false,
      fatal: config.fatal,
    };
  
    if (typeof _code === 'number' && isObject(options)) {
      options.code = _code;
    } else if (isObject(_code)) { // no 'code'
      options = _code;
    } else if (typeof _code === 'number') { // no 'options'
      options = { code: _code };
    } else if (typeof _code !== 'number') { // only 'msg'
      options = {};
    }
    options = Object.assign({}, DEFAULT_OPTIONS, options);
  
    if (!state.errorCode) state.errorCode = options.code;
  
    var logEntry = convertErrorOutput(options.prefix + msg);
    state.error = state.error ? state.error + '\n' : '';
    state.error += logEntry;
  
    // Throw an error, or log the entry
    if (options.fatal) {
      var err = new Error(logEntry);
      err.code = options.code;
      throw err;
    }
    if (msg.length > 0 && !options.silent) log(logEntry);
  
    if (!options.continue) {
      throw new CommandError(new ShellString('', state.error, state.errorCode));
    }
  }
  exports.error = error;
  
  //@
  //@ ### ShellString(str)
  //@
  //@ Examples:
  //@
  //@ ```javascript
  //@ var foo = new ShellString('hello world');
  //@ ```
  //@
  //@ This is a dedicated type returned by most ShellJS methods, which wraps a
  //@ string (or array) value. This has all the string (or array) methods, but
  //@ also exposes extra methods: [`.to()`](#shellstringprototypetofile),
  //@ [`.toEnd()`](#shellstringprototypetoendfile), and all the pipe-able methods
  //@ (ex. `.cat()`, `.grep()`, etc.). This can be easily converted into a string
  //@ by calling `.toString()`.
  //@
  //@ This type also exposes the corresponding command's stdout, stderr, and
  //@ return status code via the `.stdout` (string), `.stderr` (string), and
  //@ `.code` (number) properties respectively.
  function ShellString(stdout, stderr, code) {
    var that;
    if (stdout instanceof Array) {
      that = stdout;
      that.stdout = stdout.join('\n');
      if (stdout.length > 0) that.stdout += '\n';
    } else {
      that = new String(stdout);
      that.stdout = stdout;
    }
    that.stderr = stderr;
    that.code = code;
    // A list of all commands that can appear on the right-hand side of a pipe
    // (populated by calls to common.wrap())
    pipeMethods.forEach(function (cmd) {
      that[cmd] = shellMethods[cmd].bind(that);
    });
    return that;
  }
  
  exports.ShellString = ShellString;
  
  // Returns {'alice': true, 'bob': false} when passed a string and dictionary as follows:
  //   parseOptions('-a', {'a':'alice', 'b':'bob'});
  // Returns {'reference': 'string-value', 'bob': false} when passed two dictionaries of the form:
  //   parseOptions({'-r': 'string-value'}, {'r':'reference', 'b':'bob'});
  // Throws an error when passed a string that does not start with '-':
  //   parseOptions('a', {'a':'alice'}); // throws
  function parseOptions(opt, map, errorOptions) {
    errorOptions = errorOptions || {};
    // Validate input
    if (typeof opt !== 'string' && !isObject(opt)) {
      throw new TypeError('options must be strings or key-value pairs');
    } else if (!isObject(map)) {
      throw new TypeError('parseOptions() internal error: map must be an object');
    } else if (!isObject(errorOptions)) {
      throw new TypeError(
          'parseOptions() internal error: errorOptions must be object',
      );
    }
  
    if (opt === '--') {
      // This means there are no options.
      return {};
    }
  
    // All options are false by default
    var options = {};
    Object.keys(map).forEach(function (letter) {
      var optName = map[letter];
      if (optName[0] !== '!') {
        options[optName] = false;
      }
    });
  
    if (opt === '') return options; // defaults
  
    if (typeof opt === 'string') {
      if (opt[0] !== '-') {
        throw new Error("Options string must start with a '-'");
      }
  
      // e.g. chars = ['R', 'f']
      var chars = opt.slice(1).split('');
  
      chars.forEach(function (c) {
        if (c in map) {
          var optionName = map[c];
          if (optionName[0] === '!') {
            options[optionName.slice(1)] = false;
          } else {
            options[optionName] = true;
          }
        } else {
          error('option not recognized: ' + c, errorOptions);
        }
      });
    } else { // opt is an Object
      Object.keys(opt).forEach(function (key) {
        if (key[0] === '-') {
          // key is a string of the form '-r', '-d', etc.
          var c = key[1];
          if (c in map) {
            var optionName = map[c];
            options[optionName] = opt[key]; // assign the given value
          } else {
            error('option not recognized: ' + c, errorOptions);
          }
        } else if (key in options) {
          // key is a "long option", so it should be the same
          options[key] = opt[key];
        } else {
          error('option not recognized: {' + key + ':...}', errorOptions);
        }
      });
    }
    return options;
  }
  exports.parseOptions = parseOptions;
  
  function globOptions() {
    // These options are just to make fast-glob be compatible with POSIX (bash)
    // wildcard behavior.
    var defaultGlobOptions = {
      onlyFiles: false,
      followSymbolicLinks: false,
    };
  
    var newGlobOptions = Object.assign({}, config.globOptions);
    var optionRenames = {
      // node-glob's 'nodir' is not quote the same as fast-glob's 'onlyFiles'.
      // Compatibility for this is implemented at the call site.
      mark: 'markDirectories',
      matchBase: 'baseNameMatch',
    };
    Object.keys(optionRenames).forEach(function (oldKey) {
      var newKey = optionRenames[oldKey];
      if (oldKey in config.globOptions) {
        newGlobOptions[newKey] = config.globOptions[oldKey];
      }
    });
    var invertedOptionRenames = {
      nobrace: 'braceExpansion',
      noglobstar: 'globstar',
      noext: 'extglob',
      nocase: 'caseSensitiveMatch',
    };
    Object.keys(invertedOptionRenames).forEach(function (oldKey) {
      var newKey = invertedOptionRenames[oldKey];
      if (oldKey in config.globOptions) {
        newGlobOptions[newKey] = !config.globOptions[oldKey];
      }
    });
    return Object.assign({}, defaultGlobOptions, newGlobOptions);
  }
  
  
  // Workaround for https://github.com/shelljs/shelljs/issues/1197
  // fast-glob's dependency glob-parent does not recognize the '?' wildcard in
  // non-final path segments, causing those patterns to be treated as literal
  // directory names. Replace standalone '?' with the equivalent character class
  // '[^/]' which glob-parent handles correctly.
  function convertQuestionMarkForGlob(pattern) {
    if (pattern.indexOf('?') === -1) {
      return pattern;
    }
    var result = '';
    var inBracket = false;
    for (var i = 0; i < pattern.length; i++) {
      var ch = pattern[i];
      if (ch === '\\' && i + 1 < pattern.length) {
        // Escaped character: keep as-is
        result += ch + pattern[i + 1];
        i++;
      } else if (ch === '[' && !inBracket) {
        inBracket = true;
        result += ch;
      } else if (ch === ']' && inBracket) {
        inBracket = false;
        result += ch;
      } else if (ch === '?' && !inBracket && pattern[i + 1] === '(') {
        // Extglob pattern ?(...)  keep as-is
        result += ch;
      } else if (ch === '?' && !inBracket) {
        result += '[^/]';
      } else {
        result += ch;
      }
    }
    return result;
  }
  
  // Expands wildcards with matching (ie. existing) file names.
  // For example:
  //   expand(['file*.js']) = ['file1.js', 'file2.js', ...]
  //   (if the files 'file1.js', 'file2.js', etc, exist in the current dir)
  function expand(list) {
    if (!Array.isArray(list)) {
      throw new TypeError('must be an array');
    }
    var expanded = [];
    list.forEach(function (listEl) {
      // Don't expand non-strings
      if (typeof listEl !== 'string') {
        expanded.push(listEl);
      } else {
        var ret;
        var globOpts = globOptions();
        try {
          ret = glob.sync(convertQuestionMarkForGlob(listEl), globOpts);
        } catch (e) {
          // if glob fails, interpret the string literally
          ret = [listEl];
        }
        // if nothing matched, interpret the string literally
        ret = ret.length > 0 ? ret.sort() : [listEl];
        if (globOpts.nodir) {
          ret = ret.filter(function (file) {
            return !statNoFollowLinks(file).isDirectory();
          });
        }
        expanded = expanded.concat(ret);
      }
    });
    return expanded;
  }
  exports.expand = expand;
  
  // Normalizes Buffer creation, using Buffer.alloc if possible.
  // Also provides a good default buffer length for most use cases.
  var buffer = typeof Buffer.alloc === 'function' ?
    function (len) {
      return Buffer.alloc(len || config.bufLength);
    } :
    function (len) {
      return new Buffer(len || config.bufLength);
    };
  exports.buffer = buffer;
  
  // Normalizes _unlinkSync() across platforms to match Unix behavior, i.e.
  // file can be unlinked even if it's read-only, see https://github.com/joyent/node/issues/3006
  function unlinkSync(file) {
    try {
      fs.unlinkSync(file);
    } catch (e) {
      // Try to override file permission
      /* istanbul ignore next */
      if (e.code === 'EPERM') {
        fs.chmodSync(file, '0666');
        fs.unlinkSync(file);
      } else {
        throw e;
      }
    }
  }
  exports.unlinkSync = unlinkSync;
  
  // wrappers around common.statFollowLinks and common.statNoFollowLinks that clarify intent
  // and improve readability
  function statFollowLinks() {
    return fs.statSync.apply(fs, arguments);
  }
  exports.statFollowLinks = statFollowLinks;
  
  function statNoFollowLinks() {
    return fs.lstatSync.apply(fs, arguments);
  }
  exports.statNoFollowLinks = statNoFollowLinks;
  
  // e.g. 'shelljs_a5f185d0443ca...'
  function randomFileName() {
    function randomHash(count) {
      if (count === 1) {
        return parseInt(16 * Math.random(), 10).toString(16);
      }
      var hash = '';
      for (var i = 0; i < count; i++) {
        hash += randomHash(1);
      }
      return hash;
    }
  
    return 'shelljs_' + randomHash(20);
  }
  exports.randomFileName = randomFileName;
  
  // Common wrapper for all Unix-like commands that performs glob expansion,
  // command-logging, and other nice things
  function wrap(cmd, fn, options) {
    options = options || {};
    return function () {
      var retValue = null;
  
      state.currentCmd = cmd;
      state.error = null;
      state.errorCode = 0;
  
      try {
        var args = [].slice.call(arguments, 0);
  
        // Log the command to stderr, if appropriate
        if (config.verbose) {
          console.error.apply(console, [cmd].concat(args));
        }
  
        // If this is coming from a pipe, let's set the pipedValue (otherwise, set
        // it to the empty string)
        state.pipedValue = (this && typeof this.stdout === 'string') ? this.stdout : '';
  
        if (options.unix === false) { // this branch is for exec()
          retValue = fn.apply(this, args);
        } else { // and this branch is for everything else
          if (isObject(args[0]) && args[0].constructor.name === 'Object') {
            // a no-op, allowing the syntax `touch({'-r': file}, ...)`
          } else if (args.length === 0 || typeof args[0] !== 'string' || args[0].length <= 1 || args[0][0] !== '-') {
            args.unshift(''); // only add dummy option if '-option' not already present
          }
  
          // flatten out arrays that are arguments, to make the syntax:
          //    `cp([file1, file2, file3], dest);`
          // equivalent to:
          //    `cp(file1, file2, file3, dest);`
          args = args.reduce(function (accum, cur) {
            if (Array.isArray(cur)) {
              return accum.concat(cur);
            }
            accum.push(cur);
            return accum;
          }, []);
  
          // Convert ShellStrings (basically just String objects) to regular strings
          args = args.map(function (arg) {
            if (isObject(arg) && arg.constructor.name === 'String') {
              return arg.toString();
            }
            return arg;
          });
  
          // Expand the '~' if appropriate
          var homeDir = os.homedir();
          args = args.map(function (arg) {
            if (typeof arg === 'string' && arg.slice(0, 2) === '~/' || arg === '~') {
              return arg.replace(/^~/, homeDir);
            }
            return arg;
          });
  
          // Perform glob-expansion on all arguments after globStart, but preserve
          // the arguments before it (like regexes for sed and grep)
          if (!config.noglob && options.allowGlobbing === true) {
            args = args.slice(0, options.globStart).concat(expand(args.slice(options.globStart)));
          }
  
          try {
            // parse options if options are provided
            if (isObject(options.cmdOptions)) {
              args[0] = parseOptions(args[0], options.cmdOptions);
            }
  
            retValue = fn.apply(this, args);
          } catch (e) {
            /* istanbul ignore else */
            if (e instanceof CommandError) {
              retValue = e.returnValue;
            } else {
              throw e; // this is probably a bug that should be thrown up the call stack
            }
          }
        }
      } catch (e) {
        /* istanbul ignore next */
        if (!state.error) {
          // If state.error hasn't been set it's an error thrown by Node, not us - probably a bug...
          e.name = 'ShellJSInternalError';
          throw e;
        }
        if (config.fatal || options.handlesFatalDynamically) throw e;
      }
  
      if (options.wrapOutput &&
          (typeof retValue === 'string' || Array.isArray(retValue))) {
        retValue = new ShellString(retValue, state.error, state.errorCode);
      }
  
      state.currentCmd = 'shell.js';
      return retValue;
    };
  } // wrap
  exports.wrap = wrap;
  
  // This returns all the input that is piped into the current command (or the
  // empty string, if this isn't on the right-hand side of a pipe
  function _readFromPipe() {
    return state.pipedValue;
  }
  exports.readFromPipe = _readFromPipe;
  
  var DEFAULT_WRAP_OPTIONS = {
    allowGlobbing: true,
    canReceivePipe: false,
    cmdOptions: null,
    globStart: 1,
    handlesFatalDynamically: false,
    pipeOnly: false,
    wrapOutput: true,
    unix: true,
  };
  
  // This is populated during plugin registration
  var pipeMethods = [];
  
  // Register a new ShellJS command
  function _register(name, implementation, wrapOptions) {
    wrapOptions = wrapOptions || {};
  
    // Validate options
    Object.keys(wrapOptions).forEach(function (option) {
      if (!DEFAULT_WRAP_OPTIONS.hasOwnProperty(option)) {
        throw new Error("Unknown option '" + option + "'");
      }
      if (typeof wrapOptions[option] !== typeof DEFAULT_WRAP_OPTIONS[option]) {
        throw new TypeError("Unsupported type '" + typeof wrapOptions[option] +
          "' for option '" + option + "'");
      }
    });
  
    // If an option isn't specified, use the default
    wrapOptions = Object.assign({}, DEFAULT_WRAP_OPTIONS, wrapOptions);
  
    if (shell.hasOwnProperty(name)) {
      throw new Error('Command `' + name + '` already exists');
    }
  
    if (wrapOptions.pipeOnly) {
      wrapOptions.canReceivePipe = true;
      shellMethods[name] = wrap(name, implementation, wrapOptions);
    } else {
      shell[name] = wrap(name, implementation, wrapOptions);
    }
  
    if (wrapOptions.canReceivePipe) {
      pipeMethods.push(name);
    }
  }
  exports.register = _register;
  