From 8ac84ec23c1d2a224e5d02f397b1042229840517 Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 4 Jan 2021 19:59:19 +1300 Subject: [PATCH] Positional options (#1427) * First cut at optionsBeforeArguments * Different to mix global options and subcommands, and options and arguments. * Different to mix global options and subcommands, and options and arguments. * Add _parseOptionsFollowingArguments * Use allow wording * Another try at naming * Exclude options from special processing, which fixes help * Add help checks for new option configuration * Rename after discovering needed for any positional options * Rework logic to hopefully cope with default commands. * Expand basic tests. Positional options are tricky! * Add first default command tests * Fill out more tests * Add setters, and throw when passThrough without enabling positional * Rename test file * Add TypeScript * Add tests. Fix help handling by making explicit. * Reorder tests * Use usual indentation * Make _enablePositionalOptions inherited to simpify nested commands * Add examples * Add tests for some less common setups * Test the boring true/false parameters * Fix typo * Add new section to README with parsing configuration. * Tweak wording in README --- Readme.md | 36 +- examples/pass-through-options.js | 23 ++ examples/positional-options.js | 27 ++ index.js | 63 ++- tests/command.chain.test.js | 12 + tests/command.positionalOptions.test.js | 515 ++++++++++++++++++++++++ typings/commander-tests.ts | 8 + typings/index.d.ts | 21 + 8 files changed, 702 insertions(+), 3 deletions(-) create mode 100644 examples/pass-through-options.js create mode 100644 examples/positional-options.js create mode 100644 tests/command.positionalOptions.test.js diff --git a/Readme.md b/Readme.md index 476700e4f..4d2d98177 100644 --- a/Readme.md +++ b/Readme.md @@ -35,6 +35,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md) - [Custom event listeners](#custom-event-listeners) - [Bits and pieces](#bits-and-pieces) - [.parse() and .parseAsync()](#parse-and-parseasync) + - [Parsing Configuration](#parsing-configuration) - [Legacy options as properties](#legacy-options-as-properties) - [TypeScript](#typescript) - [createCommand()](#createcommand) @@ -84,7 +85,7 @@ For example `-a -b -p 80` may be written as `-ab -p80` or even `-abp80`. You can use `--` to indicate the end of the options, and any remaining arguments will be used without being interpreted. -Options on the command line are not positional, and can be specified before or after other arguments. +By default options on the command line are not positional, and can be specified before or after other arguments. ### Common option types, boolean and value @@ -684,6 +685,39 @@ program.parse(); // Implicit, and auto-detect electron program.parse(['-f', 'filename'], { from: 'user' }); ``` +### Parsing Configuration + +If the default parsing does not suit your needs, there are some behaviours to support other usage patterns. + +By default program options are recognised before and after subcommands. To only look for program options before subcommands, use `.enablePositionalOptions()`. This lets you use +an option for a different purpose in subcommands. + +Example file: [positional-options.js](./examples/positional-options.js) + +With positional options, the `-b` is a program option in the first line and a subcommand option in the second: + +```sh +program -b subcommand +program subcommand -b +``` + +By default options are recognised before and after command-arguments. To only process options that come +before the command-arguments, use `.passThroughOptions()`. This lets you pass the arguments and following options through to another program +without needing to use `--` to end the option processing. +To use pass through options in a subcommand, the program needs to enable positional options. + +Example file: [pass-through-options.js](./examples/pass-through-options.js) + +With pass through options, the `--port=80` is a program option in the line and passed through as a command-argument in the second: + +```sh +program --port=80 arg +program arg --port=80 +``` + + +By default the option processing shows an error for an unknown option. To have an unknown option treated as an ordinary command-argument and continue looking for options, use `.allowUnknownOption()`. This lets you mix known and unknown options. + ### Legacy options as properties Before Commander 7, the option values were stored as properties on the command. diff --git a/examples/pass-through-options.js b/examples/pass-through-options.js new file mode 100644 index 000000000..af5c8497c --- /dev/null +++ b/examples/pass-through-options.js @@ -0,0 +1,23 @@ +#!/usr/bin/env node + +// const { Command } = require('commander'); // (normal include) +const { Command } = require('../'); // include commander in git clone of commander repo +const program = new Command(); + +program + .arguments(' [args...]') + .passThroughOptions() + .option('-d, --dry-run') + .action((utility, args, options) => { + const action = options.dryRun ? 'Would run' : 'Running'; + console.log(`${action}: ${utility} ${args.join(' ')}`); + }); + +program.parse(); + +// Try the following: +// +// node pass-through-options.js git status +// node pass-through-options.js git --version +// node pass-through-options.js --dry-run git checkout -b new-branch +// node pass-through-options.js git push --dry-run diff --git a/examples/positional-options.js b/examples/positional-options.js new file mode 100644 index 000000000..32edd3918 --- /dev/null +++ b/examples/positional-options.js @@ -0,0 +1,27 @@ +#!/usr/bin/env node + +// const { Command } = require('commander'); // (normal include) +const { Command } = require('../'); // include commander in git clone of commander repo +const program = new Command(); + +program + .enablePositionalOptions() + .option('-p, --progress'); + +program + .command('upload ') + .option('-p, --port ', 'port number', 80) + .action((file, options) => { + if (program.opts().progress) console.log('Starting upload...'); + console.log(`Uploading ${file} to port ${options.port}`); + if (program.opts().progress) console.log('Finished upload'); + }); + +program.parse(); + +// Try the following: +// +// node positional-options.js upload test.js +// node positional-options.js -p upload test.js +// node positional-options.js upload -p 8080 test.js +// node positional-options.js -p upload -p 8080 test.js diff --git a/index.js b/index.js index d946f3ffe..3b3fc664c 100644 --- a/index.js +++ b/index.js @@ -552,6 +552,8 @@ class Command extends EventEmitter { this._combineFlagAndOptionalValue = true; this._description = ''; this._argsDescription = undefined; + this._enablePositionalOptions = false; + this._passThroughOptions = false; // see .configureOutput() for docs this._outputConfiguration = { @@ -633,6 +635,7 @@ class Command extends EventEmitter { cmd._exitCallback = this._exitCallback; cmd._storeOptionsAsProperties = this._storeOptionsAsProperties; cmd._combineFlagAndOptionalValue = this._combineFlagAndOptionalValue; + cmd._enablePositionalOptions = this._enablePositionalOptions; cmd._executableFile = opts.executableFile || null; // Custom name for executable file, set missing to null to match constructor this.commands.push(cmd); @@ -1133,6 +1136,35 @@ class Command extends EventEmitter { return this; }; + /** + * Enable positional options. Positional means global options are specified before subcommands which lets + * subcommands reuse the same option names, and also enables subcommands to turn on passThroughOptions. + * The default behaviour is non-positional and global options may appear anywhere on the command line. + * + * @param {Boolean} [positional=true] + */ + enablePositionalOptions(positional = true) { + this._enablePositionalOptions = !!positional; + return this; + }; + + /** + * Pass through options that come after command-arguments rather than treat them as command-options, + * so actual command-options come before command-arguments. Turning this on for a subcommand requires + * positional options to have been enabled on the program (parent commands). + * The default behaviour is non-positional and options may appear before or after command-arguments. + * + * @param {Boolean} [passThrough=true] + * for unknown options. + */ + passThroughOptions(passThrough = true) { + this._passThroughOptions = !!passThrough; + if (!!this.parent && passThrough && !this.parent._enablePositionalOptions) { + throw new Error('passThroughOptions can not be used without turning on enablePositionOptions for parent command(s)'); + } + return this; + }; + /** * Whether to store option values as properties on command object, * or store separately (specify false). In both cases the option values can be accessed using .opts(). @@ -1609,11 +1641,38 @@ class Command extends EventEmitter { } } - // looks like an option but unknown, unknowns from here - if (arg.length > 1 && arg[0] === '-') { + // Not a recognised option by this command. + // Might be a command-argument, or subcommand option, or unknown option, or help command or option. + + // An unknown option means further arguments also classified as unknown so can be reprocessed by subcommands. + if (maybeOption(arg)) { dest = unknown; } + // If using positionalOptions, stop processing our options at subcommand. + if ((this._enablePositionalOptions || this._passThroughOptions) && operands.length === 0 && unknown.length === 0) { + if (this._findCommand(arg)) { + operands.push(arg); + if (args.length > 0) unknown.push(...args); + break; + } else if (arg === this._helpCommandName && this._hasImplicitHelpCommand()) { + operands.push(arg); + if (args.length > 0) operands.push(...args); + break; + } else if (this._defaultCommandName) { + unknown.push(arg); + if (args.length > 0) unknown.push(...args); + break; + } + } + + // If using passThroughOptions, stop processing options at first command-argument. + if (this._passThroughOptions) { + dest.push(arg); + if (args.length > 0) dest.push(...args); + break; + } + // add arg dest.push(arg); } diff --git a/tests/command.chain.test.js b/tests/command.chain.test.js index a53fdbb84..4cadbd074 100644 --- a/tests/command.chain.test.js +++ b/tests/command.chain.test.js @@ -141,4 +141,16 @@ describe('Command methods that should return this for chaining', () => { const result = program.configureOutput({ }); expect(result).toBe(program); }); + + test('when call .passThroughOptions() then returns this', () => { + const program = new Command(); + const result = program.passThroughOptions(); + expect(result).toBe(program); + }); + + test('when call .enablePositionalOptions() then returns this', () => { + const program = new Command(); + const result = program.enablePositionalOptions(); + expect(result).toBe(program); + }); }); diff --git a/tests/command.positionalOptions.test.js b/tests/command.positionalOptions.test.js new file mode 100644 index 000000000..ab3c37ec0 --- /dev/null +++ b/tests/command.positionalOptions.test.js @@ -0,0 +1,515 @@ +const commander = require('../'); + +// The changes to parsing for positional options are subtle, and took extra care to work with +// implicit help and default commands. Lots of tests. + +describe('program with passThrough', () => { + function makeProgram() { + const program = new commander.Command(); + program.passThroughOptions(); + program + .option('-d, --debug') + .arguments(''); + return program; + } + + test('when option before command-argument then option parsed', () => { + const program = makeProgram(); + program.parse(['--debug', 'arg'], { from: 'user' }); + expect(program.args).toEqual(['arg']); + expect(program.opts().debug).toBe(true); + }); + + test('when known option after command-argument then option passed through', () => { + const program = makeProgram(); + program.parse(['arg', '--debug'], { from: 'user' }); + expect(program.args).toEqual(['arg', '--debug']); + expect(program.opts().debug).toBeUndefined(); + }); + + test('when unknown option after command-argument then option passed through', () => { + const program = makeProgram(); + program.parse(['arg', '--pass'], { from: 'user' }); + expect(program.args).toEqual(['arg', '--pass']); + }); + + test('when action handler and unknown option after command-argument then option passed through', () => { + const program = makeProgram(); + const mockAction = jest.fn(); + program.action(mockAction); + program.parse(['arg', '--pass'], { from: 'user' }); + expect(mockAction).toHaveBeenCalledWith(['arg', '--pass'], program.opts(), program); + }); + + test('when help option (without command-argument) then help called', () => { + const program = makeProgram(); + const mockHelp = jest.fn(() => ''); + + program + .exitOverride() + .configureHelp({ formatHelp: mockHelp }); + try { + program.parse(['--help'], { from: 'user' }); + } catch (err) { + } + expect(mockHelp).toHaveBeenCalled(); + }); + + test('when help option after command-argument then option passed through', () => { + const program = makeProgram(); + program.parse(['arg', '--help'], { from: 'user' }); + expect(program.args).toEqual(['arg', '--help']); + }); + + test('when version option after command-argument then option passed through', () => { + const program = makeProgram(); + program.version('1.2.3'); + program.parse(['arg', '--version'], { from: 'user' }); + expect(program.args).toEqual(['arg', '--version']); + }); +}); + +// ----------------------------------------------------------- + +describe('program with positionalOptions and subcommand', () => { + function makeProgram() { + const program = new commander.Command(); + program + .enablePositionalOptions() + .option('-s, --shared ') + .arguments(''); + const sub = program + .command('sub') + .arguments('[arg]') + .option('-s, --shared ') + .action(() => {}); // Not used, but normal to have action handler on subcommand. + return { program, sub }; + } + + test('when global option before subcommand then global option parsed', () => { + const { program } = makeProgram(); + program.parse(['--shared', 'program', 'sub'], { from: 'user' }); + expect(program.opts().shared).toEqual('program'); + }); + + test('when shared option after subcommand then parsed by subcommand', () => { + const { program, sub } = makeProgram(); + program.parse(['sub', '--shared', 'local'], { from: 'user' }); + expect(sub.opts().shared).toEqual('local'); + expect(program.opts().shared).toBeUndefined(); + }); + + test('when shared option after subcommand argument then parsed by subcommand', () => { + const { program, sub } = makeProgram(); + program.parse(['sub', 'arg', '--shared', 'local'], { from: 'user' }); + expect(sub.opts().shared).toEqual('local'); + expect(sub.args).toEqual(['arg']); + expect(program.opts().shared).toBeUndefined(); + }); + + test('when shared option before and after subcommand then both parsed', () => { + const { program, sub } = makeProgram(); + program.parse(['--shared', 'program', 'sub', '--shared', 'local'], { from: 'user' }); + expect(program.opts().shared).toEqual('program'); + expect(sub.opts().shared).toEqual('local'); + }); + + test.each([ + [[], 1, 0], + [['sub'], 0, 0], + [['--help'], 1, 0], + [['sub', '--help'], 0, 1], + [['sub', 'foo', '--help'], 0, 1], + [['help'], 1, 0], + [['help', 'sub'], 0, 1] + ])('help: when user args %p then program/sub help called %p/%p', (userArgs, expectProgramHelpCount, expectSubHelpCount) => { + const { program, sub } = makeProgram(); + const mockProgramHelp = jest.fn(); + program + .exitOverride() + .configureHelp({ formatHelp: mockProgramHelp }); + const mockSubHelp = jest.fn(); + sub + .exitOverride() + .configureHelp({ formatHelp: mockSubHelp }); + + try { + program.parse(userArgs, { from: 'user' }); + } catch (err) { + } + expect(mockProgramHelp).toHaveBeenCalledTimes(expectProgramHelpCount); + expect(mockSubHelp).toHaveBeenCalledTimes(expectSubHelpCount); + }); +}); + +// --------------------------------------------------------------- + +describe('program with positionalOptions and default subcommand (called sub)', () => { + function makeProgram() { + const program = new commander.Command(); + program + .enablePositionalOptions() + .option('-s, --shared') + .option('-g, --global') + .arguments(''); + const sub = program + .command('sub', { isDefault: true }) + .arguments('[args...]') + .option('-s, --shared') + .option('-d, --default') + .action(() => {}); // Not used, but normal to have action handler on subcommand. + program.command('another'); // Not used, but normal to have more than one subcommand if have a default. + return { program, sub }; + } + + test('when program option before sub option then program option read by program', () => { + const { program } = makeProgram(); + program.parse(['--global', '--default'], { from: 'user' }); + expect(program.opts().global).toBe(true); + }); + + test('when program option before sub option then sub option read by sub', () => { + const { program, sub } = makeProgram(); + program.parse(['--global', '--default'], { from: 'user' }); + expect(sub.opts().default).toBe(true); + }); + + test('when shared option before sub argument then option read by program', () => { + const { program } = makeProgram(); + program.parse(['--shared', 'foo'], { from: 'user' }); + expect(program.opts().shared).toBe(true); + }); + + test('when shared option after sub argument then option read by sub', () => { + const { program, sub } = makeProgram(); + program.parse(['foo', '--shared'], { from: 'user' }); + expect(sub.opts().shared).toBe(true); + }); + + test.each([ + [[], 0, 0], + [['--help'], 1, 0], + [['help'], 1, 0] + ])('help: when user args %p then program/sub help called %p/%p', (userArgs, expectProgramHelpCount, expectSubHelpCount) => { + const { program, sub } = makeProgram(); + const mockProgramHelp = jest.fn(); + program + .exitOverride() + .configureHelp({ formatHelp: mockProgramHelp }); + const mockSubHelp = jest.fn(); + sub + .exitOverride() + .configureHelp({ formatHelp: mockSubHelp }); + + try { + program.parse(userArgs, { from: 'user' }); + } catch (err) { + } + expect(mockProgramHelp).toHaveBeenCalledTimes(expectProgramHelpCount); + expect(mockSubHelp).toHaveBeenCalledTimes(expectSubHelpCount); + }); +}); + +// ------------------------------------------------------------------------------ + +describe('subcommand with passThrough', () => { + function makeProgram() { + const program = new commander.Command(); + program + .enablePositionalOptions() + .option('-s, --shared ') + .arguments(''); + const sub = program + .command('sub') + .passThroughOptions() + .arguments('[args...]') + .option('-s, --shared ') + .option('-d, --debug') + .action(() => {}); // Not used, but normal to have action handler on subcommand. + return { program, sub }; + } + + test('when option before command-argument then option parsed', () => { + const { program, sub } = makeProgram(); + program.parse(['sub', '--debug', 'arg'], { from: 'user' }); + expect(sub.args).toEqual(['arg']); + expect(sub.opts().debug).toBe(true); + }); + + test('when known option after command-argument then option passed through', () => { + const { program, sub } = makeProgram(); + program.parse(['sub', 'arg', '--debug'], { from: 'user' }); + expect(sub.args).toEqual(['arg', '--debug']); + expect(sub.opts().debug).toBeUndefined(); + }); + + test('when unknown option after command-argument then option passed through', () => { + const { program, sub } = makeProgram(); + program.parse(['sub', 'arg', '--pass'], { from: 'user' }); + expect(sub.args).toEqual(['arg', '--pass']); + }); + + test('when action handler and unknown option after command-argument then option passed through', () => { + const { program, sub } = makeProgram(); + const mockAction = jest.fn(); + sub.action(mockAction); + program.parse(['sub', 'arg', '--pass'], { from: 'user' }); + expect(mockAction).toHaveBeenCalledWith(['arg', '--pass'], sub.opts(), sub); + }); + + test('when help option after command-argument then option passed through', () => { + const { program, sub } = makeProgram(); + program.parse(['sub', 'arg', '--help'], { from: 'user' }); + expect(sub.args).toEqual(['arg', '--help']); + }); + + test('when version option after command-argument then option passed through', () => { + const { program, sub } = makeProgram(); + program.version('1.2.3'); + program.parse(['sub', 'arg', '--version'], { from: 'user' }); + expect(sub.args).toEqual(['arg', '--version']); + }); + + test('when shared option before sub and after sub and after sub parameter then all three parsed', () => { + const { program, sub } = makeProgram(); + program.parse(['--shared=global', 'sub', '--shared=local', 'arg', '--shared'], { from: 'user' }); + expect(program.opts().shared).toEqual('global'); + expect(sub.opts().shared).toEqual('local'); + expect(sub.args).toEqual(['arg', '--shared']); + }); +}); + +// ------------------------------------------------------------------------------ + +describe('default command with passThrough', () => { + function makeProgram() { + const program = new commander.Command(); + program + .enablePositionalOptions(); + const sub = program + .command('sub', { isDefault: true }) + .passThroughOptions() + .arguments('[args...]') + .option('-d, --debug') + .action(() => {}); // Not used, but normal to have action handler on subcommand. + return { program, sub }; + } + + test('when option before command-argument then option parsed', () => { + const { program, sub } = makeProgram(); + program.parse(['--debug', 'arg'], { from: 'user' }); + expect(sub.args).toEqual(['arg']); + expect(sub.opts().debug).toBe(true); + }); + + test('when known option after command-argument then option passed through', () => { + const { program, sub } = makeProgram(); + program.parse(['arg', '--debug'], { from: 'user' }); + expect(sub.args).toEqual(['arg', '--debug']); + expect(sub.opts().debug).toBeUndefined(); + }); + + test('when unknown option after command-argument then option passed through', () => { + const { program, sub } = makeProgram(); + program.parse(['arg', '--pass'], { from: 'user' }); + expect(sub.args).toEqual(['arg', '--pass']); + }); + + test('when action handler and unknown option after command-argument then option passed through', () => { + const { program, sub } = makeProgram(); + const mockAction = jest.fn(); + sub.action(mockAction); + program.parse(['arg', '--pass'], { from: 'user' }); + expect(mockAction).toHaveBeenCalledWith(['arg', '--pass'], sub.opts(), sub); + }); +}); + +// ------------------------------------------------------------------------------ + +describe('program with action handler and positionalOptions and subcommand', () => { + function makeProgram() { + const program = new commander.Command(); + program + .enablePositionalOptions() + .option('-g, --global') + .arguments('') + .action(() => {}); + const sub = program + .command('sub') + .arguments('[arg]') + .action(() => {}); + return { program, sub }; + } + + test('when global option before parameter then global option parsed', () => { + const { program } = makeProgram(); + program.parse(['--global', 'foo'], { from: 'user' }); + expect(program.opts().global).toBe(true); + }); + + test('when global option after parameter then global option parsed', () => { + const { program } = makeProgram(); + program.parse(['foo', '--global'], { from: 'user' }); + expect(program.opts().global).toBe(true); + }); + + test('when global option after parameter with same name as subcommand then global option parsed', () => { + const { program } = makeProgram(); + program.parse(['foo', 'sub', '--global'], { from: 'user' }); + expect(program.opts().global).toBe(true); + }); +}); + +// ------------------------------------------------------------------------------ + +test('when program not positional and turn on passthrough in subcommand then error', () => { + const program = new commander.Command(); + const sub = program.command('sub'); + + expect(() => { + sub.passThroughOptions(); + }).toThrow(); +}); + +// ------------------------------------------------------------------------------ + +describe('program with action handler and passThrough and subcommand', () => { + function makeProgram() { + const program = new commander.Command(); + program + .passThroughOptions() + .option('-g, --global') + .arguments('') + .action(() => {}); + const sub = program + .command('sub') + .arguments('[arg]') + .option('-g, --group') + .option('-d, --debug') + .action(() => {}); + return { program, sub }; + } + + test('when global option before parameter then global option parsed', () => { + const { program } = makeProgram(); + program.parse(['--global', 'foo'], { from: 'user' }); + expect(program.opts().global).toBe(true); + }); + + test('when global option after parameter then passed through', () => { + const { program } = makeProgram(); + program.parse(['foo', '--global'], { from: 'user' }); + expect(program.args).toEqual(['foo', '--global']); + }); + + test('when subcommand option after subcommand then option parsed', () => { + const { program, sub } = makeProgram(); + program.parse(['sub', '--debug'], { from: 'user' }); + expect(sub.opts().debug).toBe(true); + }); + + // This is somewhat of a side-affect of supporting previous test. + test('when shared option after subcommand then parsed by subcommand', () => { + const { program, sub } = makeProgram(); + program.parse(['sub', '-g'], { from: 'user' }); + expect(sub.opts().group).toBe(true); + expect(program.opts().global).toBeUndefined(); + }); +}); + +// ------------------------------------------------------------------------------ + +describe('program with allowUnknownOption', () => { + test('when passThroughOptions and unknown option then arguments from unknown passed through', () => { + const program = new commander.Command(); + program + .passThroughOptions() + .allowUnknownOption() + .option('--debug'); + + program.parse(['--unknown', '--debug'], { from: 'user' }); + expect(program.args).toEqual(['--unknown', '--debug']); + }); + + test('when positionalOptions and unknown option then known options then known option parsed', () => { + const program = new commander.Command(); + program + .enablePositionalOptions() + .allowUnknownOption() + .option('--debug'); + + program.parse(['--unknown', '--debug'], { from: 'user' }); + expect(program.opts().debug).toBe(true); + expect(program.args).toEqual(['--unknown']); + }); +}); + +// ------------------------------------------------------------------------------ + +describe('passThroughOptions(xxx) and option after command-argument', () => { + function makeProgram() { + const program = new commander.Command(); + program + .option('-d, --debug') + .arguments(''); + return program; + } + + test('when passThroughOptions() then option passed through', () => { + const program = makeProgram(); + program.passThroughOptions(); + program.parse(['foo', '--debug'], { from: 'user' }); + expect(program.args).toEqual(['foo', '--debug']); + }); + + test('when passThroughOptions(true) then option passed through', () => { + const program = makeProgram(); + program.passThroughOptions(true); + program.parse(['foo', '--debug'], { from: 'user' }); + expect(program.args).toEqual(['foo', '--debug']); + }); + + test('when passThroughOptions(false) then option parsed', () => { + const program = makeProgram(); + program.passThroughOptions(false); + program.parse(['foo', '--debug'], { from: 'user' }); + expect(program.args).toEqual(['foo']); + expect(program.opts().debug).toEqual(true); + }); +}); + +// ------------------------------------------------------------------------------ + +describe('enablePositionalOptions(xxx) and shared option after subcommand', () => { + function makeProgram() { + const program = new commander.Command(); + program + .option('-d, --debug'); + const sub = program + .command('sub') + .option('-d, --debug'); + return { program, sub }; + } + + test('when enablePositionalOptions() then option parsed by subcommand', () => { + const { program, sub } = makeProgram(); + program.enablePositionalOptions(); + program.parse(['sub', '--debug'], { from: 'user' }); + expect(sub.opts().debug).toEqual(true); + }); + + test('when enablePositionalOptions(true) then option parsed by subcommand', () => { + const { program, sub } = makeProgram(); + program.enablePositionalOptions(true); + program.parse(['sub', '--debug'], { from: 'user' }); + expect(sub.opts().debug).toEqual(true); + }); + + test('when enablePositionalOptions(false) then option parsed by program', () => { + const { program, sub } = makeProgram(); + program.enablePositionalOptions(false); + program.parse(['sub', '--debug'], { from: 'user' }); + expect(sub.opts().debug).toBeUndefined(); + expect(program.opts().debug).toEqual(true); + }); +}); diff --git a/typings/commander-tests.ts b/typings/commander-tests.ts index 68346a619..7e0e71b8a 100644 --- a/typings/commander-tests.ts +++ b/typings/commander-tests.ts @@ -151,6 +151,14 @@ const allowUnknownOptionThis2: commander.Command = program.allowUnknownOption(fa const allowExcessArgumentsThis1: commander.Command = program.allowExcessArguments(); const allowExcessArgumentsThis2: commander.Command = program.allowExcessArguments(false); +// enablePositionalOptions +const enablePositionalOptionsThis1: commander.Command = program.enablePositionalOptions(); +const enablePositionalOptionsThis2: commander.Command = program.enablePositionalOptions(false); + +// passThroughOptions +const passThroughOptionsThis1: commander.Command = program.passThroughOptions(); +const passThroughOptionsThis2: commander.Command = program.passThroughOptions(false); + // parse const parseThis1: commander.Command = program.parse(); const parseThis2: commander.Command = program.parse(process.argv); diff --git a/typings/index.d.ts b/typings/index.d.ts index ee7e1015a..64f030b82 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -408,6 +408,27 @@ declare namespace commander { */ allowExcessArguments(allowExcess?: boolean): this; + /** + * Enable positional options. Positional means global options are specified before subcommands which lets + * subcommands reuse the same option names, and also enables subcommands to turn on passThroughOptions. + * + * The default behaviour is non-positional and global options may appear anywhere on the command line. + * + * @returns `this` command for chaining + */ + enablePositionalOptions(positional?: boolean): this; + + /** + * Pass through options that come after command-arguments rather than treat them as command-options, + * so actual command-options come before command-arguments. Turning this on for a subcommand requires + * positional options to have been enabled on the program (parent commands). + * + * The default behaviour is non-positional and options may appear before or after command-arguments. + * + * @returns `this` command for chaining + */ + passThroughOptions(passThrough?: boolean): this; + /** * Parse `argv`, setting options and invoking commands when defined. *