From dcb126f47c975d84e977ec4a30c9211434ada8a5 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 2 Feb 2020 18:38:21 +1300 Subject: [PATCH 1/5] Add "from" parameter to .parse() --- index.js | 64 +++++++++++++++++++++++++++++++------ tests/command.parse.test.js | 54 +++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 tests/command.parse.test.js diff --git a/index.js b/index.js index 311781ff0..5dfef76ee 100644 --- a/index.js +++ b/index.js @@ -100,6 +100,8 @@ class Command extends EventEmitter { this.options = []; this._allowUnknownOption = false; this._args = []; + this.rawArgs = null; + this._scriptPath = null; this._name = name || ''; this._optionValues = {}; this._storeOptionsAsProperties = true; // backwards compatible by default @@ -620,19 +622,59 @@ class Command extends EventEmitter { /** * Parse `argv`, setting options and invoking commands when defined. * - * @param {Array} argv + * @param {string[]} [argv] + * @param {Object} [parseOptions] + * @param {string} parseOptions.from - where the args are from: 'node', 'user', 'electron' * @return {Command} for chaining * @api public */ - parse(argv) { - // store raw args - this.rawArgs = argv; + parse(argv, parseOptions) { + if (argv !== undefined && !Array.isArray(argv)) { + throw new Error('first parameter to parse must be array or undefined'); + } + parseOptions = parseOptions || {}; + + // Default to using process.argv + if (argv === undefined) { + argv = process.argv; + if (process.versions && process.versions.electron) { + parseOptions.from = 'electron'; + } + } + this.rawArgs = argv.slice(); + + // make it a little easier for callers by supporting various argv conventions + let userArgs; + switch (parseOptions.from) { + case undefined: + case 'node': + this._scriptPath = argv[1]; + userArgs = argv.slice(2); + break; + case 'electron': + if (process.defaultApp) { + this._scriptPath = argv[1]; + userArgs = argv.slice(2); + } else { + userArgs = argv.slice(1); + } + break; + case 'user': + userArgs = argv.slice(0); + break; + default: + throw new Error(`unexpected parse option { from: '${parseOptions.from}' }`); + } + if (!this._scriptPath && process.mainModule) { + this._scriptPath = process.mainModule.filename; + } // Guess name, used in usage in help. - this._name = this._name || path.basename(argv[1], path.extname(argv[1])); + this._name = this._name || (this._scriptPath && path.basename(this._scriptPath, path.extname(this._scriptPath))); - this._parseCommand([], argv.slice(2)); + // Let's go! + this._parseCommand([], userArgs); return this; }; @@ -642,13 +684,15 @@ class Command extends EventEmitter { * * Use parseAsync instead of parse if any of your action handlers are async. Returns a Promise. * - * @param {Array} argv + * @param {string[]} [argv] + * @param {Object} [parseOptions] + * @param {string} parseOptions.from - where the args are from: 'node', 'user', 'electron' * @return {Promise} * @api public */ - parseAsync(argv) { - this.parse(argv); + parseAsync(argv, parseOptions) { + this.parse(argv, parseOptions); return Promise.all(this._actionResults); }; @@ -667,7 +711,7 @@ class Command extends EventEmitter { this._checkForMissingMandatoryOptions(); // Want the entry script as the reference for command name and directory for searching for other files. - const scriptPath = this.rawArgs[1]; + const scriptPath = this._scriptPath; let baseDir; try { diff --git a/tests/command.parse.test.js b/tests/command.parse.test.js new file mode 100644 index 000000000..cf5bec428 --- /dev/null +++ b/tests/command.parse.test.js @@ -0,0 +1,54 @@ +const commander = require('../'); + +// Testing some Electron conventions but not directly using Electron to avoid overheads. +// https://github.com/electron/electron/issues/4690#issuecomment-217435222 +// https://www.electronjs.org/docs/api/process#processdefaultapp-readonly + +describe('.parse() user args', () => { + test('when no args then use process.argv and app/script/args', () => { + const program = new commander.Command(); + const hold = process.argv; + process.argv = 'node script.js user'.split(' '); + program.parse(); + process.argv = hold; + expect(program.args).toEqual(['user']); + }); + + // implicit also supports detecting electron but more implementation knowledge required than useful to test + + test('when args then app/script/args', () => { + const program = new commander.Command(); + program.parse('node script.js user'.split(' ')); + expect(program.args).toEqual(['user']); + }); + + test('when args from "node" then app/script/args', () => { + const program = new commander.Command(); + program.parse('node script.js user'.split(' '), { from: 'node' }); + expect(program.args).toEqual(['user']); + }); + + test('when args from "electron" and not default app then app/args', () => { + const program = new commander.Command(); + const hold = process.defaultApp; + process.defaultApp = undefined; + program.parse('customApp user'.split(' '), { from: 'electron' }); + process.defaultApp = hold; + expect(program.args).toEqual(['user']); + }); + + test('when args from "electron" and default app then app/script/args', () => { + const program = new commander.Command(); + const hold = process.defaultApp; + process.defaultApp = true; + program.parse('electron script user'.split(' '), { from: 'electron' }); + process.defaultApp = hold; + expect(program.args).toEqual(['user']); + }); + + test('when args from "user" then args', () => { + const program = new commander.Command(); + program.parse('user'.split(' '), { from: 'user' }); + expect(program.args).toEqual(['user']); + }); +}); From adf1c6509c07266db63e7e52cdb912e329040a63 Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 6 Feb 2020 18:36:09 +1300 Subject: [PATCH 2/5] Add docs for ParseOptions --- index.js | 15 +++++++++++++-- typings/commander-tests.ts | 2 ++ typings/index.d.ts | 19 +++++++++++++++++-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 5dfef76ee..889e3b278 100644 --- a/index.js +++ b/index.js @@ -622,8 +622,17 @@ class Command extends EventEmitter { /** * Parse `argv`, setting options and invoking commands when defined. * - * @param {string[]} [argv] - * @param {Object} [parseOptions] + * The default expectation is that the arguments are from node and have the application as argv[0] + * and the script being run in argv[1], with user parameters after that. + * + * Examples: + * + * program.parse(); // implicitly use process.argv and auto-detect node vs electron conventions + * program.parse(process.argv); + * program.parse(my-args, { from: 'user' }); // just user supplied arguments, nothing special about argv[0] + * + * @param {string[]} [argv] - optional, defaults to process.argv + * @param {Object} [parseOptions] - optionally specify style of options with from: node/user/electron * @param {string} parseOptions.from - where the args are from: 'node', 'user', 'electron' * @return {Command} for chaining * @api public @@ -684,6 +693,8 @@ class Command extends EventEmitter { * * Use parseAsync instead of parse if any of your action handlers are async. Returns a Promise. * + * (See .parse for additional details and examples.) + * * @param {string[]} [argv] * @param {Object} [parseOptions] * @param {string} parseOptions.from - where the args are from: 'node', 'user', 'electron' diff --git a/typings/commander-tests.ts b/typings/commander-tests.ts index 521565a87..595b65857 100644 --- a/typings/commander-tests.ts +++ b/typings/commander-tests.ts @@ -145,6 +145,8 @@ program.exitOverride((err):void => { }); program.parse(process.argv); +program.parse(); +program.parse(["foo"], { from: "user" }); program.parseAsync(process.argv).then(() => { console.log('parseAsync success'); diff --git a/typings/index.d.ts b/typings/index.d.ts index e6142bab8..c738f824a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -23,6 +23,10 @@ declare namespace commander { } type OptionConstructor = { new (flags: string, description?: string): Option }; + interface ParseOptions { + from: "node" | "electron" | "user"; + } + interface Command { [key: string]: any; // options as properties @@ -196,19 +200,30 @@ declare namespace commander { /** * Parse `argv`, setting options and invoking commands when defined. + * + * The default expectation is that the arguments are from node and have the application as argv[0] + * and the script being run in argv[1], with user parameters after that. * + * Examples: + * + * program.parse(); // implicitly use process.argv and auto-detect node vs electron conventions + * program.parse(process.argv); + * program.parse(my-args, { from: 'user' }); // just user supplied arguments, nothing special about argv[0] + * * @returns Command for chaining */ - parse(argv: string[]): Command; + parse(argv?: string[], options?: ParseOptions): Command; /** * Parse `argv`, setting options and invoking commands when defined. * * Use parseAsync instead of parse if any of your action handlers are async. Returns a Promise. + * + * (See .parse for additional details and examples.) * * @returns Promise */ - parseAsync(argv: string[]): Promise; + parseAsync(argv?: string[], options?: ParseOptions): Promise; /** * Parse options from `argv` removing known options, From be08a36ee36320581e76013e850d8b9eb60e8858 Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 6 Feb 2020 19:53:19 +1300 Subject: [PATCH 3/5] Add .parse options to README --- Readme.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Readme.md b/Readme.md index 65d8aa7c8..af4302551 100644 --- a/Readme.md +++ b/Readme.md @@ -32,6 +32,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md) - [.help(cb)](#helpcb) - [Custom event listeners](#custom-event-listeners) - [Bits and pieces](#bits-and-pieces) + - [.parse() and .parseAsync()](#parse-and-parseasync) - [Avoiding option name clashes](#avoiding-option-name-clashes) - [TypeScript](#typescript) - [Node options such as `--harmony`](#node-options-such-as---harmony) @@ -598,6 +599,25 @@ program.on('command:*', function (operands) { ## Bits and pieces +### .parse() and .parseAsync() + +The first argument to `.parse` is the array of strings to parse. You may omit the parameter to implicitly use `process.argv`. + +If the arguments follow different conventions than node you can pass a `from` option in the second parameter: + +- 'node': default, `argv[0]` is the application and `argv[1]` is the script being run, with user parameters after that +- 'electron': `argv[1]` varies depending on whether the electron application is packaged +- 'user': all of the arguments from the user + +For example: + +```js +program.parse(process.argv); // Explicit, node conventions +program.parse(); // Implicit, and auto-detect electron +program.parse(['-f', 'filename'], { from: 'user' }); +``` + + ### Avoiding option name clashes The original and default behaviour is that the option values are stored From fb3deab5cfe25ddb153fa714dc8ec929eb775d89 Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 6 Feb 2020 20:14:53 +1300 Subject: [PATCH 4/5] Lint, whitespace --- Readme.md | 1 - 1 file changed, 1 deletion(-) diff --git a/Readme.md b/Readme.md index af4302551..ba7085d7d 100644 --- a/Readme.md +++ b/Readme.md @@ -617,7 +617,6 @@ program.parse(); // Implicit, and auto-detect electron program.parse(['-f', 'filename'], { from: 'user' }); ``` - ### Avoiding option name clashes The original and default behaviour is that the option values are stored From 69286931f486453ee4d97a62e826ef00a9e88d0d Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 7 Feb 2020 12:17:59 +1300 Subject: [PATCH 5/5] Fill in parseAsync docs --- index.js | 11 +++++++++-- typings/index.d.ts | 25 ++++++++++++++++--------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/index.js b/index.js index 889e3b278..2285a0dcc 100644 --- a/index.js +++ b/index.js @@ -627,8 +627,8 @@ class Command extends EventEmitter { * * Examples: * - * program.parse(); // implicitly use process.argv and auto-detect node vs electron conventions * program.parse(process.argv); + * program.parse(); // implicitly use process.argv and auto-detect node vs electron conventions * program.parse(my-args, { from: 'user' }); // just user supplied arguments, nothing special about argv[0] * * @param {string[]} [argv] - optional, defaults to process.argv @@ -693,7 +693,14 @@ class Command extends EventEmitter { * * Use parseAsync instead of parse if any of your action handlers are async. Returns a Promise. * - * (See .parse for additional details and examples.) + * The default expectation is that the arguments are from node and have the application as argv[0] + * and the script being run in argv[1], with user parameters after that. + * + * Examples: + * + * program.parseAsync(process.argv); + * program.parseAsync(); // implicitly use process.argv and auto-detect node vs electron conventions + * program.parseAsync(my-args, { from: 'user' }); // just user supplied arguments, nothing special about argv[0] * * @param {string[]} [argv] * @param {Object} [parseOptions] diff --git a/typings/index.d.ts b/typings/index.d.ts index c738f824a..3ce65ce8c 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -206,8 +206,8 @@ declare namespace commander { * * Examples: * - * program.parse(); // implicitly use process.argv and auto-detect node vs electron conventions * program.parse(process.argv); + * program.parse(); // implicitly use process.argv and auto-detect node vs electron conventions * program.parse(my-args, { from: 'user' }); // just user supplied arguments, nothing special about argv[0] * * @returns Command for chaining @@ -215,14 +215,21 @@ declare namespace commander { parse(argv?: string[], options?: ParseOptions): Command; /** - * Parse `argv`, setting options and invoking commands when defined. - * - * Use parseAsync instead of parse if any of your action handlers are async. Returns a Promise. - * - * (See .parse for additional details and examples.) - * - * @returns Promise - */ + * Parse `argv`, setting options and invoking commands when defined. + * + * Use parseAsync instead of parse if any of your action handlers are async. Returns a Promise. + * + * The default expectation is that the arguments are from node and have the application as argv[0] + * and the script being run in argv[1], with user parameters after that. + * + * Examples: + * + * program.parseAsync(process.argv); + * program.parseAsync(); // implicitly use process.argv and auto-detect node vs electron conventions + * program.parseAsync(my-args, { from: 'user' }); // just user supplied arguments, nothing special about argv[0] + * + * @returns Promise + */ parseAsync(argv?: string[], options?: ParseOptions): Promise; /**