From b51adbe896039d9fffb2c05c333b67b52a9bc9bd Mon Sep 17 00:00:00 2001 From: EGOIST <0x142857@gmail.com> Date: Mon, 26 Nov 2018 13:23:22 +0800 Subject: [PATCH] fix: unknown options should be checked against original cli options --- package.json | 3 +- src/Command.ts | 10 +++-- src/index.ts | 112 ++++++++++++++++++++++++++++++++++--------------- src/utils.ts | 8 +++- tsconfig.json | 2 +- yarn.lock | 8 ---- 6 files changed, 95 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index f4aded5..2c491f5 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "devDependencies": { "@types/execa": "^0.9.0", "@types/jest": "^23.3.9", + "@types/minimist": "^1.2.0", "cz-conventional-changelog": "^2.1.0", "eslint-config-rem": "^3.0.0", "execa": "^1.0.0", @@ -38,7 +39,7 @@ "typescript": "^3.1.6" }, "dependencies": { - "minimost": "^1.2.0" + "minimist": "^1.2.0" }, "release": { "branch": "master" diff --git a/src/Command.ts b/src/Command.ts index eb37c20..97c082a 100644 --- a/src/Command.ts +++ b/src/Command.ts @@ -212,6 +212,12 @@ export default class Command { return this } + /** + * Check if the parsed options contain any unknown options + * Exit and output error when true + * @param options Original options, i.e. not camelCased one + * @param globalCommand + */ checkUnknownOptions(options: { [k: string]: any }, globalCommand: Command) { if (!this.config.allowUnknownOptions) { for (const name of Object.keys(options)) { @@ -225,12 +231,10 @@ export default class Command { name.length > 1 ? `--${name}` : `-${name}` }\`` ) - process.exitCode = 1 - return true + process.exit(1) } } } - return false } } diff --git a/src/index.ts b/src/index.ts index dbabe19..2f05dd6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,9 @@ import { EventEmitter } from 'events' import path from 'path' -import minimost, { Opts as MinimostOpts } from 'minimost' +import minimist, { Opts as MinimistOpts } from 'minimist' import Command, { HelpCallback, CommandExample } from './Command' import { OptionConfig } from './Option' -import { getMinimostOptions } from './utils' +import { getMinimistOptions, camelcase } from './utils' interface ParsedArgv { args: string[] @@ -12,6 +12,16 @@ interface ParsedArgv { } } +interface MinimistResult extends ParsedArgv { + args: string[] + options: { + [k: string]: any + } + originalOptions: { + [k: string]: any + } +} + const NAME_OF_GLOBAL_COMMAND = 'this-does-not-matter' class CAC extends EventEmitter { @@ -28,7 +38,7 @@ class CAC extends EventEmitter { */ args: string[] /** - * Parsed CLI options + * Parsed CLI options, camelCased */ options: { [k: string]: any } @@ -106,75 +116,110 @@ class CAC extends EventEmitter { this.bin = argv[1] ? path.basename(argv[1]) : 'cli' for (const command of this.commands) { - const minimostOptions = getMinimostOptions([ + const minimistOptions = getMinimistOptions([ ...this.globalCommand.options, ...command.options ]) - const parsed = this.minimost(argv.slice(2), minimostOptions) - if (command.isMatched(parsed.args[0])) { + const { args, options, originalOptions } = this.minimist( + argv.slice(2), + minimistOptions + ) + if (command.isMatched(args[0])) { this.matchedCommand = command - this.args = parsed.args - this.options = parsed.options - this.emit(`command:${parsed.args[0]}`, command) - this.runCommandAction(command, this.globalCommand, parsed) - return parsed + this.args = args + this.options = options + this.emit(`command:${args[0]}`, command) + this.runCommandAction(command, this.globalCommand, { + args, + options, + originalOptions + }) + return { args, options } } } // Try the default command for (const command of this.commands) { if (command.name === '') { - const minimostOptions = getMinimostOptions([ + const minimistOptions = getMinimistOptions([ ...this.globalCommand.options, ...command.options ]) - const parsed = this.minimost(argv.slice(2), minimostOptions) - this.args = parsed.args - this.options = parsed.options + const { args, options, originalOptions } = this.minimist( + argv.slice(2), + minimistOptions + ) this.matchedCommand = command + this.args = args + this.options = options this.emit(`command:!`, command) - this.runCommandAction(command, this.globalCommand, parsed) - return parsed + this.runCommandAction(command, this.globalCommand, { + args, + options, + originalOptions + }) + return { args, options } } } - const globalMinimostOptions = getMinimostOptions(this.globalCommand.options) - const parsed = this.minimost(argv.slice(2), globalMinimostOptions) - this.args = parsed.args - this.options = parsed.options + const globalMinimistOptions = getMinimistOptions(this.globalCommand.options) + const { args, options } = this.minimist( + argv.slice(2), + globalMinimistOptions + ) + this.args = args + this.options = options - if (parsed.options.help && this.globalCommand.hasOption('help')) { + if (options.help && this.globalCommand.hasOption('help')) { this.outputHelp() - return parsed } if ( - parsed.options.version && + options.version && this.globalCommand.hasOption('version') && this.globalCommand.versionNumber ) { this.outputVersion() - return parsed } this.emit('command:*') - return parsed + return { args, options } } - minimost(argv: string[], minimostOptions: MinimostOpts) { - const { input: args, flags: options } = minimost(argv, minimostOptions) + private minimist( + argv: string[], + minimistOptions: MinimistOpts + ): MinimistResult { + const parsed = minimist( + argv, + Object.assign( + { + '--': true + }, + minimistOptions + ) + ) + + const args = parsed._ + delete parsed._ + + const options: { [k: string]: any } = {} + for (const key of Object.keys(parsed)) { + options[camelcase(key)] = parsed[key] + } return { args, - options + options, + originalOptions: parsed } } - runCommandAction( + private runCommandAction( command: Command, globalCommand: Command, - { args, options }: ParsedArgv + { args, options, originalOptions }: MinimistResult ) { if (options.help && globalCommand.hasOption('help')) { return this.outputHelp(true) @@ -186,7 +231,7 @@ class CAC extends EventEmitter { if (!command.commandAction) return - if (command.checkUnknownOptions(options, globalCommand)) return + command.checkUnknownOptions(originalOptions, globalCommand) // The first one is command name if (!command.isDefaultCommand) { @@ -199,8 +244,7 @@ class CAC extends EventEmitter { console.error( `error: missing required args for command "${command.rawName}"` ) - process.exitCode = 1 - return + process.exit(1) } const actionArgs: any[] = [] diff --git a/src/utils.ts b/src/utils.ts index ea7785e..5f46c8a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -35,7 +35,7 @@ export const findAllBrackets = (v: string) => { return res } -export const getMinimostOptions = (options: Option[]) => { +export const getMinimistOptions = (options: Option[]) => { return { default: options.reduce((res: { [k: string]: any }, option) => { if (option.config.default !== undefined) { @@ -68,3 +68,9 @@ export const findLongest = (arr: string[]) => { export const padRight = (str: string, length: number) => { return str.length >= length ? str : `${str}${' '.repeat(length - str.length)}` } + +export const camelcase = (input: string) => { + return input.replace(/([a-z])-([a-z])/g, (_, p1, p2) => { + return p1 + p2.toUpperCase() + }) +} diff --git a/tsconfig.json b/tsconfig.json index 99cbe41..a52a279 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,5 +21,5 @@ "module": "commonjs", "outDir": "dist" }, - "include": ["src"] + "include": ["src", "declarations.d.ts"] } diff --git a/yarn.lock b/yarn.lock index 2313c24..c3c1d3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4337,14 +4337,6 @@ minimist@~0.0.1: resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= -minimost@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/minimost/-/minimost-1.2.0.tgz#a37f91d60395fc003180d208ca9e0316bcc4e3a2" - integrity sha512-/+eWyOtXw41WIUV9rBgrXna11bxbqymebSeW2arsfp/MCGCwe+2czzsOueEtLZgH4xb4QXhje5H9MLCsCPibLA== - dependencies: - "@types/minimist" "^1.2.0" - minimist "^1.2.0" - minipass@^2.2.1, minipass@^2.3.3, minipass@^2.3.4: version "2.3.5" resolved "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848"