diff --git a/command/_argument_types.ts b/command/_argument_types.ts index 63057dd9..32c916ce 100644 --- a/command/_argument_types.ts +++ b/command/_argument_types.ts @@ -13,6 +13,7 @@ import type { BooleanType } from "./types/boolean.ts"; import type { FileType } from "./types/file.ts"; import type { IntegerType } from "./types/integer.ts"; import type { NumberType } from "./types/number.ts"; +import type { SecretType } from "./types/secret.ts"; import type { StringType } from "./types/string.ts"; type DefaultTypes = { @@ -21,6 +22,7 @@ type DefaultTypes = { string: StringType; boolean: BooleanType; file: FileType; + secret: SecretType; }; type OptionalOrRequiredValue = diff --git a/command/command.ts b/command/command.ts index 8bdd37a5..d004cb37 100644 --- a/command/command.ts +++ b/command/command.ts @@ -58,6 +58,7 @@ import type { CompleteHandler, CompleteOptions, Completion, + DefaultText, DefaultValue, Description, EnvVar, @@ -80,6 +81,7 @@ import { BooleanType } from "./types/boolean.ts"; import { FileType } from "./types/file.ts"; import { IntegerType } from "./types/integer.ts"; import { NumberType } from "./types/number.ts"; +import { SecretType } from "./types/secret.ts"; import { StringType } from "./types/string.ts"; import { checkVersion } from "./upgrade/_check_version.ts"; @@ -129,6 +131,7 @@ export class Command< string: string; boolean: boolean; file: string; + secret: string; }, TCommandGlobalTypes extends Record | void = TParentCommandGlobals extends number ? any : void, @@ -1247,12 +1250,14 @@ export class Command< TCommandTypes, TCommandGlobalTypes, TParentCommandTypes, - TParentCommand + TParentCommand, + TDefaultValue >, "value" > & { default?: DefaultValue; + defaultText?: DefaultText; required?: TRequired; collect?: TCollect; value?: OptionValueHandler< @@ -1324,13 +1329,15 @@ export class Command< TCommandTypes, TCommandGlobalTypes, TParentCommandTypes, - TParentCommand + TParentCommand, + TDefaultValue >, "value" > & { global: true; default?: DefaultValue; + defaultText?: DefaultText; required?: TRequired; collect?: TCollect; value?: OptionValueHandler< @@ -1385,12 +1392,14 @@ export class Command< TCommandTypes, TCommandGlobalTypes, TParentCommandTypes, - TParentCommand + TParentCommand, + TDefaultValue >, "value" > & { default?: DefaultValue; + defaultText?: DefaultText; required?: TRequired; collect?: TCollect; conflicts?: TConflicts; @@ -1855,6 +1864,8 @@ export class Command< this.type("boolean", new BooleanType(), { global: true }); !this.types.has("file") && this.type("file", new FileType(), { global: true }); + !this.types.has("secret") && + this.type("secret", new SecretType(), { global: true }); if (!this._help) { this.help({}); diff --git a/command/help/_help_generator.ts b/command/help/_help_generator.ts index 0de96c2b..9b563227 100644 --- a/command/help/_help_generator.ts +++ b/command/help/_help_generator.ts @@ -311,14 +311,27 @@ export class HelpGenerator { option.required && hints.push(yellow(`required`)); - if (typeof option.default !== "undefined") { + const type = this.cmd.getType(option.args[0]?.type)?.handler; + + if ( + typeof option.default !== "undefined" || + typeof option.defaultText !== "undefined" + ) { const defaultValue = typeof option.default === "function" ? option.default() : option.default; - if (typeof defaultValue !== "undefined") { + const defaultText = typeof option.defaultText === "function" + ? option.defaultText(defaultValue) + : (typeof option.defaultText !== "undefined" + ? option.defaultText + : ((type instanceof Type && type.defaultText) + ? type.defaultText() + : defaultValue)); + + if (typeof defaultText !== "undefined") { hints.push( - bold(`Default: `) + inspect(defaultValue, this.options.colors), + bold(`Default: `) + inspect(defaultText, this.options.colors), ); } } @@ -333,7 +346,6 @@ export class HelpGenerator { italic(option.conflicts.map(getFlag).join(", ")), ); - const type = this.cmd.getType(option.args[0]?.type)?.handler; if (type instanceof Type) { const possibleValues = type.values?.(this.cmd, this.cmd.getParent()); if (possibleValues?.length) { diff --git a/command/mod.ts b/command/mod.ts index 4eeb1e1f..8185eae6 100644 --- a/command/mod.ts +++ b/command/mod.ts @@ -38,6 +38,7 @@ export { EnumType } from "./types/enum.ts"; export { FileType } from "./types/file.ts"; export { IntegerType } from "./types/integer.ts"; export { NumberType } from "./types/number.ts"; +export { SecretType } from "./types/secret.ts"; export { StringType } from "./types/string.ts"; export { Type } from "./type.ts"; export { ValidationError, type ValidationErrorOptions } from "./_errors.ts"; diff --git a/command/test/command/generic_types_test.ts b/command/test/command/generic_types_test.ts index 3169e9a3..599a4148 100644 --- a/command/test/command/generic_types_test.ts +++ b/command/test/command/generic_types_test.ts @@ -983,6 +983,8 @@ import { assertType, type IsAny, type IsExact } from "@std/testing/types"; integer: number; string: string; boolean: boolean; + file: string; + secret: string; }, void, undefined diff --git a/command/test/command/help_command_test.ts b/command/test/command/help_command_test.ts index 3962c646..1e222e4e 100644 --- a/command/test/command/help_command_test.ts +++ b/command/test/command/help_command_test.ts @@ -26,6 +26,21 @@ function command(defaultOptions?: boolean, hintOption?: boolean) { "I have a default value!", { default: "test" }, ) + .option( + "--default-func [val:string]", + "I have a default handler!", + { default: () => "test" }, + ) + .option( + "-T, --default-text [val:string]", + "I have a default text!", + { defaultText: "test" }, + ) + .option( + "--default-text-func [val:string]", + "I have a default text handler!", + { default: "test", defaultText: (value) => value }, + ) .option("-r, --required [val:string]", "I am required!", { required: true }) .option( "-H, --hidden [val:string]", @@ -87,15 +102,18 @@ Description: Options: - -h, --help - Show this help. - -V, --version - Show the version number for this program. - -t, --test [val:string] - test description - -D, --default [val:string] - I have a default value! (Default: "test") - -r, --required [val:string] - I am required! (required) - -d, --depends [val:string] - I depend on test! (Depends: --test) - -c, --conflicts [val:string] - I conflict with test! (Conflicts: --test) - -a, --all - I have many hints! (required, Default: "test", Depends: --test, Conflicts: - --depends) + -h, --help - Show this help. + -V, --version - Show the version number for this program. + -t, --test [val:string] - test description + -D, --default [val:string] - I have a default value! (Default: "test") + --default-func [val:string] - I have a default handler! (Default: "test") + -T, --default-text [val:string] - I have a default text! (Default: "test") + --default-text-func [val:string] - I have a default text handler! (Default: "test") + -r, --required [val:string] - I am required! (required) + -d, --depends [val:string] - I depend on test! (Depends: --test) + -c, --conflicts [val:string] - I conflict with test! (Conflicts: --test) + -a, --all - I have many hints! (required, Default: "test", Depends: --test, Conflicts: + --depends) Commands: @@ -130,13 +148,16 @@ Description: Options: - -t, --test [val:string] - test description - -D, --default [val:string] - I have a default value! (Default: "test") - -r, --required [val:string] - I am required! (required) - -d, --depends [val:string] - I depend on test! (Depends: --test) - -c, --conflicts [val:string] - I conflict with test! (Conflicts: --test) - -a, --all - I have many hints! (required, Default: "test", Depends: --test, Conflicts: - --depends) + -t, --test [val:string] - test description + -D, --default [val:string] - I have a default value! (Default: "test") + --default-func [val:string] - I have a default handler! (Default: "test") + -T, --default-text [val:string] - I have a default text! (Default: "test") + --default-text-func [val:string] - I have a default text handler! (Default: "test") + -r, --required [val:string] - I am required! (required) + -d, --depends [val:string] - I depend on test! (Depends: --test) + -c, --conflicts [val:string] - I conflict with test! (Conflicts: --test) + -a, --all - I have many hints! (required, Default: "test", Depends: --test, Conflicts: + --depends) Commands: diff --git a/command/test/command/option_test.ts b/command/test/command/option_test.ts index ebe7f9ac..76c99939 100644 --- a/command/test/command/option_test.ts +++ b/command/test/command/option_test.ts @@ -39,6 +39,7 @@ test("command - option - option properties", () => { standalone: true, collect: true, default: false, + defaultText: "false", }) .getOption("foo-bar", true) as Option; @@ -57,6 +58,7 @@ test("command - option - option properties", () => { assertEquals(option.standalone, true); assertEquals(option.collect, true); assertEquals(option.default, false); + assertEquals(option.defaultText, "false"); assertEquals(option.args, [{ action: "boolean", diff --git a/command/test/type/secret_test.ts b/command/test/type/secret_test.ts new file mode 100644 index 00000000..a0a33c3d --- /dev/null +++ b/command/test/type/secret_test.ts @@ -0,0 +1,45 @@ +import { test } from "@cliffy/internal/testing/test"; +import { + assertEquals, + assertMatch, + assertNotMatch, + assertRejects, +} from "@std/assert"; +import { Command } from "../../command.ts"; + +const cmd = new Command() + .throwErrors() + .option("-f, --flag [value:secret]", "description ...") + .option("-d, --default [value:secret]", "description ...", { + default: "DEFAULT", + }) + .option("--no-flag", "description ...") + .action(() => {}); + +test("command - type - secret - with no value", async () => { + const { options, args } = await cmd.parse(["-f"]); + + assertEquals(options, { flag: true, default: "DEFAULT" }); + assertEquals(args, []); + assertNotMatch(cmd.getHelp(), /"DEFAULT"/g); + assertMatch(cmd.getHelp(), /"\*\*\*\*\*\*"/g); +}); + +test("command - type - secret - with valid value", async () => { + const { options, args } = await cmd.parse(["--flag", "value"]); + + assertEquals(options, { flag: "value", default: "DEFAULT" }); + assertEquals(args, []); + assertNotMatch(cmd.getHelp(), /"DEFAULT"/g); + assertMatch(cmd.getHelp(), /"\*\*\*\*\*\*"/g); +}); + +test("command - type - secret - no arguments allowed", async () => { + await assertRejects( + async () => { + await cmd.parse(["-f", "value", "unknown"]); + }, + Error, + `No arguments allowed for command "COMMAND".`, + ); +}); diff --git a/command/test/type/string_test.ts b/command/test/type/string_test.ts index 417d60e7..24c2c2db 100644 --- a/command/test/type/string_test.ts +++ b/command/test/type/string_test.ts @@ -1,25 +1,30 @@ import { test } from "@cliffy/internal/testing/test"; -import { assertEquals, assertRejects } from "@std/assert"; +import { assertEquals, assertMatch, assertRejects } from "@std/assert"; import { Command } from "../../command.ts"; const cmd = new Command() .throwErrors() .option("-f, --flag [value:string]", "description ...") + .option("-d, --default [value:string]", "description ...", { + default: "DEFAULT", + }) .option("--no-flag", "description ...") .action(() => {}); test("command - type - string - with no value", async () => { const { options, args } = await cmd.parse(["-f"]); - assertEquals(options, { flag: true }); + assertEquals(options, { flag: true, default: "DEFAULT" }); assertEquals(args, []); + assertMatch(cmd.getHelp(), /"DEFAULT"/g); }); test("command - type - string - with valid value", async () => { const { options, args } = await cmd.parse(["--flag", "value"]); - assertEquals(options, { flag: "value" }); + assertEquals(options, { flag: "value", default: "DEFAULT" }); assertEquals(args, []); + assertMatch(cmd.getHelp(), /"DEFAULT"/g); }); test("command - type - string - no arguments allowed", async () => { diff --git a/command/type.ts b/command/type.ts index a7bf8a4c..bb7cbc6f 100644 --- a/command/type.ts +++ b/command/type.ts @@ -1,8 +1,8 @@ import type { Command } from "./command.ts"; -import type { TypeOrTypeHandler } from "./types.ts"; import type { ArgumentValue, CompleteHandlerResult, + TypeOrTypeHandler, ValuesHandlerResult, } from "./types.ts"; @@ -34,6 +34,12 @@ import type { export abstract class Type { public abstract parse(type: ArgumentValue): TValue; + /** + * Returns the default display text value for the type. This text will be shown + * unless a custom value is provided for option default text. + */ + public defaultText?(): string; + /** * Returns values displayed in help text. If no complete method is provided, * these values are also used for shell completions. diff --git a/command/types.ts b/command/types.ts index 8ab6a61a..5c6e3557 100644 --- a/command/types.ts +++ b/command/types.ts @@ -188,6 +188,16 @@ export type OptionValueHandler = ValueHandler< TReturn >; +/** Default display text or a callback method that returns the default display text. */ +export type DefaultText = + | string + | DefaultTextHandler; + +/** Default text callback function to lazy load the default display text. */ +export type DefaultTextHandler = ( + defaultValue: TValue, +) => string; + type ExcludedCommandOptions = | "name" | "args" @@ -220,6 +230,7 @@ export interface GlobalOptionOptions< TParentCommand extends Command | undefined = TOptions extends number ? any : undefined, + TDefaultValue = unknown, > extends Omit { override?: boolean; hidden?: boolean; @@ -235,7 +246,8 @@ export interface GlobalOptionOptions< >; prepend?: boolean; value?: OptionValueHandler; - default?: DefaultValue; + default?: DefaultValue; + defaultText?: DefaultText; } export interface OptionOptions< @@ -257,6 +269,7 @@ export interface OptionOptions< TParentCommand extends Command | undefined = TOptions extends number ? any : undefined, + TDefaultValue = unknown, > extends GlobalOptionOptions< TOptions, @@ -266,7 +279,8 @@ export interface OptionOptions< TTypes, TGlobalTypes, TParentTypes, - TParentCommand + TParentCommand, + TDefaultValue > { global?: boolean; } diff --git a/command/types/secret.ts b/command/types/secret.ts new file mode 100644 index 00000000..44a9ddbb --- /dev/null +++ b/command/types/secret.ts @@ -0,0 +1,8 @@ +import { StringType } from "./string.ts"; + +/** Secret type. Allows any value, and does not show values in help text. */ +export class SecretType extends StringType { + public override defaultText(): string { + return "******"; + } +} diff --git a/examples/command/common_option_types.ts b/examples/command/common_option_types.ts index 72317fd0..bc6c0530 100755 --- a/examples/command/common_option_types.ts +++ b/examples/command/common_option_types.ts @@ -13,6 +13,8 @@ const { options } = await new Command() .option("-p, --pizza-type ", "Flavour of pizza.") // Option with required number value. .option("-a, --amount ", "Pieces of pizza.") + // Option that hides its default value. + .option("-t, --token ", "Token.", { default: () => "SECRET" }) // One required and one optional command argument. .arguments(" [output:string]") .parse(Deno.args);