From 1d4cca395a98b395e6318f0505fc73bef8b01350 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Tue, 18 Feb 2020 12:47:29 -0800 Subject: [PATCH] feat: introduces strictCommands() subset of strict mode (#1540) --- docs/api.md | 7 ++++ lib/validation.js | 26 ++++++++++++++ test/validation.js | 87 ++++++++++++++++++++++++++++++++++++++++++++++ yargs.js | 18 +++++++++- 4 files changed, 137 insertions(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index e6659dd79..9f55c0788 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1401,6 +1401,13 @@ corresponding description, will be reported as an error. Unrecognized commands will also be reported as errors. +.strictCommands([enabled=true]) +--------- + +Similar to `.strict()`, except that it only applies to unrecognized commands. A +user can still provide arbitrary options, but unknown positional commands +will raise an error. + .string(key) ------------ diff --git a/lib/validation.js b/lib/validation.js index 35659a356..5af0c724b 100644 --- a/lib/validation.js +++ b/lib/validation.js @@ -120,6 +120,32 @@ module.exports = function validation (yargs, usage, y18n) { } } + self.unknownCommands = function unknownCommands (argv, aliases, positionalMap) { + const commandKeys = yargs.getCommandInstance().getCommands() + const unknown = [] + const currentContext = yargs.getContext() + + if ((currentContext.commands.length > 0) || (commandKeys.length > 0)) { + argv._.slice(currentContext.commands.length).forEach((key) => { + if (commandKeys.indexOf(key) === -1) { + unknown.push(key) + } + }) + } + + if (unknown.length > 0) { + usage.fail(__n( + 'Unknown command: %s', + 'Unknown commands: %s', + unknown.length, + unknown.join(', ') + )) + return true + } else { + return false + } + } + // check for a key that is not an alias, or for which every alias is new, // implying that it was invented by the parser, e.g., during camelization self.isValidAndSomeAliasIsNotNew = function isValidAndSomeAliasIsNotNew (key, aliases) { diff --git a/test/validation.js b/test/validation.js index e9dc0d65a..ebd7d7331 100644 --- a/test/validation.js +++ b/test/validation.js @@ -939,4 +939,91 @@ describe('validation tests', () => { .parse() }) }) + + describe('strictCommands', () => { + it('succeeds in parse if command is known', () => { + const parsed = yargs('foo -a 10') + .strictCommands() + .command('foo', 'foo command') + .parse() + parsed.a.should.equal(10) + parsed._.should.eql(['foo']) + }) + + it('succeeds in parse if top level and inner command are known', () => { + const parsed = yargs('foo bar --cool beans') + .strictCommands() + .command('foo', 'foo command', (yargs) => { + yargs.command('bar') + }) + .parse() + parsed.cool.should.equal('beans') + parsed._.should.eql(['foo', 'bar']) + }) + + it('fails with error if command is unknown', (done) => { + yargs('blerg -a 10') + .strictCommands() + .command('foo', 'foo command') + .fail((msg) => { + msg.should.equal('Unknown command: blerg') + return done() + }) + .parse() + }) + + it('fails with error if inner command is unknown', (done) => { + yargs('foo blarg --cool beans') + .strictCommands() + .command('foo', 'foo command', (yargs) => { + yargs.command('bar') + }) + .fail((msg) => { + msg.should.equal('Unknown command: blarg') + return done() + }) + .parse() + }) + + // TODO(bcoe): consider implementing this behvaior in the next major version of yargs: + // + // // for the special case of yargs.demandCommand() and yargs.demandCommand(1), if + // // yargs has been configured with commands, we automatically enable strictCommands. + // if (commandKeys.length && demandedCommands._ && demandedCommands._.min === 1 && demandedCommands._.max === Infinity) { + // yargs.strictCommands() + // } + // it('enables strict commands if commands used in conjunction with demandCommand', (done) => { + // yargs('blerg -a 10') + // .demandCommand() + // .command('foo', 'foo command') + // .fail((msg) => { + // msg.should.equal('Unknown command: blerg') + // return done() + // }) + // .parse() + // }) + + it('does not apply implicit strictCommands to inner commands', () => { + const parse = yargs('foo blarg --cool beans') + .demandCommand() + .command('foo', 'foo command', (yargs) => { + yargs.command('bar') + }) + .parse() + parse.cool.should.equal('beans') + parse._.should.eql(['foo', 'blarg']) + }) + + it('allows strictCommands to be applied to inner commands', (done) => { + yargs('foo blarg') + .command('foo', 'foo command', (yargs) => { + yargs.command('bar').strictCommands() + }) + .fail((msg) => { + msg.should.equal('Unknown command: blarg') + return done() + }) + .parse() + }) + }) }) diff --git a/yargs.js b/yargs.js index d9a30ac5b..238900f58 100644 --- a/yargs.js +++ b/yargs.js @@ -167,6 +167,7 @@ function Yargs (processArgs, cwd, parentRequire) { validation.freeze() command.freeze() frozen.strict = strict + frozen.strictCommands = strictCommands frozen.completionCommand = completionCommand frozen.output = output frozen.exitError = exitError @@ -190,6 +191,7 @@ function Yargs (processArgs, cwd, parentRequire) { validation.unfreeze() command.unfreeze() strict = frozen.strict + strictCommands = frozen.strictCommands completionCommand = frozen.completionCommand parseFn = frozen.parseFn parseContext = frozen.parseContext @@ -795,6 +797,14 @@ function Yargs (processArgs, cwd, parentRequire) { } self.getStrict = () => strict + let strictCommands = false + self.strictCommands = function (enabled) { + argsert('[boolean]', [enabled], arguments.length) + strictCommands = enabled !== false + return self + } + self.getStrictCommands = () => strictCommands + let parserConfig = {} self.parserConfiguration = function parserConfiguration (config) { argsert('', [config], arguments.length) @@ -1219,7 +1229,13 @@ function Yargs (processArgs, cwd, parentRequire) { if (parseErrors) throw new YError(parseErrors.message) validation.nonOptionCount(argv) validation.requiredArguments(argv) - if (strict) validation.unknownArguments(argv, aliases, positionalMap) + let failedStrictCommands = false + if (strictCommands) { + failedStrictCommands = validation.unknownCommands(argv, aliases, positionalMap) + } + if (strict && !failedStrictCommands) { + validation.unknownArguments(argv, aliases, positionalMap) + } validation.customChecks(argv, aliases) validation.limitedChoices(argv) validation.implications(argv)