From b215fba0ed6e124e5aad6cf22c8d5875661c63a3 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Tue, 8 Sep 2020 21:07:56 -0700 Subject: [PATCH] feat: adds strictOptions() (#1738) --- docs/api.md | 7 +++++ lib/validation.ts | 7 +++-- lib/yargs-factory.ts | 15 ++++++++++ package.json | 2 +- test/validation.cjs | 66 ++++++++++++++++++++++++++++++++------------ 5 files changed, 75 insertions(+), 22 deletions(-) diff --git a/docs/api.md b/docs/api.md index c06f73985..96fb0304b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1518,6 +1518,13 @@ 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. +.strictOptions([enabled=true]) +--------- + +Similar to `.strict()`, except that it only applies to unrecognized options. A +user can still provide arbitrary positional commands, but unknown options +will raise an error. + .string(key) ------------ diff --git a/lib/validation.ts b/lib/validation.ts index 809e4cde3..aefb623b4 100644 --- a/lib/validation.ts +++ b/lib/validation.ts @@ -114,7 +114,7 @@ export function validation (yargs: YargsInstance, usage: UsageInstance, y18n: Y1 } // check for unknown arguments (strict-mode). - self.unknownArguments = function unknownArguments (argv, aliases, positionalMap, isDefaultCommand) { + self.unknownArguments = function unknownArguments (argv, aliases, positionalMap, isDefaultCommand, checkPositionals = true) { const commandKeys = yargs.getCommandInstance().getCommands() const unknown: string[] = [] const currentContext = yargs.getContext() @@ -129,7 +129,7 @@ export function validation (yargs: YargsInstance, usage: UsageInstance, y18n: Y1 } }) - if ((currentContext.commands.length > 0) || (commandKeys.length > 0) || isDefaultCommand) { + if (checkPositionals && ((currentContext.commands.length > 0) || (commandKeys.length > 0) || isDefaultCommand)) { argv._.slice(currentContext.commands.length).forEach((key) => { if (commandKeys.indexOf('' + key) === -1) { unknown.push('' + key) @@ -428,7 +428,8 @@ export interface ValidationInstance { argv: Arguments, aliases: DetailedArguments['aliases'], positionalMap: Dictionary, - isDefaultCommand: boolean + isDefaultCommand: boolean, + checkPositionals?: boolean, ): void unknownCommands(argv: Arguments): boolean } diff --git a/lib/yargs-factory.ts b/lib/yargs-factory.ts index 1f5c1de29..d87087337 100644 --- a/lib/yargs-factory.ts +++ b/lib/yargs-factory.ts @@ -167,6 +167,7 @@ function Yargs (processArgs: string | string[] = [], cwd = shim.process.cwd(), p groups, strict, strictCommands, + strictOptions, completionCommand, output, exitError, @@ -195,6 +196,7 @@ function Yargs (processArgs: string | string[] = [], cwd = shim.process.cwd(), p parsed: self.parsed, strict, strictCommands, + strictOptions, completionCommand, parseFn, parseContext, @@ -950,6 +952,14 @@ function Yargs (processArgs: string | string[] = [], cwd = shim.process.cwd(), p } self.getStrictCommands = () => strictCommands + let strictOptions = false + self.strictOptions = function (enabled) { + argsert('[boolean]', [enabled], arguments.length) + strictOptions = enabled !== false + return self + } + self.getStrictOptions = () => strictOptions + let parserConfig: Configuration = {} self.parserConfiguration = function parserConfiguration (config) { argsert('', [config], arguments.length) @@ -1379,6 +1389,8 @@ function Yargs (processArgs: string | string[] = [], cwd = shim.process.cwd(), p } if (strict && !failedStrictCommands) { validation.unknownArguments(argv, aliases, positionalMap, isDefaultCommand) + } else if (strictOptions) { + validation.unknownArguments(argv, aliases, {}, false, false) } validation.customChecks(argv, aliases) validation.limitedChoices(argv) @@ -1524,6 +1536,7 @@ export interface YargsInstance { getParserConfiguration (): Configuration getStrict (): boolean getStrictCommands (): boolean + getStrictOptions (): boolean getUsageInstance (): UsageInstance getValidationInstance (): ValidationInstance global (keys: string | string[], global?: boolean): YargsInstance @@ -1578,6 +1591,7 @@ export interface YargsInstance { skipValidation (keys: string | string[]): YargsInstance strict (enable?: boolean): YargsInstance strictCommands (enable?: boolean): YargsInstance + strictOptions (enable?: boolean): YargsInstance string (key: string | string []): YargsInstance terminalWidth (): number | null updateStrings (obj: Dictionary): YargsInstance @@ -1711,6 +1725,7 @@ interface FrozenYargsInstance { groups: Dictionary strict: boolean strictCommands: boolean + strictOptions: boolean completionCommand: string | null output: string exitError: YError | string | undefined | null diff --git a/package.json b/package.json index 190aab45f..72e8662ff 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "index.cjs", "helpers.mjs", "index.mjs", - "yargs.mjs", + "yargs.cjs", "build", "locales", "LICENSE", diff --git a/test/validation.cjs b/test/validation.cjs index c131160d6..eb21323ba 100644 --- a/test/validation.cjs +++ b/test/validation.cjs @@ -999,24 +999,6 @@ describe('validation tests', () => { .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() @@ -1040,4 +1022,52 @@ describe('validation tests', () => { .parse() }) }) + + describe('strictOptions', () => { + it('succeeds if option is known and command is unknown', (done) => { + yargs() + .command('foo', 'foo command') + .option('a', { + describe: 'a is for option' + }) + .strictOptions() + .parse('bar -a 10', (err, argv) => { + expect(err).to.equal(null) + argv.a.should.equal(10) + return done() + }) + }) + + it('fails if option is unknown', (done) => { + yargs() + .strictOptions() + .parse('bar -a 10', (err, argv) => { + expect(err).to.match(/Unknown argument: a/) + argv.a.should.equal(10) + return done() + }) + }) + + it('applies strict options when commands are invoked', () => { + yargs() + .strictOptions() + .parse('foo --cool --awesome', (err) => { + expect(err).to.match(/Unknown arguments: cool, awesome/) + }) + }) + + it('allows strict options to be turned off', () => { + const y = yargs() + .strictOptions() + .command('foo', 'foo command', (yargs) => { + yargs.strictOptions(false) + }) + y.parse('foo --cool --awesome', (err) => { + expect(err).to.equal(null) + }) + y.parse('--cool --awesome', (err) => { + expect(err).to.match(/Unknown arguments: cool, awesome/) + }) + }) + }) })