Skip to content

Commit

Permalink
Support options with only a short flag (#1256)
Browse files Browse the repository at this point in the history
* Support short flag alone

* Weaken option parsing for backwards compatibility

* Have .version allow short only flag

* Add tests for lone short and long flags
  • Loading branch information
shadowspawn committed May 26, 2020
1 parent 168ff5b commit a31bb3f
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 49 deletions.
2 changes: 1 addition & 1 deletion Readme.md
Expand Up @@ -292,7 +292,7 @@ $ ./examples/pizza -V
```
You may change the flags and description by passing additional parameters to the `version` method, using
the same syntax for flags as the `option` method. The version flags can be named anything, but a long name is required.
the same syntax for flags as the `option` method.
```js
program.version('0.0.1', '-v, --vers', 'output the current version');
Expand Down
51 changes: 38 additions & 13 deletions index.js
Expand Up @@ -23,10 +23,13 @@ class Option {
this.required = flags.includes('<'); // A value must be supplied when the option is specified.
this.optional = flags.includes('['); // A value is optional when the option is specified.
this.mandatory = false; // The option must have a value after parsing, which usually means it must be specified on command line.
this.negate = flags.includes('-no-');
const flagParts = flags.split(/[ ,|]+/);
if (flagParts.length > 1 && !/^[[<]/.test(flagParts[1])) this.short = flagParts.shift();
this.long = flagParts.shift();
const optionFlags = _parseOptionFlags(flags);
this.short = optionFlags.shortFlag;
this.long = optionFlags.longFlag;
this.negate = false;
if (this.long) {
this.negate = this.long.startsWith('--no-');
}
this.description = description || '';
this.defaultValue = undefined;
}
Expand All @@ -39,7 +42,10 @@ class Option {
*/

name() {
return this.long.replace(/^--/, '');
if (this.long) {
return this.long.replace(/^--/, '');
}
return this.short.replace(/^-/, '');
};

/**
Expand Down Expand Up @@ -1187,9 +1193,9 @@ class Command extends EventEmitter {
flags = flags || '-V, --version';
description = description || 'output the version number';
const versionOption = new Option(flags, description);
this._versionOptionName = versionOption.long.substr(2) || 'version';
this._versionOptionName = versionOption.attributeName();
this.options.push(versionOption);
this.on('option:' + this._versionOptionName, () => {
this.on('option:' + versionOption.name(), () => {
process.stdout.write(str + '\n');
this._exit(0, 'commander.version', str);
});
Expand Down Expand Up @@ -1552,12 +1558,9 @@ class Command extends EventEmitter {
this._helpFlags = flags || this._helpFlags;
this._helpDescription = description || this._helpDescription;

const splitFlags = this._helpFlags.split(/[ ,|]+/);

this._helpShortFlag = undefined;
if (splitFlags.length > 1) this._helpShortFlag = splitFlags.shift();

this._helpLongFlag = splitFlags.shift();
const helpFlags = _parseOptionFlags(this._helpFlags);
this._helpShortFlag = helpFlags.shortFlag;
this._helpLongFlag = helpFlags.longFlag;

return this;
};
Expand Down Expand Up @@ -1708,6 +1711,28 @@ function humanReadableArgName(arg) {
: '[' + nameOutput + ']';
}

/**
* Parse the short and long flag out of something like '-m,--mixed <value>'
*
* @api private
*/

function _parseOptionFlags(flags) {
let shortFlag;
let longFlag;
// Use original very loose parsing to maintain backwards compatibility for now,
// which allowed for example unintended `-sw, --short-word` [sic].
const flagParts = flags.split(/[ |,]+/);
if (flagParts.length > 1 && !/^[[<]/.test(flagParts[1])) shortFlag = flagParts.shift();
longFlag = flagParts.shift();
// Add support for lone short flag without significantly changing parsing!
if (!shortFlag && /^-[^-]$/.test(longFlag)) {
shortFlag = longFlag;
longFlag = undefined;
}
return { shortFlag, longFlag };
}

/**
* Scan arguments and increment port number for inspect calls (to avoid conflicts when spawning new command).
*
Expand Down
80 changes: 62 additions & 18 deletions tests/command.helpOption.test.js
@@ -1,22 +1,66 @@
const commander = require('../');

test('when helpOption has custom flags then custom flag invokes help', () => {
// Optional. Suppress normal output to keep test output clean.
const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { });
const program = new commander.Command();
program
.exitOverride()
.helpOption('--custom-help', 'custom help output');
expect(() => {
program.parse(['node', 'test', '--custom-help']);
}).toThrow('(outputHelp)');
writeSpy.mockClear();
});
describe('helpOption', () => {
let writeSpy;

beforeAll(() => {
// Optional. Suppress normal output to keep test output clean.
writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { });
});

afterEach(() => {
writeSpy.mockClear();
});

afterAll(() => {
writeSpy.mockRestore();
});

test('when helpOption has custom flags then custom short flag invokes help', () => {
const program = new commander.Command();
program
.exitOverride()
.helpOption('-c,--custom-help', 'custom help output');
expect(() => {
program.parse(['-c'], { from: 'user' });
}).toThrow('(outputHelp)');
});

test('when helpOption has custom flags then custom long flag invokes help', () => {
const program = new commander.Command();
program
.exitOverride()
.helpOption('-c,--custom-help', 'custom help output');
expect(() => {
program.parse(['--custom-help'], { from: 'user' });
}).toThrow('(outputHelp)');
});

test('when helpOption has just custom short flag then custom short flag invokes help', () => {
const program = new commander.Command();
program
.exitOverride()
.helpOption('-c', 'custom help output');
expect(() => {
program.parse(['-c'], { from: 'user' });
}).toThrow('(outputHelp)');
});

test('when helpOption has just custom long flag then custom long flag invokes help', () => {
const program = new commander.Command();
program
.exitOverride()
.helpOption('--custom-help', 'custom help output');
expect(() => {
program.parse(['--custom-help'], { from: 'user' });
}).toThrow('(outputHelp)');
});

test('when helpOption has custom description then helpInformation include custom description', () => {
const program = new commander.Command();
program
.helpOption('-C,--custom-help', 'custom help output');
const helpInformation = program.helpInformation();
expect(helpInformation).toMatch(/-C,--custom-help +custom help output/);
test('when helpOption has custom description then helpInformation include custom description', () => {
const program = new commander.Command();
program
.helpOption('-C,--custom-help', 'custom help output');
const helpInformation = program.helpInformation();
expect(helpInformation).toMatch(/-C,--custom-help +custom help output/);
});
});
30 changes: 15 additions & 15 deletions tests/helpwrap.test.js
Expand Up @@ -8,16 +8,16 @@ test('when long option description then wrap and indent', () => {
process.stdout.columns = 80;
const program = new commander.Command();
program
.option('-x -extra-long-option-switch', 'kjsahdkajshkahd kajhsd akhds kashd kajhs dkha dkh aksd ka dkha kdh kasd ka kahs dkh sdkh askdh aksd kashdk ahsd kahs dkha skdh');
.option('-x --extra-long-option-switch', 'kjsahdkajshkahd kajhsd akhds kashd kajhs dkha dkh aksd ka dkha kdh kasd ka kahs dkh sdkh askdh aksd kashdk ahsd kahs dkha skdh');

const expectedOutput =
`Usage: [options]
Options:
-x -extra-long-option-switch kjsahdkajshkahd kajhsd akhds kashd kajhs dkha
dkh aksd ka dkha kdh kasd ka kahs dkh sdkh
askdh aksd kashdk ahsd kahs dkha skdh
-h, --help display help for command
-x --extra-long-option-switch kjsahdkajshkahd kajhsd akhds kashd kajhs dkha
dkh aksd ka dkha kdh kasd ka kahs dkh sdkh
askdh aksd kashdk ahsd kahs dkha skdh
-h, --help display help for command
`;

expect(program.helpInformation()).toBe(expectedOutput);
Expand All @@ -29,15 +29,15 @@ test('when long option description and default then wrap and indent', () => {
process.stdout.columns = 80;
const program = new commander.Command();
program
.option('-x -extra-long-option <value>', 'kjsahdkajshkahd kajhsd akhds', 'aaa bbb ccc ddd eee fff ggg');
.option('-x --extra-long-option <value>', 'kjsahdkajshkahd kajhsd akhds', 'aaa bbb ccc ddd eee fff ggg');

const expectedOutput =
`Usage: [options]
Options:
-x -extra-long-option <value> kjsahdkajshkahd kajhsd akhds (default: "aaa
bbb ccc ddd eee fff ggg")
-h, --help display help for command
-x --extra-long-option <value> kjsahdkajshkahd kajhsd akhds (default: "aaa
bbb ccc ddd eee fff ggg")
-h, --help display help for command
`;

expect(program.helpInformation()).toBe(expectedOutput);
Expand All @@ -49,20 +49,20 @@ test('when long command description then wrap and indent', () => {
process.stdout.columns = 80;
const program = new commander.Command();
program
.option('-x -extra-long-option-switch', 'x')
.option('-x --extra-long-option-switch', 'x')
.command('alpha', 'Lorem mollit quis dolor ex do eu quis ad insa a commodo esse.');

const expectedOutput =
`Usage: [options] [command]
Options:
-x -extra-long-option-switch x
-h, --help display help for command
-x --extra-long-option-switch x
-h, --help display help for command
Commands:
alpha Lorem mollit quis dolor ex do eu quis ad insa
a commodo esse.
help [command] display help for command
alpha Lorem mollit quis dolor ex do eu quis ad
insa a commodo esse.
help [command] display help for command
`;

expect(program.helpInformation()).toBe(expectedOutput);
Expand Down
12 changes: 10 additions & 2 deletions tests/options.flags.test.js
Expand Up @@ -2,15 +2,23 @@ const commander = require('../');

// Test the various ways flags can be specified in the first parameter to `.option`

test('when only short flag defined and not specified then value is undefined', () => {
const program = new commander.Command();
program
.option('-p', 'add pepper');
program.parse(['node', 'test']);
expect(program.p).toBeUndefined();
});

// Sanity check that pepper is not true normally, as otherwise all the following tests would pass for thr wrong reasons!
test('when only short flag defined and specified then value is true', () => {
const program = new commander.Command();
program
.option('-p', 'add pepper');
program.parse(['node', 'test', '-p']);
expect(program.P).toBe(true);
expect(program.p).toBe(true);
});

// Sanity check that pepper is not true normally, as otherwise all the following tests would pass for thr wrong reasons!
test('when only long flag defined and not specified then value is undefined', () => {
const program = new commander.Command();
program
Expand Down
24 changes: 24 additions & 0 deletions tests/options.version.test.js
Expand Up @@ -89,6 +89,18 @@ describe('.version', () => {
}).toThrow(myVersion);
});

test('when specify just custom short flag then display version', () => {
const myVersion = '1.2.3';
const program = new commander.Command();
program
.exitOverride()
.version(myVersion, '-r');

expect(() => {
program.parse(['node', 'test', '-r']);
}).toThrow(myVersion);
});

test('when specify custom long flag then display version', () => {
const myVersion = '1.2.3';
const program = new commander.Command();
Expand All @@ -101,6 +113,18 @@ describe('.version', () => {
}).toThrow(myVersion);
});

test('when specify just custom long flag then display version', () => {
const myVersion = '1.2.3';
const program = new commander.Command();
program
.exitOverride()
.version(myVersion, '--revision');

expect(() => {
program.parse(['node', 'test', '--revision']);
}).toThrow(myVersion);
});

test('when custom .version then helpInformation includes custom version help', () => {
const myVersion = '1.2.3';
const myVersionFlags = '-r, --revision';
Expand Down

0 comments on commit a31bb3f

Please sign in to comment.