diff --git a/docs/docs/features/argument-parsing/examples/boolean-flag.txt b/docs/docs/features/argument-parsing/examples/boolean-flag.txt index dce4623..8947bd2 100644 --- a/docs/docs/features/argument-parsing/examples/boolean-flag.txt +++ b/docs/docs/features/argument-parsing/examples/boolean-flag.txt @@ -19,9 +19,9 @@ export const root = buildCommand({ docs: { brief: "Example for live playground with boolean flag", customUsage: [ - "--quiet", - "--quiet=yes", - "--noQuiet", + { input: "--quiet", brief: "Flag with no value" }, + { input: "--quiet=yes", brief: "Flag with explicit value" }, + { input: "--noQuiet", brief: "Negated flag" }, ], }, }); diff --git a/docs/docs/features/command-routing/commands.mdx b/docs/docs/features/command-routing/commands.mdx index 50af806..c9d07b3 100644 --- a/docs/docs/features/command-routing/commands.mdx +++ b/docs/docs/features/command-routing/commands.mdx @@ -68,7 +68,7 @@ The parameters object is a specification of all of the parameters (arguments and A base level of documentation is required, and more is always appreciated. All commands must specify a value for `brief` that contains a short line of text to be used when referring to this command throughout the help text of the application. -Optionally, you can further customize the help text for this specific command by including `fullDescription` or `customUsage`. The former will override `brief` in the command's help text and can contain multiple lines of text rather than just one. The `customUsage` property will replace the auto-generated usage lines, and is useful when there's some [additional validation of user inputs](../out-of-scope.mdx#cross-argument-validation) that isn't represented natively by Stricli. +Optionally, you can further customize the help text for this specific command by including `fullDescription` or `customUsage`. The former will override `brief` in the command's help text and can contain multiple lines of text rather than just one. The `customUsage` property will replace the auto-generated usage lines, and is useful when there's some [additional validation of user inputs](../out-of-scope.mdx#cross-argument-validation) that isn't represented natively by Stricli. You can also provide an object with `input` and `brief` to print a description after the usage line. ```ts // output-next-line @@ -84,6 +84,7 @@ buildCommand({ customUsage: [ "-a -b", "-c", + { input: "-d", brief: "Brief description of this use case" }, ], }, ... @@ -98,6 +99,8 @@ run --help USAGE run -a -b run -c + run -d + Brief description of this use case run --help This is the full description of the command. diff --git a/packages/core/src/routing/command/documentation.ts b/packages/core/src/routing/command/documentation.ts index bae4c64..14428f0 100644 --- a/packages/core/src/routing/command/documentation.ts +++ b/packages/core/src/routing/command/documentation.ts @@ -6,6 +6,11 @@ import { formatDocumentationForPositionalParameters } from "../../parameter/posi import type { CommandParameters } from "../../parameter/types"; import type { HelpFormattingArguments } from "../types"; +export interface CustomUsage { + readonly input: string; + readonly brief: string; +} + export interface CommandDocumentation { /** * In-line documentation for this command. @@ -18,7 +23,7 @@ export interface CommandDocumentation { /** * Sample usage to replace the generated usage lines. */ - readonly customUsage?: readonly string[]; + readonly customUsage?: readonly (string | CustomUsage)[]; } /** @@ -34,8 +39,13 @@ export function* generateCommandHelpLines( const prefix = args.prefix.join(" "); yield args.ansiColor ? `\x1B[1m${headers.usage}\x1B[22m` : headers.usage; if (customUsage) { - for (const customUsageLine of customUsage) { - yield ` ${prefix} ${customUsageLine}`; + for (const usage of customUsage) { + if (typeof usage === "string") { + yield ` ${prefix} ${usage}`; + } else { + const brief = args.ansiColor ? `\x1B[3m${usage.brief}\x1B[23m` : usage.brief; + yield ` ${prefix} ${usage.input}\n ${brief}`; + } } } else { yield ` ${formatUsageLineForParameters(parameters, args)}`; diff --git a/packages/core/tests/application.spec.ts b/packages/core/tests/application.spec.ts index 7c7696a..398728d 100644 --- a/packages/core/tests/application.spec.ts +++ b/packages/core/tests/application.spec.ts @@ -674,7 +674,11 @@ describe("Application", () => { }, docs: { brief: "basic command", - customUsage: ["custom usage 1", "custom usage 2", "custom usage 3"], + customUsage: [ + "custom usage 1", + { input: "custom-two", brief: "enhanced custom usage 2" }, + "custom usage 3", + ], }, }); const appWithAlternateUsage = buildApplication(commandWithAlternateUsage, { @@ -1526,6 +1530,65 @@ describe("Application", () => { compareToBaseline(this, ApplicationRunResultBaselineFormat, result); }); }); + + describe("nested basic route map with hidden routes, always show help-all (alias)", () => { + // GIVEN + const rootRouteMap = buildRouteMapForFakeContext({ + routes: { + sub: buildBasicRouteMap("sub"), + subHidden: buildBasicRouteMap("subHidden"), + }, + docs: { + brief: "root route map", + hideRoute: { + subHidden: true, + }, + }, + }); + const app = buildApplication(rootRouteMap, { + name: "cli", + documentation: { + alwaysShowHelpAllFlag: true, + useAliasInUsageLine: true, + }, + }); + + // WHEN + it("display help text for root (implicit)", async function () { + const result = await runWithInputs(app, []); + compareToBaseline(this, ApplicationRunResultBaselineFormat, result); + }); + + it("display help text for root", async function () { + const result = await runWithInputs(app, ["--help"]); + compareToBaseline(this, ApplicationRunResultBaselineFormat, result); + }); + + it("display help text for root including hidden", async function () { + const result = await runWithInputs(app, ["--help-all"]); + compareToBaseline(this, ApplicationRunResultBaselineFormat, result); + }); + + it("fails for undefined route", async function () { + const result = await runWithInputs(app, ["undefined"]); + compareToBaseline(this, ApplicationRunResultBaselineFormat, result); + }); + + it("fails for undefined flag", async function () { + const result = await runWithInputs(app, ["--undefined"]); + compareToBaseline(this, ApplicationRunResultBaselineFormat, result); + }); + + it("displays help text for nested hidden route map (implicit)", async function () { + const result = await runWithInputs(app, ["subHidden"]); + compareToBaseline(this, ApplicationRunResultBaselineFormat, result); + }); + + it("displays help text for nested hidden route map", async function () { + const result = await runWithInputs(app, ["subHidden", "--help"]); + compareToBaseline(this, ApplicationRunResultBaselineFormat, result); + }); + }); }); describe("proposeCompletions", () => { diff --git a/packages/core/tests/baselines/reference/application.txt b/packages/core/tests/baselines/reference/application.txt index 1495914..d6334ac 100644 --- a/packages/core/tests/baselines/reference/application.txt +++ b/packages/core/tests/baselines/reference/application.txt @@ -245,7 +245,8 @@ ExitCode=Success :: STDOUT USAGE cli custom usage 1 - cli custom usage 2 + cli custom-two + enhanced custom usage 2 cli custom usage 3 cli --help @@ -1085,6 +1086,117 @@ ExitCode=UnknownCommand :: STDERR No command registered for `undefined` +:::: Application / run / nested basic route map with hidden routes, always show help-all (alias) / display help text for root +ExitCode=Success +:: STDOUT +USAGE + cli sub command ... + cli -h + cli -H + +root route map + +FLAGS + -h --help Print help information and exit + -H --helpAll Print help information (including hidden commands/flags) and exit + +COMMANDS + sub sub + +:: STDERR + +:::: Application / run / nested basic route map with hidden routes, always show help-all (alias) / display help text for root (implicit) +ExitCode=Success +:: STDOUT +USAGE + cli sub command ... + cli -h + cli -H + +root route map + +FLAGS + -h --help Print help information and exit + -H --helpAll Print help information (including hidden commands/flags) and exit + +COMMANDS + sub sub + +:: STDERR + +:::: Application / run / nested basic route map with hidden routes, always show help-all (alias) / display help text for root including hidden +ExitCode=Success +:: STDOUT +USAGE + cli sub command ... + cli subHidden command ... + cli -h + cli -H + +root route map + +FLAGS + -h --help Print help information and exit + -H --helpAll Print help information (including hidden commands/flags) and exit + +COMMANDS + sub sub + subHidden subHidden + +:: STDERR + +:::: Application / run / nested basic route map with hidden routes, always show help-all (alias) / displays help text for nested hidden route map +ExitCode=Success +:: STDOUT +USAGE + cli subHidden command + cli subHidden -h + cli subHidden -H + +subHidden + +FLAGS + -h --help Print help information and exit + -H --helpAll Print help information (including hidden commands/flags) and exit + +COMMANDS + command basic command + +:: STDERR + +:::: Application / run / nested basic route map with hidden routes, always show help-all (alias) / displays help text for nested hidden route map (implicit) +ExitCode=Success +:: STDOUT +USAGE + cli subHidden command + cli subHidden -h + cli subHidden -H + +subHidden + +FLAGS + -h --help Print help information and exit + -H --helpAll Print help information (including hidden commands/flags) and exit + +COMMANDS + command basic command + +:: STDERR + +:::: Application / run / nested basic route map with hidden routes, always show help-all (alias) / fails for undefined flag +ExitCode=UnknownCommand +:: STDOUT + +:: STDERR +No command registered for `--undefined` + +:::: Application / run / nested basic route map with hidden routes, always show help-all (alias) / fails for undefined route +ExitCode=UnknownCommand +:: STDOUT + +:: STDERR +No command registered for `undefined` + :::: Application / run / nested basic route map with hidden routes, always show help-all / display help text for root ExitCode=Success :: STDOUT diff --git a/packages/core/tests/baselines/reference/routing/command.txt b/packages/core/tests/baselines/reference/routing/command.txt index 8c1f307..7567ba3 100644 --- a/packages/core/tests/baselines/reference/routing/command.txt +++ b/packages/core/tests/baselines/reference/routing/command.txt @@ -256,6 +256,26 @@ USAGE brief +FLAGS + -a --alpha alpha flag brief + --bravo... bravo flag brief + [--charlie] charlie flag brief + -d --delta/--noDelta delta flag brief + -h --help Print help information and exit + +ARGUMENTS + args... string array brief + +:::: Command / printHelp / mixed parameters, enhanced custom usage +USAGE + prefix -a 1 + enhanced usage line #1 + prefix -a 2 -d + enhanced usage line #2 + prefix --help + +brief + FLAGS -a --alpha alpha flag brief --bravo... bravo flag brief @@ -307,6 +327,49 @@ USAGE Longer description of this command's behavior, only printed during --help +FLAGS + -a --alpha alpha flag brief + --bravo... bravo flag brief + [--charlie] charlie flag brief + -d --delta/--noDelta delta flag brief + -h --help Print help information and exit + +ARGUMENTS + args... string array brief + +:::: Command / printHelp / mixed parameters, help all, force alias in usage line +USAGE + prefix (-a value) (--bravo value)... [--charlie c] (-d) ... + prefix -h + prefix -H + +brief + +FLAGS + -a --alpha alpha flag brief + --bravo... bravo flag brief + [--charlie] charlie flag brief + -d --delta/--noDelta delta flag brief + -h --help Print help information and exit + -H --helpAll Print help information (including hidden commands/flags) and exit + +ARGUMENTS + args... string array brief + +:::: Command / printHelp / mixed parameters, mixed custom usage +USAGE + prefix -a 1 + enhanced usage line #1 + prefix normal custom usage A + prefix normal custom usage B + prefix -a 2 -d + enhanced usage line #2 + prefix normal custom usage C + prefix normal custom usage D + prefix --help + +brief + FLAGS -a --alpha alpha flag brief --bravo... bravo flag brief @@ -442,6 +505,18 @@ brief FLAGS -h --help Print help information and exit +:::: Command / printHelp / no parameters, help all, force alias in usage line +USAGE + prefix + prefix -h + prefix -H + +brief + +FLAGS + -h --help Print help information and exit + -H --helpAll Print help information (including hidden commands/flags) and exit + :::: Command / run / command function returns error / with custom exit code ExitCode=Unknown(10) :: STDOUT diff --git a/packages/core/tests/routing/command.spec.ts b/packages/core/tests/routing/command.spec.ts index 121a598..ae9f740 100644 --- a/packages/core/tests/routing/command.spec.ts +++ b/packages/core/tests/routing/command.spec.ts @@ -330,6 +330,32 @@ describe("Command", function () { compareToBaseline(this, StringArrayBaselineFormat, helpString.split("\n")); }); + it("no parameters, help all, force alias in usage line", function () { + // GIVEN + const command = buildCommand({ + loader: async () => { + return { + default: (flags: {}) => {}, + }; + }, + parameters: {}, + docs: { brief: "brief" }, + }); + + // WHEN + const helpString = command.formatHelp({ + ...defaultArgs, + includeHelpAllFlag: true, + config: { + ...defaultArgs.config, + useAliasInUsageLine: true, + }, + }); + + // THEN + compareToBaseline(this, StringArrayBaselineFormat, helpString.split("\n")); + }); + it("mixed parameters", function () { // GIVEN const command = buildCommand({ @@ -1201,6 +1227,150 @@ describe("Command", function () { compareToBaseline(this, StringArrayBaselineFormat, helpString.split("\n")); }); + it("mixed parameters, enhanced custom usage", function () { + // GIVEN + const command = buildCommand({ + loader: async () => { + return { + default: ( + flags: { alpha: number; bravo: number[]; charlie?: number; delta: boolean }, + ...args: string[] + ) => {}, + }; + }, + parameters: { + positional: { + kind: "array", + parameter: { + brief: "string array brief", + parse: (x) => x, + }, + }, + flags: { + alpha: { + brief: "alpha flag brief", + kind: "parsed", + parse: numberParser, + }, + bravo: { + brief: "bravo flag brief", + kind: "parsed", + variadic: true, + parse: numberParser, + }, + charlie: { + brief: "charlie flag brief", + placeholder: "c", + kind: "parsed", + optional: true, + parse: numberParser, + }, + delta: { + brief: "delta flag brief", + kind: "boolean", + }, + }, + aliases: { + a: "alpha", + d: "delta", + }, + }, + docs: { + brief: "brief", + customUsage: [ + { + input: "-a 1", + brief: "enhanced usage line #1", + }, + { + input: "-a 2 -d", + brief: "enhanced usage line #2", + }, + ], + }, + }); + + // WHEN + const helpString = command.formatHelp(defaultArgs); + + // THEN + compareToBaseline(this, StringArrayBaselineFormat, helpString.split("\n")); + }); + + it("mixed parameters, mixed custom usage", function () { + // GIVEN + const command = buildCommand({ + loader: async () => { + return { + default: ( + flags: { alpha: number; bravo: number[]; charlie?: number; delta: boolean }, + ...args: string[] + ) => {}, + }; + }, + parameters: { + positional: { + kind: "array", + parameter: { + brief: "string array brief", + parse: (x) => x, + }, + }, + flags: { + alpha: { + brief: "alpha flag brief", + kind: "parsed", + parse: numberParser, + }, + bravo: { + brief: "bravo flag brief", + kind: "parsed", + variadic: true, + parse: numberParser, + }, + charlie: { + brief: "charlie flag brief", + placeholder: "c", + kind: "parsed", + optional: true, + parse: numberParser, + }, + delta: { + brief: "delta flag brief", + kind: "boolean", + }, + }, + aliases: { + a: "alpha", + d: "delta", + }, + }, + docs: { + brief: "brief", + customUsage: [ + { + input: "-a 1", + brief: "enhanced usage line #1", + }, + "normal custom usage A", + "normal custom usage B", + { + input: "-a 2 -d", + brief: "enhanced usage line #2", + }, + "normal custom usage C", + "normal custom usage D", + ], + }, + }); + + // WHEN + const helpString = command.formatHelp(defaultArgs); + + // THEN + compareToBaseline(this, StringArrayBaselineFormat, helpString.split("\n")); + }); + it("mixed parameters with `original` display case style", function () { // GIVEN const command = buildCommand({ @@ -1776,6 +1946,73 @@ describe("Command", function () { // THEN compareToBaseline(this, StringArrayBaselineFormat, helpString.split("\n")); }); + + it("mixed parameters, help all, force alias in usage line", function () { + // GIVEN + const command = buildCommand({ + loader: async () => { + return { + default: ( + flags: { alpha: number; bravo: number[]; charlie?: number; delta: boolean }, + ...args: string[] + ) => {}, + }; + }, + parameters: { + positional: { + kind: "array", + parameter: { + brief: "string array brief", + parse: (x) => x, + }, + }, + flags: { + alpha: { + brief: "alpha flag brief", + kind: "parsed", + parse: numberParser, + }, + bravo: { + brief: "bravo flag brief", + kind: "parsed", + variadic: true, + parse: numberParser, + }, + charlie: { + brief: "charlie flag brief", + placeholder: "c", + kind: "parsed", + optional: true, + parse: numberParser, + }, + delta: { + brief: "delta flag brief", + kind: "boolean", + }, + }, + aliases: { + a: "alpha", + d: "delta", + }, + }, + docs: { + brief: "brief", + }, + }); + + // WHEN + const helpString = command.formatHelp({ + ...defaultArgs, + includeHelpAllFlag: true, + config: { + ...defaultArgs.config, + useAliasInUsageLine: true, + }, + }); + + // THEN + compareToBaseline(this, StringArrayBaselineFormat, helpString.split("\n")); + }); }); describe("run", function () {