Skip to content

Commit

Permalink
Adds a --log-level built-in option to all commands (#3107)
Browse files Browse the repository at this point in the history
  • Loading branch information
IMax153 committed Jun 29, 2024
1 parent cb76bcb commit fcb7411
Show file tree
Hide file tree
Showing 10 changed files with 180 additions and 92 deletions.
5 changes: 5 additions & 0 deletions .changeset/popular-bananas-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/cli": patch
---

Adds a `--log-level` built-in option to all commands which can be used to control the minimum `LogLevel` of the command's associated handler
6 changes: 6 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ For a more detailed walkthrough, take a read through the [Tutorial](#tutorial) b

Here's a breakdown of the key built-in options available in `@effect/cli`:

- **Log Level (`[--log-level]`)**:

- **Description**: Sets the **minimum** log level for a `Command`'s handler method
- **Usage**: `--log-level (all | trace | debug | info | warning | error | fatal | none)`
- **Functionality**: Allows you to specify the **minimum** log level for a `Command`'s handler method. By setting this option, you can control the verbosity of the log output, ensuring that only logs of a certain priority or higher are output by your program.

- **Shell Completions (`[--completions]`)**:

- **Description**: Automatically generates shell completion scripts to enhance user experience. Shell completions suggest possible command options when you type a command and hit the tab key.
Expand Down
18 changes: 10 additions & 8 deletions packages/cli/examples/naval-fate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Args, CliConfig, Command, Options } from "@effect/cli"
import { NodeContext, NodeKeyValueStore, NodeRuntime } from "@effect/platform-node"
import * as Console from "effect/Console"
import * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as NavalFateStore from "./naval-fate/store.js"
Expand Down Expand Up @@ -102,14 +101,18 @@ const command = Command.make("naval_fate").pipe(
])
)

const cliContext = Context.make(
CliConfig.CliConfig,
CliConfig.make({ showBuiltIns: false })
const ConfigLive = CliConfig.layer({
showBuiltIns: false
})

const NavalFateLive = NavalFateStore.layer.pipe(
Layer.provide(NodeKeyValueStore.layerFileSystem("naval-fate-store"))
)

const MainLayer = NavalFateStore.layer.pipe(
Layer.provide(NodeKeyValueStore.layerFileSystem("naval-fate-store")),
Layer.merge(NodeContext.layer)
const MainLayer = Layer.mergeAll(
ConfigLive,
NavalFateLive,
NodeContext.layer
)

const cli = Command.run(command, {
Expand All @@ -118,7 +121,6 @@ const cli = Command.run(command, {
})

Effect.suspend(() => cli(process.argv)).pipe(
Effect.provide(cliContext),
Effect.provide(MainLayer),
Effect.tapErrorCause(Effect.logError),
NodeRuntime.runMain
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/BuiltInOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* @since 1.0.0
*/

import type { LogLevel } from "effect/LogLevel"
import type { Option } from "effect/Option"
import type { Command } from "./CommandDescriptor.js"
import type { HelpDoc } from "./HelpDoc.js"
Expand All @@ -14,11 +15,21 @@ import type { Usage } from "./Usage.js"
* @category models
*/
export type BuiltInOptions =
| SetLogLevel
| ShowHelp
| ShowCompletions
| ShowWizard
| ShowVersion

/**
* @since 1.0.0
* @category models
*/
export interface SetLogLevel {
readonly _tag: "SetLogLevel"
readonly level: LogLevel
}

/**
* @since 1.0.0
* @category models
Expand Down
32 changes: 28 additions & 4 deletions packages/cli/src/internal/builtInOptions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as LogLevel from "effect/LogLevel"
import * as Option from "effect/Option"
import type * as BuiltInOptions from "../BuiltInOptions.js"
import type * as Command from "../CommandDescriptor.js"
Expand All @@ -6,6 +7,14 @@ import type * as Options from "../Options.js"
import type * as Usage from "../Usage.js"
import * as InternalOptions from "./options.js"

/** @internal */
export const setLogLevel = (
level: LogLevel.LogLevel
): BuiltInOptions.BuiltInOptions => ({
_tag: "SetLogLevel",
level
})

/** @internal */
export const showCompletions = (
shellType: BuiltInOptions.BuiltInOptions.ShellType
Expand Down Expand Up @@ -64,28 +73,40 @@ export const completionsOptions: Options.Options<
["zsh", "zsh" as const]
]).pipe(
InternalOptions.optional,
InternalOptions.withDescription("Generate a completion script for a specific shell")
InternalOptions.withDescription("Generate a completion script for a specific shell.")
)

/** @internal */
export const logLevelOptions: Options.Options<
Option.Option<LogLevel.LogLevel>
> = InternalOptions.choiceWithValue(
"log-level",
LogLevel.allLevels.map((level) => [level._tag.toLowerCase(), level] as const)
).pipe(
InternalOptions.optional,
InternalOptions.withDescription("Sets the minimum log level for a command.")
)

/** @internal */
export const helpOptions: Options.Options<boolean> = InternalOptions.boolean("help").pipe(
InternalOptions.withAlias("h"),
InternalOptions.withDescription("Show the help documentation for a command")
InternalOptions.withDescription("Show the help documentation for a command.")
)

/** @internal */
export const versionOptions: Options.Options<boolean> = InternalOptions.boolean("version").pipe(
InternalOptions.withDescription("Show the version of the application")
InternalOptions.withDescription("Show the version of the application.")
)

/** @internal */
export const wizardOptions: Options.Options<boolean> = InternalOptions.boolean("wizard").pipe(
InternalOptions.withDescription("Start wizard mode for a command")
InternalOptions.withDescription("Start wizard mode for a command.")
)

/** @internal */
export const builtIns = InternalOptions.all({
completions: completionsOptions,
logLevel: logLevelOptions,
help: helpOptions,
wizard: wizardOptions,
version: versionOptions
Expand All @@ -101,6 +122,9 @@ export const builtInOptions = <A>(
if (Option.isSome(builtIn.completions)) {
return Option.some(showCompletions(builtIn.completions.value))
}
if (Option.isSome(builtIn.logLevel)) {
return Option.some(setLogLevel(builtIn.logLevel.value))
}
if (builtIn.help) {
return Option.some(showHelp(usage, helpDoc))
}
Expand Down
14 changes: 14 additions & 0 deletions packages/cli/src/internal/cliApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import { dual, pipe } from "effect/Function"
import * as HashMap from "effect/HashMap"
import * as Logger from "effect/Logger"
import * as Option from "effect/Option"
import { pipeArguments } from "effect/Pipeable"
import * as Unify from "effect/Unify"
Expand Down Expand Up @@ -161,6 +162,19 @@ const handleBuiltInOption = <R, E, A>(
R | CliApp.CliApp.Environment | Terminal.Terminal
> => {
switch (builtIn._tag) {
case "SetLogLevel": {
const nextArgs = executable.split(/\s+/)
// Filter out the log level option before re-executing the command
for (let i = 0; i < args.length; i++) {
if (args[i] === "--log-level" || args[i - 1] === "--log-level") {
continue
}
nextArgs.push(args[i])
}
return run(self, nextArgs, execute).pipe(
Logger.withMinimumLogLevel(builtIn.level)
)
}
case "ShowHelp": {
const banner = InternalHelpDoc.h1(InternalSpan.code(self.name))
const header = InternalHelpDoc.p(InternalSpan.spans([
Expand Down
98 changes: 58 additions & 40 deletions packages/cli/test/CliApp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as HelpDoc from "@effect/cli/HelpDoc"
import * as MockConsole from "@effect/cli/test/services/MockConsole"
import * as ValidationError from "@effect/cli/ValidationError"
import { NodeContext } from "@effect/platform-node"
import { Array, Console, Effect, Layer } from "effect"
import { Array, Console, Effect, FiberRef, Layer, LogLevel } from "effect"
import { describe, expect, it } from "vitest"

const MainLive = Effect.gen(function*(_) {
Expand Down Expand Up @@ -37,47 +37,65 @@ describe("CliApp", () => {
)))
}).pipe(runEffect))

it("should display built-in options in help if `CliConfig.showBuiltIns` is true", () => {
const CliConfigLive = CliConfig.layer({
showBuiltIns: true // this is the default
})
return Effect.gen(function*() {
const cli = Command.run(Command.make("foo"), {
name: "Test",
version: "1.0.0"
describe("Built-In Options Processing", () => {
it("should display built-in options in help if `CliConfig.showBuiltIns` is true", () => {
const CliConfigLive = CliConfig.layer({
showBuiltIns: true // this is the default
})
yield* cli([])
const lines = yield* MockConsole.getLines()
const output = lines.join("\n")
expect(output).toContain("--completions sh | bash | fish | zsh")
expect(output).toContain("(-h, --help)")
expect(output).toContain("--wizard")
expect(output).toContain("--version")
}).pipe(
Effect.provide(Layer.mergeAll(MainLive, CliConfigLive)),
Effect.runPromise
)
})

it("should not display built-in options in help if `CliConfig.showBuiltIns` is false", () => {
const CliConfigLive = CliConfig.layer({
showBuiltIns: false
return Effect.gen(function*() {
const cli = Command.run(Command.make("foo"), {
name: "Test",
version: "1.0.0"
})
yield* cli([])
const lines = yield* MockConsole.getLines()
const output = lines.join("\n")
expect(output).toContain("--completions sh | bash | fish | zsh")
expect(output).toContain("(-h, --help)")
expect(output).toContain("--wizard")
expect(output).toContain("--version")
}).pipe(
Effect.provide(Layer.mergeAll(MainLive, CliConfigLive)),
Effect.runPromise
)
})
return Effect.gen(function*() {
const cli = Command.run(Command.make("foo"), {
name: "Test",
version: "1.0.0"

it("should not display built-in options in help if `CliConfig.showBuiltIns` is false", () => {
const CliConfigLive = CliConfig.layer({
showBuiltIns: false
})
yield* cli([])
const lines = yield* MockConsole.getLines()
const output = lines.join("\n")
expect(output).not.toContain("--completions sh | bash | fish | zsh")
expect(output).not.toContain("(-h, --help)")
expect(output).not.toContain("--wizard")
expect(output).not.toContain("--version")
}).pipe(
Effect.provide(Layer.mergeAll(MainLive, CliConfigLive)),
Effect.runPromise
)
return Effect.gen(function*() {
const cli = Command.run(Command.make("foo"), {
name: "Test",
version: "1.0.0"
})
yield* cli([])
const lines = yield* MockConsole.getLines()
const output = lines.join("\n")
expect(output).not.toContain("--completions sh | bash | fish | zsh")
expect(output).not.toContain("(-h, --help)")
expect(output).not.toContain("--wizard")
expect(output).not.toContain("--version")
}).pipe(
Effect.provide(Layer.mergeAll(MainLive, CliConfigLive)),
Effect.runPromise
)
})

it("should set the minimum log level for a command", () =>
Effect.gen(function*() {
let logLevel: LogLevel.LogLevel | undefined = undefined
const logging = Command.make("logging").pipe(Command.withHandler(() =>
Effect.gen(function*() {
logLevel = yield* FiberRef.get(FiberRef.currentMinimumLogLevel)
})
))
const cli = Command.run(logging, {
name: "Test",
version: "1.0.0"
})
yield* cli(["node", "logging.js", "--log-level", "debug"])
expect(logLevel).toEqual(LogLevel.Debug)
}).pipe(runEffect))
})
})
8 changes: 4 additions & 4 deletions packages/cli/test/snapshots/bash-completions
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
" done",
" case "${cmd}" in",
" forge)",
" opts="-h --completions --help --wizard --version cache"",
" opts="-h --completions --log-level --help --wizard --version cache"",
" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then",
" COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )",
" return 0",
Expand All @@ -40,7 +40,7 @@
" return 0",
" ;;",
" forge__cache)",
" opts="-h --verbose --completions --help --wizard --version clean ls"",
" opts="-h --verbose --completions --log-level --help --wizard --version clean ls"",
" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then",
" COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )",
" return 0",
Expand All @@ -58,7 +58,7 @@
" return 0",
" ;;",
" forge__cache__clean)",
" opts="-h --completions --help --wizard --version"",
" opts="-h --completions --log-level --help --wizard --version"",
" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then",
" COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )",
" return 0",
Expand All @@ -72,7 +72,7 @@
" return 0",
" ;;",
" forge__cache__ls)",
" opts="-h --completions --help --wizard --version"",
" opts="-h --completions --log-level --help --wizard --version"",
" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then",
" COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )",
" return 0",
Expand Down
36 changes: 20 additions & 16 deletions packages/cli/test/snapshots/fish-completions
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
[
"complete -c forge -n "__fish_use_subcommand" -l completions -r -f -a "{sh'',bash'',fish'',zsh''}" -d 'Generate a completion script for a specific shell'",
"complete -c forge -n "__fish_use_subcommand" -s h -l help -d 'Show the help documentation for a command'",
"complete -c forge -n "__fish_use_subcommand" -l wizard -d 'Start wizard mode for a command'",
"complete -c forge -n "__fish_use_subcommand" -l version -d 'Show the version of the application'",
"complete -c forge -n "__fish_use_subcommand" -l completions -r -f -a "{sh'',bash'',fish'',zsh''}" -d 'Generate a completion script for a specific shell.'",
"complete -c forge -n "__fish_use_subcommand" -l log-level -r -f -a "{all'',trace'',debug'',info'',warning'',error'',fatal'',none''}" -d 'Sets the minimum log level for a command.'",
"complete -c forge -n "__fish_use_subcommand" -s h -l help -d 'Show the help documentation for a command.'",
"complete -c forge -n "__fish_use_subcommand" -l wizard -d 'Start wizard mode for a command.'",
"complete -c forge -n "__fish_use_subcommand" -l version -d 'Show the version of the application.'",
"complete -c forge -n "__fish_use_subcommand" -f -a "cache" -d 'The cache command does cache things'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls" -l completions -r -f -a "{sh'',bash'',fish'',zsh''}" -d 'Generate a completion script for a specific shell'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls" -s h -l help -d 'Show the help documentation for a command'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls" -l wizard -d 'Start wizard mode for a command'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls" -l version -d 'Show the version of the application'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls" -l completions -r -f -a "{sh'',bash'',fish'',zsh''}" -d 'Generate a completion script for a specific shell.'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls" -l log-level -r -f -a "{all'',trace'',debug'',info'',warning'',error'',fatal'',none''}" -d 'Sets the minimum log level for a command.'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls" -s h -l help -d 'Show the help documentation for a command.'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls" -l wizard -d 'Start wizard mode for a command.'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls" -l version -d 'Show the version of the application.'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls" -l verbose -d 'Output in verbose mode'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls" -f -a "clean"",
"complete -c forge -n "__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from clean; and not __fish_seen_subcommand_from ls" -f -a "ls"",
"complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from clean" -l completions -r -f -a "{sh'',bash'',fish'',zsh''}" -d 'Generate a completion script for a specific shell'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from clean" -s h -l help -d 'Show the help documentation for a command'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from clean" -l wizard -d 'Start wizard mode for a command'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from clean" -l version -d 'Show the version of the application'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from ls" -l completions -r -f -a "{sh'',bash'',fish'',zsh''}" -d 'Generate a completion script for a specific shell'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from ls" -s h -l help -d 'Show the help documentation for a command'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from ls" -l wizard -d 'Start wizard mode for a command'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from ls" -l version -d 'Show the version of the application'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from clean" -l completions -r -f -a "{sh'',bash'',fish'',zsh''}" -d 'Generate a completion script for a specific shell.'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from clean" -l log-level -r -f -a "{all'',trace'',debug'',info'',warning'',error'',fatal'',none''}" -d 'Sets the minimum log level for a command.'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from clean" -s h -l help -d 'Show the help documentation for a command.'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from clean" -l wizard -d 'Start wizard mode for a command.'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from clean" -l version -d 'Show the version of the application.'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from ls" -l completions -r -f -a "{sh'',bash'',fish'',zsh''}" -d 'Generate a completion script for a specific shell.'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from ls" -l log-level -r -f -a "{all'',trace'',debug'',info'',warning'',error'',fatal'',none''}" -d 'Sets the minimum log level for a command.'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from ls" -s h -l help -d 'Show the help documentation for a command.'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from ls" -l wizard -d 'Start wizard mode for a command.'",
"complete -c forge -n "__fish_seen_subcommand_from cache; and __fish_seen_subcommand_from ls" -l version -d 'Show the version of the application.'",
]
Loading

0 comments on commit fcb7411

Please sign in to comment.