'use strict' /* global describe, it, beforeEach */ const checkUsage = require('../build/test/helpers/utils').checkOutput const expect = require('chai').expect const english = require('../locales/en.json') let yargs = require('../') require('chai').should() describe('validation tests', () => { beforeEach(() => { yargs.reset() }) describe('implies', () => { const implicationsFailedPattern = new RegExp(english['Implications failed:']) it("fails if '_' populated, and implied argument not set", (done) => { yargs(['cat']) .implies({ 1: 'foo' // 1 arg in _ means --foo is required }) .fail((msg) => { msg.should.match(implicationsFailedPattern) return done() }) .parse() }) it("fails if key implies values in '_', but '_' is not populated", (done) => { yargs(['--foo']) .boolean('foo') .implies({ foo: 1 // --foo means 1 arg in _ is required }) .fail((msg) => { msg.should.match(implicationsFailedPattern) return done() }) .parse() }) it('fails if either implied argument is not set and displays only failed', (done) => { yargs(['-f', '-b']) .implies('f', ['b', 'c']) .fail((msg1) => { yargs(['-f', '-c']) .implies('f', ['b', 'c']) .fail((msg2) => { msg1.should.match(/f -> c/) msg1.should.not.match(/f -> b/) msg2.should.match(/f -> b/) msg2.should.not.match(/f -> c/) return done() }) .parse() }) .parse() }) it("fails if --no-foo's implied argument is not set", (done) => { yargs([]) .implies({ '--no-bar': 'foo' // when --bar is not given, --foo is required }) .fail((msg) => { msg.should.match(implicationsFailedPattern) return done() }) .parse() }) it('fails if a key is set, along with a key that it implies should not be set', (done) => { yargs(['--bar', '--foo']) .implies({ bar: '--no-foo' // --bar means --foo cannot be given }) .fail((msg) => { msg.should.match(implicationsFailedPattern) return done() }) .parse() }) it('fails if implied key (with "no" in the name) is not set', () => { let failCalled = false yargs('--bar') .implies({ bar: 'noFoo' // --bar means --noFoo (or --no-foo with boolean-negation disabled) is required // note that this has nothing to do with --foo }) .fail((msg) => { failCalled = true msg.should.match(implicationsFailedPattern) }) .parse() failCalled.should.equal(true) }) it('doesn\'t fail if implied key (with "no" in the name) is set', () => { let failCalled = false const argv = yargs('--bar --noFoo') .implies({ bar: 'noFoo' // --bar means --noFoo (or --no-foo with boolean-negation disabled) is required // note that this has nothing to do with --foo }) .fail((msg) => { failCalled = true }) .parse() failCalled.should.equal(false) expect(argv.bar).to.equal(true) expect(argv.noFoo).to.equal(true) expect(argv.foo).to.equal(undefined) }) it('fails if implied key (with "no" in the name) is given when it should not', () => { let failCalled = false yargs('--bar --noFoo') .implies({ bar: '--no-noFoo' // --bar means --noFoo (or --no-foo with boolean-negation disabled) cannot be given // note that this has nothing to do with --foo }) .fail((msg) => { failCalled = true msg.should.match(implicationsFailedPattern) }) .parse() failCalled.should.equal(true) }) it('doesn\'t fail if implied key (with "no" in the name) that should not be given is not set', () => { let failCalled = false const argv = yargs('--bar') .implies({ bar: '--no-noFoo' // --bar means --noFoo (or --no-foo with boolean-negation disabled) cannot be given // note that this has nothing to do with --foo }) .fail((msg) => { failCalled = true }) .parse() failCalled.should.equal(false) expect(argv.bar).to.equal(true) expect(argv.noFoo).to.equal(undefined) expect(argv.foo).to.equal(undefined) }) it('allows key to be specified with option shorthand', (done) => { yargs('--bar') .option('bar', { implies: 'foo' }) .fail((msg) => { msg.should.match(implicationsFailedPattern) return done() }) .parse() }) }) describe('conflicts', () => { it('fails if both arguments are supplied', (done) => { yargs(['-f', '-b']) .conflicts('f', 'b') .fail((msg) => { msg.should.equal('Arguments f and b are mutually exclusive') return done() }) .parse() }) it('fails if argument is supplied along with either conflicting argument', (done) => { yargs(['-f', '-b']) .conflicts('f', ['b', 'c']) .fail((msg1) => { yargs(['-f', '-c']) .conflicts('f', ['b', 'c']) .fail((msg2) => { msg1.should.equal('Arguments f and b are mutually exclusive') msg2.should.equal('Arguments f and c are mutually exclusive') return done() }) .parse() }) .parse() }) it('should not fail if no conflicting arguments are provided', () => { yargs(['-b', '-c']) .conflicts('f', ['b', 'c']) .fail((msg) => { expect.fail() }) .parse() }) it('should not fail if argument with conflict is provided, but not the argument it conflicts with', () => { yargs(['command', '-f', '-c']) .command('command') .option('f', { describe: 'a foo' }) .option('b', { describe: 'a bar' }) .conflicts('f', 'b') .fail((msg) => { expect.fail() }) .parse() }) it('should not fail if conflicting argument is provided, without argument with conflict', () => { yargs(['command', '-b', '-c']) .command('command') .option('f', { describe: 'a foo' }) .option('b', { describe: 'a bar' }) .conflicts('f', 'b') .fail((msg) => { expect.fail() }) .parse() }) it('allows an object to be provided defining conflicting option pairs', (done) => { yargs(['-t', '-s']) .conflicts({ c: 'a', s: 't' }) .fail((msg) => { msg.should.equal('Arguments s and t are mutually exclusive') return done() }) .parse() }) it('takes into account aliases when applying conflicts logic', (done) => { yargs(['-t', '-c']) .conflicts({ c: 'a', s: 't' }) .alias('c', 's') .fail((msg) => { msg.should.equal('Arguments s and t are mutually exclusive') return done() }) .parse() }) it('allows key to be specified with option shorthand', (done) => { yargs(['-f', '-b']) .option('f', { conflicts: 'b' }) .fail((msg) => { msg.should.equal('Arguments f and b are mutually exclusive') return done() }) .parse() }) it('should fail if alias of conflicting argument is provided', (done) => { yargs(['-f', '--batman=99']) .conflicts('f', 'b') .alias('b', 'batman') .fail((msg) => { msg.should.equal('Arguments f and b are mutually exclusive') return done() }) .parse() }) it('should fail if alias of argument with conflict is provided', (done) => { yargs(['--foo', '-b']) .conflicts('f', 'b') .alias('foo', 'f') .fail((msg) => { msg.should.equal('Arguments f and b are mutually exclusive') return done() }) .parse() }) function loadLocale (locale) { delete require.cache[require.resolve('../')] yargs = require('../') process.env.LC_ALL = locale } it('should use appropriate translation', done => { loadLocale('pirate') try { yargs(['-f', '-b']) .conflicts('f', 'b') .fail(msg => { msg.should.equal('Yon scurvy dogs f and b be as bad as rum and a prudish wench') return done() }) .parse() } finally { loadLocale('en_US.UTF-8') } }) }) describe('demand', () => { it('fails with standard error message if msg is not defined', (done) => { yargs([]) .demand(1) .fail((msg) => { msg.should.equal('Not enough non-option arguments: got 0, need at least 1') return done() }) .parse() expect.fail('no parsing failure') }) it('fails in strict mode with invalid command', (done) => { yargs(['koala']) .command('wombat', 'wombat burrows') .command('kangaroo', 'kangaroo handlers') .demand(1) .strict() .fail((msg) => { msg.should.equal('Unknown argument: koala') return done() }) .parse() expect.fail('no parsing failure') }) it('fails in strict mode with extra positionals', (done) => { yargs(['kangaroo', 'jumping', 'fast']) .command('kangaroo ', 'kangaroo handlers') .strict() .fail((msg) => { msg.should.equal('Unknown argument: fast') return done() }) .parse() expect.fail('no parsing failure') }) it('fails in strict mode with extra positionals for default command', (done) => { yargs(['jumping', 'fast']) .command('$0 ', 'kangaroo handlers') .strict() .fail((msg) => { msg.should.equal('Unknown argument: fast') return done() }) .parse() expect.fail('no parsing failure') }) it('does not fail in strict mode when no commands configured', () => { const argv = yargs('koala') .demand(1) .strict() .fail((msg) => { expect.fail() }) .parse() argv._[0].should.equal('koala') }) // addresses: https://github.com/yargs/yargs/issues/791 it('should recognize context variables in strict mode', (done) => { yargs() .command('foo ') .strict() .parse('foo 99', { x: 33 }, (err, argv, output) => { expect(err).to.equal(null) expect(output).to.equal('') argv.y.should.equal(99) argv.x.should.equal(33) argv._.should.include('foo') return done() }) }) // addresses: https://github.com/yargs/yargs/issues/791 it('should recognize context variables in strict mode, when running sub-commands', (done) => { yargs() .command('request', 'request command', (yargs) => { yargs .command('get', 'sub-command') .option('y', { describe: 'y inner option' }) }) .strict() .parse('request get --y=22', { x: 33 }, (err, argv, output) => { expect(err).to.equal(null) expect(output).to.equal('') argv.y.should.equal(22) argv.x.should.equal(33) argv._.should.include('request') argv._.should.include('get') return done() }) }) it('fails when a required argument is missing', (done) => { yargs('-w 10 marsupial') .demand(1, ['w', 'b']) .fail((msg) => { msg.should.equal('Missing required argument: b') return done() }) .parse() }) it('fails when required arguments are present, but a command is missing', (done) => { yargs('-w 10 -m wombat') .demand(1, ['w', 'm']) .fail((msg) => { msg.should.equal('Not enough non-option arguments: got 0, need at least 1') return done() }) .parse() }) it('fails without a message if msg is null', (done) => { yargs([]) .demand(1, null) .fail((msg) => { expect(msg).to.equal(null) return done() }) .parse() }) // address regression in: https://github.com/yargs/yargs/pull/740 it('custom failure message should be printed for both min and max constraints', (done) => { yargs(['foo']) .demand(0, 0, 'hey! give me a custom exit message') .fail((msg) => { expect(msg).to.equal('hey! give me a custom exit message') return done() }) .parse() }) it('interprets min relative to command', () => { let failureMsg yargs('lint') .command('lint', 'Lint a file', (yargs) => { yargs.demand(1).fail((msg) => { failureMsg = msg }) }) .parse() expect(failureMsg).to.equal('Not enough non-option arguments: got 0, need at least 1') }) it('interprets max relative to command', () => { let failureMsg yargs('lint one.js two.js') .command('lint', 'Lint a file', (yargs) => { yargs.demand(0, 1).fail((msg) => { failureMsg = msg }) }) .parse() expect(failureMsg).to.equal('Too many non-option arguments: got 2, maximum of 1') }) }) describe('requiresArg', () => { it('fails when a required argument value of type number is missing', (done) => { yargs() .option('w', { type: 'number', requiresArg: true }) .parse('-w', (err, argv, output) => { expect(err).to.not.equal(undefined) expect(err).to.have.property('message', 'Not enough arguments following: w') return done() }) }) it('fails when a required argument value of type string is missing', (done) => { yargs() .option('w', { type: 'string', requiresArg: true }) .parse('-w', (err, argv, output) => { expect(err).to.not.equal(undefined) expect(err).to.have.property('message', 'Not enough arguments following: w') return done() }) }) it('fails when a required argument value of type boolean is missing', (done) => { yargs() .option('w', { type: 'boolean', requiresArg: true }) .parse('-w', (err, argv, output) => { expect(err).to.not.equal(undefined) expect(err).to.have.property('message', 'Not enough arguments following: w') return done() }) }) it('fails when a required argument value of type array is missing', (done) => { yargs() .option('w', { type: 'array', requiresArg: true }) .parse('-w', (err, argv, output) => { expect(err).to.not.equal(undefined) expect(err).to.have.property('message', 'Not enough arguments following: w') return done() }) }) // see: https://github.com/yargs/yargs/issues/1041 it('does not fail if argument with required value is not provided', (done) => { yargs() .option('w', { type: 'number', requiresArg: true }) .command('woo') .parse('', (err, argv, output) => { expect(err).to.equal(null) return done() }) }) it('does not fail if argument with required value is not provided to subcommand', (done) => { yargs() .option('w', { type: 'number', requiresArg: true }) .command('woo') .parse('woo', (err, argv, output) => { expect(err).to.equal(null) return done() }) }) }) describe('choices', () => { it('fails with one invalid value', (done) => { yargs(['--state', 'denial']) .choices('state', ['happy', 'sad', 'hungry']) .fail((msg) => { msg.split('\n').should.deep.equal([ 'Invalid values:', ' Argument: state, Given: "denial", Choices: "happy", "sad", "hungry"' ]) return done() }) .parse() }) it('fails with one valid and one invalid value', (done) => { yargs(['--characters', 'susie', '--characters', 'linus']) .choices('characters', ['calvin', 'hobbes', 'susie', 'moe']) .fail((msg) => { msg.split('\n').should.deep.equal([ 'Invalid values:', ' Argument: characters, Given: "linus", Choices: "calvin", "hobbes", "susie", "moe"' ]) return done() }) .parse() }) it('fails with multiple invalid values for same argument', (done) => { yargs(['--category', 'comedy', '--category', 'drama']) .choices('category', ['animal', 'vegetable', 'mineral']) .fail((msg) => { msg.split('\n').should.deep.equal([ 'Invalid values:', ' Argument: category, Given: "comedy", "drama", Choices: "animal", "vegetable", "mineral"' ]) return done() }) .parse() }) it('fails with case-insensitive value', (done) => { yargs(['--env', 'DEV']) .choices('env', ['dev', 'prd']) .fail((msg) => { msg.split('\n').should.deep.equal([ 'Invalid values:', ' Argument: env, Given: "DEV", Choices: "dev", "prd"' ]) return done() }) .parse() }) it('fails with multiple invalid arguments', (done) => { yargs(['--system', 'osx', '--arch', '64']) .choices('system', ['linux', 'mac', 'windows']) .choices('arch', ['x86', 'x64', 'arm']) .fail((msg) => { msg.split('\n').should.deep.equal([ 'Invalid values:', ' Argument: system, Given: "osx", Choices: "linux", "mac", "windows"', ' Argument: arch, Given: 64, Choices: "x86", "x64", "arm"' ]) return done() }) .parse() }) // addresses: https://github.com/yargs/yargs/issues/849 it('succeeds when demandOption is true and valid choice is provided', (done) => { yargs('one -a 10 marsupial') .command('one', 'level one', (yargs) => { yargs .options({ a: { demandOption: true, choices: [10, 20] } }) }, (argv) => { argv._[0].should.equal('one') argv.a.should.equal(10) return done() }) .fail((msg) => { expect.fail() }) .parse() }) // addresses: https://github.com/yargs/yargs/issues/849 it('fails when demandOption is true and choice is not provided', (done) => { yargs('one -a 10 marsupial') .command('one', 'level one', (yargs) => { yargs .options({ c: { choices: ['1', '2'] } }) .demandOption('c') }, (argv) => { expect.fail() }) .fail((msg) => { msg.should.equal('Missing required argument: c') return done() }) .parse() }) // addresses: https://github.com/yargs/yargs/issues/849 it('succeeds when demandOption is false and no choice is provided', () => { yargs('one') .command('one', 'level one', (yargs) => { yargs .options({ a: { demandOption: false, choices: [10, 20] } }) }, (argv) => { argv._[0].should.equal('one') }) .fail((msg) => { expect.fail() }) .parse() }) // addresses: https://github.com/yargs/yargs/issues/849 it('succeeds when demandOption is not provided and no choice is provided', () => { yargs('one') .command('one', 'level one', (yargs) => { yargs .options({ a: { choices: [10, 20] } }) }, (argv) => { argv._[0].should.equal('one') }) .fail((msg) => { expect.fail() }) .parse() }) }) describe('config', () => { it('should raise an appropriate error if JSON file is not found', (done) => { yargs(['--settings', 'fake.json', '--foo', 'bar']) .alias('z', 'zoom') .config('settings') .fail((msg) => { msg.should.eql('Invalid JSON config file: fake.json') return done() }) .parse() }) // see: https://github.com/yargs/yargs/issues/172 it('should not raise an exception if config file is set as default argument value', () => { let fail = false yargs([]) .option('config', { default: 'foo.json' }) .config('config') .fail(() => { fail = true }) .parse() fail.should.equal(false) }) it('should be displayed in the help message', () => { const r = checkUsage(() => yargs(['--help']) .config('settings') .help('help') .version(false) .wrap(null) .parse() ) r.should.have.property('logs').with.length(1) r.logs.join('\n').split(/\n+/).should.deep.equal([ 'Options:', ' --settings Path to JSON config file', ' --help Show help [boolean]' ]) }) it('should be displayed in the help message with its default name', () => { const checkUsage = require('../build/test/helpers/utils').checkOutput const r = checkUsage(() => yargs(['--help']) .config() .help('help') .version(false) .wrap(null) .parse() ) r.should.have.property('logs').with.length(1) r.logs.join('\n').split(/\n+/).should.deep.equal([ 'Options:', ' --config Path to JSON config file', ' --help Show help [boolean]' ]) }) it('should allow help message to be overridden', () => { const checkUsage = require('../build/test/helpers/utils').checkOutput const r = checkUsage(() => yargs(['--help']) .config('settings', 'pork chop sandwiches') .help('help') .version(false) .wrap(null) .parse() ) r.should.have.property('logs').with.length(1) r.logs.join('\n').split(/\n+/).should.deep.equal([ 'Options:', ' --settings pork chop sandwiches', ' --help Show help [boolean]' ]) }) it('outputs an error returned by the parsing function', () => { const checkUsage = require('../build/test/helpers/utils').checkOutput const r = checkUsage(() => yargs(['--settings=./package.json']) .config('settings', 'path to config file', configPath => Error('someone set us up the bomb')) .help('help') .wrap(null) .parse() ) r.errors.join('\n').split(/\n+/).should.deep.equal([ 'Options:', ' --version Show version number [boolean]', ' --settings path to config file', ' --help Show help [boolean]', 'someone set us up the bomb' ]) }) it('outputs an error if thrown by the parsing function', () => { const checkUsage = require('../build/test/helpers/utils').checkOutput const r = checkUsage(() => yargs(['--settings=./package.json']) .config('settings', 'path to config file', (configPath) => { throw Error('someone set us up the bomb') }) .wrap(null) .parse() ) r.errors.join('\n').split(/\n+/).should.deep.equal([ 'Options:', ' --help Show help [boolean]', ' --version Show version number [boolean]', ' --settings path to config file', 'someone set us up the bomb' ]) }) }) describe('defaults', () => { // See https://github.com/chevex/yargs/issues/31 it('should not fail when demanded options with defaults are missing', () => { yargs() .fail((msg) => { throw new Error(msg) }) .option('some-option', { describe: 'some option', demand: true, default: 88 }) .strict() .parse([]) }) }) describe('strict mode', () => { it('does not fail when command with subcommands called', () => { yargs('one') .command('one', 'level one', yargs => yargs .command('twoA', 'level two A') .command('twoB', 'level two B') .strict() .fail((msg) => { expect.fail() }), (argv) => { argv._[0].should.equal('one') }) .parse() }) it('does not fail for hidden options', () => { const args = yargs('--foo hey') .strict() .option('foo', { boolean: true, describe: false }) .fail((msg) => { expect.fail() }) .parse() args.foo.should.equal(true) }) it('does not fail if an alias is provided, rather than option itself', () => { const args = yargs('--cat hey') .strict() .option('foo', { boolean: true, describe: false }) .alias('foo', 'bar') .alias('bar', 'cat') .fail((msg) => { expect.fail() }) .parse() args.cat.should.equal(true) args.foo.should.equal(true) args.bar.should.equal(true) }) it('does not fail when unrecognized option is passed after --', () => { const args = yargs('ahoy ben -- --arrr') .strict() .command('ahoy ', 'piratical courtesy') .option('arrr', { boolean: true, describe: false }) .fail((msg) => { expect.fail(msg) }) .parse() args.matey.should.equal('ben') args._.should.deep.equal(['ahoy', '--arrr']) }) }) describe('demandOption', () => { it('allows an array of options to be demanded', (done) => { yargs('-a 10 marsupial') .demandOption(['a', 'b']) .fail((msg) => { msg.should.equal('Missing required argument: b') return done() }) .parse() }) it('allows demandOption in option shorthand', (done) => { yargs('-a 10 marsupial') .option('c', { demandOption: true }) .fail((msg) => { msg.should.equal('Missing required argument: c') return done() }) .parse() }) }) describe('demandCommand', () => { it('should return a custom failure message when too many non-hyphenated arguments are found after a demand count', () => { const r = checkUsage(() => yargs(['src', 'dest']) .usage('Usage: $0 [x] [y] [z] {OPTIONS} [extra_files...]') .demandCommand(0, 1, 'src and dest files are both required', 'too many arguments are provided') .wrap(null) .help(false) .version(false) .parse() ) r.should.have.property('result') r.should.have.property('logs').with.length(0) r.should.have.property('exit').and.to.equal(true) r.result.should.have.property('_').and.deep.equal(['src', 'dest']) r.errors.join('\n').split(/\n+/).should.deep.equal([ 'Usage: usage [x] [y] [z] {OPTIONS} [extra_files...]', 'too many arguments are provided' ]) }) // see: https://github.com/yargs/yargs/pull/438 it('allows a custom min message to be provided', (done) => { yargs('-a 10 marsupial') .demandCommand(2, 'totes got $0 totes expected $1') .fail((msg) => { msg.should.equal('totes got 1 totes expected 2') return done() }) .parse() }) // see: https://github.com/yargs/yargs/pull/438 it('allows a custom min and max message to be provided', (done) => { yargs('-a 10 marsupial mammal bro') .demandCommand(1, 2, 'totes too few, got $0 totes expected $1', 'totes too many, got $0 totes expected $1') .fail((msg) => { msg.should.equal('totes too many, got 3 totes expected 2') return done() }) .parse() }) it('defaults to demanding 1 command', (done) => { yargs('-a 10') .demandCommand() .fail((msg) => { msg.should.equal('Not enough non-option arguments: got 0, need at least 1') return done() }) .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() }) }) })