<|start_filename|>test/command.js<|end_filename|>
var test = require("tape");

var cliparse = require("../src/cliparse.js");
var command = require("../src/command.js");
var parsers = require("../src/parsers.js");

/*
 * option
 */

test('default command use case', function(t) {
    t.plan(1);
    var cmd = cliparse.command('name');
    var result = command.parse(cmd, [], [], {});
    t.same(result.success, { options: {}, args: [], namedArgs: {}, unnamedArgs: [] });
});

test('command with one flag', function(t) {
    t.plan(2);
    var option = cliparse.flag('test');
    var cmd = cliparse.command( 'name', { options: [option] });

    var r1 = command.parse(cmd, [], [], { test: true });
    var r2 = command.parse(cmd, [], [], {});

    t.same(r1.success, { options: { test: true }, args: [], namedArgs: {}, unnamedArgs: [] }, 'option present');
    t.same(r2.success, { options: { test: false }, args: [], namedArgs: {}, unnamedArgs: [] }, 'option not there');
});

test('command with a required option without default', function(t) {
    t.plan(2);
    var option = cliparse.flag('test', { required: true });
    var cmd = cliparse.command( 'name', { options: [option] });

    var r1 = command.parse(cmd, [], [], { test: true });
    var r2 = command.parse(cmd, [], [], {});

    t.same(r1.success, { options: { test: true }, args: [], namedArgs: {}, unnamedArgs: [] }, 'option present');
    t.same(r2.success, undefined, 'option not there');
});

test('command with arguments', function(t) {
    t.plan(3);
    var argument = cliparse.argument('test');
    var cmd = cliparse.command('name', { args: [argument]});

    var r1 = command.parse(cmd, [], ['value'], {});
    var r2 = command.parse(cmd, [], [], {});
    var r3 = command.parse(cmd, [], ['value', 'extra value'], {});

    t.same(r1.success, { options: {}, args: ['value'], namedArgs: { test: 'value' }, unnamedArgs: [] }, 'argument present');
    t.same(r2.success, undefined, 'must fail if argument not there');
    t.same(r3.success, { options: {}, args: ['value', 'extra value'], namedArgs: { test: 'value' }, unnamedArgs: ['extra value'] }, 'must recognize named argument and split unnamed argument');
});

test('command with no named arguments', function(t) {
    t.plan(3);
    var cmd = cliparse.command('name', {});

    var r1 = command.parse(cmd, [], ['value'], {});
    var r2 = command.parse(cmd, [], [], {});
    var r3 = command.parse(cmd, [], ['value', 'extra value'], {});

    t.same(r1.success, { options: {}, args: ['value'], namedArgs: {}, unnamedArgs: ['value'] }, 'argument present');
    t.same(r2.success, { options: {}, args: [], namedArgs: {}, unnamedArgs: [] }, 'no arguments means no failure');
    t.same(r3.success, { options: {}, args: ['value', 'extra value'], namedArgs: {}, unnamedArgs: ['value', 'extra value'] }, 'must put all arguments as unnamed');
});

test('retrieve flags', function(t) {
  t.plan(3);
  var flag1 = cliparse.flag('flag-one');
  var flag2 = cliparse.flag('flag-two', { aliases: ["f"] });
  var flag3 = cliparse.flag('flag-three', { aliases: ["x"]});
  var option1 = cliparse.option('opt-one');
  var option2 = cliparse.option('opt-two', { aliases: ["o"] });
  var option3 = cliparse.option('opt-three', { aliases: ["t"]});
  var cmd1 = cliparse.command('name', { options: [flag1, option2]});
  var cmd2 = cliparse.command('name', { options: [flag1, option1], commands: [ cliparse.command('inner', { options: [flag2, option2] })]});
  var cmd3 = cliparse.command('name', { options: [flag1, option1], commands: [
    cliparse.command('inner', { options: [flag2, option2], commands: [
        cliparse.command('second-inner', { options: [flag3, option3] }),
    ]})
  ]});

  var r1 = command.getFlagNames(cmd1);
  var r2 = command.getFlagNames(cmd2);
  var r3 = command.getFlagNames(cmd3);

  t.same(r1, ['flag-one'], 'simple command');
  t.same(r2, ['flag-one', 'flag-two', 'f'], 'nested commands');
  t.same(r3, ['flag-one', 'flag-two', 'f', 'flag-three', 'x'], 'nested commands two levels');
});

test('retrieve option names', function(t) {
    t.plan(3);
    var flag1 = cliparse.flag('flag-one');
    var flag2 = cliparse.flag('flag-two', { aliases: ["f"] });
    var flag3 = cliparse.flag('flag-three', { aliases: ["x"]});
    var option1 = cliparse.option('opt-one');
    var option2 = cliparse.option('opt-two', { aliases: ["o"] });
    var option3 = cliparse.option('opt-three', { aliases: ["t"]});
    var cmd1 = cliparse.command('name', { options: [flag1, option2]});
    var cmd2 = cliparse.command('name', { options: [flag1, option1], commands: [ cliparse.command('inner', { options: [flag2, option2] })]});
    var cmd3 = cliparse.command('name', { options: [flag1, option1], commands: [
        cliparse.command('inner', { options: [flag2, option2], commands: [
            cliparse.command('second-inner', { options: [flag3, option3] }),
        ]})
    ]});

    var r1 = command.getOptionNames(cmd1);
    var r2 = command.getOptionNames(cmd2);
    var r3 = command.getOptionNames(cmd3);

    t.same(r1, ['opt-two', 'o'], 'simple command');
    t.same(r2, ['opt-one', 'opt-two', 'o'], 'nested commands');
    t.same(r3, ['opt-one', 'opt-two', 'o', 'opt-three', 't'], 'nested commands two levels');
});

test('command with unknown options', function(t) {
  t.plan(1);
  var flag = cliparse.flag('test');
  var cmd = cliparse.command('name', { options: [flag]});
  var r = command.parse(cmd, [], [], {test: true, unknown: "value"});
  t.same(r.success, undefined, 'must fail on unknown options')
});


<|start_filename|>src/argument.js<|end_filename|>
var _ = require("lodash");

var autocomplete = require("./autocomplete");
var parsers = require("./parsers");
var utils = require("./utils");

var argument = module.exports = {};

argument.argument = function(name, options) {
  options = options || {};
  options.name = name || "argument";
  options.parser = options.parser || parsers.stringParser;
  options.description = options.description || "";
  options.complete = options.complete || autocomplete.defaultCompleter;
  options.default = typeof options.default !== "undefined" ? options.default : null;

  return options;
};

argument.parse = function(argument, value) {
  let result;
  if(typeof value !== 'undefined' && value !== null) {
    result = argument?.parser(value) ?? parsers.stringParser(value);
  } else if((argument?.default ?? null) !== null) {
    result = parsers.success(argument.default);
  } else {
    result = parsers.error("missing value");
  }

  result.argument = argument;
  return result;
};

argument.parseList = function(args, providedArguments) {
  const combined = _.zip(args, providedArguments);

  const results = _.map(combined, function(kv) {
    return argument.parse(kv[0], kv[1]);
  });

  if(_.every(results, parsers.isSuccess)) {
    const namedArgs = _(results)
    .takeWhile(r => typeof r.argument?.name !== 'undefined')
    .map(r => [r.argument.name, r.success])
    .fromPairs()
    .value();
    return parsers.success({
      args: _.map(results, "success"),
      namedArgs,
      unnamedArgs: _(results).drop(_.size(namedArgs)).map("success").value(),
    });
  } else {
    return parsers.error(results);
  }
};

argument.usage = function(argument) {
  if(argument.default !== null) {
    return "[" + argument.name.toUpperCase() + "]";
  } else {
    return argument.name.toUpperCase();
  }
};

argument.help = function(arg) {
  var description = arg.description;
  if(arg.default !== null) description += " (default: " + arg.default + ")";
  return [argument.usage(arg), description];
};

argument.complete = function(arg, word) {
  return arg.complete(word);
};


<|start_filename|>src/cli.js<|end_filename|>
var _ = require("lodash");
var minimist = require("minimist");
var Promise = require("bluebird").Promise;

var argument = require("./argument");
var autocomplete = require("./autocomplete");
var autocompleteScript = require("./autocompleteScript");
var command = require("./command");
var option = require("./option");
var parsers = require("./parsers");
var utils = require("./utils");

var cli = module.exports = {};

cli.cli = function(options, cb) {
  options.topLevel = true;
  options.version = options.version ?? null;
  options.helpCommand = options.helpCommand ?? true;
  options.options = [ option.helpOption, option.versionOption ].concat(options.options || []);

  if(options.helpCommand) {
    options.commands = [ command.helpCommand ].concat(options.commands || []);
  }

  return command.command(
    options.name || utils.getProcessName(process.argv),
    options,
    cb
  );
};

cli.displayVersion = function(cli) {
  console.log(cli.version !== null ? cli.version : 'N/A');
};

cli.displayUsage = function(cliApp, givenArgs) {
  var context = _.reduce(givenArgs, function(acc, arg) {
    var isDone = acc[0];
    var cmd = _.last(acc[1]);

    var matchingSubcommand = _.find(cmd.commands, function(cmd) {
      return cmd.name === arg;
    });

    if(!isDone && matchingSubcommand) {
      return [false, acc[1].concat(matchingSubcommand)];
    } else {
      return [true, acc[1]];
    }
  }, [false, [cliApp]])[1];

  console.log(command.help(context).trim());
};

cli.displayErrors = function(errors, cliApp, givenArgs) {
  var argsErrors = _.map(errors.args, function(error) {
    if(error.argument) {
      return error.argument.name + ': ' + error.error;
    } else {
      return error.error;
    }
  });
  var optionsErrors = _.map(errors.options, function(error) {
    if(error.option) {
      return error.option.name + ': ' + error.error;
    } else {
      return error.error;
    }
  });

  if(!_.isEmpty(argsErrors)) console.log(argsErrors.join('\n'));
  if(!_.isEmpty(optionsErrors)) console.log(optionsErrors.join('\n'));

  if(!_.isEmpty(argsErrors) || !_.isEmpty(optionsErrors)) console.log();

  cli.displayUsage(cliApp, givenArgs);
};

cli.execute = function(cliApp, args, options) {
  var result = command.parse(cliApp, [], args, options);

  if(parsers.isError(result)) {
    cli.displayErrors(result.error, cliApp, args);
  } else {
    if(!_.last(result.context).action) {
      cli.displayUsage(cliApp, args);
    } else {
      _.last(result.context).action(result.success);
    }
  }
};

cli.autocomplete = function(cliApp, words, index, flagNames, optionNames) {
  if(typeof words !== 'object') words = [words];
  if(index >= words.length) words.push('');
  var opts = {
     string: optionNames.concat(["_"]),
    boolean: flagNames
  }
  var argv = minimist(words, opts);
  var current = words[index] || '';
  var consumedArgs = autocomplete.currentArg(words, index, argv._).consumedArgs;
  var results;

  if(argv._[1] === 'help' && cliApp.helpCommand) {
    results = Promise.resolve(autocomplete.autocompleteHelpCommand(cliApp, _.drop(consumedArgs, 2)));
  } else {
    results = command.autocomplete(
      cliApp,
      _.drop(consumedArgs, 1),
      argv,
      words,
      index,
      []);
  }

  results.then(function(rs) {
    return autocomplete.compgen(rs, current);
  }).then(console.log);
};

cli.cleanArgv = function(argv) {
  // check the command is launched via the node interpreter (+ ensure windows compat)
  if (argv[0].match(/node/)) {
    return _.drop(argv, 2);
  } else {
    return _.drop(argv, 1);
  }
};

cli.parse = function(cliApp, argv) {
  argv = argv ?? process.argv;

  var flagNames = command.getFlagNames(cliApp);
  var optionNames = command.getOptionNames(cliApp);

  var opts = {
    string: optionNames.concat(["autocomplete-words", "_"]), // All other options should be treated as strings (and not coerced)
    boolean: flagNames // Declare flags as not expecting values
  };

  var cliValues = minimist(argv, opts);
  var optionsWithFlagDefaults = _.omit(cliValues, "_");

  // Minimist adds all the names declared as flags even though they are not
  // present (with value false). These interfere with the parsing later, so we
  // remove them here.
  var options = _.fromPairs(_.filter(_.toPairs(optionsWithFlagDefaults), function(kv) {
    return kv[1] || !_.includes(flagNames, kv[0]);
  }));
  var args = cli.cleanArgv(cliValues._);

  if(option.parse(option.helpOption, options).success === true) {
    cli.displayUsage(cliApp, args);
  } else if(!cliApp.noHelpCommand && args[0] === 'help') {
    cli.displayUsage(cliApp, _.drop(args, 1));
  } else if(option.parse(option.versionOption, options).success === true) {
    cli.displayVersion(cliApp);
  } else if(options["bash-autocomplete-script"]) {
    console.log(autocompleteScript.bashScript(options["bash-autocomplete-script"]));
  } else if(options["zsh-autocomplete-script"]) {
    console.log(autocompleteScript.bashScript(options["zsh-autocomplete-script"], true));
  } else if(options["autocomplete-words"] && options["autocomplete-index"]) {
    cli.autocomplete(cliApp, options["autocomplete-words"], options["autocomplete-index"], flagNames, optionNames);
  } else {
    cli.execute(cliApp, args, options);
  }
};
