From d78a0f5b42d41f3959c451b8e2f81e8fdbac845e Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Sun, 26 Feb 2017 02:34:48 -0600 Subject: [PATCH] feat: introduces support for default commands, using the '*' identifier (#785) --- README.md | 28 ++++++++++++ lib/command.js | 59 +++++++++++++++++++++--- lib/usage.js | 20 ++++++-- test/command.js | 119 +++++++++++++++++++++++++++++++++++++++++++----- yargs.js | 9 ++++ 5 files changed, 214 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ef2948e33..44262f5ad 100644 --- a/README.md +++ b/README.md @@ -591,6 +591,34 @@ yargs .argv ``` +### Default Commands + +To specify a default command use the character `*`. A default command +will be run if the positional arguments provided match no known +commands: + +```js +const argv = require('yargs') + .command('*', 'the default command', () => {}, (argv) => { + console.log('this command will be run by default') + }) +``` + +The command defined above will be executed if the program +is run with `./my-cli.js --x=22`. + +Default commands can also be used as a command alias, like so: + +```js +const argv = require('yargs') + .command(['serve', '*'], 'the serve command', () => {}, (argv) => { + console.log('this command will be run by default') + }) +``` + +The command defined above will be executed if the program +is run with `./my-cli.js --x=22`, or with `./my-cli.js serve --x=22`. + ### Positional Arguments Commands can accept _optional_ and _required_ positional arguments. Required diff --git a/lib/command.js b/lib/command.js index abc92c29e..27fe6d02c 100644 --- a/lib/command.js +++ b/lib/command.js @@ -2,6 +2,8 @@ const path = require('path') const inspect = require('util').inspect const camelCase = require('camelcase') +const DEFAULT_MARKER = '*' + // handles parsing positional arguments, // and populating argv with said positional // arguments. @@ -10,6 +12,7 @@ module.exports = function (yargs, usage, validation) { var handlers = {} var aliasMap = {} + var defaultCommand self.addHandler = function (cmd, description, builder, handler) { var aliases = [] if (Array.isArray(cmd)) { @@ -28,15 +31,50 @@ module.exports = function (yargs, usage, validation) { return } + // parse positionals out of cmd string var parsedCommand = self.parseCommand(cmd) + + // remove positional args from aliases only aliases = aliases.map(function (alias) { - alias = self.parseCommand(alias).cmd // remove positional args + return self.parseCommand(alias).cmd + }) + + // check for default and filter out '*'' + var isDefault = false + var parsedAliases = [parsedCommand.cmd].concat(aliases).filter(function (c) { + if (c === DEFAULT_MARKER) { + isDefault = true + return false + } + return true + }) + + // short-circuit if default with no aliases + if (isDefault && parsedAliases.length === 0) { + defaultCommand = { + original: cmd.replace(DEFAULT_MARKER, '').trim(), + handler: handler, + builder: builder || {}, + demanded: parsedCommand.demanded, + optional: parsedCommand.optional + } + return + } + + // shift cmd and aliases after filtering out '*' + if (isDefault) { + parsedCommand.cmd = parsedAliases[0] + aliases = parsedAliases.slice(1) + cmd = cmd.replace(DEFAULT_MARKER, parsedCommand.cmd) + } + + // populate aliasMap + aliases.forEach(function (alias) { aliasMap[alias] = parsedCommand.cmd - return alias }) if (description !== false) { - usage.command(cmd, description, aliases) + usage.command(cmd, description, isDefault, aliases) } handlers[parsedCommand.cmd] = { @@ -46,6 +84,8 @@ module.exports = function (yargs, usage, validation) { demanded: parsedCommand.demanded, optional: parsedCommand.optional } + + if (isDefault) defaultCommand = handlers[parsedCommand.cmd] } self.addDirectory = function (dir, context, req, callerFile, opts) { @@ -130,9 +170,13 @@ module.exports = function (yargs, usage, validation) { return handlers } + self.hasDefaultCommand = function () { + return !!defaultCommand + } + self.runCommand = function (command, yargs, parsed) { var aliases = parsed.aliases - var commandHandler = handlers[command] || handlers[aliasMap[command]] + var commandHandler = handlers[command] || handlers[aliasMap[command]] || defaultCommand var currentContext = yargs.getContext() var numFiles = currentContext.files.length var parentCommands = currentContext.commands.slice() @@ -142,7 +186,7 @@ module.exports = function (yargs, usage, validation) { var innerYargs = null var positionalMap = {} - currentContext.commands.push(command) + if (command) currentContext.commands.push(command) if (typeof commandHandler.builder === 'function') { // a function can be provided, which builds // up a yargs chain and possibly returns it. @@ -186,7 +230,7 @@ module.exports = function (yargs, usage, validation) { commandHandler.handler(innerArgv) } - currentContext.commands.pop() + if (command) currentContext.commands.pop() numFiles = currentContext.files.length - numFiles if (numFiles > 0) currentContext.files.splice(numFiles * -1, numFiles) @@ -263,6 +307,7 @@ module.exports = function (yargs, usage, validation) { self.reset = function () { handlers = {} aliasMap = {} + defaultCommand = undefined return self } @@ -275,10 +320,12 @@ module.exports = function (yargs, usage, validation) { frozen = {} frozen.handlers = handlers frozen.aliasMap = aliasMap + frozen.defaultCommand = defaultCommand } self.unfreeze = function () { handlers = frozen.handlers aliasMap = frozen.aliasMap + defaultCommand = frozen.defaultCommand frozen = undefined } diff --git a/lib/usage.js b/lib/usage.js index 84ea9a276..43f71225c 100644 --- a/lib/usage.js +++ b/lib/usage.js @@ -77,8 +77,15 @@ module.exports = function (yargs, y18n) { } var commands = [] - self.command = function (cmd, description, aliases) { - commands.push([cmd, description || '', aliases]) + self.command = function (cmd, description, isDefault, aliases) { + // the last default wins, so cancel out any previously set default + if (isDefault) { + commands = commands.map(function (cmdArray) { + cmdArray[2] = false + return cmdArray + }) + } + commands.push([cmd, description || '', isDefault, aliases]) } self.getCommands = function () { return commands @@ -166,8 +173,13 @@ module.exports = function (yargs, y18n) { {text: command[0], padding: [0, 2, 0, 2], width: maxWidth(commands, theWrap) + 4}, {text: command[1]} ) - if (command[2] && command[2].length) { - ui.div({text: '[' + __('aliases:') + ' ' + command[2].join(', ') + ']', padding: [0, 0, 0, 2], align: 'right'}) + var hints = [] + if (command[2]) hints.push('[' + __('default:').slice(0, -1) + ']') // TODO hacking around i18n here + if (command[3] && command[3].length) { + hints.push('[' + __('aliases:') + ' ' + command[3].join(', ') + ']') + } + if (hints.length) { + ui.div({text: hints.join(' '), padding: [0, 0, 0, 2], align: 'right'}) } else { ui.div() } diff --git a/test/command.js b/test/command.js index 058e2a1cb..dfa5c7060 100644 --- a/test/command.js +++ b/test/command.js @@ -181,21 +181,23 @@ describe('Command', function () { it('accepts string, string as first 2 arguments', function () { var cmd = 'foo' var desc = 'i\'m not feeling very creative at the moment' + var isDefault = false var aliases = [] var y = yargs([]).command(cmd, desc) var commands = y.getUsageInstance().getCommands() - commands[0].should.deep.equal([cmd, desc, aliases]) + commands[0].should.deep.equal([cmd, desc, isDefault, aliases]) }) it('accepts array, string as first 2 arguments', function () { var aliases = ['bar', 'baz'] var cmd = 'foo ' var desc = 'i\'m not feeling very creative at the moment' + var isDefault = false var y = yargs([]).command([cmd].concat(aliases), desc) var usageCommands = y.getUsageInstance().getCommands() - usageCommands[0].should.deep.equal([cmd, desc, aliases]) + usageCommands[0].should.deep.equal([cmd, desc, isDefault, aliases]) var cmdCommands = y.getCommandInstance().getCommands() cmdCommands.should.deep.equal(['foo', 'bar', 'baz']) }) @@ -284,6 +286,7 @@ describe('Command', function () { builder: function (yargs) { return yargs }, handler: function (argv) {} } + var isDefault = false var aliases = [] var y = yargs([]).command(module) @@ -292,7 +295,7 @@ describe('Command', function () { handlers.foo.builder.should.equal(module.builder) handlers.foo.handler.should.equal(module.handler) var commands = y.getUsageInstance().getCommands() - commands[0].should.deep.equal([module.command, module.describe, aliases]) + commands[0].should.deep.equal([module.command, module.describe, isDefault, aliases]) }) it('accepts module (description key, builder function) as 1st argument', function () { @@ -302,6 +305,7 @@ describe('Command', function () { builder: function (yargs) { return yargs }, handler: function (argv) {} } + var isDefault = false var aliases = [] var y = yargs([]).command(module) @@ -310,7 +314,7 @@ describe('Command', function () { handlers.foo.builder.should.equal(module.builder) handlers.foo.handler.should.equal(module.handler) var commands = y.getUsageInstance().getCommands() - commands[0].should.deep.equal([module.command, module.description, aliases]) + commands[0].should.deep.equal([module.command, module.description, isDefault, aliases]) }) it('accepts module (desc key, builder function) as 1st argument', function () { @@ -320,6 +324,7 @@ describe('Command', function () { builder: function (yargs) { return yargs }, handler: function (argv) {} } + var isDefault = false var aliases = [] var y = yargs([]).command(module) @@ -328,7 +333,7 @@ describe('Command', function () { handlers.foo.builder.should.equal(module.builder) handlers.foo.handler.should.equal(module.handler) var commands = y.getUsageInstance().getCommands() - commands[0].should.deep.equal([module.command, module.desc, aliases]) + commands[0].should.deep.equal([module.command, module.desc, isDefault, aliases]) }) it('accepts module (false describe, builder function) as 1st argument', function () { @@ -375,6 +380,7 @@ describe('Command', function () { }, handler: function (argv) {} } + var isDefault = false var aliases = [] var y = yargs([]).command(module) @@ -383,7 +389,7 @@ describe('Command', function () { handlers.foo.builder.should.equal(module.builder) handlers.foo.handler.should.equal(module.handler) var commands = y.getUsageInstance().getCommands() - commands[0].should.deep.equal([module.command, module.describe, aliases]) + commands[0].should.deep.equal([module.command, module.describe, isDefault, aliases]) }) it('accepts module (missing handler function) as 1st argument', function () { @@ -396,6 +402,7 @@ describe('Command', function () { } } } + var isDefault = false var aliases = [] var y = yargs([]).command(module) @@ -404,7 +411,7 @@ describe('Command', function () { handlers.foo.builder.should.equal(module.builder) expect(handlers.foo.handler).to.equal(undefined) var commands = y.getUsageInstance().getCommands() - commands[0].should.deep.equal([module.command, module.describe, aliases]) + commands[0].should.deep.equal([module.command, module.describe, isDefault, aliases]) }) it('accepts module (with command array) as 1st argument', function () { @@ -414,6 +421,7 @@ describe('Command', function () { builder: function (yargs) { return yargs }, handler: function (argv) {} } + var isDefault = false var y = yargs([]).command(module) var handlers = y.getCommandInstance().getCommandHandlers() @@ -421,7 +429,7 @@ describe('Command', function () { handlers.foo.builder.should.equal(module.builder) handlers.foo.handler.should.equal(module.handler) var usageCommands = y.getUsageInstance().getCommands() - usageCommands[0].should.deep.equal([module.command[0], module.describe, ['bar', 'baz']]) + usageCommands[0].should.deep.equal([module.command[0], module.describe, isDefault, ['bar', 'baz']]) var cmdCommands = y.getCommandInstance().getCommands() cmdCommands.should.deep.equal(['foo', 'bar', 'baz']) }) @@ -434,6 +442,7 @@ describe('Command', function () { builder: function (yargs) { return yargs }, handler: function (argv) {} } + var isDefault = false var y = yargs([]).command(module) var handlers = y.getCommandInstance().getCommandHandlers() @@ -441,7 +450,7 @@ describe('Command', function () { handlers.foo.builder.should.equal(module.builder) handlers.foo.handler.should.equal(module.handler) var usageCommands = y.getUsageInstance().getCommands() - usageCommands[0].should.deep.equal([module.command, module.describe, module.aliases]) + usageCommands[0].should.deep.equal([module.command, module.describe, isDefault, module.aliases]) var cmdCommands = y.getCommandInstance().getCommands() cmdCommands.should.deep.equal(['foo', 'bar', 'baz']) }) @@ -454,6 +463,7 @@ describe('Command', function () { builder: function (yargs) { return yargs }, handler: function (argv) {} } + var isDefault = false var y = yargs([]).command(module) var handlers = y.getCommandInstance().getCommandHandlers() @@ -461,7 +471,7 @@ describe('Command', function () { handlers.foo.builder.should.equal(module.builder) handlers.foo.handler.should.equal(module.handler) var usageCommands = y.getUsageInstance().getCommands() - usageCommands[0].should.deep.equal([module.command[0], module.describe, ['bar', 'baz', 'nat']]) + usageCommands[0].should.deep.equal([module.command[0], module.describe, isDefault, ['bar', 'baz', 'nat']]) var cmdCommands = y.getCommandInstance().getCommands() cmdCommands.should.deep.equal(['foo', 'bar', 'baz', 'nat']) }) @@ -474,6 +484,7 @@ describe('Command', function () { builder: function (yargs) { return yargs }, handler: function (argv) {} } + var isDefault = false var y = yargs([]).command(module) var handlers = y.getCommandInstance().getCommandHandlers() @@ -481,7 +492,7 @@ describe('Command', function () { handlers.foo.builder.should.equal(module.builder) handlers.foo.handler.should.equal(module.handler) var usageCommands = y.getUsageInstance().getCommands() - usageCommands[0].should.deep.equal([module.command, module.describe, ['bar']]) + usageCommands[0].should.deep.equal([module.command, module.describe, isDefault, ['bar']]) var cmdCommands = y.getCommandInstance().getCommands() cmdCommands.should.deep.equal(['foo', 'bar']) }) @@ -1189,4 +1200,90 @@ describe('Command', function () { }) }) }) + + describe('default commands', function () { + it('executes default command if no positional arguments given', function (done) { + yargs('--foo bar') + .command('*', 'default command', function () {}, function (argv) { + argv.foo.should.equal('bar') + return done() + }) + .argv + }) + + it('does not execute default command if another command is provided', function (done) { + yargs('run bcoe --foo bar') + .command('*', 'default command', function () {}, function (argv) {}) + .command('run ', 'run command', function () {}, function (argv) { + argv.name.should.equal('bcoe') + argv.foo.should.equal('bar') + return done() + }) + .argv + }) + + it('allows default command to be set as alias', function (done) { + yargs('bcoe --foo bar') + .command(['start ', '*'], 'start command', function () {}, function (argv) { + argv._.should.eql([]) + argv.name.should.equal('bcoe') + argv.foo.should.equal('bar') + return done() + }) + .argv + }) + + it('allows command to be run when alias is default command', function (done) { + yargs('start bcoe --foo bar') + .command(['start ', '*'], 'start command', function () {}, function (argv) { + argv._.should.eql(['start']) + argv.name.should.equal('bcoe') + argv.foo.should.equal('bar') + return done() + }) + .argv + }) + + it('the last default command set should take precedence', function (done) { + yargs('bcoe --foo bar') + .command(['first', '*'], 'override me', function () {}, function () {}) + .command(['second ', '*'], 'start command', function () {}, function (argv) { + argv._.should.eql([]) + argv.name.should.equal('bcoe') + argv.foo.should.equal('bar') + return done() + }) + .argv + }) + + describe('strict', function () { + it('executes default command when strict mode is enabled', function (done) { + yargs('--foo bar') + .command('*', 'default command', function () {}, function (argv) { + argv.foo.should.equal('bar') + return done() + }) + .option('foo', { + describe: 'a foo command' + }) + .strict() + .argv + }) + + it('allows default command aliases, when strict mode is enabled', function (done) { + yargs('bcoe --foo bar') + .command(['start ', '*'], 'start command', function () {}, function (argv) { + argv._.should.eql([]) + argv.name.should.equal('bcoe') + argv.foo.should.equal('bar') + return done() + }) + .strict() + .option('foo', { + describe: 'a foo command' + }) + .argv + }) + }) + }) }) diff --git a/yargs.js b/yargs.js index f3cad8d7d..1843443f7 100644 --- a/yargs.js +++ b/yargs.js @@ -988,6 +988,12 @@ function Yargs (processArgs, cwd, parentRequire) { } } + // run the default command, if defined + if (command.hasDefaultCommand() && !argv[helpOpt]) { + setPlaceholderKeys(argv) + return command.runCommand(null, self, parsed) + } + // recommend a command if recommendCommands() has // been enabled, and no commands were found to execute if (recommendCommands && firstUnknownCommand) { @@ -1001,6 +1007,9 @@ function Yargs (processArgs, cwd, parentRequire) { self.showCompletionScript() self.exit(0) } + } else if (command.hasDefaultCommand()) { + setPlaceholderKeys(argv) + return command.runCommand(null, self, parsed) } // we must run completions first, a user might