From 436eebeeca9adfa9320f8d6c3d1e1251045547cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 19 Feb 2021 16:47:05 +0000 Subject: [PATCH] Fix #37: better help output for negated options Not only doesn't show the `(default: true)` for negated options, which was quite confusing, but merges them with the line describing the affirmative case. Added a new test. Since it relies on help output, it is a bit brittle, like the other "snapshot"-based ones. I didn't the same execa snapshot for this because it would make a test take a lot longer to run (becasue spawns a process). * README.md (Options): Mention [no-]foo syntax. * src/Command.ts (outputHelp): Consider options with negated counterparts when printing. * src/__test__/index.test.ts ('negated option help output'): New test. --- README.md | 11 +++++++++++ src/Command.ts | 34 +++++++++++++++++++++++++++------- src/__test__/index.test.ts | 28 ++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 2933e4b..57e7622 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,17 @@ cli ``` This will let CAC set the default value of `config` to true, and you can use `--no-config` flag to set it to `false`. +This also results in the familiar `[no-]foo` syntax in `cli.help()` output: + +``` +Usage: + $ foo build [project] + +Options: + --[no-]config Disable config file/Use a custom config file + -h, --help Display this message + -v, --version Display version number +``` ### Variadic Arguments diff --git a/src/Command.ts b/src/Command.ts index 11ee87e..862aa46 100644 --- a/src/Command.ts +++ b/src/Command.ts @@ -184,22 +184,42 @@ class Command { ? globalOptions : [...this.options, ...(globalOptions || [])] if (options.length > 0) { - const longestOptionName = findLongest( - options.map((option) => option.rawName) - ) + const negatedOpts: { [key: string]: Option } = {} + // O(n2), let's hope there aren't a zillion options + for (const o1 of options) { + if (!o1.negated) continue + for (const o2 of options) { + if (o1.name == o2.name) negatedOpts[o2.name] = o1 + } + } + const longest = options + .map((o) => { + if (o.negated) return 0 + const l = o.rawName.length + // '5' is the length of '[no-]'. As to the 1, I have no clue... + return negatedOpts[o.name] ? l + 5 + 1 : l + }) + .reduce((acc, l) => Math.max(acc, l), 0) sections.push({ title: 'Options', body: options .map((option) => { - return ` ${padRight(option.rawName, longestOptionName.length)} ${ - option.description - } ${ + if (option.negated) return + let desc = option.description + let name = option.rawName + if (negatedOpts[option.name]) { + const ndesc = negatedOpts[option.name].description + if (ndesc) desc = `${ndesc}/${desc}` + name = name.replace(/^(--?)/, '$1[no-]') + } + + return ` ${padRight(name, longest)} ${desc} ${ option.config.default === undefined ? '' : `(default: ${option.config.default})` }` }) - .join('\n'), + .filter(Boolean).join('\n'), }) } diff --git a/src/__test__/index.test.ts b/src/__test__/index.test.ts index eccbca1..db1098c 100644 --- a/src/__test__/index.test.ts +++ b/src/__test__/index.test.ts @@ -103,6 +103,34 @@ test('negated option validation', () => { expect(options.config).toBe(false) }) +test('negated option help output', () => { + const cli = cac() + + cli.option('--config ', 'Use custom config file') + cli.option('--no-config', 'Skip') + + const saved = console.log + let output = '' + try { + console.log = (msg, more) => { + if (more) throw new Error('Unexpected multi-arg call to console.log') + output += msg + } + cli.outputHelp() + expect(output).toBe( + ` + +Usage: + $ [options] + +Options: + --[no-]config Skip/Use custom config file ` + ) + } finally { + console.log = saved + } +}) + test('array types without transformFunction', () => { const cli = cac()