Skip to content

Commit

Permalink
feat: Display text override for default option value
Browse files Browse the repository at this point in the history
This PR adds the following two features related to changing default value display for options.

1. `defaultText` option for options

It is either a string or a function that transforms the default value into string. When set, it changes the help text of the default, while the underlying default value is used for processing.

2. `"secret"` input type

This is exactly the same as string type, except it hides its default values on help text.

Testing: new and existing unittests, playing with the build

Fixes: c4spar#774
  • Loading branch information
tugrulates committed Dec 18, 2024
1 parent 1d5feed commit dab4673
Show file tree
Hide file tree
Showing 13 changed files with 159 additions and 29 deletions.
2 changes: 2 additions & 0 deletions command/_argument_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -21,6 +22,7 @@ type DefaultTypes = {
string: StringType;
boolean: BooleanType;
file: FileType;
secret: SecretType;
};

type OptionalOrRequiredValue<TType extends string> =
Expand Down
11 changes: 10 additions & 1 deletion command/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import type {
CompleteHandler,
CompleteOptions,
Completion,
DefaultText,
DefaultValue,
Description,
EnvVar,
Expand All @@ -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";

Expand Down Expand Up @@ -129,6 +131,7 @@ export class Command<
string: string;
boolean: boolean;
file: string;
secret: string;
},
TCommandGlobalTypes extends Record<string, unknown> | void =
TParentCommandGlobals extends number ? any : void,
Expand Down Expand Up @@ -1253,6 +1256,7 @@ export class Command<
>
& {
default?: DefaultValue<TDefaultValue>;
defaultText?: DefaultText<TDefaultValue>;
required?: TRequired;
collect?: TCollect;
value?: OptionValueHandler<
Expand Down Expand Up @@ -1331,6 +1335,7 @@ export class Command<
& {
global: true;
default?: DefaultValue<TDefaultValue>;
defaultText?: DefaultText<TDefaultValue>;
required?: TRequired;
collect?: TCollect;
value?: OptionValueHandler<
Expand Down Expand Up @@ -1385,12 +1390,14 @@ export class Command<
TCommandTypes,
TCommandGlobalTypes,
TParentCommandTypes,
TParentCommand
TParentCommand,
TDefaultValue
>,
"value"
>
& {
default?: DefaultValue<TDefaultValue>;
defaultText?: DefaultText<TDefaultValue>;
required?: TRequired;
collect?: TCollect;
conflicts?: TConflicts;
Expand Down Expand Up @@ -1855,6 +1862,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({});
Expand Down
21 changes: 17 additions & 4 deletions command/help/_help_generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,14 +311,28 @@ 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" &&
typeof defaultValue !== "undefined")
? 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),
);
}
}
Expand All @@ -333,7 +347,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) {
Expand Down
1 change: 1 addition & 0 deletions command/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
4 changes: 3 additions & 1 deletion command/test/command/generic_types_test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test } from "@cliffy/internal/testing/test";
import { Command, EnumType } from "../../mod.ts";
import { assertType, type IsAny, type IsExact } from "@std/testing/types";
import { Command, EnumType } from "../../mod.ts";

// Not required to execute this code, only type check.
(() => {
Expand Down Expand Up @@ -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
Expand Down
53 changes: 37 additions & 16 deletions command/test/command/help_command_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]",
Expand Down Expand Up @@ -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 <val:string> - 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 <val:string> - I have many hints! (required, Default: "test", Depends: --test, Conflicts:
--depends)
Commands:
Expand Down Expand Up @@ -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 <val:string> - 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 <val:string> - I have many hints! (required, Default: "test", Depends: --test, Conflicts:
--depends)
Commands:
Expand Down
2 changes: 2 additions & 0 deletions command/test/command/option_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ test("command - option - option properties", () => {
standalone: true,
collect: true,
default: false,
defaultText: "false",
})
.getOption("foo-bar", true) as Option;

Expand All @@ -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",
Expand Down
45 changes: 45 additions & 0 deletions command/test/type/secret_test.ts
Original file line number Diff line number Diff line change
@@ -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".`,
);
});
11 changes: 8 additions & 3 deletions command/test/type/string_test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down
8 changes: 7 additions & 1 deletion command/type.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -34,6 +34,12 @@ import type {
export abstract class Type<TValue> {
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.
Expand Down
Loading

0 comments on commit dab4673

Please sign in to comment.