From b9ca39062455d25b564269e832372c87a6569f8a Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 7 Apr 2024 12:28:03 +1200 Subject: [PATCH] Add auto-detection of args when node evaluating script code on command-line (#2164) --- Readme.md | 18 ++++++----- lib/command.js | 63 ++++++++++++++++++++++++++----------- tests/command.parse.test.js | 19 +++++++++++ typings/index.d.ts | 35 ++++++++++++++------- 4 files changed, 96 insertions(+), 39 deletions(-) diff --git a/Readme.md b/Readme.md index cdd43ed58..e4e365787 100644 --- a/Readme.md +++ b/Readme.md @@ -955,22 +955,24 @@ program.on('option:verbose', function () { ### .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`. +Call with no parameters to parse `process.argv`. Detects Electron and special node options like `node --eval`. Easy mode! -If the arguments follow different conventions than node you can pass a `from` option in the second parameter: +Or call with an array of strings to parse, and optionally where the user arguments start by specifying where the arguments are `from`: -- '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 +- `'node'`: default, `argv[0]` is the application and `argv[1]` is the script being run, with user arguments after that +- `'electron'`: `argv[0]` is the application and `argv[1]` varies depending on whether the electron application is packaged +- `'user'`: just user arguments For example: ```js -program.parse(process.argv); // Explicit, node conventions -program.parse(); // Implicit, and auto-detect electron -program.parse(['-f', 'filename'], { from: 'user' }); +program.parse(); // parse process.argv and auto-detect electron and special node flags +program.parse(process.argv); // assume argv[0] is app and argv[1] is script +program.parse(['--port', '80'], { from: 'user' }); // just user supplied arguments, nothing special about argv[0] ``` +Use parseAsync instead of parse if any of your action handlers are async. + If you want to parse multiple times, create a new program each time. Calling parse does not clear out any previous state. ### Parsing Configuration diff --git a/lib/command.js b/lib/command.js index dd33d205c..f9bf5eb1c 100644 --- a/lib/command.js +++ b/lib/command.js @@ -973,16 +973,30 @@ Expecting one of '${allowedValues.join("', '")}'`); } parseOptions = parseOptions || {}; - // Default to using process.argv - if (argv === undefined) { - argv = process.argv; - if (process.versions && process.versions.electron) { + // auto-detect argument conventions if nothing supplied + if (argv === undefined && parseOptions.from === undefined) { + if (process.versions?.electron) { parseOptions.from = 'electron'; } + // check node specific options for scenarios where user CLI args follow executable without scriptname + const execArgv = process.execArgv ?? []; + if ( + execArgv.includes('-e') || + execArgv.includes('--eval') || + execArgv.includes('-p') || + execArgv.includes('--print') + ) { + parseOptions.from = 'eval'; // internal usage, not documented + } + } + + // default to using process.argv + if (argv === undefined) { + argv = process.argv; } this.rawArgs = argv.slice(); - // make it a little easier for callers by supporting various argv conventions + // extract the user args and scriptPath let userArgs; switch (parseOptions.from) { case undefined: @@ -1002,6 +1016,9 @@ Expecting one of '${allowedValues.join("', '")}'`); case 'user': userArgs = argv.slice(0); break; + case 'eval': + userArgs = argv.slice(1); + break; default: throw new Error( `unexpected parse option { from: '${parseOptions.from}' }`, @@ -1019,17 +1036,23 @@ Expecting one of '${allowedValues.join("', '")}'`); /** * 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. + * Use parseAsync instead of parse if any of your action handlers are async. + * + * Call with no parameters to parse `process.argv`. Detects Electron and special node options like `node --eval`. Easy mode! + * + * Or call with an array of strings to parse, and optionally where the user arguments start by specifying where the arguments are `from`: + * - `'node'`: default, `argv[0]` is the application and `argv[1]` is the script being run, with user arguments after that + * - `'electron'`: `argv[0]` is the application and `argv[1]` varies depending on whether the electron application is packaged + * - `'user'`: just user arguments * * @example - * program.parse(process.argv); - * program.parse(); // implicitly use process.argv and auto-detect node vs electron conventions + * program.parse(); // parse process.argv and auto-detect electron and special node flags + * program.parse(process.argv); // assume argv[0] is app and argv[1] is script * 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' + * @param {string[]} [argv] + * @param {object} [parseOptions] + * @param {string} parseOptions.from - one of 'node', 'user', 'electron' * @return {Command} `this` command for chaining */ @@ -1043,19 +1066,21 @@ Expecting one of '${allowedValues.join("', '")}'`); /** * 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. + * Call with no parameters to parse `process.argv`. Detects Electron and special node options like `node --eval`. Easy mode! * - * 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. + * Or call with an array of strings to parse, and optionally where the user arguments start by specifying where the arguments are `from`: + * - `'node'`: default, `argv[0]` is the application and `argv[1]` is the script being run, with user arguments after that + * - `'electron'`: `argv[0]` is the application and `argv[1]` varies depending on whether the electron application is packaged + * - `'user'`: just user arguments * * @example - * await program.parseAsync(process.argv); - * await program.parseAsync(); // implicitly use process.argv and auto-detect node vs electron conventions + * await program.parseAsync(); // parse process.argv and auto-detect electron and special node flags + * await program.parseAsync(process.argv); // assume argv[0] is app and argv[1] is script * await program.parseAsync(my-args, { from: 'user' }); // just user supplied arguments, nothing special about argv[0] * * @param {string[]} [argv] - * @param {Object} [parseOptions] - * @param {string} parseOptions.from - where the args are from: 'node', 'user', 'electron' + * @param {object} [parseOptions] + * @param {string} parseOptions.from - one of 'node', 'user', 'electron' * @return {Promise} */ diff --git a/tests/command.parse.test.js b/tests/command.parse.test.js index 720bcba7a..49765703e 100644 --- a/tests/command.parse.test.js +++ b/tests/command.parse.test.js @@ -4,6 +4,9 @@ const commander = require('../'); // https://github.com/electron/electron/issues/4690#issuecomment-217435222 // https://www.electronjs.org/docs/api/process#processdefaultapp-readonly +// (If mutating process.argv and process.execArgv causes problems, could add utility +// functions to get them and then mock the functions for tests.) + describe('.parse() args from', () => { test('when no args then use process.argv and app/script/args', () => { const program = new commander.Command(); @@ -67,6 +70,22 @@ describe('.parse() args from', () => { program.parse(['node', 'script.js'], { from: 'silly' }); }).toThrow(); }); + + test.each(['-e', '--eval', '-p', '--print'])( + 'when node execArgv includes %s then app/args', + (flag) => { + const program = new commander.Command(); + const holdExecArgv = process.execArgv; + const holdArgv = process.argv; + process.argv = ['node', 'user-arg']; + process.execArgv = [flag, 'console.log("hello, world")']; + program.parse(); + process.argv = holdArgv; + process.execArgv = holdExecArgv; + expect(program.args).toEqual(['user-arg']); + process.execArgv = holdExecArgv; + }, + ); }); describe('return type', () => { diff --git a/typings/index.d.ts b/typings/index.d.ts index 6a4d47594..29c5365dc 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -726,38 +726,49 @@ export class Command { /** * 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. + * Use parseAsync instead of parse if any of your action handlers are async. + * + * Call with no parameters to parse `process.argv`. Detects Electron and special node options like `node --eval`. Easy mode! + * + * Or call with an array of strings to parse, and optionally where the user arguments start by specifying where the arguments are `from`: + * - `'node'`: default, `argv[0]` is the application and `argv[1]` is the script being run, with user arguments after that + * - `'electron'`: `argv[0]` is the application and `argv[1]` varies depending on whether the electron application is packaged + * - `'user'`: just user arguments * * @example * ``` - * program.parse(process.argv); - * program.parse(); // implicitly use process.argv and auto-detect node vs electron conventions + * program.parse(); // parse process.argv and auto-detect electron and special node flags + * program.parse(process.argv); // assume argv[0] is app and argv[1] is script * program.parse(my-args, { from: 'user' }); // just user supplied arguments, nothing special about argv[0] * ``` * * @returns `this` command for chaining */ - parse(argv?: readonly string[], options?: ParseOptions): this; + parse(argv?: readonly string[], parseOptions?: ParseOptions): this; /** * 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. + * Call with no parameters to parse `process.argv`. Detects Electron and special node options like `node --eval`. Easy mode! * - * 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. + * Or call with an array of strings to parse, and optionally where the user arguments start by specifying where the arguments are `from`: + * - `'node'`: default, `argv[0]` is the application and `argv[1]` is the script being run, with user arguments after that + * - `'electron'`: `argv[0]` is the application and `argv[1]` varies depending on whether the electron application is packaged + * - `'user'`: just user arguments * * @example * ``` - * 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] + * await program.parseAsync(); // parse process.argv and auto-detect electron and special node flags + * await program.parseAsync(process.argv); // assume argv[0] is app and argv[1] is script + * await program.parseAsync(my-args, { from: 'user' }); // just user supplied arguments, nothing special about argv[0] * ``` * * @returns Promise */ - parseAsync(argv?: readonly string[], options?: ParseOptions): Promise; + parseAsync( + argv?: readonly string[], + parseOptions?: ParseOptions, + ): Promise; /** * Parse options from `argv` removing known options,