You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
331 lines
7.9 KiB
331 lines
7.9 KiB
import { EventEmitter } from "https://deno.land/std@0.80.0/node/events.ts"; |
|
import mri from "https://cdn.skypack.dev/mri"; |
|
import Command, { GlobalCommand, CommandConfig, HelpCallback, CommandExample } from "./Command.ts"; |
|
import { OptionConfig } from "./Option.ts"; |
|
import { getMriOptions, setDotProp, setByType, getFileName, camelcaseOptionName } from "./utils.ts"; |
|
import { processArgs } from "./deno.ts"; |
|
interface ParsedArgv { |
|
args: ReadonlyArray<string>; |
|
options: { |
|
[k: string]: any; |
|
}; |
|
} |
|
|
|
class CAC extends EventEmitter { |
|
/** The program name to display in help and version message */ |
|
name: string; |
|
commands: Command[]; |
|
globalCommand: GlobalCommand; |
|
matchedCommand?: Command; |
|
matchedCommandName?: string; |
|
/** |
|
* Raw CLI arguments |
|
*/ |
|
|
|
rawArgs: string[]; |
|
/** |
|
* Parsed CLI arguments |
|
*/ |
|
|
|
args: ParsedArgv['args']; |
|
/** |
|
* Parsed CLI options, camelCased |
|
*/ |
|
|
|
options: ParsedArgv['options']; |
|
showHelpOnExit?: boolean; |
|
showVersionOnExit?: boolean; |
|
/** |
|
* @param name The program name to display in help and version message |
|
*/ |
|
|
|
constructor(name = '') { |
|
super(); |
|
this.name = name; |
|
this.commands = []; |
|
this.rawArgs = []; |
|
this.args = []; |
|
this.options = {}; |
|
this.globalCommand = new GlobalCommand(this); |
|
this.globalCommand.usage('<command> [options]'); |
|
} |
|
/** |
|
* Add a global usage text. |
|
* |
|
* This is not used by sub-commands. |
|
*/ |
|
|
|
|
|
usage(text: string) { |
|
this.globalCommand.usage(text); |
|
return this; |
|
} |
|
/** |
|
* Add a sub-command |
|
*/ |
|
|
|
|
|
command(rawName: string, description?: string, config?: CommandConfig) { |
|
const command = new Command(rawName, description || '', config, this); |
|
command.globalCommand = this.globalCommand; |
|
this.commands.push(command); |
|
return command; |
|
} |
|
/** |
|
* Add a global CLI option. |
|
* |
|
* Which is also applied to sub-commands. |
|
*/ |
|
|
|
|
|
option(rawName: string, description: string, config?: OptionConfig) { |
|
this.globalCommand.option(rawName, description, config); |
|
return this; |
|
} |
|
/** |
|
* Show help message when `-h, --help` flags appear. |
|
* |
|
*/ |
|
|
|
|
|
help(callback?: HelpCallback) { |
|
this.globalCommand.option('-h, --help', 'Display this message'); |
|
this.globalCommand.helpCallback = callback; |
|
this.showHelpOnExit = true; |
|
return this; |
|
} |
|
/** |
|
* Show version number when `-v, --version` flags appear. |
|
* |
|
*/ |
|
|
|
|
|
version(version: string, customFlags = '-v, --version') { |
|
this.globalCommand.version(version, customFlags); |
|
this.showVersionOnExit = true; |
|
return this; |
|
} |
|
/** |
|
* Add a global example. |
|
* |
|
* This example added here will not be used by sub-commands. |
|
*/ |
|
|
|
|
|
example(example: CommandExample) { |
|
this.globalCommand.example(example); |
|
return this; |
|
} |
|
/** |
|
* Output the corresponding help message |
|
* When a sub-command is matched, output the help message for the command |
|
* Otherwise output the global one. |
|
* |
|
*/ |
|
|
|
|
|
outputHelp() { |
|
if (this.matchedCommand) { |
|
this.matchedCommand.outputHelp(); |
|
} else { |
|
this.globalCommand.outputHelp(); |
|
} |
|
} |
|
/** |
|
* Output the version number. |
|
* |
|
*/ |
|
|
|
|
|
outputVersion() { |
|
this.globalCommand.outputVersion(); |
|
} |
|
|
|
private setParsedInfo({ |
|
args, |
|
options |
|
}: ParsedArgv, matchedCommand?: Command, matchedCommandName?: string) { |
|
this.args = args; |
|
this.options = options; |
|
|
|
if (matchedCommand) { |
|
this.matchedCommand = matchedCommand; |
|
} |
|
|
|
if (matchedCommandName) { |
|
this.matchedCommandName = matchedCommandName; |
|
} |
|
|
|
return this; |
|
} |
|
|
|
unsetMatchedCommand() { |
|
this.matchedCommand = undefined; |
|
this.matchedCommandName = undefined; |
|
} |
|
/** |
|
* Parse argv |
|
*/ |
|
|
|
|
|
parse(argv = processArgs, { |
|
/** Whether to run the action for matched command */ |
|
run = true |
|
} = {}): ParsedArgv { |
|
this.rawArgs = argv; |
|
|
|
if (!this.name) { |
|
this.name = argv[1] ? getFileName(argv[1]) : 'cli'; |
|
} |
|
|
|
let shouldParse = true; // Search sub-commands |
|
|
|
for (const command of this.commands) { |
|
const parsed = this.mri(argv.slice(2), command); |
|
const commandName = parsed.args[0]; |
|
|
|
if (command.isMatched(commandName)) { |
|
shouldParse = false; |
|
const parsedInfo = { ...parsed, |
|
args: parsed.args.slice(1) |
|
}; |
|
this.setParsedInfo(parsedInfo, command, commandName); |
|
this.emit(`command:${commandName}`, command); |
|
} |
|
} |
|
|
|
if (shouldParse) { |
|
// Search the default command |
|
for (const command of this.commands) { |
|
if (command.name === '') { |
|
shouldParse = false; |
|
const parsed = this.mri(argv.slice(2), command); |
|
this.setParsedInfo(parsed, command); |
|
this.emit(`command:!`, command); |
|
} |
|
} |
|
} |
|
|
|
if (shouldParse) { |
|
const parsed = this.mri(argv.slice(2)); |
|
this.setParsedInfo(parsed); |
|
} |
|
|
|
if (this.options.help && this.showHelpOnExit) { |
|
this.outputHelp(); |
|
run = false; |
|
this.unsetMatchedCommand(); |
|
} |
|
|
|
if (this.options.version && this.showVersionOnExit && this.matchedCommandName == null) { |
|
this.outputVersion(); |
|
run = false; |
|
this.unsetMatchedCommand(); |
|
} |
|
|
|
const parsedArgv = { |
|
args: this.args, |
|
options: this.options |
|
}; |
|
|
|
if (run) { |
|
this.runMatchedCommand(); |
|
} |
|
|
|
if (!this.matchedCommand && this.args[0]) { |
|
this.emit('command:*'); |
|
} |
|
|
|
return parsedArgv; |
|
} |
|
|
|
private mri(argv: string[], |
|
/** Matched command */ |
|
command?: Command): ParsedArgv { |
|
// All added options |
|
const cliOptions = [...this.globalCommand.options, ...(command ? command.options : [])]; |
|
const mriOptions = getMriOptions(cliOptions); // Extract everything after `--` since mri doesn't support it |
|
|
|
let argsAfterDoubleDashes: string[] = []; |
|
const doubleDashesIndex = argv.indexOf('--'); |
|
|
|
if (doubleDashesIndex > -1) { |
|
argsAfterDoubleDashes = argv.slice(doubleDashesIndex + 1); |
|
argv = argv.slice(0, doubleDashesIndex); |
|
} |
|
|
|
let parsed = mri(argv, mriOptions); |
|
parsed = Object.keys(parsed).reduce((res, name) => { |
|
return { ...res, |
|
[camelcaseOptionName(name)]: parsed[name] |
|
}; |
|
}, { |
|
_: [] |
|
}); |
|
const args = parsed._; |
|
const options: { |
|
[k: string]: any; |
|
} = { |
|
'--': argsAfterDoubleDashes |
|
}; // Set option default value |
|
|
|
const ignoreDefault = command && command.config.ignoreOptionDefaultValue ? command.config.ignoreOptionDefaultValue : this.globalCommand.config.ignoreOptionDefaultValue; |
|
let transforms = Object.create(null); |
|
|
|
for (const cliOption of cliOptions) { |
|
if (!ignoreDefault && cliOption.config.default !== undefined) { |
|
for (const name of cliOption.names) { |
|
options[name] = cliOption.config.default; |
|
} |
|
} // If options type is defined |
|
|
|
|
|
if (Array.isArray(cliOption.config.type)) { |
|
if (transforms[cliOption.name] === undefined) { |
|
transforms[cliOption.name] = Object.create(null); |
|
transforms[cliOption.name]['shouldTransform'] = true; |
|
transforms[cliOption.name]['transformFunction'] = cliOption.config.type[0]; |
|
} |
|
} |
|
} // Set option values (support dot-nested property name) |
|
|
|
|
|
for (const key of Object.keys(parsed)) { |
|
if (key !== '_') { |
|
const keys = key.split('.'); |
|
setDotProp(options, keys, parsed[key]); |
|
setByType(options, transforms); |
|
} |
|
} |
|
|
|
return { |
|
args, |
|
options |
|
}; |
|
} |
|
|
|
runMatchedCommand() { |
|
const { |
|
args, |
|
options, |
|
matchedCommand: command |
|
} = this; |
|
if (!command || !command.commandAction) return; |
|
command.checkUnknownOptions(); |
|
command.checkOptionValue(); |
|
command.checkRequiredArgs(); |
|
const actionArgs: any[] = []; |
|
command.args.forEach((arg, index) => { |
|
if (arg.variadic) { |
|
actionArgs.push(args.slice(index)); |
|
} else { |
|
actionArgs.push(args[index]); |
|
} |
|
}); |
|
actionArgs.push(options); |
|
return command.commandAction.apply(this, actionArgs); |
|
} |
|
|
|
} |
|
|
|
export default CAC; |