Skip to content

Commit

Permalink
Remove config for Command constructor, add Command#help() method and …
Browse files Browse the repository at this point in the history
…related refactoring
  • Loading branch information
lahmatiy committed Jan 4, 2020
1 parent e591b3f commit 0d250ad
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 64 deletions.
9 changes: 6 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
## next

- Restored wrongly removed `Command#extend()`
- Removed config argument for `Command`
- Added `Command#clone()` method
- Added `Command#hasCommand()`, `Command#getCommand(name)` and `Command#getCommands()` methods
- Added `Command#getOption(name)` and `Command#getOptions()` methods
- Added `Command#messageRef()` and `Option#messageRef()` methods
- Added `Command#createOptionValues(values)` method
- Added `Command#help()` method similar to `Command#version()`, use `Command#help(false)` to disable default help action option
- Fixed `Command#showHelp()`, it's now logs help message in console instead of returning it
- Renamed `Command#showHelp()` into `Command#outputHelp()`
- Changed `Command` to store params info (as `Command#params`) even if no params
- Renamed `Command#infoOption()` method into `actionOption()`
- Renamed `Command#shortcut()` method into `shortcutOption()`
Expand All @@ -14,13 +19,11 @@
- Ignore unknown keys
- Removed `Command#infoOptionAction` and `infoOptionAction` option for `Command` constructor
- Changed `Command#command()` to raise an exception when subcommand name already in use
- Fixed `Command#showHelp()`, it's now logs help message in console instead of returning it
- Renamed `Command#showHelp()` into `Command#outputHelp()`
- Added `Command#createOptionValues(values)` method
- Removed `Command#setOptions()` method
- Removed `Command#setOption()` method
- Removed `Command#normalize()` method (use `createOptionValues()` instead)
- Changed `Option` to store params info as `Option#params`, it always an object even if no params
- Added `Option#names()` method
- Allowed a number for options's short name
- Changed argv parse handlers to [`init()` -> `applyConfig()` -> `prepareContext()`]+ -> `action()`
- Changed exports
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ myCommand
```
.command()
// definition
.version(value)
.description(value)
.version(value, usage, description, action)
.help(usage, description, action)
.option(usage, description, ...options)
.actionOption(usage, description, action)
.shortcutOption(usage, description, handler, ...options)
Expand Down
45 changes: 29 additions & 16 deletions lib/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const self = value => value;
const defaultHelpAction = (instance, _, { commandPath }) => instance.outputHelp(commandPath);
const defaultVersionAction = instance => console.log(instance.meta.version);
const lastCommandHost = new WeakMap();
const lastAddedOption = new WeakMap();

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

module.exports = class Command {
constructor(name, params, config) {
config = config || {};

constructor(name, params) {
this.name = name;
this.params = new Params(params || '', `"${this.name}" command definition`);
this.options = new Map();
this.commands = new Map();
this.meta = {
description: '',
version: ''
version: '',
help: null
};

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

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

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

return this;
}
help(usage, description, action) {
if (this.meta.help) {
this.meta.help.names().forEach(name => this.options.delete(name));
this.meta.help = null;
}

if (usage !== false) {
this.actionOption(
usage || '-h, --help',
description || 'Output usage information',
action || defaultHelpAction
);
this.meta.help = lastAddedOption.get(this);
}

return this;
}
option(usage, description, ...optionOpts) {
const option = new Option(usage, description, ...optionOpts);
const nameType = ['Long option', 'Option', 'Short option'];
const names = option.short
? [option.long, option.name, option.short]
: [option.long, option.name];
const nameType = ['Long option', 'Short option', 'Option'];
const names = option.names();

names.forEach((name, idx) => {
if (this.hasOption(name)) {
throw new Error(
`${nameType[idx]} name "${name}" already in use by ${this.getOption(name).messageRef()}`
`${nameType[names.length === 2 ? idx * 2 : idx]} name "${name}" already in use by ${this.getOption(name).messageRef()}`
);
}
});
Expand All @@ -75,6 +88,8 @@ module.exports = class Command {
this.options.set(name, option);
}

lastAddedOption.set(this, option);

return this;
}
actionOption(usage, description, action) {
Expand Down Expand Up @@ -103,7 +118,7 @@ module.exports = class Command {
}

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

// extend & clone helpers
// helpers
extend(fn, ...args) {
fn(this, ...args);
return this;
Expand All @@ -145,8 +160,6 @@ module.exports = class Command {

return clone;
}

// values
createOptionValues(values) {
const storage = Object.create(null);

Expand Down
69 changes: 33 additions & 36 deletions lib/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,36 +54,42 @@ function breakByLines(str, offset) {
.join('\n');
}

function args(command) {
return command.params.args
.map(({ name, required }) => required ? '<' + name + '>' : '[' + name + ']')
.join(' ');
function args(params, fn = s => s) {
if (params.args.length === 0) {
return '';
}

return ' ' + fn(
params.args
.map(({ name, required }) => required ? '<' + name + '>' : '[' + name + ']')
.join(' ')
);
}

function formatLines(lines) {
const maxNameLength = Math.max(MIN_OFFSET - 2, ...lines.map(line => stringLength(line.name)));

return lines.map(line => (
' ' + pad(maxNameLength, line.name) +
' ' + breakByLines(line.description, maxNameLength + 8)
));
}

function commandsHelp(command) {
if (!command.hasCommands()) {
return '';
}

const lines = command.getCommands().sort(byName).map(subcommand => ({
name: chalk.green(subcommand.name) + chalk.gray(
(subcommand.params.maxCount ? ' ' + args(subcommand) : '')
),
description: subcommand.meta.description || ''
const lines = command.getCommands().sort(byName).map(({ name, meta, params }) => ({
description: meta.description,
name: chalk.green(name) + args(params, chalk.gray)
}));
const maxNameLength = lines.reduce(
(max, line) => Math.max(max, stringLength(line.name)),
MIN_OFFSET - 2
);

return [
'',
'Commands:',
'',
...lines.map(line => (
' ' + pad(maxNameLength, line.name) +
' ' + breakByLines(line.description, maxNameLength + 8)
)),
...formatLines(lines),
''
].join('\n');
}
Expand All @@ -94,31 +100,22 @@ function optionsHelp(command) {
}

const options = command.getOptions().sort(byName);
const hasShortOptions = options.some(option => option.short);
const lines = options.map(option => ({
name: option.usage
.replace(/^(?:-., |)/, (m) =>
m || (hasShortOptions ? ' ' : '')
)
.replace(/(^|\s)(-[^\s,]+)/ig, (m, p, flag) =>
p + chalk.yellow(flag)
),
description: option.description
const shortPlaceholder = options.some(option => option.short) ? ' ' : '';
const lines = options.map(({ short, long, params, description }) => ({
description,
name: [
short ? chalk.yellow(short) + ', ' : shortPlaceholder,
chalk.yellow(long),
args(params)
].join('')
}));
const maxNameLength = lines.reduce(
(max, line) => Math.max(max, stringLength(line.name)),
MIN_OFFSET - 2
);

// Prepend the help information
return [
'',
'Options:',
'',
...lines.map(line => (
' ' + pad(maxNameLength, line.name) +
' ' + breakByLines(line.description, maxNameLength + 8)
)),
...formatLines(lines),
''
].join('\n');
}
Expand All @@ -140,7 +137,7 @@ module.exports = function getCommandHelp(command, commandPath) {
(command.meta.description ? command.meta.description + '\n\n' : '') +
'Usage:\n\n' +
' ' + chalk.cyan(commandPath) +
(command.params.maxCount ? ' ' + chalk.magenta(args(command)) : '') +
args(command.params, chalk.magenta) +
(command.hasOptions() ? ' [' + chalk.yellow('options') + ']' : '') +
(command.hasCommands() ? ' [' + chalk.green('command') + ']' : ''),
commandsHelp(command) +
Expand Down
4 changes: 4 additions & 0 deletions lib/option.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,8 @@ module.exports = class Option {
messageRef() {
return `${this.usage} ${this.description}`;
}

names() {
return [this.long, this.short, this.name].filter(Boolean);
}
};
34 changes: 26 additions & 8 deletions test/command-help.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ describe('Command help', () => {
beforeEach(() => inspect = stdout.inspect());
afterEach(() => inspect.restore());

it('should remove default help when .help(false)', function() {
const command = cli.command('test').help(false);

assert.equal(command.hasOption('help'), false);
});

it('should show help', () => {
cli.command('test', false).run(['--help']);

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

it('help with no short options', function() {
cli.command('test', false, { defaultHelp: false })
.help('--help')
.option('--foo', 'Foo')
.run(['--help']);

assert.equal(inspect.output, [
'Usage:',
'',
' \u001b[36mtest\u001b[39m [\u001b[33moptions\u001b[39m]',
'',
'Options:',
'',
' \u001b[33m--foo\u001b[39m Foo',
' \u001b[33m--help\u001b[39m Output usage information',
'',
''
].join('\n'));
});

it('should show help all cases', () => {
cli
.command('test', '[qux]')
Expand Down Expand Up @@ -77,14 +103,6 @@ describe('Command help', () => {
].join('\n'));
});

it('should not define default help when defaultHelp in config is falsy', function() {
const command = cli.command('test', false, {
defaultHelp: false
});

assert.equal(command.hasOption('help'), false);
});

it('should show help message when Command#outputHelp called', function() {
const command = cli
.command('test', '[qux]')
Expand Down

0 comments on commit 0d250ad

Please sign in to comment.