From 2bc8982bfcc5b852c6288b3b231b63024e0e034a Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Thu, 25 May 2017 11:25:44 -0700 Subject: [PATCH] Use commander instead of optimist for CLI arguments (#2689) --- package.json | 4 +- src/runner.ts | 10 - src/tslint-cli.ts | 410 ++++++++++++++--------------- test/executable/executableTests.ts | 2 +- yarn.lock | 16 +- 5 files changed, 211 insertions(+), 231 deletions(-) diff --git a/package.json b/package.json index c78ad0dbeee..2f5106d1a6f 100644 --- a/package.json +++ b/package.json @@ -38,9 +38,9 @@ "dependencies": { "babel-code-frame": "^6.22.0", "colors": "^1.1.2", + "commander": "^2.9.0", "diff": "^3.2.0", "glob": "^7.1.1", - "optimist": "~0.6.0", "resolve": "^1.3.2", "semver": "^5.3.0", "tslib": "^1.7.1", @@ -53,12 +53,12 @@ "@types/babel-code-frame": "^6.20.0", "@types/chai": "^3.5.0", "@types/colors": "^1.1.3", + "@types/commander": "^2.9.0", "@types/diff": "^3.2.0", "@types/glob": "^5.0.30", "@types/js-yaml": "^3.5.29", "@types/mocha": "^2.2.35", "@types/node": "^7.0.16", - "@types/optimist": "^0.0.29", "@types/resolve": "^0.0.4", "@types/semver": "^5.3.30", "chai": "^3.5.0", diff --git a/src/runner.ts b/src/runner.ts index f78b0a4f587..f57ea7bded8 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -103,11 +103,6 @@ export interface IRunnerOptions { * Whether to enable type checking when linting a project. */ typeCheck?: boolean; - - /** - * Whether to show the current TSLint version. - */ - version?: boolean; } export class Runner { @@ -118,11 +113,6 @@ export class Runner { constructor(private options: IRunnerOptions, private outputStream: NodeJS.WritableStream) { } public run(onComplete: (status: number) => void) { - if (this.options.version) { - this.outputStream.write(Linter.VERSION + "\n"); - return onComplete(0); - } - if (this.options.init) { if (fs.existsSync(CONFIG_FILENAME)) { console.error(`Cannot generate ${CONFIG_FILENAME}: file already exists`); diff --git a/src/tslint-cli.ts b/src/tslint-cli.ts index 8fe30933870..48ef00b1cd3 100644 --- a/src/tslint-cli.ts +++ b/src/tslint-cli.ts @@ -15,137 +15,211 @@ * limitations under the License. */ +// tslint:disable no-console object-literal-sort-keys + +import commander = require("commander"); import * as fs from "fs"; -import * as optimist from "optimist"; import { IRunnerOptions, Runner } from "./runner"; +import { dedent } from "./utils"; interface Argv { - _: string[]; - c?: string; + config?: string; exclude?: string; - f?: boolean; fix?: boolean; force?: boolean; - h?: boolean; help?: boolean; - i?: boolean; init?: boolean; - o?: string; out?: string; - outputAbsolutePaths?: boolean; - p?: string; + outputAbsolutePaths: boolean; project?: string; - r?: string; - s?: string; - t?: string; - "type-check"?: boolean; + rulesDir?: string; + formattersDir: string; + format?: string; + typeCheck?: boolean; test?: string; - v?: boolean; + version?: boolean; } -const processed = optimist - .usage("Usage: $0 [options] file ...") - .check((argv: Argv) => { - // at least one of file, help, version, project or unqualified argument must be present - // tslint:disable-next-line strict-boolean-expressions - if (!(argv.h || argv.i || argv.test || argv.v || argv.project || argv._.length > 0)) { - // throw a string, otherwise a call stack is printed for this message - // tslint:disable-next-line:no-string-throw - throw "Missing files"; - } +interface Option { + short?: string; + // Commander will camelCase option names. + name: keyof Argv | "rules-dir" | "formatters-dir" | "type-check"; + type: "string" | "boolean"; + describe: string; // Short, used for usage message + description: string; // Long, used for `--help` +} - // tslint:disable-next-line strict-boolean-expressions - if (argv["type-check"] && !argv.project) { - // tslint:disable-next-line:no-string-throw - throw "--project must be specified in order to enable type checking."; - } +const options: Option[] = [ + { + short: "c", + name: "config", + type: "string", + describe: "configuration file", + description: dedent` + The location of the configuration file that tslint will use to + determine which rules are activated and what options to provide + to the rules. If no option is specified, the config file named + tslint.json is used, so long as it exists in the path. + The format of the file is { rules: { /* rules list */ } }, + where /* rules list */ is a key: value comma-seperated list of + rulename: rule-options pairs. Rule-options can be either a + boolean true/false value denoting whether the rule is used or not, + or a list [boolean, ...] where the boolean provides the same role + as in the non-list case, and the rest of the list are options passed + to the rule that will determine what it checks for (such as number + of characters for the max-line-length rule, or what functions to ban + for the ban rule).`, + }, + { + short: "e", + name: "exclude", + type: "string", + describe: "exclude globs from path expansion", + description: dedent` + A filename or glob which indicates files to exclude from linting. + This option can be supplied multiple times if you need multiple + globs to indicate which files to exclude.`, + }, + { + name: "fix", + type: "boolean", + describe: "fixes linting errors for select rules (this may overwrite linted files)", + description: "Fixes linting errors for select rules. This may overwrite linted files.", + }, + { + name: "force", + type: "boolean", + describe: "return status code 0 even if there are lint errors", + description: dedent` + Return status code 0 even if there are any lint errors. + Useful while running as npm script.`, + }, + { + short: "i", + name: "init", + type: "boolean", + describe: "generate a tslint.json config file in the current working directory", + description: "Generates a tslint.json config file in the current working directory.", + }, + { + short: "o", + name: "out", + type: "string", + describe: "output file", + description: dedent` + A filename to output the results to. By default, tslint outputs to + stdout, which is usually the console where you're running it from.`, + }, + { + name: "outputAbsolutePaths", + type: "boolean", + describe: "whether or not outputted file paths are absolute", + description: "If true, all paths in the output will be absolute.", + }, + { + short: "r", + name: "rules-dir", + type: "string", + describe: "rules directory", + description: dedent` + An additional rules directory, for user-created rules. + tslint will always check its default rules directory, in + node_modules/tslint/lib/rules, before checking the user-provided + rules directory, so rules in the user-provided rules directory + with the same name as the base rules will not be loaded.`, + }, + { + short: "s", + name: "formatters-dir", + type: "string", + describe: "formatters directory", + description: dedent` + An additional formatters directory, for user-created formatters. + Formatters are files that will format the tslint output, before + writing it to stdout or the file passed in --out. The default + directory, node_modules/tslint/build/formatters, will always be + checked first, so user-created formatters with the same names + as the base formatters will not be loaded.`, + }, + { + short: "t", + name: "format", + type: "string", + describe: "output format (prose, json, stylish, verbose, pmd, msbuild, checkstyle, vso, fileslist, codeFrame)", + description: dedent` + The formatter to use to format the results of the linter before + outputting it to stdout or the file passed in --out. The core + formatters are prose (human readable), json (machine readable) + and verbose. prose is the default if this option is not used. + Other built-in options include pmd, msbuild, checkstyle, and vso. + Additional formatters can be added and used if the --formatters-dir + option is set.`, + }, + { + name: "test", + type: "boolean", + describe: "test that tslint produces the correct output for the specified directory", + description: dedent` + Runs tslint on matched directories and checks if tslint outputs + match the expected output in .lint files. Automatically loads the + tslint.json files in the directories as the configuration file for + the tests. See the full tslint documentation for more details on how + this can be used to test custom rules.`, + }, + { + short: "p", + name: "project", + type: "string", + describe: "tsconfig.json file", + description: dedent` + The path or directory containing a tsconfig.json file that will be + used to determine which files will be linted. This flag also enables + rules that require the type checker.`, + }, + { + name: "type-check", + type: "boolean", + describe: "check for type errors before linting the project", + description: dedent` + Checks for type errors before linting a project. --project must be + specified in order to enable type checking.`, + }, +]; + +for (const option of options) { + const commanderStr = optionUsageTag(option) + (option.type === "string" ? ` [${option.name}]` : ""); + commander.option(commanderStr, option.describe); +} - // tslint:disable-next-line strict-boolean-expressions - if (argv.f) { - // throw a string, otherwise a call stack is printed for this message - // tslint:disable-next-line:no-string-throw - throw "-f option is no longer available. Supply files directly to the tslint command instead."; - } - }) - .options({ - "c": { - alias: "config", - describe: "configuration file", - type: "string", - }, - "e": { - alias: "exclude", - describe: "exclude globs from path expansion", - type: "string", - }, - "fix": { - describe: "fixes linting errors for select rules (this may overwrite linted files)", - type: "boolean", - }, - "force": { - describe: "return status code 0 even if there are lint errors", - type: "boolean", - }, - "h": { - alias: "help", - describe: "display detailed help", - type: "boolean", - }, - "i": { - alias: "init", - describe: "generate a tslint.json config file in the current working directory", - type: "boolean", - }, - "o": { - alias: "out", - describe: "output file", - type: "string", - }, - "outputAbsolutePaths": { - describe: "whether or not outputted file paths are absolute", - type: "boolean", - }, - "p": { - alias: "project", - describe: "tsconfig.json file", - type: "string", - }, - "r": { - alias: "rules-dir", - describe: "rules directory", - type: "string", - }, - "s": { - alias: "formatters-dir", - describe: "formatters directory", - type: "string", - }, - "t": { - alias: "format", - default: "prose", - describe: "output format (prose, json, stylish, verbose, pmd, msbuild, checkstyle, vso, fileslist, codeFrame)", - type: "string", - }, - "test": { - describe: "test that tslint produces the correct output for the specified directory", - type: "boolean", - }, - "type-check": { - describe: "check for type errors before linting the project", - type: "boolean", - }, - "v": { - alias: "version", - describe: "current version", - type: "boolean", - }, - }); -const argv = processed.argv as Argv; +commander.on("--help", () => { + const indent = "\n "; + const optionDetails = options.map((o) => + `${optionUsageTag(o)}:${o.description.startsWith("\n") ? o.description.replace(/\n/g, indent) : indent + o.description}`); + console.log(`tslint accepts the following commandline options:\n\n ${optionDetails.join("\n\n ")}\n\n`); +}); + +// Hack to get unknown option errors to work. https://github.com/visionmedia/commander.js/pull/121 +const parsed = commander.parseOptions(process.argv.slice(2)); +commander.args = parsed.args; +if (parsed.unknown.length !== 0) { + (commander.parseArgs as any)([], parsed.unknown); +} +const argv = commander.opts() as any as Argv; + +if (!(argv.init === true || argv.test !== undefined || argv.project !== undefined || commander.args.length > 0)) { + console.error("Missing files"); + process.exit(1); +} + +if (argv.typeCheck === true && argv.project === undefined) { + console.error("--project must be specified in order to enable type checking."); + process.exit(1); +} let outputStream: NodeJS.WritableStream; -if (argv.o != null) { - outputStream = fs.createWriteStream(argv.o, { +if (argv.out != null) { + outputStream = fs.createWriteStream(argv.out, { flags: "w+", mode: 420, }); @@ -153,112 +227,26 @@ if (argv.o != null) { outputStream = process.stdout; } -// tslint:disable-next-line strict-boolean-expressions -if (argv.help) { - outputStream.write(processed.help()); - const outputString = ` -tslint accepts the following commandline options: - - -c, --config: - The location of the configuration file that tslint will use to - determine which rules are activated and what options to provide - to the rules. If no option is specified, the config file named - tslint.json is used, so long as it exists in the path. - The format of the file is { rules: { /* rules list */ } }, - where /* rules list */ is a key: value comma-seperated list of - rulename: rule-options pairs. Rule-options can be either a - boolean true/false value denoting whether the rule is used or not, - or a list [boolean, ...] where the boolean provides the same role - as in the non-list case, and the rest of the list are options passed - to the rule that will determine what it checks for (such as number - of characters for the max-line-length rule, or what functions to ban - for the ban rule). - - -e, --exclude: - A filename or glob which indicates files to exclude from linting. - This option can be supplied multiple times if you need multiple - globs to indicate which files to exclude. - - --fix: - Fixes linting errors for select rules. This may overwrite linted files. - - --force: - Return status code 0 even if there are any lint errors. - Useful while running as npm script. - - -i, --init: - Generates a tslint.json config file in the current working directory. - - -o, --out: - A filename to output the results to. By default, tslint outputs to - stdout, which is usually the console where you're running it from. - - -r, --rules-dir: - An additional rules directory, for user-created rules. - tslint will always check its default rules directory, in - node_modules/tslint/lib/rules, before checking the user-provided - rules directory, so rules in the user-provided rules directory - with the same name as the base rules will not be loaded. - - -s, --formatters-dir: - An additional formatters directory, for user-created formatters. - Formatters are files that will format the tslint output, before - writing it to stdout or the file passed in --out. The default - directory, node_modules/tslint/build/formatters, will always be - checked first, so user-created formatters with the same names - as the base formatters will not be loaded. - - -t, --format: - The formatter to use to format the results of the linter before - outputting it to stdout or the file passed in --out. The core - formatters are prose (human readable), json (machine readable) - and verbose. prose is the default if this option is not used. - Other built-in options include pmd, msbuild, checkstyle, and vso. - Additional formatters can be added and used if the --formatters-dir - option is set. - - --test: - Runs tslint on matched directories and checks if tslint outputs - match the expected output in .lint files. Automatically loads the - tslint.json files in the directories as the configuration file for - the tests. See the full tslint documentation for more details on how - this can be used to test custom rules. - - -p, --project: - The path or directory containing a tsconfig.json file that will be - used to determine which files will be linted. This flag also enables - rules that require the type checker. - - --type-check - Checks for type errors before linting a project. --project must be - specified in order to enable type checking. - - -v, --version: - The current version of tslint. - - -h, --help: - Prints this help message.\n`; - outputStream.write(outputString); - process.exit(0); -} - -const options: IRunnerOptions = { - config: argv.c, +const runnerOptions: IRunnerOptions = { + config: argv.config, exclude: argv.exclude, - files: argv._, + files: commander.args, fix: argv.fix, force: argv.force, - format: argv.t, - formattersDirectory: argv.s, + format: argv.format === undefined ? "prose" : argv.format, + formattersDirectory: argv.formattersDir, init: argv.init, out: argv.out, outputAbsolutePaths: argv.outputAbsolutePaths, - project: argv.p, - rulesDirectory: argv.r, + project: argv.project, + rulesDirectory: argv.rulesDir, test: argv.test, - typeCheck: argv["type-check"], - version: argv.v, + typeCheck: argv.typeCheck, }; -new Runner(options, outputStream) +new Runner(runnerOptions, outputStream) .run((status: number) => process.exit(status)); + +function optionUsageTag({short, name}: Option) { + return short !== undefined ? `-${short}, --${name}` : `--${name}`; +} diff --git a/test/executable/executableTests.ts b/test/executable/executableTests.ts index 2fe996bd49c..b3f2121d7d8 100644 --- a/test/executable/executableTests.ts +++ b/test/executable/executableTests.ts @@ -63,7 +63,7 @@ describe("Executable", function(this: Mocha.ISuiteCallbackContext) { assert.isNotNull(err, "process should exit with error"); assert.strictEqual(err.code, 1, "error code should be 1"); - assert.include(stderr, "-f option is no longer available", "stderr should contain notification about removed flag"); + assert.include(stderr, "error: unknown option `-f'"); assert.strictEqual(stdout, "", "shouldn't contain any output in stdout"); done(); }); diff --git a/yarn.lock b/yarn.lock index f04cda3d647..3595c9305ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14,6 +14,12 @@ version "1.1.3" resolved "https://registry.yarnpkg.com/@types/colors/-/colors-1.1.3.tgz#5413b0a7a1b16dd18be0e3fd57d2feecc81cc776" +"@types/commander@^2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@types/commander/-/commander-2.9.0.tgz#dd07af1fc35d76833e0da26ea67a2be088b5fafc" + dependencies: + "@types/node" "*" + "@types/diff@^3.2.0": version "3.2.0" resolved "https://registry.yarnpkg.com/@types/diff/-/diff-3.2.0.tgz#2cf019a98b4cca072102cb48af5675502b5a831f" @@ -41,10 +47,6 @@ version "7.0.22" resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.22.tgz#4593f4d828bdd612929478ea40c67b4f403ca255" -"@types/optimist@^0.0.29": - version "0.0.29" - resolved "https://registry.yarnpkg.com/@types/optimist/-/optimist-0.0.29.tgz#a8873580b3a84b69ac1e687323b15fbbeb90479a" - "@types/resolve@^0.0.4": version "0.0.4" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.4.tgz#9b586d65a947dea88c4bc24da0b905fe9520a0d5" @@ -299,7 +301,7 @@ colors@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" -commander@2.9.0: +commander@2.9.0, commander@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" dependencies: @@ -1008,8 +1010,8 @@ mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1: minimist "0.0.8" mocha@^3.2.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.4.1.tgz#a3802b4aa381934cacb38de70cf771621da8f9af" + version "3.4.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.4.2.tgz#d0ef4d332126dbf18d0d640c9b382dd48be97594" dependencies: browser-stdout "1.3.0" commander "2.9.0"