|
import { EventEmitter } from "https://deno.land/[email protected]/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 { |
|
|
|
name: string; |
|
commands: Command[]; |
|
globalCommand: GlobalCommand; |
|
matchedCommand?: Command; |
|
matchedCommandName?: string; |
|
|
|
|
|
|
|
|
|
rawArgs: string[]; |
|
|
|
|
|
|
|
|
|
args: ParsedArgv['args']; |
|
|
|
|
|
|
|
|
|
options: ParsedArgv['options']; |
|
showHelpOnExit?: boolean; |
|
showVersionOnExit?: boolean; |
|
|
|
|
|
|
|
|
|
constructor(name = '') { |
|
super(); |
|
this.name = name; |
|
this.commands = []; |
|
this.rawArgs = []; |
|
this.args = []; |
|
this.options = {}; |
|
this.globalCommand = new GlobalCommand(this); |
|
this.globalCommand.usage('<command> [options]'); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
usage(text: string) { |
|
this.globalCommand.usage(text); |
|
return this; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
option(rawName: string, description: string, config?: OptionConfig) { |
|
this.globalCommand.option(rawName, description, config); |
|
return this; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
help(callback?: HelpCallback) { |
|
this.globalCommand.option('-h, --help', 'Display this message'); |
|
this.globalCommand.helpCallback = callback; |
|
this.showHelpOnExit = true; |
|
return this; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
version(version: string, customFlags = '-v, --version') { |
|
this.globalCommand.version(version, customFlags); |
|
this.showVersionOnExit = true; |
|
return this; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
example(example: CommandExample) { |
|
this.globalCommand.example(example); |
|
return this; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
outputHelp() { |
|
if (this.matchedCommand) { |
|
this.matchedCommand.outputHelp(); |
|
} else { |
|
this.globalCommand.outputHelp(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
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 = processArgs, { |
|
|
|
run = true |
|
} = {}): ParsedArgv { |
|
this.rawArgs = argv; |
|
|
|
if (!this.name) { |
|
this.name = argv[1] ? getFileName(argv[1]) : 'cli'; |
|
} |
|
|
|
let shouldParse = true; |
|
|
|
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) { |
|
|
|
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[], |
|
|
|
command?: Command): ParsedArgv { |
|
|
|
const cliOptions = [...this.globalCommand.options, ...(command ? command.options : [])]; |
|
const mriOptions = getMriOptions(cliOptions); |
|
|
|
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 |
|
}; |
|
|
|
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 (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]; |
|
} |
|
} |
|
} |
|
|
|
|
|
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; |