Skip to content

Commit 0d250ad

Browse files
committedJan 4, 2020
Remove config for Command constructor, add Command#help() method and related refactoring
1 parent e591b3f commit 0d250ad

File tree

6 files changed

+100
-64
lines changed

6 files changed

+100
-64
lines changed
 

‎CHANGELOG.md

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
## next
22

33
- Restored wrongly removed `Command#extend()`
4+
- Removed config argument for `Command`
45
- Added `Command#clone()` method
56
- Added `Command#hasCommand()`, `Command#getCommand(name)` and `Command#getCommands()` methods
67
- Added `Command#getOption(name)` and `Command#getOptions()` methods
78
- Added `Command#messageRef()` and `Option#messageRef()` methods
9+
- Added `Command#createOptionValues(values)` method
10+
- Added `Command#help()` method similar to `Command#version()`, use `Command#help(false)` to disable default help action option
11+
- Fixed `Command#showHelp()`, it's now logs help message in console instead of returning it
12+
- Renamed `Command#showHelp()` into `Command#outputHelp()`
813
- Changed `Command` to store params info (as `Command#params`) even if no params
914
- Renamed `Command#infoOption()` method into `actionOption()`
1015
- Renamed `Command#shortcut()` method into `shortcutOption()`
@@ -14,13 +19,11 @@
1419
- Ignore unknown keys
1520
- Removed `Command#infoOptionAction` and `infoOptionAction` option for `Command` constructor
1621
- Changed `Command#command()` to raise an exception when subcommand name already in use
17-
- Fixed `Command#showHelp()`, it's now logs help message in console instead of returning it
18-
- Renamed `Command#showHelp()` into `Command#outputHelp()`
19-
- Added `Command#createOptionValues(values)` method
2022
- Removed `Command#setOptions()` method
2123
- Removed `Command#setOption()` method
2224
- Removed `Command#normalize()` method (use `createOptionValues()` instead)
2325
- Changed `Option` to store params info as `Option#params`, it always an object even if no params
26+
- Added `Option#names()` method
2427
- Allowed a number for options's short name
2528
- Changed argv parse handlers to [`init()` -> `applyConfig()` -> `prepareContext()`]+ -> `action()`
2629
- Changed exports

‎README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,9 @@ myCommand
5959
```
6060
.command()
6161
// definition
62-
.version(value)
6362
.description(value)
63+
.version(value, usage, description, action)
64+
.help(usage, description, action)
6465
.option(usage, description, ...options)
6566
.actionOption(usage, description, action)
6667
.shortcutOption(usage, description, handler, ...options)

‎lib/command.js

+29-16
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const self = value => value;
88
const defaultHelpAction = (instance, _, { commandPath }) => instance.outputHelp(commandPath);
99
const defaultVersionAction = instance => console.log(instance.meta.version);
1010
const lastCommandHost = new WeakMap();
11+
const lastAddedOption = new WeakMap();
1112

1213
const handlers = ['init', 'applyConfig', 'finishContext', 'action'].reduce((res, name) => {
1314
res.initial[name] = name === 'action' ? self : noop;
@@ -20,24 +21,21 @@ const handlers = ['init', 'applyConfig', 'finishContext', 'action'].reduce((res,
2021
}, { initial: {}, setters: {} });
2122

2223
module.exports = class Command {
23-
constructor(name, params, config) {
24-
config = config || {};
25-
24+
constructor(name, params) {
2625
this.name = name;
2726
this.params = new Params(params || '', `"${this.name}" command definition`);
2827
this.options = new Map();
2928
this.commands = new Map();
3029
this.meta = {
3130
description: '',
32-
version: ''
31+
version: '',
32+
help: null
3333
};
3434

3535
this.handlers = { ...handlers.initial };
3636
Object.assign(this, handlers.setters);
3737

38-
if ('defaultHelp' in config === false || config.defaultHelp) {
39-
this.actionOption('-h, --help', 'Output usage information', defaultHelpAction);
40-
}
38+
this.help();
4139
}
4240

4341
// definition chaining
@@ -56,17 +54,32 @@ module.exports = class Command {
5654

5755
return this;
5856
}
57+
help(usage, description, action) {
58+
if (this.meta.help) {
59+
this.meta.help.names().forEach(name => this.options.delete(name));
60+
this.meta.help = null;
61+
}
62+
63+
if (usage !== false) {
64+
this.actionOption(
65+
usage || '-h, --help',
66+
description || 'Output usage information',
67+
action || defaultHelpAction
68+
);
69+
this.meta.help = lastAddedOption.get(this);
70+
}
71+
72+
return this;
73+
}
5974
option(usage, description, ...optionOpts) {
6075
const option = new Option(usage, description, ...optionOpts);
61-
const nameType = ['Long option', 'Option', 'Short option'];
62-
const names = option.short
63-
? [option.long, option.name, option.short]
64-
: [option.long, option.name];
76+
const nameType = ['Long option', 'Short option', 'Option'];
77+
const names = option.names();
6578

6679
names.forEach((name, idx) => {
6780
if (this.hasOption(name)) {
6881
throw new Error(
69-
`${nameType[idx]} name "${name}" already in use by ${this.getOption(name).messageRef()}`
82+
`${nameType[names.length === 2 ? idx * 2 : idx]} name "${name}" already in use by ${this.getOption(name).messageRef()}`
7083
);
7184
}
7285
});
@@ -75,6 +88,8 @@ module.exports = class Command {
7588
this.options.set(name, option);
7689
}
7790

91+
lastAddedOption.set(this, option);
92+
7893
return this;
7994
}
8095
actionOption(usage, description, action) {
@@ -103,7 +118,7 @@ module.exports = class Command {
103118
}
104119

105120
// search for existing one
106-
if (this.hasCommand(name)) {
121+
if (this.commands.has(name)) {
107122
throw new Error(
108123
`Subcommand name "${name}" already in use by ${this.getCommand(name).messageRef()}`
109124
);
@@ -121,7 +136,7 @@ module.exports = class Command {
121136
return host;
122137
}
123138

124-
// extend & clone helpers
139+
// helpers
125140
extend(fn, ...args) {
126141
fn(this, ...args);
127142
return this;
@@ -145,8 +160,6 @@ module.exports = class Command {
145160

146161
return clone;
147162
}
148-
149-
// values
150163
createOptionValues(values) {
151164
const storage = Object.create(null);
152165

‎lib/help.js

+33-36
Original file line numberDiff line numberDiff line change
@@ -54,36 +54,42 @@ function breakByLines(str, offset) {
5454
.join('\n');
5555
}
5656

57-
function args(command) {
58-
return command.params.args
59-
.map(({ name, required }) => required ? '<' + name + '>' : '[' + name + ']')
60-
.join(' ');
57+
function args(params, fn = s => s) {
58+
if (params.args.length === 0) {
59+
return '';
60+
}
61+
62+
return ' ' + fn(
63+
params.args
64+
.map(({ name, required }) => required ? '<' + name + '>' : '[' + name + ']')
65+
.join(' ')
66+
);
67+
}
68+
69+
function formatLines(lines) {
70+
const maxNameLength = Math.max(MIN_OFFSET - 2, ...lines.map(line => stringLength(line.name)));
71+
72+
return lines.map(line => (
73+
' ' + pad(maxNameLength, line.name) +
74+
' ' + breakByLines(line.description, maxNameLength + 8)
75+
));
6176
}
6277

6378
function commandsHelp(command) {
6479
if (!command.hasCommands()) {
6580
return '';
6681
}
6782

68-
const lines = command.getCommands().sort(byName).map(subcommand => ({
69-
name: chalk.green(subcommand.name) + chalk.gray(
70-
(subcommand.params.maxCount ? ' ' + args(subcommand) : '')
71-
),
72-
description: subcommand.meta.description || ''
83+
const lines = command.getCommands().sort(byName).map(({ name, meta, params }) => ({
84+
description: meta.description,
85+
name: chalk.green(name) + args(params, chalk.gray)
7386
}));
74-
const maxNameLength = lines.reduce(
75-
(max, line) => Math.max(max, stringLength(line.name)),
76-
MIN_OFFSET - 2
77-
);
7887

7988
return [
8089
'',
8190
'Commands:',
8291
'',
83-
...lines.map(line => (
84-
' ' + pad(maxNameLength, line.name) +
85-
' ' + breakByLines(line.description, maxNameLength + 8)
86-
)),
92+
...formatLines(lines),
8793
''
8894
].join('\n');
8995
}
@@ -94,31 +100,22 @@ function optionsHelp(command) {
94100
}
95101

96102
const options = command.getOptions().sort(byName);
97-
const hasShortOptions = options.some(option => option.short);
98-
const lines = options.map(option => ({
99-
name: option.usage
100-
.replace(/^(?:-., |)/, (m) =>
101-
m || (hasShortOptions ? ' ' : '')
102-
)
103-
.replace(/(^|\s)(-[^\s,]+)/ig, (m, p, flag) =>
104-
p + chalk.yellow(flag)
105-
),
106-
description: option.description
103+
const shortPlaceholder = options.some(option => option.short) ? ' ' : '';
104+
const lines = options.map(({ short, long, params, description }) => ({
105+
description,
106+
name: [
107+
short ? chalk.yellow(short) + ', ' : shortPlaceholder,
108+
chalk.yellow(long),
109+
args(params)
110+
].join('')
107111
}));
108-
const maxNameLength = lines.reduce(
109-
(max, line) => Math.max(max, stringLength(line.name)),
110-
MIN_OFFSET - 2
111-
);
112112

113113
// Prepend the help information
114114
return [
115115
'',
116116
'Options:',
117117
'',
118-
...lines.map(line => (
119-
' ' + pad(maxNameLength, line.name) +
120-
' ' + breakByLines(line.description, maxNameLength + 8)
121-
)),
118+
...formatLines(lines),
122119
''
123120
].join('\n');
124121
}
@@ -140,7 +137,7 @@ module.exports = function getCommandHelp(command, commandPath) {
140137
(command.meta.description ? command.meta.description + '\n\n' : '') +
141138
'Usage:\n\n' +
142139
' ' + chalk.cyan(commandPath) +
143-
(command.params.maxCount ? ' ' + chalk.magenta(args(command)) : '') +
140+
args(command.params, chalk.magenta) +
144141
(command.hasOptions() ? ' [' + chalk.yellow('options') + ']' : '') +
145142
(command.hasCommands() ? ' [' + chalk.green('command') + ']' : ''),
146143
commandsHelp(command) +

‎lib/option.js

+4
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,8 @@ module.exports = class Option {
6565
messageRef() {
6666
return `${this.usage} ${this.description}`;
6767
}
68+
69+
names() {
70+
return [this.long, this.short, this.name].filter(Boolean);
71+
}
6872
};

‎test/command-help.js

+26-8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ describe('Command help', () => {
77
beforeEach(() => inspect = stdout.inspect());
88
afterEach(() => inspect.restore());
99

10+
it('should remove default help when .help(false)', function() {
11+
const command = cli.command('test').help(false);
12+
13+
assert.equal(command.hasOption('help'), false);
14+
});
15+
1016
it('should show help', () => {
1117
cli.command('test', false).run(['--help']);
1218

@@ -23,6 +29,26 @@ describe('Command help', () => {
2329
].join('\n'));
2430
});
2531

32+
it('help with no short options', function() {
33+
cli.command('test', false, { defaultHelp: false })
34+
.help('--help')
35+
.option('--foo', 'Foo')
36+
.run(['--help']);
37+
38+
assert.equal(inspect.output, [
39+
'Usage:',
40+
'',
41+
' \u001b[36mtest\u001b[39m [\u001b[33moptions\u001b[39m]',
42+
'',
43+
'Options:',
44+
'',
45+
' \u001b[33m--foo\u001b[39m Foo',
46+
' \u001b[33m--help\u001b[39m Output usage information',
47+
'',
48+
''
49+
].join('\n'));
50+
});
51+
2652
it('should show help all cases', () => {
2753
cli
2854
.command('test', '[qux]')
@@ -77,14 +103,6 @@ describe('Command help', () => {
77103
].join('\n'));
78104
});
79105

80-
it('should not define default help when defaultHelp in config is falsy', function() {
81-
const command = cli.command('test', false, {
82-
defaultHelp: false
83-
});
84-
85-
assert.equal(command.hasOption('help'), false);
86-
});
87-
88106
it('should show help message when Command#outputHelp called', function() {
89107
const command = cli
90108
.command('test', '[qux]')

0 commit comments

Comments
 (0)
Please sign in to comment.