mirror of https://github.com/jkjoy/sunpeiwen.git
269 lines
6.8 KiB
TypeScript
269 lines
6.8 KiB
TypeScript
|
import CAC from "./CAC.ts";
|
||
|
import Option, { OptionConfig } from "./Option.ts";
|
||
|
import { removeBrackets, findAllBrackets, findLongest, padRight, CACError } from "./utils.ts";
|
||
|
import { platformInfo } from "./deno.ts";
|
||
|
interface CommandArg {
|
||
|
required: boolean;
|
||
|
value: string;
|
||
|
variadic: boolean;
|
||
|
}
|
||
|
interface HelpSection {
|
||
|
title?: string;
|
||
|
body: string;
|
||
|
}
|
||
|
interface CommandConfig {
|
||
|
allowUnknownOptions?: boolean;
|
||
|
ignoreOptionDefaultValue?: boolean;
|
||
|
}
|
||
|
type HelpCallback = (sections: HelpSection[]) => void | HelpSection[];
|
||
|
type CommandExample = ((bin: string) => string) | string;
|
||
|
|
||
|
class Command {
|
||
|
options: Option[];
|
||
|
aliasNames: string[];
|
||
|
/* Parsed command name */
|
||
|
|
||
|
name: string;
|
||
|
args: CommandArg[];
|
||
|
commandAction?: (...args: any[]) => any;
|
||
|
usageText?: string;
|
||
|
versionNumber?: string;
|
||
|
examples: CommandExample[];
|
||
|
helpCallback?: HelpCallback;
|
||
|
globalCommand?: GlobalCommand;
|
||
|
|
||
|
constructor(public rawName: string, public description: string, public config: CommandConfig = {}, public cli: CAC) {
|
||
|
this.options = [];
|
||
|
this.aliasNames = [];
|
||
|
this.name = removeBrackets(rawName);
|
||
|
this.args = findAllBrackets(rawName);
|
||
|
this.examples = [];
|
||
|
}
|
||
|
|
||
|
usage(text: string) {
|
||
|
this.usageText = text;
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
allowUnknownOptions() {
|
||
|
this.config.allowUnknownOptions = true;
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
ignoreOptionDefaultValue() {
|
||
|
this.config.ignoreOptionDefaultValue = true;
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
version(version: string, customFlags = '-v, --version') {
|
||
|
this.versionNumber = version;
|
||
|
this.option(customFlags, 'Display version number');
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
example(example: CommandExample) {
|
||
|
this.examples.push(example);
|
||
|
return this;
|
||
|
}
|
||
|
/**
|
||
|
* Add a option for this command
|
||
|
* @param rawName Raw option name(s)
|
||
|
* @param description Option description
|
||
|
* @param config Option config
|
||
|
*/
|
||
|
|
||
|
|
||
|
option(rawName: string, description: string, config?: OptionConfig) {
|
||
|
const option = new Option(rawName, description, config);
|
||
|
this.options.push(option);
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
alias(name: string) {
|
||
|
this.aliasNames.push(name);
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
action(callback: (...args: any[]) => any) {
|
||
|
this.commandAction = callback;
|
||
|
return this;
|
||
|
}
|
||
|
/**
|
||
|
* Check if a command name is matched by this command
|
||
|
* @param name Command name
|
||
|
*/
|
||
|
|
||
|
|
||
|
isMatched(name: string) {
|
||
|
return this.name === name || this.aliasNames.includes(name);
|
||
|
}
|
||
|
|
||
|
get isDefaultCommand() {
|
||
|
return this.name === '' || this.aliasNames.includes('!');
|
||
|
}
|
||
|
|
||
|
get isGlobalCommand(): boolean {
|
||
|
return this instanceof GlobalCommand;
|
||
|
}
|
||
|
/**
|
||
|
* Check if an option is registered in this command
|
||
|
* @param name Option name
|
||
|
*/
|
||
|
|
||
|
|
||
|
hasOption(name: string) {
|
||
|
name = name.split('.')[0];
|
||
|
return this.options.find(option => {
|
||
|
return option.names.includes(name);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
outputHelp() {
|
||
|
const {
|
||
|
name,
|
||
|
commands
|
||
|
} = this.cli;
|
||
|
const {
|
||
|
versionNumber,
|
||
|
options: globalOptions,
|
||
|
helpCallback
|
||
|
} = this.cli.globalCommand;
|
||
|
let sections: HelpSection[] = [{
|
||
|
body: `${name}${versionNumber ? `/${versionNumber}` : ''}`
|
||
|
}];
|
||
|
sections.push({
|
||
|
title: 'Usage',
|
||
|
body: ` $ ${name} ${this.usageText || this.rawName}`
|
||
|
});
|
||
|
const showCommands = (this.isGlobalCommand || this.isDefaultCommand) && commands.length > 0;
|
||
|
|
||
|
if (showCommands) {
|
||
|
const longestCommandName = findLongest(commands.map(command => command.rawName));
|
||
|
sections.push({
|
||
|
title: 'Commands',
|
||
|
body: commands.map(command => {
|
||
|
return ` ${padRight(command.rawName, longestCommandName.length)} ${command.description}`;
|
||
|
}).join('\n')
|
||
|
});
|
||
|
sections.push({
|
||
|
title: `For more info, run any command with the \`--help\` flag`,
|
||
|
body: commands.map(command => ` $ ${name}${command.name === '' ? '' : ` ${command.name}`} --help`).join('\n')
|
||
|
});
|
||
|
}
|
||
|
|
||
|
let options = this.isGlobalCommand ? globalOptions : [...this.options, ...(globalOptions || [])];
|
||
|
|
||
|
if (!this.isGlobalCommand && !this.isDefaultCommand) {
|
||
|
options = options.filter(option => option.name !== 'version');
|
||
|
}
|
||
|
|
||
|
if (options.length > 0) {
|
||
|
const longestOptionName = findLongest(options.map(option => option.rawName));
|
||
|
sections.push({
|
||
|
title: 'Options',
|
||
|
body: options.map(option => {
|
||
|
return ` ${padRight(option.rawName, longestOptionName.length)} ${option.description} ${option.config.default === undefined ? '' : `(default: ${option.config.default})`}`;
|
||
|
}).join('\n')
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (this.examples.length > 0) {
|
||
|
sections.push({
|
||
|
title: 'Examples',
|
||
|
body: this.examples.map(example => {
|
||
|
if (typeof example === 'function') {
|
||
|
return example(name);
|
||
|
}
|
||
|
|
||
|
return example;
|
||
|
}).join('\n')
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (helpCallback) {
|
||
|
sections = helpCallback(sections) || sections;
|
||
|
}
|
||
|
|
||
|
console.log(sections.map(section => {
|
||
|
return section.title ? `${section.title}:\n${section.body}` : section.body;
|
||
|
}).join('\n\n'));
|
||
|
}
|
||
|
|
||
|
outputVersion() {
|
||
|
const {
|
||
|
name
|
||
|
} = this.cli;
|
||
|
const {
|
||
|
versionNumber
|
||
|
} = this.cli.globalCommand;
|
||
|
|
||
|
if (versionNumber) {
|
||
|
console.log(`${name}/${versionNumber} ${platformInfo}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
checkRequiredArgs() {
|
||
|
const minimalArgsCount = this.args.filter(arg => arg.required).length;
|
||
|
|
||
|
if (this.cli.args.length < minimalArgsCount) {
|
||
|
throw new CACError(`missing required args for command \`${this.rawName}\``);
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Check if the parsed options contain any unknown options
|
||
|
*
|
||
|
* Exit and output error when true
|
||
|
*/
|
||
|
|
||
|
|
||
|
checkUnknownOptions() {
|
||
|
const {
|
||
|
options,
|
||
|
globalCommand
|
||
|
} = this.cli;
|
||
|
|
||
|
if (!this.config.allowUnknownOptions) {
|
||
|
for (const name of Object.keys(options)) {
|
||
|
if (name !== '--' && !this.hasOption(name) && !globalCommand.hasOption(name)) {
|
||
|
throw new CACError(`Unknown option \`${name.length > 1 ? `--${name}` : `-${name}`}\``);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Check if the required string-type options exist
|
||
|
*/
|
||
|
|
||
|
|
||
|
checkOptionValue() {
|
||
|
const {
|
||
|
options: parsedOptions,
|
||
|
globalCommand
|
||
|
} = this.cli;
|
||
|
const options = [...globalCommand.options, ...this.options];
|
||
|
|
||
|
for (const option of options) {
|
||
|
const value = parsedOptions[option.name.split('.')[0]]; // Check required option value
|
||
|
|
||
|
if (option.required) {
|
||
|
const hasNegated = options.some(o => o.negated && o.names.includes(option.name));
|
||
|
|
||
|
if (value === true || value === false && !hasNegated) {
|
||
|
throw new CACError(`option \`${option.rawName}\` value is missing`);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
class GlobalCommand extends Command {
|
||
|
constructor(cli: CAC) {
|
||
|
super('@@global@@', '', {}, cli);
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
export type { HelpCallback, CommandExample, CommandConfig };
|
||
|
export { GlobalCommand };
|
||
|
export default Command;
|