-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Refactor help internals into separate interface/class #1365
Conversation
Here is a example of a big override, to output the help in markdown. const { program } = require('commander');
function formatHelpAsMarkdown(cmd, helper) {
function formatItem(term, description) {
if (description) {
return `\`${term}\`: ${description}`;
}
return `\`${term}\``;
};
function formatList(textArray) {
return textArray.join('\n').replace(/^/gm, '* ');
}
let output = [`# ${cmd.name()}`, ''];
// Usage
output = output.concat([helper.commandUsage(cmd), '']);
// Description
if (cmd.description()) {
output = output.concat([cmd.description(), '']);
}
// Arguments
const visibleArguments = helper.visibleArguments(cmd);
if (visibleArguments.length) {
const argumentsList = visibleArguments.map((argument) => {
return formatItem(argument.term, argument.description);
});
output = output.concat(['## Arguments:', formatList(argumentsList), '']);
}
// Options
const visibleOptions = helper.visibleOptions(cmd);
if (visibleOptions.length) {
const optionList = visibleOptions.map((option) => {
return formatItem(helper.optionTerm(option), helper.optionDescription(option));
});
output = output.concat(['## Options:', formatList(optionList), '']);
}
// Commands
const visibleCommands = helper.visibleCommands(cmd);
if (visibleCommands.length) {
const commandList = visibleCommands.map((cmd) => {
return formatItem(helper.commandTerm(cmd), helper.commandDescription(cmd));
});
output = output.concat(['## Commands:', formatList(commandList), '']);
}
return output.join('\n');
}
function commandUsage(cmd) {
return `Usage: \`${cmd.name()} ${cmd.usage()}\``;
}
program
.name('mighty-markdown')
.helpOverrides({ formatHelp: formatHelpAsMarkdown, commandUsage: commandUsage })
.option('-f, --foo', 'first option')
.option('-b, --bar', 'first option')
.command('sub1', 'external')
.command('sub2', 'external')
program.parse(); node markdown.sh --help
# paste in below mighty-markdownUsage: Options:
Commands:
|
And a much smaller example, setting the wrapping width for say running a unit test: program.helpOverrides({ columns: 80 }) |
Hi @shadowspawn, Played around with new const { Command, Help: HelpTools } = require('./index');
class Help {
constructor(cmd, helper) {
this.cmd = cmd;
this.helper = helper;
};
usage() {
return this.helper.commandUsage(this.cmd);
};
description() {
return this.helper.commandDescription(this.cmd);
};
arguments() {
return this.helper.visibleArguments(this.cmd);
};
options() {
return this.helper.visibleOptions(this.cmd).map(option => ({
term: this.helper.optionTerm(option),
description: this.helper.optionDescription(option),
}));
};
commands() {
return this.helper.visibleCommands(this.cmd).map(cmd => ({
term: this.helper.commandTerm(cmd),
description: this.helper.commandDescription(cmd),
}));
};
render() {
const params = this.helper.formatParams();
const baseIndentWidth = params.baseIndentWidth || 0;
const itemIndentWidth = params.itemIndentWidth || 2;
const itemSeparatorWidth = params.itemSeparatorWidth || 2;
const columns = this.helper.columns;
const termWidth = this.helper.padWidth(this.cmd, this.helper) + baseIndentWidth;
const descriptionWidth = columns - termWidth - baseIndentWidth - itemIndentWidth - itemSeparatorWidth;
const styles = this.helper.styles();
const formatList = (arr) => {
return arr.map(({ term, description }) => {
if (description) {
return [
styles.term(this.helper.pad(term, termWidth + itemSeparatorWidth)),
this.helper.optionalWrap(description, descriptionWidth, termWidth + itemSeparatorWidth, this.helper),
].join('');
}
return term;
}).join('\n').replace(/^/gm, Array(itemIndentWidth + 1).join(' '));
};
const labels = this.helper.labels();
Object.keys(labels).map(key => {
labels[key] = styles.label(labels[key]);
});
// Usage
let output = [
labels.usage,
Array(itemIndentWidth + 1).join(' ') + this.usage(),
'',
];
// Description
if (this.description()) {
output = output.concat([
Array(itemIndentWidth + 1).join(' ') + styles.description(this.helper.optionalWrap(
this.description(),
columns - baseIndentWidth - itemIndentWidth,
itemIndentWidth,
this.helper
)),
'',
]);
}
// Arguments
if (this.arguments().length) {
output = output.concat([
labels.arguments,
formatList(this.arguments()),
'',
]);
}
// Options
if (this.options().length) {
output = output.concat([
labels.options,
formatList(this.options()),
'',
]);
}
// Commands
if (this.commands().length) {
output = output.concat([
labels.commands,
formatList(this.commands()),
'',
]);
}
return output.join('\n').replace(/^/gm, Array(baseIndentWidth + 1).join(' ')).trimEnd() + '\n';
};
}
class MyHelpTools extends HelpTools {
labels() {
return {
usage: 'Usage:',
arguments: 'Arguments:',
options: 'Options:',
commands: 'Commands:',
};
};
styles() {
return {
label: str => str,
term: str => str,
description: str => str,
};
};
commandUsage(cmd) {
return super.commandUsage(cmd).split('Usage: ')[1];
};
formatParams() {
return {
baseIndentWidth: 0,
itemIndentWidth: 2,
itemSeparatorWidth: 2
};
};
formatHelp(cmd, helper) {
return ['', (new Help(cmd, helper)).render(), ''].join('\n');
};
}
class MyCommand extends Command {
createCommand(name) {
return new MyCommand(name);
};
createHelp() {
return Object.assign(new MyHelpTools(), this._helpOverrides);
};
}
const program = new MyCommand('example');
program.version('0.0.1');
program.helpOverrides({
labels: () => {
return {
usage: '>>> Usage:\n',
arguments: '>>> Arguments:\n',
options: '>>> Options:\n',
commands: '>>> Commands:\n',
}
},
styles: () => {
return {
label: str => '\x1b[33m' + str + '\x1b[0m', // yellow
term: str => '\x1b[32m' + str + '\x1b[0m', // green
description: str => '\x1b[36m' + str + '\x1b[0m', // cyan
};
},
formatParams: () => {
return {
baseIndentWidth: 2,
};
},
});
program
.command('test <cmd> [env] [val]')
.description(Array(100).join('long description '), {
cmd: 'cmd description',
env: 'env description',
val: Array(50).join('val description ')
})
.action((cmd, env, options) => {});
program.parse(process.argv); |
Thanks @cravler . I was wondering about applying styles, and that is just one of the layers you have added, and even used both subclass and arrow function customisations. Thanks for the big try-out! (I changed |
Ready for review. This is a big refactor of existing code, and makes the methods used for building the help public so they may be configured in custom help implementations. |
(I'll hopefully have time over next week to prepare a prerelease of 7.x, and a 6.x as well.) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM.
That is great! Thank you!
Included in v7.0.0 pre-release #1386 |
Pull Request
Major help refactor (see also earlier experimentation in #1338, #1346).
Problem
The help generation is an organic collection of methods with custom logic embedded at various levels, and inconsistent handling of arguments, options, and commands.
Related issues:
Solution
Move the help related routines into a separate class. Inspired by discussion and examples from @cravler: #1346 (comment)
Implement the Help methods as if they are static to allow override not only with subclass (see
.createHelp()
), but also with plain functions (see.configureHelp()
).The impact on existing programs only using public methods should be small. All of the methods which are moved or deleted in the refactor were private, except for
Option.fullDescription
which is new in v7 so not released yet.Callers can:
For example:
ChangeLog