Skip to content

Commit

Permalink
feat: introduces support for default commands, using the '*' identifi…
Browse files Browse the repository at this point in the history
…er (#785)
  • Loading branch information
bcoe committed Feb 26, 2017
1 parent 8a992f5 commit d78a0f5
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 21 deletions.
28 changes: 28 additions & 0 deletions README.md
Expand Up @@ -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
Expand Down
59 changes: 53 additions & 6 deletions lib/command.js
Expand Up @@ -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.
Expand All @@ -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)) {
Expand All @@ -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] = {
Expand All @@ -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) {
Expand Down Expand Up @@ -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()
Expand All @@ -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.
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -263,6 +307,7 @@ module.exports = function (yargs, usage, validation) {
self.reset = function () {
handlers = {}
aliasMap = {}
defaultCommand = undefined
return self
}

Expand All @@ -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
}

Expand Down
20 changes: 16 additions & 4 deletions lib/usage.js
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
Expand Down

0 comments on commit d78a0f5

Please sign in to comment.