docs: function substitute(files) {
    const defaultOptions = {
      recursive: false, abortOnUnmatch: false, followSymLinks: false, cwd: null,
      exclude: [], ignore: [], type: 'auto', global: true,
      ignoreCase: false, multiline: true, dotall: false,
      ignoreBigFiles: true, bigFileThreshold: 50000000, skipBinaries: true
    };
    const substitutions = [];
    let options = null;
    if (arguments.length >= 3 && _.isString(arguments[1]) || _.isRegExp(arguments[1])) {
      // substitute signature = substitute(files, key, value, options)
      const key = arguments[1];
      const replacement = arguments[2];
      options = _.sanitize(arguments[3], defaultOptions);
      substitutions.push(new Substitution(key, replacement, options));
    } else if (_.isReallyObject(arguments[1])) {
      // substitute signature = substitute(files, {key: value}, options)
      options = _.sanitize(arguments[2], defaultOptions);
      _.each(arguments[1], function(replacement, pattern) {
        substitutions.push(new Substitution(pattern, replacement, options));
      });
    } else if (_.isArray(arguments[1])) {
      // substitute signature = substitute(files, [{pattern: key, value: value}], options)
      // Makes convenient adding patterns as regexp literals (you cannot directly use a pattern as an object key)
      options = _.sanitize(arguments[2], defaultOptions);
      _.each(arguments[1], function(definition) {
        const pattern = definition.pattern;
        const replacement = definition.value;
        const replacementOpts = _.opts(definition, options);
        substitutions.push(new Substitution(pattern, replacement, replacementOpts));
      });
    } else {
      throw new Error('Invalid substitute function call');
    }
    let filesToReplace = [];
    // options.ignore is deprecated in favor of options.exclude
    if (!_.isEmpty(options.ignore) && _.isEmpty(options.exclude)) options.exclude = options.ignore;
    const globOptions = {cwd: options.cwd, exclude: options.exclude, absolutize: true};
    _.each(globFile(files, globOptions), function(file) {
      if (!options.followSymLinks && isLink(file)) {
        return;
      }
      if (isFile(file, {acceptLinks: true})) {
        filesToReplace.push(file);
      } else if (isDirectory(file, {acceptLinks: false}) && options.recursive) {
        filesToReplace = filesToReplace.concat(listDirContents(file,
          {onlyFiles: true, exclude: options.exclude, followSymLinks: false, listSymLinks: options.followSymLinks}));
      } else {
        // ignore it
      }
    });
    if (filesToReplace.length > 0) {
      _massFileSubstitute(filesToReplace, substitutions, options);
    } else if (options.abortOnUnmatch) {
      throw new Error('Files to substitute resolved to an empty list');
    }
  }