Skip to content

Commit

Permalink
feat: rethink how options are inherited by commands (#766)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: by default options, and many of yargs' parsing helpers will now default to being applied globally; such that they are no-longer reset before being passed into commands.
  • Loading branch information
bcoe authored and Benjamin Coe committed Feb 18, 2017
1 parent 2dece16 commit 0df98f3
Show file tree
Hide file tree
Showing 9 changed files with 553 additions and 229 deletions.
38 changes: 20 additions & 18 deletions README.md
Expand Up @@ -408,7 +408,7 @@ explicitly set.

If `key` is an array, interpret all the elements as booleans.

.check(fn)
.check(fn, [global=true])
----------

Check that certain conditions are met in the provided arguments.
Expand All @@ -418,6 +418,9 @@ Check that certain conditions are met in the provided arguments.
If `fn` throws or returns a non-truthy value, show the thrown error, usage information, and
exit.

`global` indicates whether `check()` should be enabled both
at the top-level and for each sub-command.

<a name="choices"></a>.choices(key, choices)
----------------------

Expand Down Expand Up @@ -551,14 +554,6 @@ yargs
.argv
```

Note that commands will not automatically inherit configuration _or_ options
of their parent context. This means you'll have to re-apply configuration
if necessary, and make options global manually using the [global](#global) method.

Additionally, the [`help`](#help) and [`version`](#version)
options (if used) **always** apply globally, just like the
[`.wrap()`](#wrap) configuration.

`builder` can also be a function. This function is executed
with a `yargs` instance, and can be used to provide _advanced_ command specific help:

Expand Down Expand Up @@ -678,7 +673,7 @@ require('yargs')
console.log(`setting ${argv.key} to ${argv.value}`)
}
})
.demandCommand(1)
.demandCommand()
.help()
.wrap(72)
.argv
Expand Down Expand Up @@ -824,7 +819,7 @@ cli.js:
#!/usr/bin/env node
require('yargs')
.commandDir('cmds')
.demandCommand(1)
.demandCommand()
.help()
.argv
```
Expand Down Expand Up @@ -1112,9 +1107,9 @@ Options:
Missing required arguments: run, path
```

<a name="demandCommand"></a>.demandCommand(min, [minMsg])
<a name="demandCommand"></a>.demandCommand([min=1], [minMsg])
------------------------------
.demandCommand(min, [max], [minMsg], [maxMsg])
.demandCommand([min=1], [max], [minMsg], [maxMsg])
------------------------------

Demand in context of commands. You can demand a minimum and a maximum number a user can have within your program, as well as provide corresponding error messages if either of the demands is not met.
Expand All @@ -1134,7 +1129,9 @@ require('yargs')
.help()
.argv
```

which will provide the following output:

```bash
Commands:
configure <key> [value] Set a config variable [aliases: config, cfg]
Expand Down Expand Up @@ -1293,7 +1290,7 @@ require('yargs')

Outputs the same completion choices as `./test.js --foo`<kbd>TAB</kbd>: `--foobar` and `--foobaz`

<a name="global"></a>.global(globals)
<a name="global"></a>.global(globals, [global=true])
------------

Indicate that an option (or group of options) should not be reset when a command
Expand All @@ -1303,11 +1300,13 @@ is executed, as an example:
var argv = require('yargs')
.option('a', {
alias: 'all',
default: true
default: true,
global: false
})
.option('n', {
alias: 'none',
default: true
default: true,
global: false
})
.command('foo', 'foo command', function (yargs) {
return yargs.option('b', {
Expand All @@ -1322,7 +1321,7 @@ var argv = require('yargs')
If the `foo` command is executed the `all` option will remain, but the `none`
option will have been eliminated.

`help`, `version`, and `completion` options default to being global.
Options default to being global.

<a name="group"></a>.group(key(s), groupName)
--------------------
Expand Down Expand Up @@ -1778,12 +1777,15 @@ Specify --help for available options
Specifies either a single option key (string), or an array of options.
If any of the options is present, yargs validation is skipped.

.strict()
.strict([global=true])
---------

Any command-line argument given that is not demanded, or does not have a
corresponding description, will be reported as an error.

`global` indicates whether `strict()` should be enabled both
at the top-level and for each sub-command.

<a name="string"></a>.string(key)
------------

Expand Down
33 changes: 24 additions & 9 deletions lib/command.js
Expand Up @@ -131,47 +131,62 @@ module.exports = function (yargs, usage, validation) {
}

self.runCommand = function (command, yargs, parsed) {
var argv = parsed.argv
var aliases = parsed.aliases
var commandHandler = handlers[command] || handlers[aliasMap[command]]
var innerArgv = argv
var currentContext = yargs.getContext()
var numFiles = currentContext.files.length
var parentCommands = currentContext.commands.slice()

// what does yargs look like after the buidler is run?
var innerArgv = parsed.argv
var innerYargs = null

currentContext.commands.push(command)
if (typeof commandHandler.builder === 'function') {
// a function can be provided, which builds
// up a yargs chain and possibly returns it.
innerArgv = commandHandler.builder(yargs.reset(parsed.aliases))
innerYargs = commandHandler.builder(yargs.reset(parsed.aliases))
// if the builder function did not yet parse argv with reset yargs
// and did not explicitly set a usage() string, then apply the
// original command string as usage() for consistent behavior with
// options object below
// options object below.
if (yargs.parsed === false) {
if (typeof yargs.getUsageInstance().getUsage() === 'undefined') {
yargs.usage('$0 ' + (parentCommands.length ? parentCommands.join(' ') + ' ' : '') + commandHandler.original)
}
innerArgv = innerArgv ? innerArgv.argv : yargs.argv
innerArgv = innerYargs ? innerYargs._parseArgs(null, null, true) : yargs._parseArgs(null, null, true)
} else {
innerArgv = yargs.parsed.argv
}

if (innerYargs && yargs.parsed === false) aliases = innerYargs.parsed.aliases
else aliases = yargs.parsed.aliases
} else if (typeof commandHandler.builder === 'object') {
// as a short hand, an object can instead be provided, specifying
// the options that a command takes.
innerArgv = yargs.reset(parsed.aliases)
innerArgv.usage('$0 ' + (parentCommands.length ? parentCommands.join(' ') + ' ' : '') + commandHandler.original)
innerYargs = yargs.reset(parsed.aliases)
innerYargs.usage('$0 ' + (parentCommands.length ? parentCommands.join(' ') + ' ' : '') + commandHandler.original)
Object.keys(commandHandler.builder).forEach(function (key) {
innerArgv.option(key, commandHandler.builder[key])
innerYargs.option(key, commandHandler.builder[key])
})
innerArgv = innerArgv.argv
innerArgv = innerYargs._parseArgs(null, null, true)
aliases = innerYargs.parsed.aliases
}

if (!yargs._hasOutput()) populatePositionals(commandHandler, innerArgv, currentContext, yargs)

if (commandHandler.handler && !yargs._hasOutput()) {
commandHandler.handler(innerArgv)
}

// we apply validation post-hoc, so that custom
// checks get passed populated positional arguments.
yargs._runValidation(innerArgv, aliases)

currentContext.commands.pop()
numFiles = currentContext.files.length - numFiles
if (numFiles > 0) currentContext.files.splice(numFiles * -1, numFiles)

return innerArgv
}

Expand Down
4 changes: 2 additions & 2 deletions lib/usage.js
Expand Up @@ -436,7 +436,7 @@ module.exports = function (yargs, y18n) {
else logger.log(version)
}

self.reset = function (globalLookup) {
self.reset = function (localLookup) {
// do not reset wrap here
// do not reset fails here
failMessage = null
Expand All @@ -446,7 +446,7 @@ module.exports = function (yargs, y18n) {
examples = []
commands = []
descriptions = objFilter(descriptions, function (k, v) {
return globalLookup[k]
return !localLookup[k]
})
return self
}
Expand Down
26 changes: 18 additions & 8 deletions lib/validation.js
Expand Up @@ -194,22 +194,26 @@ module.exports = function (yargs, usage, y18n) {

// custom checks, added using the `check` option on yargs.
var checks = []
self.check = function (f) {
checks.push(f)
self.check = function (f, global) {
checks.push({
func: f,
global: global
})
}

self.customChecks = function (argv, aliases) {
for (var i = 0, f; (f = checks[i]) !== undefined; i++) {
var func = f.func
var result = null
try {
result = f(argv, aliases)
result = func(argv, aliases)
} catch (err) {
usage.fail(err.message ? err.message : err, err)
continue
}

if (!result) {
usage.fail(__('Argument check failed: %s', f.toString()))
usage.fail(__('Argument check failed: %s', func.toString()))
} else if (typeof result === 'string' || result instanceof Error) {
usage.fail(result.toString(), result)
}
Expand All @@ -224,6 +228,7 @@ module.exports = function (yargs, usage, y18n) {
self.implies(k, key[k])
})
} else {
yargs.global(key)
implied[key] = value
}
}
Expand Down Expand Up @@ -290,6 +295,7 @@ module.exports = function (yargs, usage, y18n) {
self.conflicts(k, key[k])
})
} else {
yargs.global(key)
conflicting[key] = value
}
}
Expand Down Expand Up @@ -324,12 +330,16 @@ module.exports = function (yargs, usage, y18n) {
if (recommended) usage.fail(__('Did you mean %s?', recommended))
}

self.reset = function (globalLookup) {
self.reset = function (localLookup) {
implied = objFilter(implied, function (k, v) {
return globalLookup[k]
return !localLookup[k]
})
conflicting = objFilter(conflicting, function (k, v) {
return !localLookup[k]
})
checks = checks.filter(function (c) {
return c.global
})
checks = []
conflicting = {}
return self
}

Expand Down

0 comments on commit 0df98f3

Please sign in to comment.