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.114.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;
|