Skip to content

Commit

Permalink
Add auto-detection of args when node evaluating script code on comman…
Browse files Browse the repository at this point in the history
…d-line (#2164)
  • Loading branch information
shadowspawn committed Apr 7, 2024
1 parent f7b8475 commit b9ca390
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 39 deletions.
18 changes: 10 additions & 8 deletions Readme.md
Expand Up @@ -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
Expand Down
63 changes: 44 additions & 19 deletions lib/command.js
Expand Up @@ -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:
Expand All @@ -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}' }`,
Expand All @@ -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
*/

Expand All @@ -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}
*/

Expand Down
19 changes: 19 additions & 0 deletions tests/command.parse.test.js
Expand Up @@ -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();
Expand Down Expand Up @@ -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', () => {
Expand Down
35 changes: 23 additions & 12 deletions typings/index.d.ts
Expand Up @@ -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<this>;
parseAsync(
argv?: readonly string[],
parseOptions?: ParseOptions,
): Promise<this>;

/**
* Parse options from `argv` removing known options,
Expand Down

0 comments on commit b9ca390

Please sign in to comment.