Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add auto-detection of args when node evaluating script code on command-line #2164

Merged
merged 5 commits into from Apr 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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