From 7b0c3cea78f5869ebdd9993ca2f06a43ac9ae825 Mon Sep 17 00:00:00 2001 From: Alexander Kuntsch Date: Wed, 16 Oct 2024 10:02:13 +0200 Subject: [PATCH] Add key and initialValue to option definition The `key` allows mapping an option definition to a class variable. The `initialValue` is used to show the default value in a command's usage output. --- sources/advanced/Command.ts | 2 ++ sources/advanced/options/Array.ts | 4 +++- sources/advanced/options/Boolean.ts | 6 ++++-- sources/advanced/options/Counter.ts | 4 +++- sources/advanced/options/Proxy.ts | 1 + sources/advanced/options/Rest.ts | 1 + sources/advanced/options/String.ts | 4 +++- sources/core.ts | 27 ++++++++++++++++++++------- tests/specs/advanced.test.ts | 20 ++++++++++++++++++++ 9 files changed, 57 insertions(+), 12 deletions(-) diff --git a/sources/advanced/Command.ts b/sources/advanced/Command.ts index 1ca5274..1b41a0b 100644 --- a/sources/advanced/Command.ts +++ b/sources/advanced/Command.ts @@ -59,11 +59,13 @@ export type Definition = Usage & { * The various options registered on the command. */ options: Array<{ + key?: string; preferredName: string; nameSet: Array; definition: string; description?: string; required: boolean; + initialValue: any; }>; }; diff --git a/sources/advanced/options/Array.ts b/sources/advanced/options/Array.ts index e148707..02fe94d 100644 --- a/sources/advanced/options/Array.ts +++ b/sources/advanced/options/Array.ts @@ -26,8 +26,9 @@ export function Array(descriptor: string, const nameSet = new Set(optNames); return makeCommandOption({ - definition(builder) { + definition(builder, key) { builder.addOption({ + key, names: optNames, arity, @@ -35,6 +36,7 @@ export function Array(descriptor: string, hidden: opts?.hidden, description: opts?.description, required: opts.required, + initialValue, }); }, diff --git a/sources/advanced/options/Boolean.ts b/sources/advanced/options/Boolean.ts index 341b1d7..12bcfa7 100644 --- a/sources/advanced/options/Boolean.ts +++ b/sources/advanced/options/Boolean.ts @@ -19,8 +19,9 @@ export function Boolean(descriptor: string, initialValueBase: BooleanFlags | boo const nameSet = new Set(optNames); return makeCommandOption({ - definition(builder) { + definition(builder, key) { builder.addOption({ + key, names: optNames, allowBinding: false, @@ -29,10 +30,11 @@ export function Boolean(descriptor: string, initialValueBase: BooleanFlags | boo hidden: opts.hidden, description: opts.description, required: opts.required, + initialValue, }); }, - transformer(builer, key, state) { + transformer(builder, key, state) { let currentValue = initialValue; for (const {name, value} of state.options) { diff --git a/sources/advanced/options/Counter.ts b/sources/advanced/options/Counter.ts index 352d0c9..6760147 100644 --- a/sources/advanced/options/Counter.ts +++ b/sources/advanced/options/Counter.ts @@ -20,8 +20,9 @@ export function Counter(descriptor: string, initialValueBase: CounterFlags | num const nameSet = new Set(optNames); return makeCommandOption({ - definition(builder) { + definition(builder, key) { builder.addOption({ + key, names: optNames, allowBinding: false, @@ -30,6 +31,7 @@ export function Counter(descriptor: string, initialValueBase: CounterFlags | num hidden: opts.hidden, description: opts.description, required: opts.required, + initialValue, }); }, diff --git a/sources/advanced/options/Proxy.ts b/sources/advanced/options/Proxy.ts index 17648c0..7b49f0c 100644 --- a/sources/advanced/options/Proxy.ts +++ b/sources/advanced/options/Proxy.ts @@ -22,6 +22,7 @@ export function Proxy(opts: ProxyFlags = {}) { return makeCommandOption({ definition(builder, key) { builder.addProxy({ + key, name: opts.name ?? key, required: opts.required, }); diff --git a/sources/advanced/options/Rest.ts b/sources/advanced/options/Rest.ts index f8b6444..0361a29 100644 --- a/sources/advanced/options/Rest.ts +++ b/sources/advanced/options/Rest.ts @@ -24,6 +24,7 @@ export function Rest(opts: RestFlags = {}) { return makeCommandOption({ definition(builder, key) { builder.addRest({ + key, name: opts.name ?? key, required: opts.required, }); diff --git a/sources/advanced/options/String.ts b/sources/advanced/options/String.ts index 32cbc30..b0ce777 100644 --- a/sources/advanced/options/String.ts +++ b/sources/advanced/options/String.ts @@ -42,8 +42,9 @@ function StringOption(descriptor: string, const nameSet = new Set(optNames); return makeCommandOption({ - definition(builder) { + definition(builder, key) { builder.addOption({ + key, names: optNames, arity: opts.tolerateBoolean ? 0 : arity, @@ -51,6 +52,7 @@ function StringOption(descriptor: string, hidden: opts.hidden, description: opts.description, required: opts.required, + initialValue, }); }, diff --git a/sources/core.ts b/sources/core.ts index ebb5618..352e32c 100644 --- a/sources/core.ts +++ b/sources/core.ts @@ -693,6 +693,7 @@ export type ArityDefinition = { }; export type OptDefinition = { + key?: string; preferredName: string; nameSet: Array; description?: string; @@ -700,6 +701,7 @@ export type OptDefinition = { hidden: boolean; required: boolean; allowBinding: boolean; + initialValue: any; }; export class CommandBuilder { @@ -741,7 +743,7 @@ export class CommandBuilder { } } - addRest({name = `arg`, required = 0}: {name?: string, required?: number} = {}) { + addRest({key, name = `arg`, required = 0}: {key?: string, name?: string, required?: number} = {}) { if (this.arity.extra === NoLimits) throw new Error(`Infinite lists cannot be declared multiple times in the same command`); if (this.arity.trailing.length > 0) @@ -753,12 +755,12 @@ export class CommandBuilder { this.arity.extra = NoLimits; } - addProxy({required = 0}: {name?: string, required?: number} = {}) { - this.addRest({required}); + addProxy({key, required = 0}: {key?: string, name?: string, required?: number} = {}) { + this.addRest({key, required}); this.arity.proxy = true; } - addOption({names: nameSet, description, arity = 0, hidden = false, required = false, allowBinding = true}: Partial & {names: Array}) { + addOption({key, names: nameSet, description, arity = 0, hidden = false, required = false, allowBinding = true, initialValue = undefined}: Partial & {names: Array}) { if (!allowBinding && arity > 1) throw new Error(`The arity cannot be higher than 1 when the option only supports the --arg=value syntax`); if (!Number.isInteger(arity)) @@ -773,7 +775,16 @@ export class CommandBuilder { for (const name of nameSet) this.allOptionNames.set(name, preferredName); - this.options.push({preferredName, nameSet, description, arity, hidden, required, allowBinding}); + let descriptionWithDefault: string | undefined = undefined; + + if (description && initialValue !== undefined) + descriptionWithDefault = `${description} [default: ${initialValue}]`; + else if (description) + descriptionWithDefault = description; + else if (initialValue !== undefined) + descriptionWithDefault = `[default: ${initialValue}]`; + + this.options.push({key, preferredName, nameSet, description: descriptionWithDefault, arity, hidden, required, allowBinding, initialValue}); } setContext(context: Context) { @@ -784,18 +795,20 @@ export class CommandBuilder { const segments = [this.cliOpts.binaryName]; const detailedOptionList: Array<{ + key?: string; preferredName: string; nameSet: Array; definition: string; description: string; required: boolean; + initialValue: any; }> = []; if (this.paths.length > 0) segments.push(...this.paths[0]); if (detailed) { - for (const {preferredName, nameSet, arity, hidden, description, required} of this.options) { + for (const {key, preferredName, nameSet, arity, hidden, description, required, initialValue} of this.options) { if (hidden) continue; @@ -806,7 +819,7 @@ export class CommandBuilder { const definition = `${nameSet.join(`,`)}${args.join(``)}`; if (!inlineOptions && description) { - detailedOptionList.push({preferredName, nameSet, definition, description, required}); + detailedOptionList.push({key, preferredName, nameSet, definition, description, required, initialValue}); } else { segments.push(required ? `<${definition}>` : `[${definition}]`); } diff --git a/tests/specs/advanced.test.ts b/tests/specs/advanced.test.ts index b638b00..a73de42 100644 --- a/tests/specs/advanced.test.ts +++ b/tests/specs/advanced.test.ts @@ -788,6 +788,26 @@ describe(`Advanced`, () => { expect(cli.usage(CommandA, {detailed: true})).toMatch(/\u001b\[1m\$ \u001b\[22m\.\.\. greet \[--message #0\]\n\n\u001b\[1m━━━ Options .*\n\n +\S*--verbose *\S* +Log output\n +\S*--output #0 *\S* +The output directory\n/); }); + + it(`should print flags with a description and/or initialValue separately`, async () => { + class CommandA extends Command { + verbose = Option.Boolean(`--verbose`, {description: `Log output`}); + output = Option.String(`--output`, {description: `The output directory`}); + message = Option.String(`--message`, `Hello world`); + recipient = Option.String(`--recipient`, `Bob`, {description: `The greeting's recipient`}); + + static paths = [[`greet`]]; + async execute() { + throw new Error(`not implemented, just testing usage()`); + } + } + + const cli = Cli.from([CommandA]); + + // eslint-disable-next-line no-control-regex + expect(cli.usage(CommandA, {detailed: true})).toMatch(/\u001b\[1m\$ \u001b\[22m\.\.\. greet\n\n\u001b\[1m━━━ Options .*\n\n +\S*--verbose *\S* +Log output\n +\S*--output #0 *\S* +The output directory\n +\S*--message #0 *\S* +\[default: Hello world\]\n +\S*--recipient #0 *\S* +The greeting's recipient \[default: Bob\]\n/); + }); + it(`should support tuples`, async () => { class PointCommand extends Command { point = Option.String(`--point`, {arity: 3});