diff --git a/README.md b/README.md index c14cb2928..c4c006c9c 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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. + .choices(key, choices) ---------------------- @@ -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: @@ -678,7 +673,7 @@ require('yargs') console.log(`setting ${argv.key} to ${argv.value}`) } }) - .demandCommand(1) + .demandCommand() .help() .wrap(72) .argv @@ -824,7 +819,7 @@ cli.js: #!/usr/bin/env node require('yargs') .commandDir('cmds') - .demandCommand(1) + .demandCommand() .help() .argv ``` @@ -1112,9 +1107,9 @@ Options: Missing required arguments: run, path ``` -.demandCommand(min, [minMsg]) +.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. @@ -1134,7 +1129,9 @@ require('yargs') .help() .argv ``` + which will provide the following output: + ```bash Commands: configure [value] Set a config variable [aliases: config, cfg] @@ -1293,7 +1290,7 @@ require('yargs') Outputs the same completion choices as `./test.js --foo`TAB: `--foobar` and `--foobaz` -.global(globals) +.global(globals, [global=true]) ------------ Indicate that an option (or group of options) should not be reset when a command @@ -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', { @@ -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. .group(key(s), groupName) -------------------- @@ -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. + .string(key) ------------ diff --git a/lib/command.js b/lib/command.js index c1afc1c55..4b4cbeaaa 100644 --- a/lib/command.js +++ b/lib/command.js @@ -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 } diff --git a/lib/usage.js b/lib/usage.js index 2e503acfa..0efabe868 100644 --- a/lib/usage.js +++ b/lib/usage.js @@ -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 @@ -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 } diff --git a/lib/validation.js b/lib/validation.js index 328d83aff..475ee3180 100644 --- a/lib/validation.js +++ b/lib/validation.js @@ -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) } @@ -224,6 +228,7 @@ module.exports = function (yargs, usage, y18n) { self.implies(k, key[k]) }) } else { + yargs.global(key) implied[key] = value } } @@ -290,6 +295,7 @@ module.exports = function (yargs, usage, y18n) { self.conflicts(k, key[k]) }) } else { + yargs.global(key) conflicting[key] = value } } @@ -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 } diff --git a/test/command.js b/test/command.js index be4f0dca8..ebdf881bc 100644 --- a/test/command.js +++ b/test/command.js @@ -839,4 +839,300 @@ describe('Command', function () { argv.sins.should.deep.equal([113993, 112888]) }) }) + + describe('global parsing hints', function () { + describe('config', function () { + it('does not load config for command if global is false', function (done) { + yargs('command --foo ./package.json') + .command('command', 'a command', {}, function (argv) { + expect(argv.license).to.equal(undefined) + return done() + }) + .config('foo') + .global('foo', false) + .argv + }) + + it('loads config for command by default', function (done) { + yargs('command --foo ./package.json') + .command('command', 'a command', {}, function (argv) { + argv.license.should.equal('MIT') + return done() + }) + .config('foo') + .argv + }) + }) + + describe('validation', function () { + it('resets implies logic for command if global is false', function (done) { + yargs('command --foo 99') + .command('command', 'a command', {}, function (argv) { + argv.foo.should.equal(99) + return done() + }) + .implies('foo', 'bar') + .global('foo', false) + .argv + }) + + it('applies conflicts logic for command by default', function (done) { + yargs('command --foo --bar') + .command('command', 'a command', {}, function (argv) {}) + .fail(function (msg) { + msg.should.match(/mutually exclusive/) + return done() + }) + .conflicts('foo', 'bar') + .argv + }) + + it('resets conflicts logic for command if global is false', function (done) { + yargs('command --foo --bar') + .command('command', 'a command', {}, function (argv) { + argv.foo.should.equal(true) + argv.bar.should.equal(true) + return done() + }) + .conflicts('foo', 'bar') + .global('foo', false) + .argv + }) + + it('applies custom checks globally by default', function (done) { + yargs('command blerg --foo') + .command('command ', 'a command') + .check(function (argv) { + argv.snuh.should.equal('blerg') + argv.foo.should.equal(true) + argv._.should.include('command') + done() + return true + }) + .argv + }) + + it('resets custom check if global is false', function () { + var checkCalled = false + yargs('command blerg --foo') + .command('command ', 'a command') + .check(function (argv) { + checkCalled = true + return true + }, false) + .argv + checkCalled.should.equal(false) + }) + + it('applies demandOption globally', function (done) { + yargs('command blerg --foo') + .command('command ', 'a command') + .fail(function (msg) { + msg.should.match(/Missing required argument: bar/) + return done() + }) + .demandOption('bar') + .argv + }) + }) + + describe('strict', function () { + it('defaults to false when not called', function () { + var commandCalled = false + yargs('hi') + .command('hi', 'The hi command', function (innerYargs) { + commandCalled = true + innerYargs.getStrict().should.be.false + }) + yargs.getStrict().should.be.false + yargs.argv // parse and run command + commandCalled.should.be.true + }) + + it('can be enabled just for a command', function () { + var commandCalled = false + yargs('hi') + .command('hi', 'The hi command', function (innerYargs) { + commandCalled = true + innerYargs.strict().getStrict().should.be.true + }) + yargs.getStrict().should.be.false + yargs.argv // parse and run command + commandCalled.should.be.true + }) + + it('applies strict globally by default', function () { + var commandCalled = false + yargs('hi') + .strict() + .command('hi', 'The hi command', function (innerYargs) { + commandCalled = true + innerYargs.getStrict().should.be.true + }) + yargs.getStrict().should.be.true + yargs.argv // parse and run command + commandCalled.should.be.true + }) + + it('does not apply strict globally when passed value of `false`', function () { + var commandCalled = false + yargs('hi') + .strict(false) + .command('hi', 'The hi command', function (innerYargs) { + commandCalled = true + innerYargs.getStrict().should.be.false + }) + yargs.getStrict().should.be.true + yargs.argv // parse and run command + commandCalled.should.be.true + }) + }) + + describe('types', function () { + it('applies array type globally', function () { + const argv = yargs('command --foo 1 2') + .command('command', 'a command') + .array('foo') + .argv + argv.foo.should.eql([1, 2]) + }) + + it('allows global setting to be disabled for array type', function () { + const argv = yargs('command --foo 1 2') + .command('command', 'a command') + .array('foo') + .global('foo', false) + .argv + argv.foo.should.eql(1) + }) + + it('applies choices type globally', function (done) { + yargs('command --foo 99') + .command('command', 'a command') + .choices('foo', [33, 88]) + .fail(function (msg) { + msg.should.match(/Choices: 33, 88/) + return done() + }) + .argv + }) + }) + + describe('aliases', function () { + it('defaults to applying aliases globally', function (done) { + yargs('command blerg --foo 22') + .command('command ', 'a command', {}, function (argv) { + argv.foo.should.equal(22) + argv.bar.should.equal(22) + argv.snuh.should.equal('blerg') + return done() + }) + .alias('foo', 'bar') + .argv + }) + + it('allows global application of alias to be disabled', function (done) { + yargs('command blerg --foo 22') + .command('command ', 'a command', {}, function (argv) { + argv.foo.should.equal(22) + expect(argv.bar).to.equal(undefined) + argv.snuh.should.equal('blerg') + return done() + }) + .option('foo', { + alias: 'bar', + global: false + }) + .argv + }) + }) + + describe('coerce', function () { + it('defaults to applying coerce rules globally', function (done) { + yargs('command blerg --foo 22') + .command('command ', 'a command', {}, function (argv) { + argv.foo.should.equal(44) + argv.snuh.should.equal('blerg') + return done() + }) + .coerce('foo', function (arg) { + return arg * 2 + }) + .argv + }) + }) + + describe('defaults', function () { + it('applies defaults globally', function (done) { + yargs('command --foo 22') + .command('command [snuh]', 'a command', {}, function (argv) { + argv.foo.should.equal(22) + argv.snuh.should.equal(55) + return done() + }) + .default('snuh', 55) + .argv + }) + }) + + describe('describe', function () { + it('flags an option as global if a description is set', function (done) { + yargs() + .command('command [snuh]', 'a command') + .describe('foo', 'an awesome argument') + .help() + .parse('command --help', function (err, argv, output) { + if (err) return done(err) + output.should.not.match(/Commands:/) + output.should.match(/an awesome argument/) + return done() + }) + }) + }) + + describe('help', function () { + it('applies help globally', function (done) { + yargs() + .command('command [snuh]', 'a command') + .describe('foo', 'an awesome argument') + .help('hellllllp') + .parse('command --hellllllp', function (err, argv, output) { + if (err) return done(err) + output.should.match(/--hellllllp {2}Show help/) + return done() + }) + }) + }) + + describe('version', function () { + it('applies version globally', function (done) { + yargs() + .command('command [snuh]', 'a command') + .describe('foo', 'an awesome argument') + .version('ver', 'show version', '9.9.9') + .parse('command --ver', function (err, argv, output) { + if (err) return done(err) + output.should.equal('9.9.9') + return done() + }) + }) + }) + + describe('groups', function () { + it('should apply custom option groups globally', function (done) { + yargs() + .command('command [snuh]', 'a command') + .group('foo', 'Bad Variable Names:') + .group('snuh', 'Bad Variable Names:') + .describe('foo', 'foo option') + .describe('snuh', 'snuh positional') + .help() + .parse('command --help', function (err, argv, output) { + if (err) return done(err) + output.should.match(/Bad Variable Names:\W*--foo/) + return done() + }) + }) + }) + }) }) diff --git a/test/usage.js b/test/usage.js index b3f532d76..87635aa21 100644 --- a/test/usage.js +++ b/test/usage.js @@ -84,16 +84,16 @@ describe('usage tests', function () { r.exit.should.be.ok }) - it('no failure occurs if the required arguments and the required number of commands are provided.', function () { + it('no failure occurs if the required arguments and the required number of commands are provided', function () { var r = checkUsage(function () { return yargs('wombat -w 10 -m 10') .usage('Usage: $0 -w NUM -m NUM') .command('wombat', 'wombat handlers') .demand(1, ['w', 'm']) - .strict() .wrap(null) .argv }) + r.result.should.have.property('w', 10) r.result.should.have.property('m', 10) r.result.should.have.property('_').with.length(1) @@ -170,24 +170,6 @@ describe('usage tests', function () { r.logs.should.have.length(0) r.exit.should.be.ok }) - - it('no failure occurs if the required arguments and the required number of commands are provided.', function () { - var r = checkUsage(function () { - return yargs('wombat -w 10 -m 10') - .usage('Usage: $0 -w NUM -m NUM') - .command('wombat', 'wombat handlers') - .require(1, ['w', 'm']) - .strict() - .wrap(null) - .argv - }) - r.result.should.have.property('w', 10) - r.result.should.have.property('m', 10) - r.result.should.have.property('_').with.length(1) - r.should.have.property('errors').with.length(0) - r.should.have.property('logs').with.length(0) - r.should.have.property('exit', false) - }) }) it('should show an error along with a custom message on demand fail', function () { @@ -1598,7 +1580,7 @@ describe('usage tests', function () { ]) }) - it('preserves groups with global keys', function () { + it('allows global option to be disabled', function () { var r = checkUsage(function () { return yargs(['upload', '-h']) .command('upload', 'upload something', function (yargs) { diff --git a/test/validation.js b/test/validation.js index f08080a23..a831dea71 100644 --- a/test/validation.js +++ b/test/validation.js @@ -589,5 +589,15 @@ describe('validation tests', function () { }) .argv }) + + it('defaults to demanding 1 command', function (done) { + yargs('-a 10') + .demandCommand() + .fail(function (msg) { + msg.should.equal('Not enough non-option arguments: got 0, need at least 1') + return done() + }) + .argv + }) }) }) diff --git a/test/yargs.js b/test/yargs.js index b8777e748..5d5771df8 100644 --- a/test/yargs.js +++ b/test/yargs.js @@ -220,8 +220,10 @@ describe('yargs dsl tests', function () { .implies('foo', 'snuh') .conflicts('qux', 'xyzzy') .group('foo', 'Group:') - .strict() + .strict(false) .exitProcess(false) // defaults to true. + .global('foo', false) + .global('qux', false) .env('YARGS') .reset() @@ -244,9 +246,13 @@ describe('yargs dsl tests', function () { config: {}, configObjects: [], envPrefix: 'YARGS', // preserved as global - global: ['help'], demandedCommands: {}, - demandedOptions: {} + demandedOptions: {}, + local: [ + '_', + 'foo', + 'qux' + ] } expect(y.getOptions()).to.deep.equal(emptyOptions) @@ -261,9 +267,10 @@ describe('yargs dsl tests', function () { expect(y.getGroups()).to.deep.equal({}) }) - it('does not invoke parse with an error if reset has been called', function (done) { + it('does not invoke parse with an error if reset has been called and option is not global', function (done) { var y = yargs() .demand('cake') + .global('cake', false) y.parse('hello', function (err) { err.message.should.match(/Missing required argument/) @@ -1123,6 +1130,7 @@ describe('yargs dsl tests', function () { .config('config', function (path) { return JSON.parse(fs.readFileSync(path)) }) + .global('config', false) .argv argv.foo.should.equal('baz') @@ -1131,7 +1139,8 @@ describe('yargs dsl tests', function () { it('allows key to be specified with option shorthand', function () { var argv = yargs('--config ./test/fixtures/config.json') .option('config', { - config: true + config: true, + global: false }) .argv @@ -1218,7 +1227,8 @@ describe('yargs dsl tests', function () { nargs: 2 }) .option('bar', { - nargs: 2 + nargs: 2, + global: false }) .global('foo') .reset() @@ -1238,7 +1248,8 @@ describe('yargs dsl tests', function () { .option('bar', { nargs: 2, string: true, - demand: true + demand: true, + global: false }) .global('foo') .reset({ @@ -1259,14 +1270,14 @@ describe('yargs dsl tests', function () { var y = yargs('--foo') .help('help') var options = y.getOptions() - options.global.should.include('help') + options.local.should.not.include('help') }) it('should set version to global option by default', function () { var y = yargs('--foo') .version() var options = y.getOptions() - options.global.should.include('version') + options.local.should.not.include('version') }) it('should not reset usage descriptions of global options', function () { @@ -1274,6 +1285,7 @@ describe('yargs dsl tests', function () { .describe('bar', 'my awesome bar option') .describe('foo', 'my awesome foo option') .global('foo') + .global('bar', false) .reset() var descriptions = y.getUsageInstance().getDescriptions() Object.keys(descriptions).should.include('foo') @@ -1288,7 +1300,7 @@ describe('yargs dsl tests', function () { .implies({ z: 'w' }) - .global(['x']) + .global(['z'], false) .reset() var implied = y.getValidationInstance().getImplied() Object.keys(implied).should.include('x') @@ -1298,11 +1310,11 @@ describe('yargs dsl tests', function () { it('should expose an options short-hand for declaring global options', function () { var y = yargs('--foo a b c') .option('foo', { - nargs: 2, - global: true + nargs: 2 }) .option('bar', { - nargs: 2 + nargs: 2, + global: false }) .reset() var options = y.getOptions() diff --git a/yargs.js b/yargs.js index bbb3d64c9..6e9ea9093 100644 --- a/yargs.js +++ b/yargs.js @@ -66,23 +66,23 @@ function Yargs (processArgs, cwd, parentRequire) { // logic is used to build a nested command // hierarchy. var tmpOptions = {} - tmpOptions.global = options.global ? options.global : [] + tmpOptions.local = options.local ? options.local : [] tmpOptions.configObjects = options.configObjects ? options.configObjects : [] - // if a key has been set as a global, we - // do not want to reset it or its aliases. - var globalLookup = {} - tmpOptions.global.forEach(function (g) { - globalLookup[g] = true - ;(aliases[g] || []).forEach(function (a) { - globalLookup[a] = true + // if a key has been explicitly set as local, + // we should reset it before passing options to command. + var localLookup = {} + tmpOptions.local.forEach(function (l) { + localLookup[l] = true + ;(aliases[l] || []).forEach(function (a) { + localLookup[a] = true }) }) - // preserve groups containing global keys + // preserve all groups not set to local. preservedGroups = Object.keys(groups).reduce(function (acc, groupName) { var keys = groups[groupName].filter(function (key) { - return key in globalLookup + return !(key in localLookup) }) if (keys.length > 0) { acc[groupName] = keys @@ -104,13 +104,13 @@ function Yargs (processArgs, cwd, parentRequire) { arrayOptions.forEach(function (k) { tmpOptions[k] = (options[k] || []).filter(function (k) { - return globalLookup[k] + return !localLookup[k] }) }) objectOptions.forEach(function (k) { tmpOptions[k] = objFilter(options[k], function (k, v) { - return globalLookup[k] + return !localLookup[k] }) }) @@ -119,12 +119,12 @@ function Yargs (processArgs, cwd, parentRequire) { // if this is the first time being executed, create // instances of all our helpers -- otherwise just reset. - usage = usage ? usage.reset(globalLookup) : Usage(self, y18n) - validation = validation ? validation.reset(globalLookup) : Validation(self, usage, y18n) + usage = usage ? usage.reset(localLookup) : Usage(self, y18n) + validation = validation ? validation.reset(localLookup) : Validation(self, usage, y18n) command = command ? command.reset() : Command(self, usage, validation) if (!completion) completion = Completion(self, usage, command) - strict = false + if (!strictGlobal) strict = false completionCommand = null output = '' exitError = null @@ -172,56 +172,127 @@ function Yargs (processArgs, cwd, parentRequire) { frozen = undefined } - self.boolean = function (bools) { - options.boolean.push.apply(options.boolean, [].concat(bools)) + self.boolean = function (keys) { + populateParserHintArray('boolean', keys) return self } - self.array = function (arrays) { - options.array.push.apply(options.array, [].concat(arrays)) + self.array = function (keys) { + populateParserHintArray('array', keys) return self } - self.nargs = function (key, n) { - if (typeof key === 'object') { - Object.keys(key).forEach(function (k) { - self.nargs(k, key[k]) - }) - } else { - options.narg[key] = n - } + self.number = function (keys) { + populateParserHintArray('number', keys) return self } - self.number = function (numbers) { - options.number.push.apply(options.number, [].concat(numbers)) + self.normalize = function (keys) { + populateParserHintArray('normalize', keys) return self } - self.choices = function (key, values) { - if (typeof key === 'object') { - Object.keys(key).forEach(function (k) { - self.choices(k, key[k]) - }) - } else { - options.choices[key] = (options.choices[key] || []).concat(values) + self.count = function (keys) { + populateParserHintArray('count', keys) + return self + } + + self.string = function (keys) { + populateParserHintArray('string', keys) + return self + } + + self.requiresArg = function (keys) { + populateParserHintArray('requiresArg', keys) + return self + } + + self.skipValidation = function (keys) { + populateParserHintArray('skipValidation', keys) + return self + } + + function populateParserHintArray (type, keys, value) { + keys = [].concat(keys) + keys.forEach(function (key) { + options[type].push(key) + }) + } + + self.nargs = function (key, value) { + populateParserHintObject(self.nargs, false, 'narg', key, value) + return self + } + + self.choices = function (key, value) { + populateParserHintObject(self.choices, true, 'choices', key, value) + return self + } + + self.alias = function (key, value) { + populateParserHintObject(self.alias, true, 'alias', key, value) + return self + } + + // TODO: actually deprecate self.defaults. + self.default = self.defaults = function (key, value, defaultDescription) { + if (defaultDescription) options.defaultDescription[key] = defaultDescription + if (typeof value === 'function') { + if (!options.defaultDescription[key]) options.defaultDescription[key] = usage.functionDescription(value) + value = value.call() } + populateParserHintObject(self.default, false, 'default', key, value) return self } - self.normalize = function (strings) { - options.normalize.push.apply(options.normalize, [].concat(strings)) + self.describe = function (key, desc) { + populateParserHintObject(self.describe, false, 'key', key, true) + usage.describe(key, desc) return self } + self.demandOption = function (keys, msg) { + var value = { msg: typeof msg === 'string' ? msg : undefined } + populateParserHintObject(self.demandOption, false, 'demandedOptions', keys, value) + return self + } + + self.coerce = function (keys, value) { + populateParserHintObject(self.coerce, false, 'coerce', keys, value) + return self + } + + function populateParserHintObject (builder, isArray, type, key, value) { + if (Array.isArray(key)) { + // an array of keys with one value ['x', 'y', 'z'], function parse () {} + var temp = {} + key.forEach(function (k) { + temp[k] = value + }) + builder(temp) + } else if (typeof key === 'object') { + // an object of key value pairs: {'x': parse () {}, 'y': parse() {}} + Object.keys(key).forEach(function (k) { + builder(k, key[k]) + }) + } else { + // a single key value pair 'x', parse() {} + if (isArray) { + options[type][key] = (options[type][key] || []).concat(value) + } else { + options[type][key] = value + } + } + } + self.config = function (key, msg, parseFn) { - // allow to pass a configuration object + // allow a config object to be provided directly. if (typeof key === 'object') { options.configObjects = (options.configObjects || []).concat(key) return self } - // allow to provide a parsing function + // allow for a custom parsing function. if (typeof msg === 'function') { parseFn = msg msg = null @@ -251,59 +322,8 @@ function Yargs (processArgs, cwd, parentRequire) { return self } - self.string = function (strings) { - options.string.push.apply(options.string, [].concat(strings)) - return self - } - - // The 'defaults' alias is deprecated. It will be removed in the next major version. - self.default = self.defaults = function (key, value, defaultDescription) { - if (typeof key === 'object') { - Object.keys(key).forEach(function (k) { - self.default(k, key[k]) - }) - } else { - if (defaultDescription) options.defaultDescription[key] = defaultDescription - if (typeof value === 'function') { - if (!options.defaultDescription[key]) options.defaultDescription[key] = usage.functionDescription(value) - value = value.call() - } - options.default[key] = value - } - return self - } - - self.alias = function (x, y) { - if (typeof x === 'object') { - Object.keys(x).forEach(function (key) { - self.alias(key, x[key]) - }) - } else { - options.alias[x] = (options.alias[x] || []).concat(y) - } - return self - } - - self.coerce = function (key, fn) { - if (typeof key === 'object' && !Array.isArray(key)) { - Object.keys(key).forEach(function (k) { - self.coerce(k, key[k]) - }) - } else { - [].concat(key).forEach(function (k) { - options.coerce[k] = fn - }) - } - return self - } - - self.count = function (counts) { - options.count.push.apply(options.count, [].concat(counts)) - return self - } - - // deprecated: the demand API is too overloaded, and is being - // deprecated in favor of .demandCommand() .demandOption(). + // TODO: deprecate self.demand in favor of + // .demandCommand() .demandOption(). self.demand = self.required = self.require = function (keys, max, msg) { // you can optionally provide a 'max' key, // which will raise an exception if too many '_' @@ -335,29 +355,16 @@ function Yargs (processArgs, cwd, parentRequire) { return self } - self.demandOption = function (key, msg) { - if (Array.isArray(key)) { - key.forEach(function (key) { - self.demandOption(key, msg) - }) - } else { - if (typeof msg === 'string') { - options.demandedOptions[key] = { msg: msg } - // allow edge-case of options: {a: {demand: true}, b: {demand: false}} - } else if (msg === true || typeof msg === 'undefined') { - options.demandedOptions[key] = { msg: undefined } - } - } - - return self - } - self.demandCommand = function (min, max, minMsg, maxMsg) { + if (typeof min === 'undefined') min = 1 + if (typeof max !== 'number') { minMsg = max max = Infinity } + self.global('_', false) + options.demandedCommands._ = { min: min, max: max, @@ -376,16 +383,6 @@ function Yargs (processArgs, cwd, parentRequire) { return options.demandedCommands } - self.requiresArg = function (requiresArgs) { - options.requiresArg.push.apply(options.requiresArg, [].concat(requiresArgs)) - return self - } - - self.skipValidation = function (skipValidations) { - options.skipValidation.push.apply(options.skipValidation, [].concat(skipValidations)) - return self - } - self.implies = function (key, value) { validation.implies(key, value) return self @@ -419,31 +416,27 @@ function Yargs (processArgs, cwd, parentRequire) { return self } - self.check = function (f) { - validation.check(f) + self.check = function (f, _global) { + validation.check(f, _global !== false) return self } - self.describe = function (key, desc) { - if (typeof key === 'object') { - Object.keys(key).forEach(function (k) { - options.key[k] = true + self.global = function (globals, global) { + globals = [].concat(globals) + if (global !== false) { + options.local = options.local.filter(function (l) { + return globals.indexOf(l) === -1 }) } else { - options.key[key] = true + globals.forEach(function (g) { + if (options.local.indexOf(g) === -1) options.local.push(g) + }) } - usage.describe(key, desc) - return self - } - - self.global = function (globals) { - options.global.push.apply(options.global, [].concat(globals)) return self } self.pkgConf = function (key, path) { var conf = null - var obj = pkgUp(path) // If an object exists in the key, add it to options.configObjects @@ -497,7 +490,7 @@ function Yargs (processArgs, cwd, parentRequire) { freeze() if (parseFn) exitProcess = false - var parsed = parseArgs(args, shortCircuit) + var parsed = self._parseArgs(args, shortCircuit) if (parseFn) parseFn(exitError, parsed, output) unfreeze() @@ -568,10 +561,6 @@ function Yargs (processArgs, cwd, parentRequire) { self.group(key, opt.group) } - if (opt.global) { - self.global(key) - } - if (opt.boolean || opt.type === 'boolean') { self.boolean(key) if (opt.alias) self.boolean(opt.alias) @@ -596,6 +585,10 @@ function Yargs (processArgs, cwd, parentRequire) { self.count(key) } + if (typeof opt.global === 'boolean') { + self.global(key, opt.global) + } + if (opt.defaultDescription) { options.defaultDescription[key] = opt.defaultDescription } @@ -623,8 +616,7 @@ function Yargs (processArgs, cwd, parentRequire) { self.group = function (opts, groupName) { var existing = preservedGroups[groupName] || groups[groupName] if (preservedGroups[groupName]) { - // the preserved group will be moved to the set of explicitly declared - // groups + // we now only need to track this group name in groups. delete preservedGroups[groupName] } @@ -654,8 +646,10 @@ function Yargs (processArgs, cwd, parentRequire) { } var strict = false - self.strict = function () { + var strictGlobal = false + self.strict = function (global) { strict = true + strictGlobal = global !== false return self } self.getStrict = function () { @@ -663,7 +657,7 @@ function Yargs (processArgs, cwd, parentRequire) { } self.showHelp = function (level) { - if (!self.parsed) parseArgs(processArgs) // run parser, if it has not already been executed. + if (!self.parsed) self._parseArgs(processArgs) // run parser, if it has not already been executed. usage.showHelp(level) return self } @@ -685,7 +679,6 @@ function Yargs (processArgs, cwd, parentRequire) { usage.version(ver || undefined) self.boolean(versionOpt) - self.global(versionOpt) self.describe(versionOpt, msg) return self } @@ -722,7 +715,6 @@ function Yargs (processArgs, cwd, parentRequire) { // 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 } @@ -866,7 +858,7 @@ function Yargs (processArgs, cwd, parentRequire) { var args = null try { - args = parseArgs(processArgs) + args = self._parseArgs(processArgs) } catch (err) { if (err instanceof YError) usage.fail(err.message, err) else throw err @@ -877,7 +869,10 @@ function Yargs (processArgs, cwd, parentRequire) { enumerable: true }) - function parseArgs (args, shortCircuit) { + self._parseArgs = function (args, shortCircuit, _skipValidation) { + var skipValidation = !!_skipValidation + args = args || processArgs + options.__ = y18n.__ options.configuration = pkgUp()['yargs'] || {} const parsed = Parser.detailed(args, options) @@ -966,8 +961,6 @@ function Yargs (processArgs, cwd, parentRequire) { return setPlaceholderKeys(argv) } - var skipValidation = false - // Handle 'help' and 'version' options Object.keys(argv).forEach(function (key) { if (key === helpOpt && argv[key]) { @@ -993,27 +986,31 @@ function Yargs (processArgs, cwd, parentRequire) { } // If the help or version options where used and exitProcess is false, - // or if explicitly skipped, we won't run validations + // or if explicitly skipped, we won't run validations. if (!skipValidation) { if (parsed.error) throw new YError(parsed.error.message) // if we're executed via bash completion, don't // bother with validation. if (!argv[completion.completionKey]) { - validation.nonOptionCount(argv) - validation.missingArgumentValue(argv) - validation.requiredArguments(argv) - if (strict) validation.unknownArguments(argv, aliases) - validation.customChecks(argv, aliases) - validation.limitedChoices(argv) - validation.implications(argv) - validation.conflicting(argv) + self._runValidation(argv, aliases) } } return setPlaceholderKeys(argv) } + self._runValidation = function (argv, aliases) { + validation.nonOptionCount(argv) + validation.missingArgumentValue(argv) + validation.requiredArguments(argv) + if (strict) validation.unknownArguments(argv, aliases) + validation.customChecks(argv, aliases) + validation.limitedChoices(argv) + validation.implications(argv) + validation.conflicting(argv) + } + function guessLocale () { if (!detectLocale) return