Skip to content

Commit

Permalink
feat(command): make generated help customizable (#134)
Browse files Browse the repository at this point in the history
  • Loading branch information
c4spar committed Jan 10, 2021
1 parent 8c7789b commit 0cfceb7
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 24 deletions.
41 changes: 37 additions & 4 deletions command/README.md
Expand Up @@ -68,6 +68,7 @@
- [Environment variables](#-environment-variables)
- [Add examples](#-add-examples)
- [Auto generated help](#-auto-generated-help)
- [Customize help](#customize-help)
- [Help option](#help-option)
- [Help command](#help-command)
- [Did you mean](#-did-you-mean)
Expand Down Expand Up @@ -1048,10 +1049,6 @@ $ deno run https://deno.land/x/cliffy/examples/command/examples.ts help

The help information is auto-generated based on the information you have defined on your command's.

### Help option

The `--help` and `-h` option flag prints the auto generated help.

```typescript
import { Command } from "https://deno.land/x/cliffy/command/command.ts";
import { CompletionsCommand } from "https://deno.land/x/cliffy/command/completions/mod.ts";
Expand Down Expand Up @@ -1080,6 +1077,42 @@ $ deno run https://deno.land/x/cliffy/examples/command/help_option_and_command.t

![](assets/img/help.gif)

### Customize help

Customize default help with the `.help()` method.

```typescript
import { Command } from "https://deno.land/x/cliffy/command/command.ts";

await new Command()
.help({
// Show argument types.
types: true, // default: false
// Show hints.
hints: true, // default: true
})
.option("-f, --foo [val:number]", "Some description.", { required: true, default: 2 })
.parse();
```

You can also override the help output with the `.help()` method. This overrides the output of the `.getHelp()` and
`.showHelp()` method's which is used by the `-h` and `--help` option and the `help` command. The help handler will be
used for each command, but can be overridden by child commands.

```typescript
import { Command } from "https://deno.land/x/cliffy/command/command.ts";

await new Command()
.help("My custom help")
// Can be also a function.
.help(() => "My custom help")
.parse();
```

### Help option

The `--help` and `-h` option flag prints the auto generated help.

#### Customize help option

The help option is completely customizable with the `.helpOption()` method. The first argument are the flags
Expand Down
33 changes: 28 additions & 5 deletions command/command.ts
Expand Up @@ -39,6 +39,7 @@ import { NumberType } from "./types/number.ts";
import { StringType } from "./types/string.ts";
import { Type } from "./type.ts";
import { HelpGenerator } from "./help/_help_generator.ts";
import type { HelpOptions } from "./help/_help_generator.ts";
import type {
IAction,
IArgument,
Expand Down Expand Up @@ -105,6 +106,7 @@ export class Command<O = any, A extends Array<any> = any> {
private hasDefaults = false;
private _versionOption?: IDefaultOption<O, A> | false;
private _helpOption?: IDefaultOption<O, A> | false;
private _help?: (this: Command<O, A>) => string;

/** Disable version option. */
public versionOption(enable: false): this;
Expand Down Expand Up @@ -320,6 +322,21 @@ export class Command<O = any, A extends Array<any> = any> {
return this;
}

public help(
help: string | ((this: Command<O, A>) => string) | HelpOptions,
): this {
if (typeof help === "string") {
this.cmd._help = () => help;
} else if (typeof help === "function") {
this.cmd._help = help;
} else {
this.cmd._help = function (this: Command<O, A>): string {
return HelpGenerator.generate(this, help);
};
}
return this;
}

/**
* Set the long command description.
* @param description The command description.
Expand Down Expand Up @@ -488,7 +505,7 @@ export class Command<O = any, A extends Array<any> = any> {
* cmd.parse();
* } catch(error) {
* if (error instanceof ValidationError) {
* cmd.help();
* cmd.showHelp();
* Deno.exit(1);
* }
* throw error;
Expand Down Expand Up @@ -721,6 +738,12 @@ export class Command<O = any, A extends Array<any> = any> {

this.reset();

if (!this._help) {
this._help = function (this: Command<O, A>): string {
return HelpGenerator.generate(this);
};
}

if (this.ver && this._versionOption !== false) {
this.option(
this._versionOption?.flags || "-V, --version",
Expand Down Expand Up @@ -748,7 +771,7 @@ export class Command<O = any, A extends Array<any> = any> {
global: true,
prepend: true,
action: function (this: Command) {
this.help();
this.showHelp();
Deno.exit(0);
},
}, this._helpOption?.opts ?? {}),
Expand Down Expand Up @@ -1032,7 +1055,7 @@ export class Command<O = any, A extends Array<any> = any> {
if (this.shouldThrowErrors() || !(error instanceof ValidationError)) {
return error;
}
this.help();
this.showHelp();
Deno.stderr.writeSync(
new TextEncoder().encode(
red(` ${bold("error")}: ${error.message}\n`) + "\n",
Expand Down Expand Up @@ -1140,14 +1163,14 @@ export class Command<O = any, A extends Array<any> = any> {
}

/** Output generated help without exiting. */
public help() {
public showHelp() {
Deno.stdout.writeSync(new TextEncoder().encode(this.getHelp()));
}

/** Get generated help. */
public getHelp(): string {
this.registerDefaults();
return HelpGenerator.generate(this);
return (this._help!)();
}

/*****************************************************************************
Expand Down
2 changes: 1 addition & 1 deletion command/completions/mod.ts
Expand Up @@ -27,7 +27,7 @@ To enable shell completions for this program add following line to your ${
}
`;
})
.action(() => this.help())
.action(() => this.showHelp())
.command("bash", new BashCompletionsCommand(this.#cmd))
.command("fish", new FishCompletionsCommand(this.#cmd))
.command("zsh", new ZshCompletionsCommand(this.#cmd))
Expand Down
50 changes: 38 additions & 12 deletions command/help/_help_generator.ts
Expand Up @@ -15,16 +15,27 @@ import {
import { IArgument } from "../types.ts";
import type { IEnvVar, IExample, IOption } from "../types.ts";

export interface HelpOptions {
types?: boolean;
hints?: boolean;
}

/** Help text generator. */
export class HelpGenerator {
private indent = 2;
private options: HelpOptions;

/** Generate help text for given command. */
public static generate(cmd: Command): string {
return new HelpGenerator(cmd).generate();
public static generate(cmd: Command, options?: HelpOptions): string {
return new HelpGenerator(cmd, options).generate();
}

private constructor(protected cmd: Command) {}
private constructor(private cmd: Command, options: HelpOptions = {}) {
this.options = Object.assign({
types: false,
hints: true,
}, options);
}

private generate(): string {
return this.generateHeader() +
Expand Down Expand Up @@ -91,7 +102,10 @@ export class HelpGenerator {
Table.from([
...options.map((option: IOption) => [
option.flags.split(/,? +/g).map((flag) => blue(flag)).join(", "),
highlightArguments(option.typeDefinition || ""),
highlightArguments(
option.typeDefinition || "",
this.options.types,
),
red(bold("-")) + " " +
option.description.split("\n").shift() as string,
this.generateHints(option),
Expand Down Expand Up @@ -139,6 +153,7 @@ export class HelpGenerator {
).join(", "),
highlightArguments(
command.getArgsDefinition() || "",
this.options.types,
),
red(bold("-")) + " " +
command.getDescription().split("\n").shift() as string,
Expand Down Expand Up @@ -174,7 +189,10 @@ export class HelpGenerator {
Table.from([
...envVars.map((envVar: IEnvVar) => [
envVar.names.map((name: string) => blue(name)).join(", "),
highlightArgumentDetails(envVar.details),
highlightArgumentDetails(
envVar.details,
this.options.types,
),
`${red(bold("-"))} ${envVar.description}`,
]),
])
Expand Down Expand Up @@ -202,6 +220,9 @@ export class HelpGenerator {
}

private generateHints(option: IOption): string {
if (!this.options.hints) {
return "";
}
const hints = [];

option.required && hints.push(yellow(`required`));
Expand Down Expand Up @@ -246,21 +267,26 @@ function inspect(value: unknown): string {
/**
* Colorize arguments string.
* @param argsDefinition Arguments definition: `<color1:string> <color2:string>`
* @param types Show types.
*/
function highlightArguments(argsDefinition: string) {
function highlightArguments(argsDefinition: string, types = true) {
if (!argsDefinition) {
return "";
}

return parseArgumentsDefinition(argsDefinition)
.map((arg: IArgument) => highlightArgumentDetails(arg)).join(" ");
.map((arg: IArgument) => highlightArgumentDetails(arg, types)).join(" ");
}

/**
* Colorize argument string.
* @param arg Argument details.
* @param types Show types.
*/
function highlightArgumentDetails(arg: IArgument): string {
function highlightArgumentDetails(
arg: IArgument,
types = true,
): string {
let str = "";

str += yellow(arg.optionalValue ? "[" : "<");
Expand All @@ -274,10 +300,10 @@ function highlightArgumentDetails(arg: IArgument): string {

str += name;

// if ( arg.name !== arg.type ) {
str += yellow(":");
str += red(arg.type);
// }
if (types) {
str += yellow(":");
str += red(arg.type);
}

if (arg.list) {
str += green("[]");
Expand Down
2 changes: 1 addition & 1 deletion command/help/mod.ts
Expand Up @@ -22,7 +22,7 @@ export class HelpCommand extends Command {
...this.getAliases(),
]);
}
cmd.help();
cmd.showHelp();
Deno.exit(0);
});
}
Expand Down
6 changes: 5 additions & 1 deletion command/test/command/help_command_test.ts
Expand Up @@ -9,7 +9,11 @@ function command(defaultOptions?: boolean, hintOption?: boolean) {
const cmd = new Command()
.throwErrors()
.version("1.0.0")
.description("Test description ...");
.description("Test description ...")
.help({
hints: true,
types: true,
});

if (!defaultOptions) {
cmd.versionOption(false)
Expand Down
4 changes: 4 additions & 0 deletions command/test/command/hidden_command_test.ts
Expand Up @@ -8,6 +8,10 @@ function command(): Command {
.throwErrors()
.version("1.0.0")
.description("Test description ...")
.help({
types: true,
hints: true,
})
.command("help", new HelpCommand())
.command("completions", new CompletionsCommand())
.command("hidden-command <input:string> <output:string>")
Expand Down
9 changes: 9 additions & 0 deletions examples/command/help_option_and_command.ts
Expand Up @@ -8,6 +8,10 @@ await new Command()
.name("help-option-and-command")
.version("0.1.0")
.description("Sample description ...")
.help({
types: true,
hints: true,
})
.env(
"EXAMPLE_ENVIRONMENT_VARIABLE=<value:boolean>",
"Environment variable description ...",
Expand All @@ -16,6 +20,11 @@ await new Command()
"Some example",
"Example content ...\n\nSome more example content ...",
)
.option(
"-f, --foo [val:number]",
"Some description.",
{ required: true, default: 2 },
)
.command("help", new HelpCommand())
.command("completions", new CompletionsCommand())
.parse(Deno.args);

0 comments on commit 0cfceb7

Please sign in to comment.