Skip to content

Commit

Permalink
Parse argument conventions (#1172)
Browse files Browse the repository at this point in the history
* Add "from"  parameter to .parse()

* Add docs for ParseOptions

* Add .parse options to README

* Lint, whitespace

* Fill in parseAsync docs
  • Loading branch information
shadowspawn authored Feb 7, 2020
1 parent fbdd132 commit 30ae4ac
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 18 deletions.
19 changes: 19 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md)
- [.addHelpCommand()](#addhelpcommand)
- [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 @@ -563,6 +564,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
Original file line number Diff line number Diff line change
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 @@ -621,19 +623,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 @@ -643,13 +694,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 @@ -668,7 +730,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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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

0 comments on commit 30ae4ac

Please sign in to comment.