Skip to content

Commit

Permalink
feat: introduces strictCommands() subset of strict mode (#1540)
Browse files Browse the repository at this point in the history
  • Loading branch information
bcoe committed Feb 18, 2020
1 parent 4f9fadd commit 1d4cca3
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 1 deletion.
7 changes: 7 additions & 0 deletions docs/api.md
Expand Up @@ -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.

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

Expand Down
26 changes: 26 additions & 0 deletions lib/validation.js
Expand Up @@ -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) {
Expand Down
87 changes: 87 additions & 0 deletions test/validation.js
Expand Up @@ -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()
})
})
})
18 changes: 17 additions & 1 deletion yargs.js
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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('<object>', [config], arguments.length)
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 1d4cca3

Please sign in to comment.