|
const { humanReadableArgName } = require('./argument.js'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Help { |
|
constructor() { |
|
this.helpWidth = undefined; |
|
this.minWidthToWrap = 40; |
|
this.sortSubcommands = false; |
|
this.sortOptions = false; |
|
this.showGlobalOptions = false; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
prepareContext(contextOptions) { |
|
this.helpWidth = this.helpWidth ?? contextOptions.helpWidth ?? 80; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visibleCommands(cmd) { |
|
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) => { |
|
|
|
return a.name().localeCompare(b.name()); |
|
}); |
|
} |
|
return visibleCommands; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
compareOptions(a, b) { |
|
const getSortKey = (option) => { |
|
|
|
return option.short |
|
? option.short.replace(/^-/, '') |
|
: option.long.replace(/^--/, ''); |
|
}; |
|
return getSortKey(a).localeCompare(getSortKey(b)); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visibleOptions(cmd) { |
|
const visibleOptions = cmd.options.filter((option) => !option.hidden); |
|
|
|
const helpOption = cmd._getHelpOption(); |
|
if (helpOption && !helpOption.hidden) { |
|
|
|
const removeShort = helpOption.short && cmd._findOption(helpOption.short); |
|
const removeLong = helpOption.long && cmd._findOption(helpOption.long); |
|
if (!removeShort && !removeLong) { |
|
visibleOptions.push(helpOption); |
|
} 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), |
|
); |
|
} |
|
} |
|
if (this.sortOptions) { |
|
visibleOptions.sort(this.compareOptions); |
|
} |
|
return visibleOptions; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visibleGlobalOptions(cmd) { |
|
if (!this.showGlobalOptions) return []; |
|
|
|
const globalOptions = []; |
|
for ( |
|
let ancestorCmd = cmd.parent; |
|
ancestorCmd; |
|
ancestorCmd = ancestorCmd.parent |
|
) { |
|
const visibleOptions = ancestorCmd.options.filter( |
|
(option) => !option.hidden, |
|
); |
|
globalOptions.push(...visibleOptions); |
|
} |
|
if (this.sortOptions) { |
|
globalOptions.sort(this.compareOptions); |
|
} |
|
return globalOptions; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
visibleArguments(cmd) { |
|
|
|
if (cmd._argsDescription) { |
|
cmd.registeredArguments.forEach((argument) => { |
|
argument.description = |
|
argument.description || cmd._argsDescription[argument.name()] || ''; |
|
}); |
|
} |
|
|
|
|
|
if (cmd.registeredArguments.find((argument) => argument.description)) { |
|
return cmd.registeredArguments; |
|
} |
|
return []; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
subcommandTerm(cmd) { |
|
|
|
const args = cmd.registeredArguments |
|
.map((arg) => humanReadableArgName(arg)) |
|
.join(' '); |
|
return ( |
|
cmd._name + |
|
(cmd._aliases[0] ? '|' + cmd._aliases[0] : '') + |
|
(cmd.options.length ? ' [options]' : '') + |
|
(args ? ' ' + args : '') |
|
); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
optionTerm(option) { |
|
return option.flags; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
argumentTerm(argument) { |
|
return argument.name(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
longestSubcommandTermLength(cmd, helper) { |
|
return helper.visibleCommands(cmd).reduce((max, command) => { |
|
return Math.max( |
|
max, |
|
this.displayWidth( |
|
helper.styleSubcommandTerm(helper.subcommandTerm(command)), |
|
), |
|
); |
|
}, 0); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
longestOptionTermLength(cmd, helper) { |
|
return helper.visibleOptions(cmd).reduce((max, option) => { |
|
return Math.max( |
|
max, |
|
this.displayWidth(helper.styleOptionTerm(helper.optionTerm(option))), |
|
); |
|
}, 0); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
longestGlobalOptionTermLength(cmd, helper) { |
|
return helper.visibleGlobalOptions(cmd).reduce((max, option) => { |
|
return Math.max( |
|
max, |
|
this.displayWidth(helper.styleOptionTerm(helper.optionTerm(option))), |
|
); |
|
}, 0); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
longestArgumentTermLength(cmd, helper) { |
|
return helper.visibleArguments(cmd).reduce((max, argument) => { |
|
return Math.max( |
|
max, |
|
this.displayWidth( |
|
helper.styleArgumentTerm(helper.argumentTerm(argument)), |
|
), |
|
); |
|
}, 0); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
commandUsage(cmd) { |
|
|
|
let cmdName = cmd._name; |
|
if (cmd._aliases[0]) { |
|
cmdName = cmdName + '|' + cmd._aliases[0]; |
|
} |
|
let ancestorCmdNames = ''; |
|
for ( |
|
let ancestorCmd = cmd.parent; |
|
ancestorCmd; |
|
ancestorCmd = ancestorCmd.parent |
|
) { |
|
ancestorCmdNames = ancestorCmd.name() + ' ' + ancestorCmdNames; |
|
} |
|
return ancestorCmdNames + cmdName + ' ' + cmd.usage(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
commandDescription(cmd) { |
|
|
|
return cmd.description(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
subcommandDescription(cmd) { |
|
|
|
return cmd.summary() || cmd.description(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
optionDescription(option) { |
|
const extraInfo = []; |
|
|
|
if (option.argChoices) { |
|
extraInfo.push( |
|
|
|
`choices: ${option.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`, |
|
); |
|
} |
|
if (option.defaultValue !== undefined) { |
|
|
|
|
|
const showDefault = |
|
option.required || |
|
option.optional || |
|
(option.isBoolean() && typeof option.defaultValue === 'boolean'); |
|
if (showDefault) { |
|
extraInfo.push( |
|
`default: ${option.defaultValueDescription || JSON.stringify(option.defaultValue)}`, |
|
); |
|
} |
|
} |
|
|
|
if (option.presetArg !== undefined && option.optional) { |
|
extraInfo.push(`preset: ${JSON.stringify(option.presetArg)}`); |
|
} |
|
if (option.envVar !== undefined) { |
|
extraInfo.push(`env: ${option.envVar}`); |
|
} |
|
if (extraInfo.length > 0) { |
|
return `${option.description} (${extraInfo.join(', ')})`; |
|
} |
|
|
|
return option.description; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
argumentDescription(argument) { |
|
const extraInfo = []; |
|
if (argument.argChoices) { |
|
extraInfo.push( |
|
|
|
`choices: ${argument.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`, |
|
); |
|
} |
|
if (argument.defaultValue !== undefined) { |
|
extraInfo.push( |
|
`default: ${argument.defaultValueDescription || JSON.stringify(argument.defaultValue)}`, |
|
); |
|
} |
|
if (extraInfo.length > 0) { |
|
const extraDescription = `(${extraInfo.join(', ')})`; |
|
if (argument.description) { |
|
return `${argument.description} ${extraDescription}`; |
|
} |
|
return extraDescription; |
|
} |
|
return argument.description; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
formatHelp(cmd, helper) { |
|
const termWidth = helper.padWidth(cmd, helper); |
|
const helpWidth = helper.helpWidth ?? 80; |
|
|
|
function callFormatItem(term, description) { |
|
return helper.formatItem(term, termWidth, description, helper); |
|
} |
|
|
|
|
|
let output = [ |
|
`${helper.styleTitle('Usage:')} ${helper.styleUsage(helper.commandUsage(cmd))}`, |
|
'', |
|
]; |
|
|
|
|
|
const commandDescription = helper.commandDescription(cmd); |
|
if (commandDescription.length > 0) { |
|
output = output.concat([ |
|
helper.boxWrap( |
|
helper.styleCommandDescription(commandDescription), |
|
helpWidth, |
|
), |
|
'', |
|
]); |
|
} |
|
|
|
|
|
const argumentList = helper.visibleArguments(cmd).map((argument) => { |
|
return callFormatItem( |
|
helper.styleArgumentTerm(helper.argumentTerm(argument)), |
|
helper.styleArgumentDescription(helper.argumentDescription(argument)), |
|
); |
|
}); |
|
if (argumentList.length > 0) { |
|
output = output.concat([ |
|
helper.styleTitle('Arguments:'), |
|
...argumentList, |
|
'', |
|
]); |
|
} |
|
|
|
|
|
const optionList = helper.visibleOptions(cmd).map((option) => { |
|
return callFormatItem( |
|
helper.styleOptionTerm(helper.optionTerm(option)), |
|
helper.styleOptionDescription(helper.optionDescription(option)), |
|
); |
|
}); |
|
if (optionList.length > 0) { |
|
output = output.concat([ |
|
helper.styleTitle('Options:'), |
|
...optionList, |
|
'', |
|
]); |
|
} |
|
|
|
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([ |
|
helper.styleTitle('Global Options:'), |
|
...globalOptionList, |
|
'', |
|
]); |
|
} |
|
} |
|
|
|
|
|
const commandList = helper.visibleCommands(cmd).map((cmd) => { |
|
return callFormatItem( |
|
helper.styleSubcommandTerm(helper.subcommandTerm(cmd)), |
|
helper.styleSubcommandDescription(helper.subcommandDescription(cmd)), |
|
); |
|
}); |
|
if (commandList.length > 0) { |
|
output = output.concat([ |
|
helper.styleTitle('Commands:'), |
|
...commandList, |
|
'', |
|
]); |
|
} |
|
|
|
return output.join('\n'); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
displayWidth(str) { |
|
return stripColor(str).length; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
styleTitle(str) { |
|
return str; |
|
} |
|
|
|
styleUsage(str) { |
|
|
|
|
|
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); |
|
}) |
|
.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) { |
|
|
|
|
|
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); |
|
}) |
|
.join(' '); |
|
} |
|
styleArgumentTerm(str) { |
|
return this.styleArgumentText(str); |
|
} |
|
styleOptionText(str) { |
|
return str; |
|
} |
|
styleArgumentText(str) { |
|
return str; |
|
} |
|
styleSubcommandText(str) { |
|
return str; |
|
} |
|
styleCommandText(str) { |
|
return str; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
padWidth(cmd, helper) { |
|
return Math.max( |
|
helper.longestOptionTermLength(cmd, helper), |
|
helper.longestGlobalOptionTermLength(cmd, helper), |
|
helper.longestSubcommandTermLength(cmd, helper), |
|
helper.longestArgumentTermLength(cmd, helper), |
|
); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
preformatted(str) { |
|
return /\n[^\S\r\n]/.test(str); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
formatItem(term, termWidth, description, helper) { |
|
const itemIndent = 2; |
|
const itemIndentStr = ' '.repeat(itemIndent); |
|
if (!description) return itemIndentStr + term; |
|
|
|
|
|
const paddedTerm = term.padEnd( |
|
termWidth + term.length - helper.displayWidth(term), |
|
); |
|
|
|
|
|
const spacerWidth = 2; |
|
const helpWidth = this.helpWidth ?? 80; |
|
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), |
|
); |
|
} |
|
|
|
|
|
return ( |
|
itemIndentStr + |
|
paddedTerm + |
|
' '.repeat(spacerWidth) + |
|
formattedDescription.replace(/\n/g, `\n${itemIndentStr}`) |
|
); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
boxWrap(str, width) { |
|
if (width < this.minWidthToWrap) return str; |
|
|
|
const rawLines = str.split(/\r\n|\n/); |
|
|
|
const chunkPattern = /[\s]*[^\s]+/g; |
|
const wrappedLines = []; |
|
rawLines.forEach((line) => { |
|
const chunks = line.match(chunkPattern); |
|
if (chunks === null) { |
|
wrappedLines.push(''); |
|
return; |
|
} |
|
|
|
let sumChunks = [chunks.shift()]; |
|
let sumWidth = this.displayWidth(sumChunks[0]); |
|
chunks.forEach((chunk) => { |
|
const visibleWidth = this.displayWidth(chunk); |
|
|
|
if (sumWidth + visibleWidth <= width) { |
|
sumChunks.push(chunk); |
|
sumWidth += visibleWidth; |
|
return; |
|
} |
|
wrappedLines.push(sumChunks.join('')); |
|
|
|
const nextChunk = chunk.trimStart(); |
|
sumChunks = [nextChunk]; |
|
sumWidth = this.displayWidth(nextChunk); |
|
}); |
|
wrappedLines.push(sumChunks.join('')); |
|
}); |
|
|
|
return wrappedLines.join('\n'); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function stripColor(str) { |
|
|
|
const sgrPattern = /\x1b\[\d*(;\d*)*m/g; |
|
return str.replace(sgrPattern, ''); |
|
} |
|
|
|
exports.Help = Help; |
|
exports.stripColor = stripColor; |
|
|