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 17, 2024
1 parent 1d5feed commit 2bf65c9
Show file tree
Hide file tree
Showing 9 changed files with 155 additions and 33 deletions.
16 changes: 12 additions & 4 deletions command/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {
UnknownTypeError,
ValidationError as FlagsValidationError,
} from "@cliffy/flags";
import { exit } from "@cliffy/internal/runtime/exit";
import { getArgs } from "@cliffy/internal/runtime/get-args";
import { getEnv } from "@cliffy/internal/runtime/get-env";
import { bold, brightBlue, red } from "@std/fmt/colors";
import type {
MapTypes,
Expand Down Expand Up @@ -38,9 +41,6 @@ import {
UnknownCommandError,
ValidationError,
} from "./_errors.ts";
import { exit } from "@cliffy/internal/runtime/exit";
import { getArgs } from "@cliffy/internal/runtime/get-args";
import { getEnv } from "@cliffy/internal/runtime/get-env";
import type { Merge, OneOf, ValueOf } from "./_type_utils.ts";
import {
getDescription,
Expand All @@ -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 @@ -1253,6 +1255,7 @@ export class Command<
>
& {
default?: DefaultValue<TDefaultValue>;
defaultText?: DefaultText<TDefaultValue>;
required?: TRequired;
collect?: TCollect;
value?: OptionValueHandler<
Expand Down Expand Up @@ -1331,6 +1334,7 @@ export class Command<
& {
global: true;
default?: DefaultValue<TDefaultValue>;
defaultText?: DefaultText<TDefaultValue>;
required?: TRequired;
collect?: TCollect;
value?: OptionValueHandler<
Expand Down Expand Up @@ -1385,12 +1389,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 +1861,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
23 changes: 18 additions & 5 deletions command/help/_help_generator.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { inspect } from "@cliffy/internal/runtime/inspect";
import { Table } from "@cliffy/table";
import {
bold,
Expand All @@ -11,7 +12,6 @@ import {
setColorEnabled,
yellow,
} from "@std/fmt/colors";
import { inspect } from "@cliffy/internal/runtime/inspect";
import {
dedent,
getDescription,
Expand Down 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
55 changes: 38 additions & 17 deletions command/test/command/help_command_test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { test } from "@cliffy/internal/testing/test";
import { assertEquals } from "@std/assert";
import { Command } from "../../command.ts";
import { CompletionsCommand } from "../../completions/completions_command.ts";
import { HelpCommand } from "../../help/help_command.ts";
import { Command } from "../../command.ts";

function command(defaultOptions?: boolean, hintOption?: boolean) {
const cmd = new Command()
Expand All @@ -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
20 changes: 17 additions & 3 deletions command/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import type {
} from "@cliffy/flags";
import type { MapTypes } from "./_argument_types.ts";
import type { ValidationError } from "./_errors.ts";
import type { Merge } from "./_type_utils.ts";
import type { Command } from "./command.ts";
import type { HelpOptions } from "./help/_help_generator.ts";
import type { Type } from "./type.ts";
import type { Merge } from "./_type_utils.ts";

export type { ArgumentValue, DefaultValue, TypeHandler };

Expand Down Expand Up @@ -188,6 +188,16 @@ export type OptionValueHandler<TValue = any, TReturn = TValue> = ValueHandler<
TReturn
>;

/** Default display text or a callback method that returns the default display text. */
export type DefaultText<TValue = unknown> =
| string
| DefaultTextHandler<TValue>;

/** Default text callback function to lazy load the default display text. */
export type DefaultTextHandler<TValue = unknown> = (
defaultValue: TValue,
) => string;

type ExcludedCommandOptions =
| "name"
| "args"
Expand Down Expand Up @@ -220,6 +230,7 @@ export interface GlobalOptionOptions<
TParentCommand extends Command<any> | undefined = TOptions extends number
? any
: undefined,
TDefaultValue = unknown,
> extends Omit<FlagOptions, ExcludedCommandOptions> {
override?: boolean;
hidden?: boolean;
Expand All @@ -235,7 +246,8 @@ export interface GlobalOptionOptions<
>;
prepend?: boolean;
value?: OptionValueHandler;
default?: DefaultValue;
default?: DefaultValue<TDefaultValue>;
defaultText?: DefaultText<TDefaultValue>;
}

export interface OptionOptions<
Expand All @@ -257,6 +269,7 @@ export interface OptionOptions<
TParentCommand extends Command<any> | undefined = TOptions extends number
? any
: undefined,
TDefaultValue = unknown,
> extends
GlobalOptionOptions<
TOptions,
Expand All @@ -266,7 +279,8 @@ export interface OptionOptions<
TTypes,
TGlobalTypes,
TParentTypes,
TParentCommand
TParentCommand,
TDefaultValue
> {
global?: boolean;
}
Expand Down
Loading

0 comments on commit 2bf65c9

Please sign in to comment.