Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: rethink how options are inherited by commands #766

Merged
merged 13 commits into from Jan 29, 2017
Merged
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])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very awesome, I love this one

------------------------------
.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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the little bit of refactoring in this method was to hold off on applying validation until after we've populated variadic values -- this was largely so that one can provide a .check() at the top level, and have it passed the appropriate post-processed object after commands are parsed.


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