Skip to content
This repository has been archived by the owner on Jul 16, 2024. It is now read-only.

Make help documentation print built-in options by default #416

Merged
merged 1 commit into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/tough-walls-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@effect/cli": patch
---

Make help documentation print built-in options by default

The printing of built-in options in the help documentation can be disabled by providing a custom
`CliConfig` to your CLI application with `showBuiltIns` set to `false`.
7 changes: 7 additions & 0 deletions src/CliConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ export interface CliConfig {
* Defaults to `true`.
*/
readonly showAllNames: boolean
/**
* Whether or not to display built-in options in the help documentation
* generated for a `Command`.
*
* Defaults to `true`.
*/
readonly showBuiltIns: boolean
/**
* Whether or not to display the type of an option in the usage of a
* particular command.
Expand Down
6 changes: 4 additions & 2 deletions src/Command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,10 @@ export const fromDescriptor: {
* @since 1.0.0
* @category accessors
*/
export const getHelp: <Name extends string, R, E, A>(self: Command<Name, R, E, A>) => HelpDoc =
Internal.getHelp
export const getHelp: <Name extends string, R, E, A>(
self: Command<Name, R, E, A>,
config: CliConfig
) => HelpDoc = Internal.getHelp

/**
* @since 1.0.0
Expand Down
2 changes: 1 addition & 1 deletion src/CommandDescriptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export declare namespace Command {
* @since 1.0.0
* @category combinators
*/
export const getHelp: <A>(self: Command<A>) => HelpDoc = Internal.getHelp
export const getHelp: <A>(self: Command<A>, config: CliConfig) => HelpDoc = Internal.getHelp

/**
* @since 1.0.0
Expand Down
1 change: 1 addition & 0 deletions src/internal/cliConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const defaultConfig: CliConfig.CliConfig = {
autoCorrectLimit: 2,
finalCheckBuiltIn: false,
showAllNames: true,
showBuiltIns: true,
showTypes: true
}

Expand Down
51 changes: 26 additions & 25 deletions src/internal/command.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import type { FileSystem } from "@effect/platform/FileSystem"
import type { QuitException, Terminal } from "@effect/platform/Terminal"
import type * as FileSystem from "@effect/platform/FileSystem"
import type * as Terminal from "@effect/platform/Terminal"
import * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import * as Effectable from "effect/Effectable"
import { dual } from "effect/Function"
import { globalValue } from "effect/GlobalValue"
import type { HashMap } from "effect/HashMap"
import type { HashSet } from "effect/HashSet"
import type * as HashMap from "effect/HashMap"
import type * as HashSet from "effect/HashSet"
import type * as Option from "effect/Option"
import { pipeArguments } from "effect/Pipeable"
import * as ReadonlyArray from "effect/ReadonlyArray"
import type * as Types from "effect/Types"
import type * as Args from "../Args.js"
import type * as CliApp from "../CliApp.js"
import type { CliConfig } from "../CliConfig.js"
import type * as CliConfig from "../CliConfig.js"
import type * as Command from "../Command.js"
import type * as Descriptor from "../CommandDescriptor.js"
import type { HelpDoc } from "../HelpDoc.js"
import type { Span } from "../HelpDoc/Span.js"
import type * as HelpDoc from "../HelpDoc.js"
import type * as Span from "../HelpDoc/Span.js"
import type * as Options from "../Options.js"
import type * as Prompt from "../Prompt.js"
import type { Usage } from "../Usage.js"
import type * as Usage from "../Usage.js"
import * as ValidationError from "../ValidationError.js"
import * as InternalArgs from "./args.js"
import * as InternalCliApp from "./cliApp.js"
Expand Down Expand Up @@ -223,13 +223,14 @@ export const make: {

/** @internal */
export const getHelp = <Name extends string, R, E, A>(
self: Command.Command<Name, R, E, A>
): HelpDoc => InternalDescriptor.getHelp(self.descriptor)
self: Command.Command<Name, R, E, A>,
config: CliConfig.CliConfig
): HelpDoc.HelpDoc => InternalDescriptor.getHelp(self.descriptor, config)

/** @internal */
export const getNames = <Name extends string, R, E, A>(
self: Command.Command<Name, R, E, A>
): HashSet<string> => InternalDescriptor.getNames(self.descriptor)
): HashSet.HashSet<string> => InternalDescriptor.getNames(self.descriptor)

/** @internal */
export const getBashCompletions = <Name extends string, R, E, A>(
Expand All @@ -255,13 +256,13 @@ export const getZshCompletions = <Name extends string, R, E, A>(
/** @internal */
export const getSubcommands = <Name extends string, R, E, A>(
self: Command.Command<Name, R, E, A>
): HashMap<string, Descriptor.Command<unknown>> =>
): HashMap.HashMap<string, Descriptor.Command<unknown>> =>
InternalDescriptor.getSubcommands(self.descriptor)

/** @internal */
export const getUsage = <Name extends string, R, E, A>(
self: Command.Command<Name, R, E, A>
): Usage => InternalDescriptor.getUsage(self.descriptor)
): Usage.Usage => InternalDescriptor.getUsage(self.descriptor)

const mapDescriptor = dual<
<A>(f: (_: Descriptor.Command<A>) => Descriptor.Command<A>) => <Name extends string, R, E>(
Expand Down Expand Up @@ -290,13 +291,13 @@ export const prompt = <Name extends string, A, R, E>(
/** @internal */
export const withDescription = dual<
(
help: string | HelpDoc
help: string | HelpDoc.HelpDoc
) => <Name extends string, R, E, A>(
self: Command.Command<Name, R, E, A>
) => Command.Command<Name, R, E, A>,
<Name extends string, R, E, A>(
self: Command.Command<Name, R, E, A>,
help: string | HelpDoc
help: string | HelpDoc.HelpDoc
) => Command.Command<Name, R, E, A>
>(2, (self, help) => mapDescriptor(self, InternalDescriptor.withDescription(help)))

Expand Down Expand Up @@ -389,21 +390,21 @@ export const withSubcommands = dual<
export const wizard = dual<
(
prefix: ReadonlyArray<string>,
config: CliConfig
config: CliConfig.CliConfig
) => <Name extends string, R, E, A>(
self: Command.Command<Name, R, E, A>
) => Effect.Effect<
FileSystem | Terminal,
QuitException | ValidationError.ValidationError,
FileSystem.FileSystem | Terminal.Terminal,
Terminal.QuitException | ValidationError.ValidationError,
ReadonlyArray<string>
>,
<Name extends string, R, E, A>(
self: Command.Command<Name, R, E, A>,
prefix: ReadonlyArray<string>,
config: CliConfig
config: CliConfig.CliConfig
) => Effect.Effect<
FileSystem | Terminal,
QuitException | ValidationError.ValidationError,
FileSystem.FileSystem | Terminal.Terminal,
Terminal.QuitException | ValidationError.ValidationError,
ReadonlyArray<string>
>
>(3, (self, prefix, config) => InternalDescriptor.wizard(self.descriptor, prefix, config))
Expand All @@ -413,8 +414,8 @@ export const run = dual<
(config: {
readonly name: string
readonly version: string
readonly summary?: Span | undefined
readonly footer?: HelpDoc | undefined
readonly summary?: Span.Span | undefined
readonly footer?: HelpDoc.HelpDoc | undefined
}) => <Name extends string, R, E, A>(
self: Command.Command<Name, R, E, A>
) => (
Expand All @@ -423,8 +424,8 @@ export const run = dual<
<Name extends string, R, E, A>(self: Command.Command<Name, R, E, A>, config: {
readonly name: string
readonly version: string
readonly summary?: Span | undefined
readonly footer?: HelpDoc | undefined
readonly summary?: Span.Span | undefined
readonly footer?: HelpDoc.HelpDoc | undefined
}) => (
args: ReadonlyArray<string>
) => Effect.Effect<R | CliApp.CliApp.Environment, E | ValidationError.ValidationError, void>
Expand Down
23 changes: 14 additions & 9 deletions src/internal/commandDescriptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,10 @@ export const prompt = <Name extends string, A>(
// =============================================================================

/** @internal */
export const getHelp = <A>(self: Descriptor.Command<A>): HelpDoc.HelpDoc =>
getHelpInternal(self as Instruction)
export const getHelp = <A>(
self: Descriptor.Command<A>,
config: CliConfig.CliConfig
): HelpDoc.HelpDoc => getHelpInternal(self as Instruction, config)

/** @internal */
export const getNames = <A>(self: Descriptor.Command<A>): HashSet.HashSet<string> =>
Expand Down Expand Up @@ -314,7 +316,7 @@ export const wizard = dual<
// Internals
// =============================================================================

const getHelpInternal = (self: Instruction): HelpDoc.HelpDoc => {
const getHelpInternal = (self: Instruction, config: CliConfig.CliConfig): HelpDoc.HelpDoc => {
switch (self._tag) {
case "Standard": {
const header = InternalHelpDoc.isEmpty(self.description)
Expand All @@ -324,7 +326,10 @@ const getHelpInternal = (self: Instruction): HelpDoc.HelpDoc => {
const argsSection = InternalHelpDoc.isEmpty(argsHelp)
? InternalHelpDoc.empty
: InternalHelpDoc.sequence(InternalHelpDoc.h1("ARGUMENTS"), argsHelp)
const optionsHelp = InternalOptions.getHelp(self.options)
const options = config.showBuiltIns
? Options.all([self.options, InternalBuiltInOptions.builtIns])
: self.options
const optionsHelp = InternalOptions.getHelp(options)
const optionsSection = InternalHelpDoc.isEmpty(optionsHelp)
? InternalHelpDoc.empty
: InternalHelpDoc.sequence(InternalHelpDoc.h1("OPTIONS"), optionsHelp)
Expand All @@ -336,7 +341,7 @@ const getHelpInternal = (self: Instruction): HelpDoc.HelpDoc => {
: InternalHelpDoc.sequence(InternalHelpDoc.h1("DESCRIPTION"), self.description)
}
case "Map": {
return getHelpInternal(self.command)
return getHelpInternal(self.command, config)
}
case "Subcommands": {
const getUsage = (
Expand Down Expand Up @@ -405,7 +410,7 @@ const getHelpInternal = (self: Instruction): HelpDoc.HelpDoc => {
throw new Error("[BUG]: Subcommands.usage - received empty list of subcommands to print")
}
return InternalHelpDoc.sequence(
getHelpInternal(self.parent),
getHelpInternal(self.parent, config),
InternalHelpDoc.sequence(
InternalHelpDoc.h1("COMMANDS"),
printSubcommands(ReadonlyArray.flatMap(
Expand Down Expand Up @@ -534,7 +539,7 @@ const parseInternal = (
const normalizedArgv0 = InternalCliConfig.normalizeCase(config, argv0)
const normalizedCommandName = InternalCliConfig.normalizeCase(config, self.name)
if (normalizedArgv0 === normalizedCommandName) {
const help = getHelpInternal(self)
const help = getHelpInternal(self, config)
const usage = getUsageInternal(self)
const options = InternalBuiltInOptions.builtInOptions(self, usage, help)
const argsWithoutCommand = ReadonlyArray.drop(args, 1)
Expand Down Expand Up @@ -680,7 +685,7 @@ const parseInternal = (
const helpDirectiveForParent = Effect.sync(() => {
return InternalCommandDirective.builtIn(InternalBuiltInOptions.showHelp(
getUsageInternal(self),
getHelpInternal(self)
getHelpInternal(self, config)
))
})
const helpDirectiveForChild = parseChildren.pipe(
Expand Down Expand Up @@ -1357,7 +1362,7 @@ export const helpRequestedError = <A>(
op.error = InternalHelpDoc.empty
op.showHelp = InternalBuiltInOptions.showHelp(
getUsageInternal(command as Instruction),
getHelpInternal(command as Instruction)
getHelpInternal(command as Instruction, InternalCliConfig.defaultConfig)
)
return op
}
9 changes: 6 additions & 3 deletions test/CommandDescriptor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ describe("Command", () => {
describe("Help Documentation", () => {
it("should allow adding help documentation to a command", () =>
Effect.gen(function*(_) {
const config = CliConfig.make({ showBuiltIns: false })
const cmd = Descriptor.make("tldr").pipe(Descriptor.withDescription("this is some help"))
const args = ReadonlyArray.of("tldr")
const result = yield* _(Descriptor.parse(cmd, args, CliConfig.defaultConfig))
Expand All @@ -276,18 +277,20 @@ describe("Command", () => {
HelpDoc.p("this is some help")
)
expect(result).toEqual(CommandDirective.userDefined(ReadonlyArray.empty(), expectedValue))
expect(Descriptor.getHelp(cmd)).toEqual(expectedDoc)
expect(Descriptor.getHelp(cmd, config)).toEqual(expectedDoc)
}).pipe(runEffect))

it("should allow adding help documentation to subcommands", () => {
const config = CliConfig.make({ showBuiltIns: false })
const cmd = Descriptor.make("command").pipe(Descriptor.withSubcommands([
["sub", Descriptor.make("sub").pipe(Descriptor.withDescription("this is some help"))]
]))
const expected = HelpDoc.sequence(HelpDoc.h1("DESCRIPTION"), HelpDoc.p("this is some help"))
expect(Descriptor.getHelp(cmd)).not.toEqual(expected)
expect(Descriptor.getHelp(cmd, config)).not.toEqual(expected)
})

it("should correctly display help documentation for a command", () => {
const config = CliConfig.make({ showBuiltIns: false })
const child2 = Descriptor.make("child2").pipe(
Descriptor.withDescription("help 2")
)
Expand All @@ -299,7 +302,7 @@ describe("Command", () => {
Descriptor.withSubcommands([["child1", child1]])
)
const result = Render.prettyDefault(
Doc.unAnnotate(HelpDoc.toAnsiDoc(Descriptor.getHelp(parent)))
Doc.unAnnotate(HelpDoc.toAnsiDoc(Descriptor.getHelp(parent, config)))
)
expect(result).toBe(String.stripMargin(
`|COMMANDS
Expand Down