123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- 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;
|