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

Parse argument conventions #1172

Merged
merged 5 commits into from Feb 7, 2020
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
19 changes: 19 additions & 0 deletions Readme.md
Expand Up @@ -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)
Expand Down Expand Up @@ -598,6 +599,24 @@ 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
Expand Down
82 changes: 72 additions & 10 deletions index.js
Expand Up @@ -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
Expand Down Expand Up @@ -620,19 +622,68 @@ class Command extends EventEmitter {
/**
* Parse `argv`, setting options and invoking commands when defined.
*
* @param {Array} argv
* 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(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
* @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
*/

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;
};
Expand All @@ -642,13 +693,24 @@ class Command extends EventEmitter {
*
* Use parseAsync instead of parse if any of your action handlers are async. Returns a Promise.
*
* @param {Array} argv
* 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]
* @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);
};

Expand All @@ -667,7 +729,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 {
Expand Down
54 changes: 54 additions & 0 deletions 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']);
});
});
2 changes: 2 additions & 0 deletions typings/commander-tests.ts
Expand Up @@ -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');
Expand Down
38 changes: 30 additions & 8 deletions typings/index.d.ts
Expand Up @@ -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

Expand Down Expand Up @@ -196,19 +200,37 @@ 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(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
*/
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.
*
* @returns Promise
*/
parseAsync(argv: string[]): Promise<any>;
* 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<any>;

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