diff --git a/README.md b/README.md index 8835faee5..8dfdf5a95 100644 --- a/README.md +++ b/README.md @@ -1099,15 +1099,31 @@ var yargs = require('yargs')(['--help']) Options: --help Show help [boolean] -.help([option, [description]]) ------------------------------- +.help() +----------------------------------------- +.help([option | boolean]) +----------------------------------------- +.help([option, [description | boolean]]) +----------------------------------------- +.help([option, [description, [boolean]]]) +----------------------------------------- + +Add an option (e.g. `--help`) and implicit command that displays the usage +string and exits the process. -Add an option (e.g. `--help`) that displays the usage string and exits the -process. If present, the `description` parameter customizes the description of +If present, the `description` parameter customizes the description of the help option in the usage string. -If invoked without parameters, `.help()` will make `--help` the option to trigger -help output. +If a boolean argument is provided, it will enable or disable the use of an +implicit command. The implicit command is enabled by default, but it can be +disabled by passing `false`. + +Note that any multi-char aliases (e.g. `help`) used for the help option will +also be used for the implicit command. If there are no multi-char aliases (e.g. +`h`), then all single-char aliases will be used for the command. + +If invoked without parameters, `.help()` will use `--help` as the option and +`help` as the implicit command to trigger help output. Example: diff --git a/test/command.js b/test/command.js index 96ad4c4ee..ae126c2f1 100644 --- a/test/command.js +++ b/test/command.js @@ -470,6 +470,83 @@ describe('Command', function () { }) }) + describe('help command', function () { + it('displays command help appropriately', function () { + var sub = { + command: 'sub', + desc: 'Run the subcommand', + builder: {}, + handler: function (argv) {} + } + + var cmd = { + command: 'cmd ', + desc: 'Try a command', + builder: function (yargs) { + return yargs.command(sub) + }, + handler: function (argv) {} + } + + var helpCmd = checkOutput(function () { + return yargs('help cmd') + .help().wrap(null) + .command(cmd) + .argv + }, [ './command' ]) + + var cmdHelp = checkOutput(function () { + return yargs('cmd help') + .help().wrap(null) + .command(cmd) + .argv + }, [ './command' ]) + + var helpCmdSub = checkOutput(function () { + return yargs('help cmd sub') + .help().wrap(null) + .command(cmd) + .argv + }, [ './command' ]) + + var cmdHelpSub = checkOutput(function () { + return yargs('cmd help sub') + .help().wrap(null) + .command(cmd) + .argv + }, [ './command' ]) + + var cmdSubHelp = checkOutput(function () { + return yargs('cmd sub help') + .help().wrap(null) + .command(cmd) + .argv + }, [ './command' ]) + + var expectedCmd = [ + './command cmd ', + 'Commands:', + ' sub Run the subcommand', + 'Options:', + ' --help Show help [boolean]', + '' + ] + + var expectedSub = [ + './command cmd sub', + 'Options:', + ' --help Show help [boolean]', + '' + ] + + helpCmd.logs.join('\n').split(/\n+/).should.deep.equal(expectedCmd) + cmdHelp.logs.join('\n').split(/\n+/).should.deep.equal(expectedCmd) + helpCmdSub.logs.join('\n').split(/\n+/).should.deep.equal(expectedSub) + cmdHelpSub.logs.join('\n').split(/\n+/).should.deep.equal(expectedSub) + cmdSubHelp.logs.join('\n').split(/\n+/).should.deep.equal(expectedSub) + }) + }) + // addresses https://github.com/yargs/yargs/issues/514. it('respects order of positional arguments when matching commands', function () { var output = [] diff --git a/test/yargs.js b/test/yargs.js index aacf7b8bb..32c6a9179 100644 --- a/test/yargs.js +++ b/test/yargs.js @@ -686,6 +686,11 @@ describe('yargs dsl tests', function () { a1.why.should.equal('hello world') a2.why.should.equal('hello world') }) + + it('ignores implicit help command (with short-circuit)', function () { + var parsed = yargs.help().parse('help', true) + parsed._.should.deep.equal(['help']) + }) }) describe('config', function () { @@ -1008,6 +1013,262 @@ describe('yargs dsl tests', function () { argv.koala.should.equal(true) }) }) + + describe('.help()', function () { + it('enables `--help` option and `help` command without arguments', function () { + var option = checkOutput(function () { + return yargs('--help') + .help() + .wrap(null) + .argv + }) + var command = checkOutput(function () { + return yargs('help') + .help() + .wrap(null) + .argv + }) + var expected = [ + 'Options:', + ' --help Show help [boolean]', + '' + ] + option.logs[0].split('\n').should.deep.equal(expected) + command.logs[0].split('\n').should.deep.equal(expected) + }) + + it('enables `--help` option and `help` command with `true` argument', function () { + var option = checkOutput(function () { + return yargs('--help') + .help(true) + .wrap(null) + .argv + }) + var command = checkOutput(function () { + return yargs('help') + .help(true) + .wrap(null) + .argv + }) + var expected = [ + 'Options:', + ' --help Show help [boolean]', + '' + ] + option.logs[0].split('\n').should.deep.equal(expected) + command.logs[0].split('\n').should.deep.equal(expected) + }) + + it('enables only `--help` option with `false` argument', function () { + var option = checkOutput(function () { + return yargs('--help') + .help(false) + .wrap(null) + .argv + }) + var command = checkOutput(function () { + return yargs('help') + .help(false) + .wrap(null) + .argv + }) + option.logs[0].split('\n').should.deep.equal([ + 'Options:', + ' --help Show help [boolean]', + '' + ]) + command.result.should.have.property('_').and.deep.equal(['help']) + }) + + it('enables given string as help option and command with string argument', function () { + var option = checkOutput(function () { + return yargs('--info') + .help('info') + .wrap(null) + .argv + }) + var command = checkOutput(function () { + return yargs('info') + .help('info') + .wrap(null) + .argv + }) + var helpOption = checkOutput(function () { + return yargs('--help') + .help('info') + .wrap(null) + .argv + }) + var expected = [ + 'Options:', + ' --info Show help [boolean]', + '' + ] + option.logs[0].split('\n').should.deep.equal(expected) + command.logs[0].split('\n').should.deep.equal(expected) + helpOption.result.should.have.property('help').and.be.true + }) + + it('enables given string as help option and command with string argument and `true` argument', function () { + var option = checkOutput(function () { + return yargs('--info') + .help('info', true) + .wrap(null) + .argv + }) + var command = checkOutput(function () { + return yargs('info') + .help('info', true) + .wrap(null) + .argv + }) + var helpOption = checkOutput(function () { + return yargs('--help') + .help('info', true) + .wrap(null) + .argv + }) + var expected = [ + 'Options:', + ' --info Show help [boolean]', + '' + ] + option.logs[0].split('\n').should.deep.equal(expected) + command.logs[0].split('\n').should.deep.equal(expected) + helpOption.result.should.have.property('help').and.be.true + }) + + it('enables given string as help option only with string argument and `false` argument', function () { + var option = checkOutput(function () { + return yargs('--info') + .help('info', false) + .wrap(null) + .argv + }) + var command = checkOutput(function () { + return yargs('info') + .help('info', false) + .wrap(null) + .argv + }) + option.logs[0].split('\n').should.deep.equal([ + 'Options:', + ' --info Show help [boolean]', + '' + ]) + command.result.should.have.property('_').and.deep.equal(['info']) + }) + + it('enables given string as help option and command with custom description with two string arguments', function () { + var option = checkOutput(function () { + return yargs('--info') + .help('info', 'Display info') + .wrap(null) + .argv + }) + var command = checkOutput(function () { + return yargs('info') + .help('info', 'Display info') + .wrap(null) + .argv + }) + var expected = [ + 'Options:', + ' --info Display info [boolean]', + '' + ] + option.logs[0].split('\n').should.deep.equal(expected) + command.logs[0].split('\n').should.deep.equal(expected) + }) + + it('enables given string as help option and command with custom description with two string arguments and `true` argument', function () { + var option = checkOutput(function () { + return yargs('--info') + .help('info', 'Display info', true) + .wrap(null) + .argv + }) + var command = checkOutput(function () { + return yargs('info') + .help('info', 'Display info', true) + .wrap(null) + .argv + }) + var expected = [ + 'Options:', + ' --info Display info [boolean]', + '' + ] + option.logs[0].split('\n').should.deep.equal(expected) + command.logs[0].split('\n').should.deep.equal(expected) + }) + + it('enables given string as help option only and custom description with two string arguments and `false` argument', function () { + var option = checkOutput(function () { + return yargs('--info') + .help('info', 'Display info', false) + .wrap(null) + .argv + }) + var command = checkOutput(function () { + return yargs('info') + .help('info', 'Display info', false) + .wrap(null) + .argv + }) + option.logs[0].split('\n').should.deep.equal([ + 'Options:', + ' --info Display info [boolean]', + '' + ]) + command.result.should.have.property('_').and.deep.equal(['info']) + }) + }) + + describe('.help() with .alias()', function () { + it('uses multi-char (but not single-char) help alias as command', function () { + var info = checkOutput(function () { + return yargs('info') + .help().alias('h', 'help').alias('h', 'info') + .wrap(null) + .argv + }) + var h = checkOutput(function () { + return yargs('h') + .help().alias('h', 'help').alias('h', 'info') + .wrap(null) + .argv + }) + info.logs[0].split('\n').should.deep.equal([ + 'Options:', + ' -h, --help, --info Show help [boolean]', + '' + ]) + h.result.should.have.property('_').and.deep.equal(['h']) + }) + + it('uses single-char help alias as command if there are no multi-char aliases', function () { + var h = checkOutput(function () { + return yargs('h') + .help('h').alias('h', '?') + .wrap(null) + .argv + }) + var q = checkOutput(function () { + return yargs('?') + .help('h').alias('h', '?') + .wrap(null) + .argv + }) + var expected = [ + 'Options:', + ' -h, -? Show help [boolean]', + '' + ] + h.logs[0].split('\n').should.deep.equal(expected) + q.logs[0].split('\n').should.deep.equal(expected) + }) + }) }) describe('yargs context', function () { diff --git a/yargs.js b/yargs.js index ed6fd5685..3377b1ddf 100644 --- a/yargs.js +++ b/yargs.js @@ -520,12 +520,33 @@ function Yargs (processArgs, cwd, parentRequire) { } var helpOpt = null - self.addHelpOpt = self.help = function (opt, msg) { - opt = opt || 'help' - helpOpt = opt - self.boolean(opt) - self.global(opt) - self.describe(opt, msg || usage.deferY18nLookup('Show help')) + var useHelpOptAsCommand = false // a call to .help() will enable this + self.addHelpOpt = self.help = function (opt, msg, addImplicitCmd) { + // argument shuffle + if (arguments.length === 0) { + useHelpOptAsCommand = true + } else if (arguments.length === 1) { + if (typeof opt === 'boolean') { + useHelpOptAsCommand = opt + opt = null + } else { + useHelpOptAsCommand = true + } + } else if (arguments.length === 2) { + if (typeof msg === 'boolean') { + useHelpOptAsCommand = msg + msg = null + } else { + useHelpOptAsCommand = true + } + } else { + useHelpOptAsCommand = Boolean(addImplicitCmd) + } + // use arguments, fallback to defaults for opt and msg + helpOpt = opt || 'help' + self.boolean(helpOpt) + self.global(helpOpt) + self.describe(helpOpt, msg || usage.deferY18nLookup('Show help')) return self } @@ -654,22 +675,45 @@ function Yargs (processArgs, cwd, parentRequire) { return argv } - // if there's a handler associated with a - // command defer processing to it. - var handlerKeys = command.getCommands() - for (var i = 0, cmd; (cmd = argv._[i]) !== undefined; i++) { - if (~handlerKeys.indexOf(cmd) && cmd !== completionCommand) { - setPlaceholderKeys(argv) - return command.runCommand(cmd, self, parsed) + if (argv._.length) { + // check for helpOpt in argv._ before running commands + // assumes helpOpt must be valid if useHelpOptAsCommand is true + if (useHelpOptAsCommand) { + // consider any multi-char helpOpt alias as a valid help command + // unless all helpOpt aliases are single-char + // note that parsed.aliases is a normalized bidirectional map :) + var helpCmds = [helpOpt].concat(aliases[helpOpt]) + var multiCharHelpCmds = helpCmds.filter(function (k) { + return k.length > 1 + }) + if (multiCharHelpCmds.length) helpCmds = multiCharHelpCmds + // look for and strip any helpCmds from argv._ + argv._ = argv._.filter(function (cmd) { + if (~helpCmds.indexOf(cmd)) { + argv[helpOpt] = true + return false + } + return true + }) + } + + // if there's a handler associated with a + // command defer processing to it. + var handlerKeys = command.getCommands() + for (var i = 0, cmd; (cmd = argv._[i]) !== undefined; i++) { + if (~handlerKeys.indexOf(cmd) && cmd !== completionCommand) { + setPlaceholderKeys(argv) + return command.runCommand(cmd, self, parsed) + } } - } - // generate a completion script for adding to ~/.bashrc. - if (completionCommand && ~argv._.indexOf(completionCommand) && !argv[completion.completionKey]) { - if (exitProcess) setBlocking(true) - self.showCompletionScript() - if (exitProcess) { - process.exit(0) + // generate a completion script for adding to ~/.bashrc. + if (completionCommand && ~argv._.indexOf(completionCommand) && !argv[completion.completionKey]) { + if (exitProcess) setBlocking(true) + self.showCompletionScript() + if (exitProcess) { + process.exit(0) + } } }