This commit is contained in:
2026-02-13 23:49:12 +01:00
parent 206b8f08db
commit 2e02d6d763
36 changed files with 1816 additions and 4178 deletions

447
node_modules/commander/lib/help.js generated vendored
View File

@@ -12,11 +12,24 @@ const { humanReadableArgName } = require('./argument.js');
class Help {
constructor() {
this.helpWidth = undefined;
this.minWidthToWrap = 40;
this.sortSubcommands = false;
this.sortOptions = false;
this.showGlobalOptions = false;
}
/**
* prepareContext is called by Commander after applying overrides from `Command.configureHelp()`
* and just before calling `formatHelp()`.
*
* Commander just uses the helpWidth and the rest is provided for optional use by more complex subclasses.
*
* @param {{ error?: boolean, helpWidth?: number, outputHasColors?: boolean }} contextOptions
*/
prepareContext(contextOptions) {
this.helpWidth = this.helpWidth ?? contextOptions.helpWidth ?? 80;
}
/**
* Get an array of the visible subcommands. Includes a placeholder for the implicit help command, if there is one.
*
@@ -25,19 +38,14 @@ class Help {
*/
visibleCommands(cmd) {
const visibleCommands = cmd.commands.filter(cmd => !cmd._hidden);
if (cmd._hasImplicitHelpCommand()) {
// Create a command matching the implicit help command.
const [, helpName, helpArgs] = cmd._helpCommandnameAndArgs.match(/([^ ]+) *(.*)/);
const helpCommand = cmd.createCommand(helpName)
.helpOption(false);
helpCommand.description(cmd._helpCommandDescription);
if (helpArgs) helpCommand.arguments(helpArgs);
const visibleCommands = cmd.commands.filter((cmd) => !cmd._hidden);
const helpCommand = cmd._getHelpCommand();
if (helpCommand && !helpCommand._hidden) {
visibleCommands.push(helpCommand);
}
if (this.sortSubcommands) {
visibleCommands.sort((a, b) => {
// @ts-ignore: overloaded return type
// @ts-ignore: because overloaded return type
return a.name().localeCompare(b.name());
});
}
@@ -49,12 +57,14 @@ class Help {
*
* @param {Option} a
* @param {Option} b
* @returns number
* @returns {number}
*/
compareOptions(a, b) {
const getSortKey = (option) => {
// WYSIWYG for order displayed in help. Short used for comparison if present. No special handling for negated.
return option.short ? option.short.replace(/^-/, '') : option.long.replace(/^--/, '');
return option.short
? option.short.replace(/^-/, '')
: option.long.replace(/^--/, '');
};
return getSortKey(a).localeCompare(getSortKey(b));
}
@@ -68,19 +78,23 @@ class Help {
visibleOptions(cmd) {
const visibleOptions = cmd.options.filter((option) => !option.hidden);
// Implicit help
const showShortHelpFlag = cmd._hasHelpOption && cmd._helpShortFlag && !cmd._findOption(cmd._helpShortFlag);
const showLongHelpFlag = cmd._hasHelpOption && !cmd._findOption(cmd._helpLongFlag);
if (showShortHelpFlag || showLongHelpFlag) {
let helpOption;
if (!showShortHelpFlag) {
helpOption = cmd.createOption(cmd._helpLongFlag, cmd._helpDescription);
} else if (!showLongHelpFlag) {
helpOption = cmd.createOption(cmd._helpShortFlag, cmd._helpDescription);
} else {
helpOption = cmd.createOption(cmd._helpFlags, cmd._helpDescription);
// Built-in help option.
const helpOption = cmd._getHelpOption();
if (helpOption && !helpOption.hidden) {
// Automatically hide conflicting flags. Bit dubious but a historical behaviour that is convenient for single-command programs.
const removeShort = helpOption.short && cmd._findOption(helpOption.short);
const removeLong = helpOption.long && cmd._findOption(helpOption.long);
if (!removeShort && !removeLong) {
visibleOptions.push(helpOption); // no changes needed
} else if (helpOption.long && !removeLong) {
visibleOptions.push(
cmd.createOption(helpOption.long, helpOption.description),
);
} else if (helpOption.short && !removeShort) {
visibleOptions.push(
cmd.createOption(helpOption.short, helpOption.description),
);
}
visibleOptions.push(helpOption);
}
if (this.sortOptions) {
visibleOptions.sort(this.compareOptions);
@@ -99,8 +113,14 @@ class Help {
if (!this.showGlobalOptions) return [];
const globalOptions = [];
for (let ancestorCmd = cmd.parent; ancestorCmd; ancestorCmd = ancestorCmd.parent) {
const visibleOptions = ancestorCmd.options.filter((option) => !option.hidden);
for (
let ancestorCmd = cmd.parent;
ancestorCmd;
ancestorCmd = ancestorCmd.parent
) {
const visibleOptions = ancestorCmd.options.filter(
(option) => !option.hidden,
);
globalOptions.push(...visibleOptions);
}
if (this.sortOptions) {
@@ -119,13 +139,14 @@ class Help {
visibleArguments(cmd) {
// Side effect! Apply the legacy descriptions before the arguments are displayed.
if (cmd._argsDescription) {
cmd.registeredArguments.forEach(argument => {
argument.description = argument.description || cmd._argsDescription[argument.name()] || '';
cmd.registeredArguments.forEach((argument) => {
argument.description =
argument.description || cmd._argsDescription[argument.name()] || '';
});
}
// If there are any arguments with a description then return all the arguments.
if (cmd.registeredArguments.find(argument => argument.description)) {
if (cmd.registeredArguments.find((argument) => argument.description)) {
return cmd.registeredArguments;
}
return [];
@@ -140,11 +161,15 @@ class Help {
subcommandTerm(cmd) {
// Legacy. Ignores custom usage string, and nested commands.
const args = cmd.registeredArguments.map(arg => humanReadableArgName(arg)).join(' ');
return cmd._name +
const args = cmd.registeredArguments
.map((arg) => humanReadableArgName(arg))
.join(' ');
return (
cmd._name +
(cmd._aliases[0] ? '|' + cmd._aliases[0] : '') +
(cmd.options.length ? ' [options]' : '') + // simplistic check for non-help option
(args ? ' ' + args : '');
(args ? ' ' + args : '')
);
}
/**
@@ -179,7 +204,12 @@ class Help {
longestSubcommandTermLength(cmd, helper) {
return helper.visibleCommands(cmd).reduce((max, command) => {
return Math.max(max, helper.subcommandTerm(command).length);
return Math.max(
max,
this.displayWidth(
helper.styleSubcommandTerm(helper.subcommandTerm(command)),
),
);
}, 0);
}
@@ -193,7 +223,10 @@ class Help {
longestOptionTermLength(cmd, helper) {
return helper.visibleOptions(cmd).reduce((max, option) => {
return Math.max(max, helper.optionTerm(option).length);
return Math.max(
max,
this.displayWidth(helper.styleOptionTerm(helper.optionTerm(option))),
);
}, 0);
}
@@ -207,7 +240,10 @@ class Help {
longestGlobalOptionTermLength(cmd, helper) {
return helper.visibleGlobalOptions(cmd).reduce((max, option) => {
return Math.max(max, helper.optionTerm(option).length);
return Math.max(
max,
this.displayWidth(helper.styleOptionTerm(helper.optionTerm(option))),
);
}, 0);
}
@@ -221,7 +257,12 @@ class Help {
longestArgumentTermLength(cmd, helper) {
return helper.visibleArguments(cmd).reduce((max, argument) => {
return Math.max(max, helper.argumentTerm(argument).length);
return Math.max(
max,
this.displayWidth(
helper.styleArgumentTerm(helper.argumentTerm(argument)),
),
);
}, 0);
}
@@ -239,7 +280,11 @@ class Help {
cmdName = cmdName + '|' + cmd._aliases[0];
}
let ancestorCmdNames = '';
for (let ancestorCmd = cmd.parent; ancestorCmd; ancestorCmd = ancestorCmd.parent) {
for (
let ancestorCmd = cmd.parent;
ancestorCmd;
ancestorCmd = ancestorCmd.parent
) {
ancestorCmdNames = ancestorCmd.name() + ' ' + ancestorCmdNames;
}
return ancestorCmdNames + cmdName + ' ' + cmd.usage();
@@ -253,7 +298,7 @@ class Help {
*/
commandDescription(cmd) {
// @ts-ignore: overloaded return type
// @ts-ignore: because overloaded return type
return cmd.description();
}
@@ -266,7 +311,7 @@ class Help {
*/
subcommandDescription(cmd) {
// @ts-ignore: overloaded return type
// @ts-ignore: because overloaded return type
return cmd.summary() || cmd.description();
}
@@ -283,15 +328,20 @@ class Help {
if (option.argChoices) {
extraInfo.push(
// use stringify to match the display of the default value
`choices: ${option.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`);
`choices: ${option.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`,
);
}
if (option.defaultValue !== undefined) {
// default for boolean and negated more for programmer than end user,
// but show true/false for boolean option as may be for hand-rolled env or config processing.
const showDefault = option.required || option.optional ||
const showDefault =
option.required ||
option.optional ||
(option.isBoolean() && typeof option.defaultValue === 'boolean');
if (showDefault) {
extraInfo.push(`default: ${option.defaultValueDescription || JSON.stringify(option.defaultValue)}`);
extraInfo.push(
`default: ${option.defaultValueDescription || JSON.stringify(option.defaultValue)}`,
);
}
}
// preset for boolean and negated are more for programmer than end user
@@ -320,17 +370,20 @@ class Help {
if (argument.argChoices) {
extraInfo.push(
// use stringify to match the display of the default value
`choices: ${argument.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`);
`choices: ${argument.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`,
);
}
if (argument.defaultValue !== undefined) {
extraInfo.push(`default: ${argument.defaultValueDescription || JSON.stringify(argument.defaultValue)}`);
extraInfo.push(
`default: ${argument.defaultValueDescription || JSON.stringify(argument.defaultValue)}`,
);
}
if (extraInfo.length > 0) {
const extraDescripton = `(${extraInfo.join(', ')})`;
const extraDescription = `(${extraInfo.join(', ')})`;
if (argument.description) {
return `${argument.description} ${extraDescripton}`;
return `${argument.description} ${extraDescription}`;
}
return extraDescripton;
return extraDescription;
}
return argument.description;
}
@@ -345,65 +398,177 @@ class Help {
formatHelp(cmd, helper) {
const termWidth = helper.padWidth(cmd, helper);
const helpWidth = helper.helpWidth || 80;
const itemIndentWidth = 2;
const itemSeparatorWidth = 2; // between term and description
function formatItem(term, description) {
if (description) {
const fullText = `${term.padEnd(termWidth + itemSeparatorWidth)}${description}`;
return helper.wrap(fullText, helpWidth - itemIndentWidth, termWidth + itemSeparatorWidth);
}
return term;
}
function formatList(textArray) {
return textArray.join('\n').replace(/^/gm, ' '.repeat(itemIndentWidth));
const helpWidth = helper.helpWidth ?? 80; // in case prepareContext() was not called
function callFormatItem(term, description) {
return helper.formatItem(term, termWidth, description, helper);
}
// Usage
let output = [`Usage: ${helper.commandUsage(cmd)}`, ''];
let output = [
`${helper.styleTitle('Usage:')} ${helper.styleUsage(helper.commandUsage(cmd))}`,
'',
];
// Description
const commandDescription = helper.commandDescription(cmd);
if (commandDescription.length > 0) {
output = output.concat([helper.wrap(commandDescription, helpWidth, 0), '']);
output = output.concat([
helper.boxWrap(
helper.styleCommandDescription(commandDescription),
helpWidth,
),
'',
]);
}
// Arguments
const argumentList = helper.visibleArguments(cmd).map((argument) => {
return formatItem(helper.argumentTerm(argument), helper.argumentDescription(argument));
return callFormatItem(
helper.styleArgumentTerm(helper.argumentTerm(argument)),
helper.styleArgumentDescription(helper.argumentDescription(argument)),
);
});
if (argumentList.length > 0) {
output = output.concat(['Arguments:', formatList(argumentList), '']);
output = output.concat([
helper.styleTitle('Arguments:'),
...argumentList,
'',
]);
}
// Options
const optionList = helper.visibleOptions(cmd).map((option) => {
return formatItem(helper.optionTerm(option), helper.optionDescription(option));
return callFormatItem(
helper.styleOptionTerm(helper.optionTerm(option)),
helper.styleOptionDescription(helper.optionDescription(option)),
);
});
if (optionList.length > 0) {
output = output.concat(['Options:', formatList(optionList), '']);
output = output.concat([
helper.styleTitle('Options:'),
...optionList,
'',
]);
}
if (this.showGlobalOptions) {
const globalOptionList = helper.visibleGlobalOptions(cmd).map((option) => {
return formatItem(helper.optionTerm(option), helper.optionDescription(option));
});
if (helper.showGlobalOptions) {
const globalOptionList = helper
.visibleGlobalOptions(cmd)
.map((option) => {
return callFormatItem(
helper.styleOptionTerm(helper.optionTerm(option)),
helper.styleOptionDescription(helper.optionDescription(option)),
);
});
if (globalOptionList.length > 0) {
output = output.concat(['Global Options:', formatList(globalOptionList), '']);
output = output.concat([
helper.styleTitle('Global Options:'),
...globalOptionList,
'',
]);
}
}
// Commands
const commandList = helper.visibleCommands(cmd).map((cmd) => {
return formatItem(helper.subcommandTerm(cmd), helper.subcommandDescription(cmd));
return callFormatItem(
helper.styleSubcommandTerm(helper.subcommandTerm(cmd)),
helper.styleSubcommandDescription(helper.subcommandDescription(cmd)),
);
});
if (commandList.length > 0) {
output = output.concat(['Commands:', formatList(commandList), '']);
output = output.concat([
helper.styleTitle('Commands:'),
...commandList,
'',
]);
}
return output.join('\n');
}
/**
* Return display width of string, ignoring ANSI escape sequences. Used in padding and wrapping calculations.
*
* @param {string} str
* @returns {number}
*/
displayWidth(str) {
return stripColor(str).length;
}
/**
* Style the title for displaying in the help. Called with 'Usage:', 'Options:', etc.
*
* @param {string} str
* @returns {string}
*/
styleTitle(str) {
return str;
}
styleUsage(str) {
// Usage has lots of parts the user might like to color separately! Assume default usage string which is formed like:
// command subcommand [options] [command] <foo> [bar]
return str
.split(' ')
.map((word) => {
if (word === '[options]') return this.styleOptionText(word);
if (word === '[command]') return this.styleSubcommandText(word);
if (word[0] === '[' || word[0] === '<')
return this.styleArgumentText(word);
return this.styleCommandText(word); // Restrict to initial words?
})
.join(' ');
}
styleCommandDescription(str) {
return this.styleDescriptionText(str);
}
styleOptionDescription(str) {
return this.styleDescriptionText(str);
}
styleSubcommandDescription(str) {
return this.styleDescriptionText(str);
}
styleArgumentDescription(str) {
return this.styleDescriptionText(str);
}
styleDescriptionText(str) {
return str;
}
styleOptionTerm(str) {
return this.styleOptionText(str);
}
styleSubcommandTerm(str) {
// This is very like usage with lots of parts! Assume default string which is formed like:
// subcommand [options] <foo> [bar]
return str
.split(' ')
.map((word) => {
if (word === '[options]') return this.styleOptionText(word);
if (word[0] === '[' || word[0] === '<')
return this.styleArgumentText(word);
return this.styleSubcommandText(word); // Restrict to initial words?
})
.join(' ');
}
styleArgumentTerm(str) {
return this.styleArgumentText(str);
}
styleOptionText(str) {
return str;
}
styleArgumentText(str) {
return str;
}
styleSubcommandText(str) {
return str;
}
styleCommandText(str) {
return str;
}
/**
* Calculate the pad width from the maximum term length.
*
@@ -417,46 +582,128 @@ class Help {
helper.longestOptionTermLength(cmd, helper),
helper.longestGlobalOptionTermLength(cmd, helper),
helper.longestSubcommandTermLength(cmd, helper),
helper.longestArgumentTermLength(cmd, helper)
helper.longestArgumentTermLength(cmd, helper),
);
}
/**
* Wrap the given string to width characters per line, with lines after the first indented.
* Do not wrap if insufficient room for wrapping (minColumnWidth), or string is manually formatted.
* Detect manually wrapped and indented strings by checking for line break followed by whitespace.
*
* @param {string} str
* @returns {boolean}
*/
preformatted(str) {
return /\n[^\S\r\n]/.test(str);
}
/**
* Format the "item", which consists of a term and description. Pad the term and wrap the description, indenting the following lines.
*
* So "TTT", 5, "DDD DDDD DD DDD" might be formatted for this.helpWidth=17 like so:
* TTT DDD DDDD
* DD DDD
*
* @param {string} term
* @param {number} termWidth
* @param {string} description
* @param {Help} helper
* @returns {string}
*/
formatItem(term, termWidth, description, helper) {
const itemIndent = 2;
const itemIndentStr = ' '.repeat(itemIndent);
if (!description) return itemIndentStr + term;
// Pad the term out to a consistent width, so descriptions are aligned.
const paddedTerm = term.padEnd(
termWidth + term.length - helper.displayWidth(term),
);
// Format the description.
const spacerWidth = 2; // between term and description
const helpWidth = this.helpWidth ?? 80; // in case prepareContext() was not called
const remainingWidth = helpWidth - termWidth - spacerWidth - itemIndent;
let formattedDescription;
if (
remainingWidth < this.minWidthToWrap ||
helper.preformatted(description)
) {
formattedDescription = description;
} else {
const wrappedDescription = helper.boxWrap(description, remainingWidth);
formattedDescription = wrappedDescription.replace(
/\n/g,
'\n' + ' '.repeat(termWidth + spacerWidth),
);
}
// Construct and overall indent.
return (
itemIndentStr +
paddedTerm +
' '.repeat(spacerWidth) +
formattedDescription.replace(/\n/g, `\n${itemIndentStr}`)
);
}
/**
* Wrap a string at whitespace, preserving existing line breaks.
* Wrapping is skipped if the width is less than `minWidthToWrap`.
*
* @param {string} str
* @param {number} width
* @param {number} indent
* @param {number} [minColumnWidth=40]
* @return {string}
*
* @returns {string}
*/
boxWrap(str, width) {
if (width < this.minWidthToWrap) return str;
wrap(str, width, indent, minColumnWidth = 40) {
// Full \s characters, minus the linefeeds.
const indents = ' \\f\\t\\v\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\ufeff';
// Detect manually wrapped and indented strings by searching for line break followed by spaces.
const manualIndent = new RegExp(`[\\n][${indents}]+`);
if (str.match(manualIndent)) return str;
// Do not wrap if not enough room for a wrapped column of text (as could end up with a word per line).
const columnWidth = width - indent;
if (columnWidth < minColumnWidth) return str;
const rawLines = str.split(/\r\n|\n/);
// split up text by whitespace
const chunkPattern = /[\s]*[^\s]+/g;
const wrappedLines = [];
rawLines.forEach((line) => {
const chunks = line.match(chunkPattern);
if (chunks === null) {
wrappedLines.push('');
return;
}
const leadingStr = str.slice(0, indent);
const columnText = str.slice(indent).replace('\r\n', '\n');
const indentString = ' '.repeat(indent);
const zeroWidthSpace = '\u200B';
const breaks = `\\s${zeroWidthSpace}`;
// Match line end (so empty lines don't collapse),
// or as much text as will fit in column, or excess text up to first break.
const regex = new RegExp(`\n|.{1,${columnWidth - 1}}([${breaks}]|$)|[^${breaks}]+?([${breaks}]|$)`, 'g');
const lines = columnText.match(regex) || [];
return leadingStr + lines.map((line, i) => {
if (line === '\n') return ''; // preserve empty lines
return ((i > 0) ? indentString : '') + line.trimEnd();
}).join('\n');
let sumChunks = [chunks.shift()];
let sumWidth = this.displayWidth(sumChunks[0]);
chunks.forEach((chunk) => {
const visibleWidth = this.displayWidth(chunk);
// Accumulate chunks while they fit into width.
if (sumWidth + visibleWidth <= width) {
sumChunks.push(chunk);
sumWidth += visibleWidth;
return;
}
wrappedLines.push(sumChunks.join(''));
const nextChunk = chunk.trimStart(); // trim space at line break
sumChunks = [nextChunk];
sumWidth = this.displayWidth(nextChunk);
});
wrappedLines.push(sumChunks.join(''));
});
return wrappedLines.join('\n');
}
}
/**
* Strip style ANSI escape sequences from the string. In particular, SGR (Select Graphic Rendition) codes.
*
* @param {string} str
* @returns {string}
* @package
*/
function stripColor(str) {
// eslint-disable-next-line no-control-regex
const sgrPattern = /\x1b\[\d*(;\d*)*m/g;
return str.replace(sgrPattern, '');
}
exports.Help = Help;
exports.stripColor = stripColor;