'use strict' /* global context, describe, it, beforeEach, afterEach */ const expect = require('chai').expect const fs = require('fs') const path = require('path') const checkOutput = require('../build/test/helpers/utils').checkOutput const english = require('../locales/en.json') let yargs const { YError } = require('../build/lib/yerror') require('chai').should() const noop = () => {} const implicationsFailedPattern = new RegExp(english['Implications failed:']) describe('yargs dsl tests', () => { const oldProcess = { versions: {} } beforeEach(() => { oldProcess.argv = process.argv oldProcess.defaultApp = process.defaultApp oldProcess.versions.electron = process.versions.electron yargs = require('../') }) afterEach(() => { process.argv = oldProcess.argv process.defaultApp = oldProcess.defaultApp process.versions.electron = oldProcess.versions.electron delete require.cache[require.resolve('../')] }) it('should use bin name for $0, eliminating path', () => { process.argv[1] = '/usr/local/bin/ndm' process.env._ = '/usr/local/bin/ndm' process.execPath = '/usr/local/bin/ndm' const argv = yargs([]).parse() argv.$0.should.equal('ndm') yargs.$0.should.equal('ndm') }) it('should not remove the 1st argument of bundled electron apps', () => { delete require.cache[require.resolve('../')] process.argv = ['/usr/local/bin/app', '-f', 'toto', 'tutu'] process.versions.electron = '10.0.0-nightly.20200211' yargs = require('../') const argv = yargs.parse() argv.should.have.property('f') argv.f.should.equal('toto') argv._.should.deep.equal(['tutu']) }) it('should remove the 1st argument of unbundled electron apps', () => { delete require.cache[require.resolve('../')] process.argv = ['/usr/local/bin/electron', 'app.js', '-f', 'toto', 'tutu'] process.versions.electron = '10.0.0-nightly.20200211' // Same syntax as in electron Object.defineProperty(process, 'defaultApp', { configurable: false, enumerable: true, value: true }) yargs = require('../') const argv = yargs.parse() argv.should.have.property('f') argv.f.should.equal('toto') argv._.should.deep.equal(['tutu']) }) it('accepts an object for aliases', () => { const argv = yargs([]) .alias({ cool: 'cat' }) .default('cool', 33) .parse() argv.cat.should.eql(33) }) it('do not populates argv with placeholder keys for unset options', () => { const argv = yargs([]) .option('cool', {}) .parse() Object.keys(argv).should.not.include('cool') }) it('accepts an object for implies', () => { const r = checkOutput(() => yargs(['--x=33']) .implies({ x: 'y' }) .parse() ) r.errors[2].should.match(implicationsFailedPattern) }) it('accepts an object for describes', () => { const r = checkOutput(() => yargs([]) .describe({ x: 'really cool key' }) .demand('x') .wrap(null) .parse() ) r.errors[0].should.match(/really cool key/) r.result.should.not.have.property('[object Object]') }) it('a function can be provided, to execute when a parsing failure occurs', (done) => { yargs(['--x=33']) .implies({ x: 'y' }) .fail((msg) => { msg.should.match(implicationsFailedPattern) return done() }) .parse() }) it('should set alias to string if option is string', () => { const argv = yargs(['--cat=99']) .options('c', { alias: 'cat', string: true }) .parse() argv.cat.should.eql('99') argv.c.should.eql('99') }) it('should allow a valid choice', () => { const argv = yargs(['--looks=good']) .option('looks', { choices: ['good', 'bad'] }) .parse() argv.looks.should.eql('good') }) it('should ignore a missing array choice with an empty default', () => { const argv = yargs(['--looks', '--looks', 'good']) .option('looks', { type: 'array', default: [], choices: ['good', 'bad'] }) .parse() argv.looks.should.deep.eql(['good']) }) it('should allow defaultDescription to be set with .option()', () => { const optDefaultDescriptions = yargs([]) .option('port', { defaultDescription: '80 for HTTP and 443 for HTTPS' }) .getOptions().defaultDescription optDefaultDescriptions.should.deep.equal({ port: '80 for HTTP and 443 for HTTPS' }) }) it('should not require config object for an option', () => { const r = checkOutput(() => yargs([]) .option('x') .parse() ) expect(r.errors).to.deep.equal([]) }) describe('hide', () => { it('should add the key to hiddenOptions', () => { const options = yargs('') .hide('someKey') .getOptions() options.should.have.property('hiddenOptions') options.hiddenOptions.should.include('someKey') }) }) describe('showHidden', () => { it('should have a default show-hidden private option pre-configured', () => { const options = yargs('').getOptions() options.should.have.property('showHiddenOpt') options.showHiddenOpt.should.eql('show-hidden') }) it('should not have show-hidden as an actual option described by default', () => { const options = yargs('').getOptions() options.key.should.not.have.property('show-hidden') }) it('should set show-hidden option', () => { const options = yargs('') .showHidden() .getOptions() options.key.should.have.property('show-hidden') }) it('should set custom-show-hidden option', () => { const options = yargs('') .showHidden('custom-show-hidden') .getOptions() options.key.should.have.property('custom-show-hidden') options.should.have.property('showHiddenOpt') options.showHiddenOpt.should.eql('custom-show-hidden') }) }) describe('showHelpOnFail', () => { it('should display custom failure message, if string is provided as first argument', () => { const r = checkOutput(() => yargs([]) .showHelpOnFail('pork chop sandwiches') .demand('cat') .parse() ) r.errors[4].should.match(/pork chop sandwiches/) }) it('calling with no arguments should default to displaying help', () => { const r = checkOutput(() => yargs([]) .showHelpOnFail() .demand('cat') .parse() ) r.errors[2].should.match(/required argument/) }) }) describe('exitProcess', () => { describe('when exitProcess is set to false and a failure occurs', () => { it('should throw an exception', () => { checkOutput(() => { expect(() => { yargs([]) .demand('cat') .showHelpOnFail(false) .exitProcess(false) .parse() }).to.throw(/Missing required argument/) }) }) it('should output the errors to stderr once', () => { const r = checkOutput(() => { try { yargs([]) .demand('cat') .showHelpOnFail(false) .exitProcess(false) .parse() } catch (err) { // ignore the error, we only test the output here } }) expect(r.logs).to.deep.equal([]) expect(r.errors).to.deep.equal(['Missing required argument: cat']) }) }) it('should set exit process to true, if no argument provided', () => { const r = checkOutput(() => yargs([]) .demand('cat') .exitProcess() .parse() ) r.exit.should.eql(true) }) }) describe('reset', () => { it('should put yargs back into its initial state', () => { // create a command line with all the things. // so that we can confirm they're reset. const y = yargs(['--help']) .command('foo', 'bar', noop) .default('foo', 'bar') .describe('foo', 'foo variable') .demandCommand(1) .demandOption('foo') .string('foo') .alias('foo', 'bar') .string('foo') .choices('foo', ['bar', 'baz']) .coerce('foo', foo => `${foo}bar`) .implies('foo', 'snuh') .conflicts('qux', 'xyzzy') .group('foo', 'Group:') .exitProcess(false) // defaults to true. .global('foo', false) .global('qux', false) .env('YARGS') .reset() const emptyOptions = { array: [], boolean: ['help', 'version'], string: [], alias: {}, default: {}, key: { help: true, version: true }, narg: {}, defaultDescription: {}, choices: {}, coerce: {}, skipValidation: [], count: [], normalize: [], number: [], config: {}, configObjects: [], envPrefix: 'YARGS', // preserved as global hiddenOptions: [], demandedCommands: {}, demandedOptions: {}, deprecatedOptions: {}, local: [ '_', 'foo', 'qux' ] } expect(y.getOptions()).to.deep.equal(emptyOptions) expect(y.getUsageInstance().getDescriptions()).to.deep.equal({ help: '__yargsString__:Show help', version: '__yargsString__:Show version number' }) expect(y.getValidationInstance().getImplied()).to.deep.equal({}) expect(y.getValidationInstance().getConflicting()).to.deep.equal({}) expect(y.getCommandInstance().getCommandHandlers()).to.deep.equal({}) expect(y.getExitProcess()).to.equal(false) expect(y.getDemandedOptions()).to.deep.equal({}) expect(y.getDemandedCommands()).to.deep.equal({}) expect(y.getGroups()).to.deep.equal({}) }) it('does not invoke parse with an error if reset has been called and option is not global', (done) => { const y = yargs() .demand('cake') .global('cake', false) y.parse('hello', (err) => { err.message.should.match(/Missing required argument/) }) y.reset() y.parse('cake', (err) => { expect(err).to.equal(null) return done() }) }) }) describe('command', () => { it('executes command handler with parsed argv', (done) => { yargs(['blerg']) .command( 'blerg', 'handle blerg things', noop, (argv) => { // we should get the argv from the prior yargs. argv._[0].should.equal('blerg') return done() } ) .exitProcess(false) // defaults to true. .parse() }) it('runs all middleware before reaching the handler', function (done) { yargs(['foo']) .command( 'foo', 'handle foo things', function () {}, function (argv) { // we should get the argv filled with data from the middleware argv._[0].should.equal('foo') argv.hello.should.equal('world') return done() }, [function (argv) { return { hello: 'world' } }] ) .exitProcess(false) // defaults to true. .parse() }) it('recommends a similar command if no command handler is found', () => { const r = checkOutput(() => { yargs(['boat']) .command('goat') .recommendCommands() .parse() }) r.errors[2].should.match(/Did you mean goat/) }) it('does not recommend a similiar command if no similar command exists', () => { const r = checkOutput(() => { yargs(['foo']) .command('nothingSimilar') .recommendCommands() .parse() }) r.logs.length.should.equal(0) }) it('recommends the longest match first', () => { const r = checkOutput(() => { yargs(['boat']) .command('bot') .command('goat') .recommendCommands() .parse() }) r.errors[2].should.match(/Did you mean goat/) }) // see: https://github.com/yargs/yargs/issues/822 it('does not print command recommendation if help message will be shown', (done) => { const parser = yargs() .command('goat') .help() .recommendCommands() parser.parse('boat help', {}, (err, _argv, output) => { if (err) return done(err) output.split('Commands:').length.should.equal(2) return done() }) }) it("skips executing root-level command if builder's help is executed", () => { const r = checkOutput(() => { yargs(['blerg', '-h']) .command( 'blerg', 'handle blerg things', yargs => yargs .command('snuh', 'snuh command') .help('h') .wrap(null), () => { throw Error('should not happen') } ) .help('h') .parse() }) r.logs[0].split('\n').should.deep.equal([ 'usage blerg', '', 'handle blerg things', '', 'Commands:', ' usage blerg snuh snuh command', '', 'Options:', ' --version Show version number [boolean]', ' -h Show help [boolean]' ]) }) it('executes top-level help if no handled command is provided', () => { const r = checkOutput(() => { yargs(['snuh', '-h']) .command('blerg', 'handle blerg things', yargs => yargs .command('snuh', 'snuh command') .help('h') .parse() ) .help('h') .wrap(null) .parse() }) r.logs[0].split('\n').should.deep.equal([ 'usage [command]', '', 'Commands:', ' usage blerg handle blerg things', '', 'Options:', ' --version Show version number [boolean]', ' -h Show help [boolean]' ]) }) it("accepts an object for describing a command's options", () => { const r = checkOutput(() => { yargs(['blerg', '-h']) .command('blerg ', 'handle blerg things', { foo: { default: 99 }, bar: { default: 'hello world' } }) .help('h') .wrap(null) .parse() }) const usageString = r.logs[0] usageString.should.match(/usage blerg /) usageString.should.match(/--foo.*default: 99/) usageString.should.match(/--bar.*default: "hello world"/) }) it("accepts a module with a 'builder' and 'handler' key", () => { const argv = yargs(['blerg', 'bar']) .command('blerg ', 'handle blerg things', require('./fixtures/command')) .parse() argv.banana.should.equal('cool') argv.batman.should.equal('sad') argv.foo.should.equal('bar') global.commandHandlerCalledWith.banana.should.equal('cool') global.commandHandlerCalledWith.batman.should.equal('sad') global.commandHandlerCalledWith.foo.should.equal('bar') delete global.commandHandlerCalledWith }) it("accepts a module with a keys 'command', 'describe', 'builder', and 'handler'", () => { const argv = yargs(['blerg', 'bar']) .command(require('./fixtures/command-module')) .parse() argv.banana.should.equal('cool') argv.batman.should.equal('sad') argv.foo.should.equal('bar') global.commandHandlerCalledWith.banana.should.equal('cool') global.commandHandlerCalledWith.batman.should.equal('sad') global.commandHandlerCalledWith.foo.should.equal('bar') delete global.commandHandlerCalledWith }) it('derives \'command\' string from filename when missing', () => { const argv = yargs('nameless --foo bar') .command(require('./fixtures/cmddir_noname/nameless')) .parse() argv.banana.should.equal('cool') argv.batman.should.equal('sad') argv.foo.should.equal('bar') global.commandHandlerCalledWith.banana.should.equal('cool') global.commandHandlerCalledWith.batman.should.equal('sad') global.commandHandlerCalledWith.foo.should.equal('bar') delete global.commandHandlerCalledWith }) it('throws error for non-module command object missing \'command\' string', () => { expect(() => { yargs.command({ desc: 'A command with no name', builder (yargs) { return yargs }, handler (argv) {} }) }).to.throw(/No command name given for module: {(\n )? desc: 'A command with no name',\n {2}builder: \[Function(: builder)?],\n {2}handler: \[Function(: handler)?](\n| )}/) }) }) describe('terminalWidth', () => { it('returns the maximum width of the terminal', function () { if (process.stdout.isTTY) { yargs.terminalWidth().should.be.gte(0) } else { expect(yargs.terminalWidth()).to.equal(null) } }) }) describe('number', () => { it('accepts number arguments when a number type is specified', () => { const argv = yargs('-w banana') .number('w') .parse() expect(typeof argv.w).to.equal('number') }) it('should expose an options short-hand for numbers', () => { const argv = yargs('-w banana') .option('w', { number: true }) .alias('w', 'x') .parse() expect(typeof argv.w).to.equal('number') expect(typeof argv.x).to.equal('number') }) }) describe('choices', () => { it('accepts an object', () => { const optChoices = yargs([]) .choices({ color: ['red', 'green', 'blue'], stars: [1, 2, 3, 4, 5] }) .choices({ size: ['xl', 'l', 'm', 's', 'xs'] }) .getOptions().choices optChoices.should.deep.equal({ color: ['red', 'green', 'blue'], stars: [1, 2, 3, 4, 5], size: ['xl', 'l', 'm', 's', 'xs'] }) }) it('accepts a string and array', () => { const optChoices = yargs([]) .choices('meat', ['beef', 'chicken', 'pork', 'bison']) .choices('temp', ['rare', 'med-rare', 'med', 'med-well', 'well']) .getOptions().choices optChoices.should.deep.equal({ meat: ['beef', 'chicken', 'pork', 'bison'], temp: ['rare', 'med-rare', 'med', 'med-well', 'well'] }) }) it('accepts a string and single value', () => { const optChoices = yargs([]) .choices('gender', 'male') .choices('gender', 'female') .getOptions().choices optChoices.should.deep.equal({ gender: ['male', 'female'] }) }) }) describe('locale', () => { function loadLocale (locale) { delete require.cache[require.resolve('../')] yargs = require('../') process.env.LC_ALL = locale } it('uses english as a default locale', () => { ['LANGUAGE', 'LC_ALL', 'LANG', 'LC_MESSAGES'].forEach((e) => { delete process.env[e] }) yargs.locale().should.equal('en_US') }) it("detects the operating system's locale", () => { loadLocale('es_ES.UTF-8') yargs.locale().should.equal('es_ES') loadLocale('en_US.UTF-8') }) it("should not detect the OS locale if detectLocale is 'false'", () => { loadLocale('es_ES.UTF-8') const r = checkOutput(() => { yargs(['snuh', '-h']) .command('blerg', 'blerg command') .help('h') .wrap(null) .detectLocale(false) .parse() }) yargs.locale().should.equal('en') yargs.getDetectLocale().should.equal(false) r.logs.join(' ').should.match(/Commands:/) loadLocale('en_US.UTF-8') }) it("allows a locale other than the default 'en' to be specified", () => { const r = checkOutput(() => { yargs(['snuh', '-h']) .command('blerg', 'blerg command') .help('h') .wrap(null) .locale('pirate') .parse() }) r.logs.join(' ').should.match(/Choose yer command:/) }) it('handles a missing locale', () => { loadLocale('zz_ZZ.UTF-8') const r = checkOutput(() => { yargs(['snuh', '-h']) .command('blerg', 'blerg command') .help('h') .wrap(null) .parse() }) yargs.locale().should.equal('zz_ZZ') loadLocale('en_US.UTF-8') r.logs.join(' ').should.match(/Commands:/) }) it('properly translates a region-specific locale file', () => { loadLocale('pt_BR.UTF-8') const r = checkOutput(() => { yargs(['-h']) .help('h') .wrap(null) .parse() }) yargs.locale().should.equal('pt_BR') loadLocale('en_US.UTF-8') r.logs.join(' ').should.match(/Exibe ajuda/) }) it('uses locale string for help option default desc on .locale().help()', () => { const r = checkOutput(() => { yargs(['-h']) .locale('pirate') .help('h') .wrap(null) .parse() }) r.logs.join(' ').should.match(/Parlay this here code of conduct/) }) it('uses locale string for help option default desc on .help().locale()', () => { const r = checkOutput(() => { yargs(['-h']) .help('h') .locale('pirate') .wrap(null) .parse() }) r.logs.join(' ').should.match(/Parlay this here code of conduct/) }) describe('updateLocale', () => { it('allows you to override the default locale strings', () => { const r = checkOutput(() => { yargs(['snuh', '-h']) .command('blerg', 'blerg command') .help('h') .wrap(null) .updateLocale({ 'Commands:': 'COMMANDS!' }) .parse() }) r.logs.join(' ').should.match(/COMMANDS!/) }) it('also works on default option group', () => { const r = checkOutput(() => { yargs(['-h']) .help('h') .wrap(null) .updateLocale({ 'Options:': 'OPTIONS!' }) .parse() }) r.logs.join(' ').should.match(/OPTIONS!/) }) it('allows you to use updateStrings() as an alias for updateLocale()', () => { const r = checkOutput(() => { yargs(['snuh', '-h']) .command('blerg', 'blerg command') .help('h') .wrap(null) .updateStrings({ 'Commands:': '!SDNAMMOC' }) .parse() }) r.logs.join(' ').should.match(/!SDNAMMOC/) }) }) }) describe('env', () => { it('translates no arg as empty prefix (parser applies all env vars)', () => { const options = yargs.env().getOptions() options.envPrefix.should.equal('') }) it('accepts true as a valid prefix (parser applies all env vars)', () => { const options = yargs.env(true).getOptions() options.envPrefix.should.equal(true) }) it('accepts empty string as a valid prefix (parser applies all env vars)', () => { const options = yargs.env('').getOptions() options.envPrefix.should.equal('') }) it('accepts a string prefix', () => { const options = yargs.env('COOL').getOptions() options.envPrefix.should.equal('COOL') }) it('translates false as undefined prefix (disables parsing of env vars)', () => { const options = yargs.env(false).getOptions() expect(options.envPrefix).to.equal(undefined) }) }) describe('parse', () => { it('parses a simple string', () => { const a1 = yargs.parse('-x=2 --foo=bar') const a2 = yargs('-x=2 --foo=bar').parse() a1.x.should.equal(2) a2.x.should.equal(2) a1.foo.should.equal('bar') a2.foo.should.equal('bar') }) it('parses a quoted string', () => { const a1 = yargs.parse('-x=\'marks "the" spot\' --foo "break \'dance\'"') const a2 = yargs('-x=\'marks "the" spot\' --foo "break \'dance\'"').parse() a1.x.should.equal('marks "the" spot') a2.x.should.equal('marks "the" spot') a1.foo.should.equal("break 'dance'") a2.foo.should.equal("break 'dance'") }) it('parses an array', () => { const a1 = yargs.parse(['-x', '99', '--why=hello world']) const a2 = yargs(['-x', '99', '--why=hello world']).parse() a1.x.should.equal(99) a2.x.should.equal(99) a1.why.should.equal('hello world') a2.why.should.equal('hello world') }) it('ignores implicit help command (with short-circuit)', () => { const parsed = yargs.help().parse('help', true) parsed._.should.deep.equal(['help']) }) it('allows an optional context object to be provided', () => { const a1 = yargs.parse('-x=2 --foo=bar', { context: 'look at me go!' }) a1.x.should.equal(2) a1.foo.should.equal('bar') a1.context.should.equal('look at me go!') }) // see https://github.com/yargs/yargs/issues/724 it('overrides parsed value of argv with context object', () => { const a1 = yargs.parse('-x=33', { x: 42 }) a1.x.should.equal(42) }) it('parses process.parse() if no arguments are provided', () => { const r = checkOutput(() => { yargs(['--help']) .command('blerg', 'blerg command') .wrap(null) .parse() }) r.logs[0].should.match(/Commands:[\s\S]*blerg command/) }) it('can be called multiple times with the same behavior', () => { const counter = { foobar: 0 } yargs(['test', 'foobar']) .command( 'test ', 'increases counter', yargs => yargs.positional('name', { aliases: 'n', describe: 'a name', choices: ['foobar'], type: 'string' }), argv => { counter[argv.name]++ } ) .fail((msg) => { expect.fail(undefined, undefined, msg) }) yargs.parse() yargs.parse() yargs.parse() expect(counter.foobar).to.equal(3) }) }) describe('parsed', () => { it('should be false before parsing', () => { const warn = global.console.warn global.console.warn = () => {} yargs.parsed.should.equal(false) global.console.warn = warn }) it('should not be false after parsing', () => { const warn = global.console.warn global.console.warn = () => {} yargs.parse() yargs.parsed.should.not.equal(false) global.console.warn = warn }) }) // yargs.parse(['foo', '--bar'], function (err, argv, output) {} context('function passed as second argument to parse', () => { it('does not print to stdout', () => { const r = checkOutput(() => { yargs() .help('h') .parse('-h', (_err, argv, output) => {}) }) r.logs.length.should.equal(0) r.errors.length.should.equal(0) }) it('gets passed error as first argument', () => { let err = null const r = checkOutput(() => { yargs() .demand('robin') .parse('batman', (_err, argv, output) => { err = _err }) }) r.logs.length.should.equal(0) r.errors.length.should.equal(0) err.should.match(/Missing required argument/) }) it('gets passed argv as second argument', () => { let argv = null const r = checkOutput(() => { yargs() .demand('robin') .parse('batman --foo', (_err, _argv, output) => { argv = _argv }) }) r.logs.length.should.equal(0) r.errors.length.should.equal(0) argv.foo.should.equal(true) }) it('gets passed output as third argument', () => { let output = null const r = checkOutput(() => { yargs() .demand('robin') .help() .parse('--help', (_err, argv, _output) => { output = _output }) }) r.logs.length.should.equal(0) r.errors.length.should.equal(0) output.should.match(/--robin.*\[required]/) }) it('reinstates original exitProcess setting after invocation', () => { let callbackCalled = false const r = checkOutput(() => { yargs .exitProcess(true) .help() .parse('--help', () => { callbackCalled = true yargs.getExitProcess().should.equal(false) }) }) r.logs.length.should.equal(0) r.errors.length.should.equal(0) r.exit.should.equal(false) callbackCalled.should.equal(true) yargs.getExitProcess().should.equal(true) }) it('does not call callback if subsequently called without callback', () => { let callbackCalled = 0 const callback = () => { callbackCalled++ } yargs.help() const r1 = checkOutput(() => { yargs.parse('--help', callback) }) const r2 = checkOutput(() => { yargs.parse('--help') }) callbackCalled.should.equal(1) r1.logs.length.should.equal(0) r1.errors.length.should.equal(0) r1.exit.should.equal(false) r2.exit.should.equal(true) r2.errors.length.should.equal(0) r2.logs[0].should.match(/--help.*Show help.*\[boolean]/) }) it('resets error state between calls to parse', () => { const y = yargs() .demand(2) let err1 = null let out1 = null let argv1 = null y.parse('foo', (err, argv, output) => { err1 = err argv1 = argv out1 = output }) err1.message.should.match(/Not enough non-option arguments/) argv1._.should.include('foo') out1.should.match(/Not enough non-option arguments/) let err2 = null let argv2 = null let out2 = null y.parse('foo bar', (err, argv, output) => { err2 = err argv2 = argv out2 = output }) expect(err2).to.equal(null) argv2._.should.deep.equal([ 'foo', 'bar' ]) expect(out2).to.equal('') }) describe('commands', () => { it('does not invoke command handler if output is populated', () => { let err = null let handlerCalled = false const r = checkOutput(() => { yargs() .command('batman ', 'batman command', noop, () => { handlerCalled = true }) .parse('batman --what', (_err, argv, output) => { err = _err }) }) r.logs.length.should.equal(0) r.errors.length.should.equal(0) err.message.should.match(/Not enough non-option arguments/) handlerCalled.should.equal(false) }) it('invokes command handler normally if no output is populated', () => { let argv = null let output = null const r = checkOutput(() => { yargs() .command('batman ', 'batman command', noop, (_argv) => { argv = _argv }) .parse('batman robin --what', (_err, argv, _output) => { output = _output }) }) r.logs.length.should.equal(0) r.errors.length.should.equal(0) output.should.equal('') argv['api-token'].should.equal('robin') argv.what.should.equal(true) }) it('allows context object to be passed to parse', () => { let argv = null yargs() .command('batman ', 'batman command', noop, (_argv) => { argv = _argv }) .parse('batman robin --what', { state: 'grumpy but rich' }, (_err, argv, _output) => {}) argv.state.should.equal('grumpy but rich') argv['api-token'].should.equal('robin') argv.what.should.equal(true) }) // see: https://github.com/yargs/yargs/issues/671 it('does not fail if context object has cyclical reference', () => { let argv = null const context = { state: 'grumpy but rich' } context.res = context yargs() .command('batman ', 'batman command', noop, (_argv) => { argv = _argv }) .parse('batman robin --what', context, (_err, argv, _output) => {}) argv.state.should.equal('grumpy but rich') argv['api-token'].should.equal('robin') argv.what.should.equal(true) }) it('allows nested sub-commands to be invoked multiple times', () => { const context = { counter: 0 } checkOutput(() => { const parser = yargs() .commandDir('fixtures/cmddir') parser.parse('dream within-a-dream --what', { context }, (_err, argv, _output) => {}) parser.parse('dream within-a-dream --what', { context }, (_err, argv, _output) => {}) parser.parse('dream within-a-dream --what', { context }, (_err, argv, _output) => {}) }) context.counter.should.equal(3) }) it('overwrites the prior context object, when parse is called multiple times', () => { let argv = null const parser = yargs() .command('batman ', 'batman command', noop, (_argv) => {}) parser.parse('batman robin --what', { state: 'grumpy but rich' }, (_err, _argv, _output) => {}) parser.parse('batman robin --what', { state: 'the hero we need' }, (_err, _argv, _output) => { argv = _argv }) argv.state.should.equal('the hero we need') }) it('populates argv appropriately when parse is called multiple times', () => { const parser = yargs() .command('batman ', 'batman command', noop, (_argv) => {}) .command('robin ', 'robin command', noop, (_argv) => {}) let argv1 = null parser.parse('batman abc123', (_err, argv, _output) => { argv1 = argv }) let argv2 = null parser.parse('robin blue', (_err, argv, _output) => { argv2 = argv }) expect(argv1.egg).to.equal(undefined) argv1['api-token'].should.equal('abc123') expect(argv2['api-token']).to.equal(undefined) argv2.egg.should.equal('blue') }) it('populates output appropriately when parse is called multiple times', () => { const parser = yargs() .command('batman ', 'batman command', noop, (_argv) => {}) .command('robin ', 'robin command', noop, (_argv) => {}) .wrap(null) let output1 = null parser.parse('batman help', (_err, _argv, output) => { output1 = output }) let output2 = null parser.parse('robin help', (_err, _argv, output) => { output2 = output }) output1.split('\n').should.deep.equal([ 'ndm batman ', '', 'batman command', '', 'Options:', ' --help Show help [boolean]', ' --version Show version number [boolean]' ]) output2.split('\n').should.deep.equal([ 'ndm robin ', '', 'robin command', '', 'Options:', ' --help Show help [boolean]', ' --version Show version number [boolean]' ]) }) it('resets errors when parse is called multiple times', () => { const parser = yargs() .command('batman ', 'batman command', noop, (_argv) => {}) .command('robin ', 'robin command', noop, (_argv) => {}) .wrap(null) let error1 = null let output1 = null parser.parse('batman', (err, _argv, output) => { error1 = err output1 = output }) let error2 = null let output2 = null parser.parse('robin help', (err, _argv, output) => { error2 = err output2 = output }) error1.message.should.match(/Not enough non-option arguments/) output1.split('\n').should.deep.equal([ 'ndm batman ', '', 'batman command', '', 'Options:', ' --help Show help [boolean]', ' --version Show version number [boolean]', '', 'Not enough non-option arguments: got 0, need at least 1' ]) expect(error2).to.equal(undefined) output2.split('\n').should.deep.equal([ 'ndm robin ', '', 'robin command', '', 'Options:', ' --help Show help [boolean]', ' --version Show version number [boolean]' ]) }) it('preserves top-level config when parse is called multiple times', () => { let x = 'wrong' let err let output // set some top-level, reset-able config const parser = yargs() .demand(1, 'Must call a command') .strict() .wrap(null) .command('one ', 'The one and only command') // first call parse with command, which calls reset parser.parse('one two', (_, argv) => { x = argv.x }) // then call parse without command, which should enforce top-level config parser.parse('', (_err, argv, _output) => { err = _err || {} output = _output || '' }) x.should.equal('two') err.should.have.property('message').and.equal('Must call a command') output.split('\n').should.deep.equal([ 'ndm ', '', 'Commands:', ' ndm one The one and only command', '', 'Options:', ' --help Show help [boolean]', ' --version Show version number [boolean]', '', 'Must call a command' ]) }) }) }) describe('config', () => { it('allows a parsing function to be provided as a second argument', () => { const argv = yargs('--config ./test/fixtures/config.json') .config('config', cfgPath => JSON.parse(fs.readFileSync(cfgPath))) .global('config', false) .parse() argv.foo.should.equal('baz') }) it('allows key to be specified with option shorthand', () => { const argv = yargs('--config ./test/fixtures/config.json') .option('config', { config: true, global: false }) .parse() argv.foo.should.equal('baz') }) it('can be disabled with option shorthand', () => { const argv = yargs('--config ./test/fixtures/config.json') .option('config', { config: false, global: false }) .parse() argv.config.should.equal('./test/fixtures/config.json') }) it('allows to pass a configuration object', () => { const argv = yargs .config({ foo: 1, bar: 2 }) .parse() argv.foo.should.equal(1) argv.bar.should.equal(2) }) describe('extends', () => { it('applies default configurations when given config object', () => { const argv = yargs .config({ extends: './test/fixtures/extends/config_1.json', a: 1 }) .parse() argv.a.should.equal(1) argv.b.should.equal(22) argv.z.should.equal(15) }) it('protects against circular extended configurations', () => { expect(() => { yargs.config({ extends: './test/fixtures/extends/circular_1.json' }) }).to.throw(YError) }) it('handles aboslute paths', () => { const absolutePath = path.join(process.cwd(), 'test', 'fixtures', 'extends', 'config_1.json') const argv = yargs .config({ a: 2, extends: absolutePath }) .parse() argv.a.should.equal(2) argv.b.should.equal(22) argv.z.should.equal(15) }) it('tolerates null prototype config objects', () => { const argv = yargs .config({ __proto__: null, a: 2, extends: './test/fixtures/extends/config_1.json' }) .parse() argv.a.should.equal(2) argv.b.should.equal(22) argv.z.should.equal(15) }) // see: https://www.npmjs.com/package/yargs-test-extends it('allows a module to be extended, rather than a JSON file', () => { const argv = yargs() .config({ a: 2, extends: 'yargs-test-extends' }) .parse() argv.a.should.equal(2) argv.c.should.equal(201) }) it('ignores an extends key that does not look like a path or module', () => { const argv = yargs() .config({ a: 2, extends: 'batman' }) .parse() argv.a.should.equal(2) argv.extends.should.equal('batman') }) it('allows files with .*rc extension to be extended', () => { const argv = yargs() .config({ extends: './test/fixtures/extends/.myrc', a: 3 }) .parse() argv.a.should.equal(3) argv.b.should.equal(22) argv.c.should.equal(201) argv.z.should.equal(15) }) it('deep merges configs when extending when deep-merge-config=true', () => { const argv = yargs() .parserConfiguration({ 'deep-merge-config': true }) .config({ extends: './test/fixtures/extends/config_deep.json', a: { b: 11, d: [5, 2], f: 'no', g: { h: 122 }, i: [1, 2] } }) .parse() argv.a.b.should.equal(11) argv.a.c.should.equal(12) argv.a.d.should.deep.equal([5, 2]) argv.a.e.should.eql([1]) argv.a.f.should.equal('no') argv.a.g.h.should.equal(122) argv.a.i.should.deep.equal([1, 2]) argv.test.yes.should.equal(1) }) it('deep merges multiple configs when extending when deep-merge-config=true', () => { const argv = yargs() .parserConfiguration({ 'deep-merge-config': true }) .config({ extends: './test/fixtures/extends/config_deep_2.json', a: { g: 3 } }) .parse() argv.test.yes.should.equal(1) argv.a.b.should.equal(12) argv.a.c.should.equal(12) argv.a.g.should.equal(3) }) it('does not deep merge objects by default', () => { const argv = yargs() .config({ extends: './test/fixtures/extends/config_deep.json', a: { b: 11, d: [5, 2], f: 'no', g: { h: 122 } } }) .parse() argv.a.b.should.equal(11) argv.a.should.not.have.property('c') argv.a.d.should.deep.equal([5, 2]) argv.a.should.not.have.property('e') argv.a.f.should.equal('no') argv.a.g.h.should.equal(122) }) }) }) describe('normalize', () => { it('normalizes paths passed as arguments', () => { const argv = yargs('--path /foo/bar//baz/asdf/quux/..') .normalize(['path']) .parse() argv.path.should.equal(['', 'foo', 'bar', 'baz', 'asdf'].join(path.sep)) }) it('normalizes path when when it is updated', () => { const argv = yargs('--path /batman') .normalize(['path']) .parse() argv.path = '/foo/bar//baz/asdf/quux/..' argv.path.should.equal(['', 'foo', 'bar', 'baz', 'asdf'].join(path.sep)) }) it('allows key to be specified with option shorthand', () => { const argv = yargs('--path /batman') .option('path', { normalize: true }) .parse() argv.path = '/foo/bar//baz/asdf/quux/..' argv.path.should.equal(['', 'foo', 'bar', 'baz', 'asdf'].join(path.sep)) }) it('can be disabled with option shorthand', () => { const argv = yargs('--path /batman') .option('path', { normalize: false }) .parse() argv.path = 'mongodb://url' argv.path.should.equal('mongodb://url') }) }) describe('narg', () => { it('accepts a key as the first argument and a count as the second', () => { const argv = yargs('--foo a b c') .nargs('foo', 2) .parse() argv.foo.should.deep.equal(['a', 'b']) argv._.should.deep.equal(['c']) }) it('accepts a hash of keys and counts', () => { const argv = yargs('--foo a b c') .nargs({ foo: 2 }) .parse() argv.foo.should.deep.equal(['a', 'b']) argv._.should.deep.equal(['c']) }) it('allows key to be specified with option shorthand', () => { const argv = yargs('--foo a b c') .option('foo', { nargs: 2 }) .parse() argv.foo.should.deep.equal(['a', 'b']) argv._.should.deep.equal(['c']) }) }) describe('global', () => { it('does not reset a global options when reset is called', () => { const y = yargs('--foo a b c') .option('foo', { nargs: 2 }) .option('bar', { nargs: 2, global: false }) .global('foo') .reset() const options = y.getOptions() options.key.foo.should.equal(true) expect(options.key.bar).to.equal(undefined) }) it('does not reset alias of global option', () => { const y = yargs('--foo a b c') .option('foo', { nargs: 2, alias: 'awesome-sauce' }) .string('awesome-sauce') .demand('awesomeSauce') .option('bar', { nargs: 2, string: true, demand: true, global: false }) .global('foo') .reset({ foo: ['awesome-sauce', 'awesomeSauce'] }) const options = y.getOptions() options.key.foo.should.equal(true) options.string.should.include('awesome-sauce') Object.keys(options.demandedOptions).should.include('awesomeSauce') expect(options.key.bar).to.equal(undefined) options.string.should.not.include('bar') Object.keys(options.demandedOptions).should.not.include('bar') }) it('should set help to global option by default', () => { const y = yargs('--foo') .help('help') const options = y.getOptions() options.local.should.not.include('help') }) it('should set version to global option by default', () => { const y = yargs('--foo') .version() const options = y.getOptions() options.local.should.not.include('version') }) it('should not reset usage descriptions of global options', () => { const y = yargs('--foo') .describe('bar', 'my awesome bar option') .describe('foo', 'my awesome foo option') .global('foo') .global('bar', false) .reset() const descriptions = y.getUsageInstance().getDescriptions() Object.keys(descriptions).should.include('foo') Object.keys(descriptions).should.not.include('bar') }) it('should not reset implications of global options', () => { const y = yargs(['--x=33']) .implies({ x: 'y' }) .implies({ z: 'w' }) .global(['z'], false) .reset() const implied = y.getValidationInstance().getImplied() Object.keys(implied).should.include('x') Object.keys(implied).should.not.include('z') }) it('should expose an options short-hand for declaring global options', () => { const y = yargs('--foo a b c') .option('foo', { nargs: 2 }) .option('bar', { nargs: 2, global: false }) .reset() const options = y.getOptions() options.key.foo.should.equal(true) expect(options.key.bar).to.equal(undefined) }) }) describe('pkgConf', () => { it('uses values from package.json', () => { const argv = yargs('--foo a').pkgConf('repository').parse() argv.foo.should.equal('a') argv.type.should.equal('git') }) it('combines yargs defaults with package.json values', () => { const argv = yargs('--foo a') .default('b', 99) .pkgConf('repository') .parse() argv.b.should.equal(99) argv.foo.should.equal('a') argv.type.should.equal('git') }) it('should use value from package.json, if argv value is using default value', () => { const argv = yargs('--foo a') .default('b', 99) .pkgConf('repository') .default('type', 'default') .parse() argv.b.should.equal(99) argv.foo.should.equal('a') argv.type.should.equal('git') }) it('should apply value from config object to all aliases', () => { const argv = yargs('--foo a') .pkgConf('repository') .alias('type', 't') .alias('t', 'u') .parse() argv.foo.should.equal('a') argv.type.should.equal('git') argv.t.should.equal('git') argv.u.should.equal('git') }) it('is cool with a key not existing', () => { const argv = yargs('--foo a') .default('b', 99) .pkgConf('banana') .parse() argv.b.should.equal(99) argv.foo.should.equal('a') expect(argv.type).to.equal(undefined) }) it('allows an alternative cwd to be specified', () => { const argv = yargs('--foo a') .pkgConf('blerg', './test/fixtures') .parse() argv.foo.should.equal('a') argv.dotNotation.should.equal(false) }) it('doesn\'t mess up other pkg lookups when cwd is specified', () => { const r = checkOutput(() => yargs('--version') .pkgConf('repository', './test/fixtures') .version() .parse() ) const options = yargs.getOptions() // assert pkgConf lookup (test/fixtures/package.json) options.configObjects.should.deep.equal([{ type: 'svn' }]) // assert parseArgs and guessVersion lookup (package.json) expect(options.configuration['dot-notation']).to.equal(undefined) r.logs[0].should.not.equal('9.9.9') // breaks when yargs gets to this version }) // see https://github.com/yargs/yargs/issues/485 it('handles an invalid package.json', () => { const argv = yargs('--foo a') .pkgConf('yargs', './test/fixtures/broken-json') .parse() argv.foo.should.equal('a') }) it('should apply default configurations from extended packages', () => { const argv = yargs().pkgConf('foo', 'test/fixtures/extends/packageA').parse() argv.a.should.equal(80) argv.b.should.equals('riffiwobbles') }) it('should apply extended configurations from cwd when no path is given', () => { const argv = yargs('', 'test/fixtures/extends/packageA').pkgConf('foo').parse() argv.a.should.equal(80) argv.b.should.equals('riffiwobbles') }) }) describe('parserConfiguration', () => { it('overrides the default parser configuration', () => { const argv = yargs('--foo.bar 1 --no-baz 2') .parserConfiguration({ 'boolean-negation': false, 'dot-notation': false }) .parse() expect(argv['foo.bar']).to.equal(1) argv.noBaz.should.equal(2) }) it('supports --unknown-options-as-args', () => { const argv = yargs('--foo.bar 1 --no-baz 2') .parserConfiguration({ 'unknown-options-as-args': true }) .parse() argv._.should.deep.eql(['--foo.bar', '1', '--no-baz', '2']) const argv2 = yargs('foo --foo.bar --cool 1 --no-baz 2') .command('foo', 'my foo command', (yargs) => { yargs.boolean('cool') }, () => {}) .parserConfiguration({ 'unknown-options-as-args': true }) .parse() argv2._.should.deep.eql(['foo', '--foo.bar', '1', '--no-baz', '2']) argv2.cool.should.equal(true) }) }) describe('skipValidation', () => { it('skips validation if an option with skipValidation is present', () => { const argv = yargs(['--koala', '--skip']) .demand(1) .fail((msg) => { expect.fail() }) .skipValidation(['skip', 'reallySkip']) .parse() argv.koala.should.equal(true) }) it('does not skip validation if no option with skipValidation is present', (done) => { const argv = yargs(['--koala']) .demand(1) .fail(msg => done()) .skipValidation(['skip', 'reallySkip']) .parse() argv.koala.should.equal(true) }) it('allows key to be specified with option shorthand', () => { const argv = yargs(['--koala', '--skip']) .demand(1) .fail((msg) => { expect.fail() }) .option('skip', { skipValidation: true }) .parse() argv.koala.should.equal(true) }) it('allows having an option that skips validation but not skipping validation if that option is not used', () => { let skippedValidation = true yargs(['--no-skip']) .demand(5) .option('skip', { skipValidation: true }) .fail((msg) => { skippedValidation = false }) .parse() expect(skippedValidation).to.equal(false) }) }) describe('.help()', () => { it('enables `--help` option and `help` command without arguments', () => { const option = checkOutput(() => yargs('--help') .wrap(null) .parse() ) const command = checkOutput(() => yargs('help') .wrap(null) .parse() ) const expected = [ 'Options:', ' --help Show help [boolean]', ' --version Show version number [boolean]' ] option.logs[0].split('\n').should.deep.equal(expected) command.logs[0].split('\n').should.deep.equal(expected) }) it('enables `--help` option and `help` command with `true` argument', () => { const option = checkOutput(() => yargs('--help') .help(true) .wrap(null) .parse() ) const command = checkOutput(() => yargs('help') .help(true) .wrap(null) .parse() ) const expected = [ 'Options:', ' --version Show version number [boolean]', ' --help Show help [boolean]' ] option.logs[0].split('\n').should.deep.equal(expected) command.logs[0].split('\n').should.deep.equal(expected) }) it('enables given string as help option and command with string argument', () => { const option = checkOutput(() => yargs('--info') .help('info') .wrap(null) .parse() ) const command = checkOutput(() => yargs('info') .help('info') .wrap(null) .parse() ) const helpOption = checkOutput(() => yargs('--help') .help('info') .wrap(null) .parse() ) const expected = [ 'Options:', ' --version Show version number [boolean]', ' --info Show help [boolean]' ] option.logs[0].split('\n').should.deep.equal(expected) command.logs[0].split('\n').should.deep.equal(expected) helpOption.result.should.have.property('help').and.equal(true) }) it('enables given string as help option and command with custom description with two string arguments', () => { const option = checkOutput(() => yargs('--info') .help('info', 'Display info') .wrap(null) .parse() ) const command = checkOutput(() => yargs('info') .help('info', 'Display info') .wrap(null) .parse() ) const expected = [ 'Options:', ' --version Show version number [boolean]', ' --info Display info [boolean]' ] option.logs[0].split('\n').should.deep.equal(expected) command.logs[0].split('\n').should.deep.equal(expected) }) it('enables given string as help option and command with custom description with two string arguments and `true` argument', () => { const option = checkOutput(() => yargs('--info') .help('info', 'Display info', true) .wrap(null) .parse() ) const command = checkOutput(() => yargs('info') .help('info', 'Display info', true) .wrap(null) .parse() ) const expected = [ 'Options:', ' --version Show version number [boolean]', ' --info Display info [boolean]' ] option.logs[0].split('\n').should.deep.equal(expected) command.logs[0].split('\n').should.deep.equal(expected) }) }) describe('.help() with .alias()', () => { it('uses multi-char (but not single-char) help alias as command', () => { const info = checkOutput(() => yargs('info') .help().alias('h', 'help').alias('h', 'info') .wrap(null) .parse() ) const h = checkOutput(() => yargs('h') .help().alias('h', 'help').alias('h', 'info') .wrap(null) .parse() ) info.logs[0].split('\n').should.deep.equal([ 'Options:', ' --version Show version number [boolean]', ' -h, --help, --info Show help [boolean]' ]) h.result.should.have.property('_').and.deep.equal(['h']) }) }) describe('.coerce()', () => { it('supports string and function args (as option key and coerce function)', () => { const argv = yargs(['--file', path.join(__dirname, 'fixtures', 'package.json')]) .coerce('file', arg => JSON.parse(fs.readFileSync(arg, 'utf8'))) .parse() expect(argv.file).to.have.property('version').and.equal('9.9.9') }) it('supports object arg (as map of multiple options)', () => { const argv = yargs('--expand abc --range 1..3') .coerce({ expand (arg) { return arg.split('') }, range (arg) { const arr = arg.split('..').map(Number) return { begin: arr[0], end: arr[1] } } }) .parse() expect(argv.expand).to.deep.equal(['a', 'b', 'c']) expect(argv.range).to.have.property('begin').and.equal(1) expect(argv.range).to.have.property('end').and.equal(3) }) it('supports array and function args (as option keys and coerce function)', () => { const argv = yargs(['--src', 'in', '--dest', 'out']) .coerce(['src', 'dest'], arg => path.resolve(arg)) .parse() argv.src.should.match(/in/).and.have.length.above(2) argv.dest.should.match(/out/).and.have.length.above(3) }) it('allows an error to be handled by fail() handler', () => { let msg let err let jsonErrMessage yargs('--json invalid') .coerce('json', (arg) => { try { JSON.parse(arg) } catch (err) { jsonErrMessage = err.message } return JSON.parse(arg) }) .fail((m, e) => { msg = m err = e }) .parse() expect(msg).to.equal(jsonErrMessage) expect(err).to.not.equal(undefined) }) it('supports an option alias', () => { const argv = yargs('-d 2016-08-12') .coerce('date', Date.parse) .alias('date', 'd') .parse() argv.date.should.equal(1470960000000) }) it('supports a global option within command', () => { let regex yargs('check --regex x') .global('regex') .coerce('regex', RegExp) .command('check', 'Check something', {}, (argv) => { regex = argv.regex }) .parse() expect(regex).to.be.an.instanceof(RegExp) regex.toString().should.equal('/x/') }) it('is supported by .option()', () => { const argv = yargs('--env SHELL=/bin/bash') .option('env', { coerce (arg) { const arr = arg.split('=') return { name: arr[0], value: arr[1] || '' } } }) .parse() expect(argv.env).to.have.property('name').and.equal('SHELL') expect(argv.env).to.have.property('value').and.equal('/bin/bash') }) it('supports positional and variadic args for a command', () => { let age let dates yargs('add 30days 2016-06-13 2016-07-18') .command('add [dates..]', 'Testing', yargs => yargs .coerce('age', arg => parseInt(arg, 10) * 86400000) .coerce('dates', arg => arg.map(str => new Date(str))), (argv) => { age = argv.age dates = argv.dates }) .parse() expect(age).to.equal(2592000000) expect(dates).to.have.lengthOf(2) dates[0].toString().should.equal(new Date('2016-06-13').toString()) dates[1].toString().should.equal(new Date('2016-07-18').toString()) }) it('returns camelcase args for a command', () => { let age1 let age2 let dates yargs('add 30days 2016-06-13 2016-07-18') .command('add [dates..]', 'Testing', yargs => yargs .coerce('age-in-days', arg => parseInt(arg, 10) * 86400000) .coerce('dates', arg => arg.map(str => new Date(str))), (argv) => { age1 = argv.ageInDays age2 = argv['age-in-days'] dates = argv.dates }) .parse() expect(age1).to.equal(2592000000) expect(age2).to.equal(2592000000) expect(dates).to.have.lengthOf(2) dates[0].toString().should.equal(new Date('2016-06-13').toString()) dates[1].toString().should.equal(new Date('2016-07-18').toString()) }) it('allows an error from positional arg to be handled by fail() handler', () => { let msg let err yargs('throw ball') .command('throw ', false, yargs => yargs .coerce('msg', (arg) => { throw new Error(arg) }) .fail((m, e) => { msg = m err = e })) .parse() expect(msg).to.equal('ball') expect(err).to.not.equal(undefined) }) }) describe('stop parsing', () => { it('populates argv._ with unparsed arguments after "--"', () => { const argv = yargs.parse('--foo 33 --bar=99 -- --grep=foobar') argv.foo.should.equal(33) argv.bar.should.equal(99) argv._.length.should.equal(1) argv._[0].should.equal('--grep=foobar') }) }) describe('yargs context', () => { beforeEach(() => { delete require.cache[require.resolve('../')] yargs = require('../') }) it('should begin with initial state', () => { const context = yargs.getContext() context.resets.should.equal(0) context.commands.should.deep.equal([]) }) it('should track number of resets', () => { const context = yargs.getContext() yargs.reset() context.resets.should.equal(1) yargs.reset() yargs.reset() context.resets.should.equal(3) }) it('should track commands being executed', () => { let context yargs('one two') .command('one', 'level one', (yargs) => { context = yargs.getContext() context.commands.should.deep.equal(['one']) return yargs.command('two', 'level two', (yargs) => { context.commands.should.deep.equal(['one', 'two']) }, (argv) => { context.commands.should.deep.equal(['one', 'two']) }) }, (argv) => { context.commands.should.deep.equal(['one']) }) .parse() context.commands.should.deep.equal([]) }) }) describe('positional', () => { it('defaults array with no arguments to []', () => { const args = yargs('cmd') .command('cmd [foo..]', 'run the cmd', (yargs) => { yargs.positional('foo', { describe: 'foo positionals' }) }) .parse() args.foo.should.eql([]) }) it('populates array with appropriate arguments', () => { const args = yargs('cmd /tmp/foo/bar a b') .command('cmd [foo..]', 'run the cmd', (yargs) => { yargs .positional('file', { describe: 'the required bit' }) .positional('foo', { describe: 'the variadic bit' }) }) .parse() args.file.should.equal('/tmp/foo/bar') args.foo.should.eql(['a', 'b']) }) it('allows a conflicting argument to be specified', (done) => { yargs() .command('cmd ', 'a command', (yargs) => { yargs.positional('hero', { conflicts: 'conflicting' }) }).parse('cmd batman --conflicting', (err) => { err.message.should.equal( 'Arguments hero and conflicting are mutually exclusive' ) return done() }) }) it('allows a default to be set', () => { const argv = yargs('cmd') .command('cmd [heroes...]', 'a command', (yargs) => { yargs.positional('heroes', { default: ['batman', 'Iron Man'] }) }).parse() argv.heroes.should.eql(['batman', 'Iron Man']) }) it('allows a defaultDescription to be set', () => { const r = checkOutput(() => yargs('cmd --help').wrap(null) .command('cmd [heroes...]', 'a command', (yargs) => { yargs.positional('heroes', { default: ['batman', 'Iron Man'], defaultDescription: 'batman and Iron Man' }) }).parse() ) r.logs.join('\n').split(/\n+/).should.deep.equal([ 'usage cmd [heroes...]', 'a command', 'Positionals:', ' heroes [array] [default: batman and Iron Man]', 'Options:', ' --help Show help [boolean]', ' --version Show version number [boolean]' ]) }) it('allows an implied argument to be specified', (done) => { yargs() .command('cmd ', 'a command', (yargs) => { yargs.positional('hero', { implies: 'universe' }) }).parse('cmd batman', (err) => { err.message.should.match(/hero -> universe/) return done() }) }) it('allows an alias to be provided', () => { const argv = yargs('cmd') .command('cmd [heroes...]', 'a command', (yargs) => { yargs.positional('heroes', { alias: 'do-gooders', default: ['batman', 'robin'] }) }).parse() argv.heroes.should.eql(['batman', 'robin']) argv.doGooders.should.eql(['batman', 'robin']) argv['do-gooders'].should.eql(['batman', 'robin']) }) it('allows normalize to be specified', () => { const argv = yargs('cmd /tmp/awesome/../ /tmp/awesome/b/../') .command('cmd ', 'a command', (yargs) => { yargs.positional('files', { normalize: true }) }).parse() argv.files.should.eql([ '/tmp/'.replace(/\//g, path.sep), '/tmp/awesome/'.replace(/\//g, path.sep) ]) }) it('allows a choices array to be specified', (done) => { yargs() .command('cmd ', 'a command', (yargs) => { yargs.positional('hero', { choices: ['batman', 'Iron Man', 'robin'] }) }).parse('cmd joker', (err) => { err.message.should.match( /Argument: hero, Given: "joker", Choices: "batman"/ ) return done() }) }) it('allows a coerce method to be provided', () => { const argv = yargs('cmd batman') .command('cmd ', 'a command', (yargs) => { yargs.positional('hero', { coerce: function (arg) { return arg.toUpperCase() }, alias: 'do-gooder' }) }).parse() argv.hero.should.equal('BATMAN') argv.doGooder.should.equal('BATMAN') }) it('allows a boolean type to be specified', () => { const argv = yargs('cmd false') .command('cmd [run]', 'a command', (yargs) => { yargs.positional('run', { type: 'boolean' }) }).parse() argv.run.should.equal(false) }) it('allows a number type to be specified', () => { const argv = yargs('cmd nan') .command('cmd [count]', 'a command', (yargs) => { yargs.positional('count', { type: 'number' }) }).parse() isNaN(argv.count).should.equal(true) }) it('allows a string type to be specified', () => { const argv = yargs('cmd 33') .command('cmd [str]', 'a command', (yargs) => { yargs.positional('str', { type: 'string' }) }).parse() argv.str.should.equal('33') }) it('allows positional arguments for subcommands to be configured', () => { const argv = yargs('cmd subcommand 33') .command('cmd', 'a command', (yargs) => { yargs.command('subcommand [str]', 'a subcommand', (yargs) => { yargs.positional('str', { type: 'string' }) }) }).parse() argv.str.should.equal('33') }) it("can only be used as part of a command's builder function", () => { expect(() => { yargs('foo') .positional('foo', { describe: 'I should not work' }) }).to.throw(/\.positional\(\) can only be called/) }) // see: https://github.com/yargs/yargs-parser/pull/110 it('does not parse large scientific notation values, when type string', (done) => { yargs('cmd') .command('cmd deploy ', 'a command', (yargs) => { yargs .version(false) .positional('version', { type: 'string' }) }).parse('cmd deploy 123e123', (err, argv) => { if (err) { return done(err) } argv.version.should.eql('123e123') return done() }) }) }) // see: https://github.com/babel/babel/pull/10733 it('should not fail if command handler freezes object', () => { const argv = yargs() .command('cmd', 'a command', (yargs) => { yargs.parserConfiguration({ 'populate--': true }) }, (argv) => { Object.freeze(argv) argv._.should.eql(['cmd']) argv['--'].should.eql(['foo']) }).parse(['cmd', '--', 'foo']) argv._.should.eql(['cmd', 'foo']) // This should actually not be undefined, once we fix // #1482. argv['--'].should.eql(['foo']) }) describe('onFinishCommand', () => { it('use with promise', (done) => { const result = 'noop-result' let calledTimes = 0 yargs(['noop']).command('noop', 'a noop command', noop, async () => { return result }).onFinishCommand(async (commandResult) => { commandResult.should.eql(result) calledTimes++ }) .parse('noop') setTimeout(() => { calledTimes.should.eql(1) done() }, 5) }) it('use without promise', (done) => { const result = 'noop-result' let calledTimes = 0 yargs(['noop']).command('noop', 'a noop command', noop, () => { return result }).onFinishCommand((commandResult) => { commandResult.should.eql(result) calledTimes++ }) .parse('noop') setTimeout(() => { calledTimes.should.eql(1) done() }, 5) }) }) // See: https://github.com/yargs/yargs/issues/1098 it('should allow array and requires arg to be used in conjunction', () => { const argv = yargs(['-i', 'item1', 'item2', 'item3']) .option('i', { alias: 'items', type: 'array', requiresArg: true }) .argv argv.items.should.eql(['item1', 'item2', 'item3']) argv.i.should.eql(['item1', 'item2', 'item3']) }) // See: https://github.com/yargs/yargs/issues/1570 describe('"nargs" with "array"', () => { it('should not consume more than nargs items', () => { const argv = yargs(['-i', 'item1', 'item2', '-i', 'item3', 'item4']) .option('i', { alias: 'items', type: 'array', nargs: 1 }) .argv argv.items.should.eql(['item1', 'item3']) argv.i.should.eql(['item1', 'item3']) argv._.should.eql(['item2', 'item4']) }) it('should apply nargs with higher precedence than requiresArg: true', () => { const argv = yargs(['-i', 'item1', 'item2', '-i', 'item3', 'item4']) .option('i', { alias: 'items', type: 'array', nargs: 1, requiresArg: true }) .argv argv.items.should.eql(['item1', 'item3']) argv.i.should.eql(['item1', 'item3']) argv._.should.eql(['item2', 'item4']) }) // TODO: make this work with aliases, using a check similar to // checkAllAliases() in yargs-parser. it('should apply nargs with higher precedence than requiresArg()', () => { const argv = yargs(['-i', 'item1', 'item2', '-i', 'item3', 'item4']) .option('items', { alias: 'i', type: 'array', nargs: 1 }) .requiresArg(['items']) .argv argv.items.should.eql(['item1', 'item3']) argv.i.should.eql(['item1', 'item3']) argv._.should.eql(['item2', 'item4']) }) it('should raise error if not enough values follow nargs key', (done) => { yargs .option('i', { alias: 'items', type: 'array', nargs: 1 }) .parse(['-i'], (err) => { err.message.should.match(/Not enough arguments following: i/) return done() }) }) }) // See: https://github.com/nodejs/node/issues/31951 describe('should not pollute the prototype', () => { it('does not pollute, when .parse() is called', function () { yargs.parse(['-f.__proto__.foo', '99', '-x.y.__proto__.bar', '100', '--__proto__', '200']) Object.keys({}.__proto__).length.should.equal(0) // eslint-disable-line expect({}.foo).to.equal(undefined) expect({}.bar).to.equal(undefined) }) it('does not pollute, when .argv is called', function () { yargs(['-f.__proto__.foo', '99', '-x.y.__proto__.bar', '100', '--__proto__', '200']).argv // eslint-disable-line Object.keys({}.__proto__).length.should.equal(0) // eslint-disable-line expect({}.foo).to.equal(undefined) expect({}.bar).to.equal(undefined) }) // TODO(bcoe): due to replacement of __proto__ with ___proto___ parser // hints are not properly applied, we should move to an alternate approach // in the future: it('does not pollute, when options are set', function () { yargs .option('__proto__', { describe: 'pollute pollute', nargs: 33 }) .default('__proto__', { hello: 'world' }) .parse(['--foo']) Object.keys({}.__proto__).length.should.equal(0) // eslint-disable-line }) }) describe('parsing --value as a value in -f=--value and --bar=--value', function () { it('should work in the general case', function () { const argv = yargs(['-f=--item1', 'item2', '--bar=--item3', 'item4']) .argv argv.f.should.eql('--item1') argv.bar.should.eql('--item3') argv._.should.eql(['item2', 'item4']) }) it('should work with array', function () { const argv = yargs(['-f=--item1', 'item2', '--bar=--item3', 'item4']) .array(['f', 'bar']) .argv argv.f.should.eql(['--item1', 'item2']) argv.bar.should.eql(['--item3', 'item4']) argv._.should.eql([]) }) it('should work with nargs', function () { const argv = yargs(['-f=--item1', 'item2', 'item3', '--bar=--item4', 'item5', 'item6']) .option('f', { nargs: 2 }) .option('bar', { nargs: 2 }) .argv argv.f.should.eql(['--item1', 'item2']) argv.bar.should.eql(['--item4', 'item5']) argv._.should.eql(['item3', 'item6']) }) it('should work with both array and nargs', function () { const argv = yargs(['-f=--item1', 'item2', '-f', 'item3', 'item4', '--bar=--item5', 'item6', '--bar', 'item7', 'item8']) .option('f', { alias: 'bar', array: true, nargs: 1 }) .argv argv.f.should.eql(['--item1', 'item3', '--item5', 'item7']) argv.bar.should.eql(['--item1', 'item3', '--item5', 'item7']) argv._.should.eql(['item2', 'item4', 'item6', 'item8']) }) }) it('throws error for unsupported Node.js versions', () => { process.env.YARGS_MIN_NODE_VERSION = '55' delete require.cache[require.resolve('../yargs')] expect(() => { require('../yargs') }).to.throw(/yargs supports a minimum Node.js version of 55/) delete process.env.YARGS_MIN_NODE_VERSION }) })