--- url: /cookbook.md description: >- Practical recipes for common command-line interface patterns using Optique: subcommands, dependent options, mutually exclusive flags, key–value pairs, and more complex CLI designs with detailed explanations. --- # CLI patterns cookbook This cookbook provides practical recipes for common command-line interface patterns using Optique. Each pattern demonstrates not just how to implement a specific feature, but the underlying principles that make it work, helping you understand how to adapt these techniques to your own applications. The examples focus on real-world CLI patterns you'll encounter when building command-line tools: handling mutually exclusive options, implementing dependent flags, parsing key–value pairs, and organizing complex subcommand structures. ## Core patterns These recipes use only *@optique/core* and *@optique/run*. ### Subcommands with distinct behaviors *** Many CLI tools organize functionality into subcommands, where each subcommand has its own set of options and arguments. This pattern is essential for tools that perform multiple related operations, like Git (`git commit`, `git push`) or Docker (`docker run`, `docker build`). ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { optional } from "@optique/core/modifiers"; import { argument, command, constant, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { run } from "@optique/run"; // ---cut-before--- const addCommand = command( "add", object({ action: constant("add"), key: argument(string({ metavar: "KEY" })), value: argument(string({ metavar: "VALUE" })), }), ); const removeCommand = command( "remove", object({ action: constant("remove"), key: argument(string({ metavar: "KEY" })), }), ); const editCommand = command( "edit", object({ action: constant("edit"), key: argument(string({ metavar: "KEY" })), value: argument(string({ metavar: "VALUE" })), }), ); const listCommand = command( "list", object({ action: constant("list"), pattern: optional( option("-p", "--pattern", string({ metavar: "PATTERN" })), ), }), ); const parser = or(addCommand, removeCommand, editCommand, listCommand); const result = run(parser); // ^? // The result type consists of a discriminated union of all commands. ``` The key insight here is using [`or()`](./concepts/constructs.md#or-parser) to create a discriminated union of different command parsers. Each [`command()`](./concepts/primitives.md#command-parser) parser: 1. *Matches a specific keyword* (`"add"`, `"remove"`, etc.) as the first argument 2. *Provides a unique type tag* using [`constant()`](./concepts/primitives.md#constant-parser) to distinguish commands in the result type 3. *Defines command-specific arguments* that only apply to that particular command The `constant("add")` pattern is crucial because it creates a literal type that TypeScript can use for exhaustive checking. When you handle the result, TypeScript knows exactly which fields are available based on the `action` value: ```typescript twoslash const result = 0 as unknown as { readonly action: "add"; readonly key: string; readonly value: string; } | { readonly action: "remove"; readonly key: string; } | { readonly action: "edit"; readonly key: string; readonly value: string; } | { readonly action: "list"; readonly pattern: string | undefined; }; // ---cut-before--- if (result.action === "add") { // TypeScript knows: result.key and result.value are available console.log(`Adding ${result.key}=${result.value}`); } else if (result.action === "remove") { // TypeScript knows: only result.key is available console.log(`Removing ${result.key}`); } ``` This pattern scales well because adding new subcommands only requires extending the `or()` combinator with new command parsers. ### Positional prefixes before subcommands Some tools accept a small positional prefix before the subcommand itself. For example, a deployment tool might accept an optional profile before commands such as `build` or `deploy`: ```bash tool build app tool staging deploy production --force ``` Use [`seq()`](./concepts/constructs.md#seq-parser) when the order of parsers is part of the grammar. The optional profile is considered first, then the command parser is considered after it: ```typescript twoslash import { object, or, seq } from "@optique/core/constructs"; import { optional } from "@optique/core/modifiers"; import { argument, command, constant, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { run } from "@optique/run"; // ---cut-before--- const parser = seq( optional(argument(string({ metavar: "PROFILE" }))), or( command( "build", object({ action: constant("build"), target: argument(string({ metavar: "TARGET" })), }), ), command( "deploy", object({ action: constant("deploy"), environment: argument(string({ metavar: "ENV" })), force: option("--force"), }), ), ), ); const [profile, commandResult] = run(parser); // ^? const profileName = profile ?? "default"; if (commandResult.action === "build") { console.log(`Building ${commandResult.target} with ${profileName}.`); } else { console.log(`Deploying ${commandResult.environment} with ${profileName}.`); } ``` The important distinction from [`tuple()`](./concepts/constructs.md#tuple-parser) is that `seq()` advances through child parsers in declaration order. A fixed optional positional parser can be skipped when the next token is a later command name, so `tool build app` is parsed as “no profile, then the `build` command.” `seq()` deliberately avoids backtracking. If you put a variadic positional parser before a command, it may consume too much input before the command has a chance to match. Keep the prefix fixed, or add a clear boundary such as an option, command name, or `--`. ### Mutually exclusive options Sometimes you need to accept different sets of options that cannot be used together. This pattern is common in tools that can operate in different modes, where each mode requires its own configuration. ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { withDefault } from "@optique/core/modifiers"; import { argument, constant, option } from "@optique/core/primitives" import { integer, string, url } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; import { print, run } from "@optique/run"; // ---cut-before--- const parser = or( object({ mode: constant("server"), host: withDefault( option( "-h", "--host", string({ metavar: "HOST" }), ), "0.0.0.0", ), port: option( "-p", "--port", integer({ metavar: "PORT", min: 1, max: 0xffff }), ), }), object({ mode: constant("client"), url: argument(url()), }), ); const result = run(parser); // ^? // The result type is a discriminated union of server and client modes. ``` This pattern uses [`or()`](./concepts/constructs.md#or-parser) at the parser level rather than just for individual flags. Each branch of the `or()` represents a complete, valid configuration: Server mode : Requires `--port` option and accepts optional `--host` Client mode : Requires a URL argument The [`constant()`](./concepts/primitives.md#constant-parser) combinator in each branch serves as a discriminator, making it easy to determine which mode was selected and what options are available. The type system prevents you from accidentally accessing client-only fields when in server mode. The [`withDefault()`](./concepts/modifiers.md#withdefault-parser) wrapper ensures that optional fields have sensible defaults, but only within their respective modes. The client mode doesn't get a default host because it doesn't use one. ### Mutually exclusive flags For simpler cases where you need exactly one of several flags, you can use mutually exclusive flags that map to different values. ```typescript twoslash import { or } from "@optique/core/constructs"; import { map, withDefault } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { message } from "@optique/core/message"; import { run } from "@optique/run"; // ---cut-before--- const modeParser = withDefault( or( map(option("-a", "--mode-a"), () => "a" as const), map(option("-b", "--mode-b"), () => "b" as const), map(option("-c", "--mode-c"), () => "c" as const), ), "default" as const, ); const result = run(modeParser); // ^? // The result type is a union of "a", "b", "c", or "default". ``` This pattern combines [`or()`](./concepts/constructs.md#or-parser) with [`map()`](./concepts/modifiers.md#map-parser) to transform boolean flag presence into more meaningful values. Each [`option()`](./concepts/primitives.md#option-parser) parser only succeeds when its flag is present, and `map()` transforms the boolean result into a string literal. The [`withDefault()`](./concepts/modifiers.md#withdefault-parser) wrapper handles the case where no flags are provided, giving you a fallback behavior. This is different from the previous pattern because: * *Conflict detection*: If multiple flags are provided, the parser rejects them with an error (e.g., `--mode-a` and `--mode-b` cannot be used together) * *Simpler structure*: Returns a simple string rather than an object * *Default handling*: Has a meaningful fallback when no options are given ### Optional mutually exclusive flags Sometimes you want mutually exclusive options where *none* of them need to be provided. For example, a verbosity setting where you can specify `--verbose` or `--quiet`, but the default behavior applies when neither is given. The key insight is that [`or()`](./concepts/constructs.md#or-parser) requires at least one alternative to match. To make all alternatives optional, wrap the `or()` with [`optional()`](./concepts/modifiers.md#optional-parser): ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { map, optional, withDefault } from "@optique/core/modifiers"; import { flag } from "@optique/core/primitives"; import { run } from "@optique/run"; // ---cut-before--- // Using optional(): returns undefined when no flag is provided const outputMode = optional( or( map(flag("--verbose", "-v"), () => "verbose" as const), map(flag("--quiet", "-q"), () => "quiet" as const), ), ); // Using withDefault(): returns a default value when no flag is provided const outputModeWithDefault = withDefault( or( map(flag("--verbose", "-v"), () => "verbose" as const), map(flag("--quiet", "-q"), () => "quiet" as const), ), "normal" as const, ); const result1 = run(outputMode); // ^? const result2 = run(outputModeWithDefault); // ^? console.debug(result1, result2); ``` This pattern differs from the basic [mutually exclusive flags](#mutually-exclusive-flags) pattern in an important way: * *Without wrapper*: `or(A, B)` requires at least one to match—parsing fails if neither is provided * *With `optional()`*: Returns `undefined` when no alternative matches * *With `withDefault()`*: Returns a fallback value when no alternative matches Choose based on your needs: * Use `optional(or(...))` when the absence of a choice is meaningful (e.g., “use system default”) * Use `withDefault(or(...), fallback)` when you always want a concrete value ### Dependent options Some CLI tools have options that only make sense when another option is present. This creates a dependency relationship where certain options are only valid in specific contexts. ```typescript twoslash import { merge, object } from "@optique/core/constructs"; import { withDefault } from "@optique/core/modifiers"; import { flag, option } from "@optique/core/primitives"; import { message } from "@optique/core/message"; import { run } from "@optique/run"; // ---cut-before--- const unionParser = withDefault( object({ flag: flag("-f", "--flag"), dependentFlag: option("-d", "--dependent-flag"), dependentFlag2: option("-D", "--dependent-flag-2"), }), { flag: false as const } as const, ); const parser = merge( unionParser, object({ normalFlag: option("-n", "--normal-flag"), }), ); const result = run(parser); // ^? // The result type enforces that dependentFlag and dependentFlag2 are only // available when flag is true. ``` This pattern uses conditional typing to enforce dependencies at compile time. The [`withDefault()`](./concepts/modifiers.md#withdefault-parser) combinator creates a union type where: When `flag: false` : Only the main flag is available When `flag: true` : Additional dependent options become available This ensures that TypeScript prevents accessing dependent options unless the main flag is `true`. The [`merge()`](./concepts/constructs.md#merge-parser) combinator allows you to combine the conditional parser with other independent options that are always available. The key insight is that dependent options are often about context: when certain features are enabled, additional configuration becomes relevant. ### Inter-option value dependencies *This API is available since Optique 0.10.0.* Sometimes one option's *valid values* depend on another option's value. For example, a `--log-level` option might accept `debug` and `trace` in development mode, but only `warn` and `error` in production. The [`dependency()`](./concepts/dependencies.md) system provides type-safe support for these relationships. ```typescript twoslash import { dependency } from "@optique/core/dependency"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { choice } from "@optique/core/valueparser"; import { run } from "@optique/run"; // Create a dependency source from the mode option const modeParser = dependency(choice(["dev", "prod"] as const)); // Create a derived parser whose valid values depend on mode const logLevelParser = modeParser.derive({ metavar: "LEVEL", mode: "sync", factory: (mode) => choice(mode === "dev" ? ["debug", "info", "warn", "error"] : ["warn", "error"]), defaultValue: () => "dev" as const, }); const parser = object({ mode: option("--mode", modeParser), logLevel: option("--log-level", logLevelParser), }); const config = run(parser); // ^? // In dev mode: --log-level debug ✓ // In prod mode: --log-level debug ✗ (invalid) ``` This pattern differs from the [dependent options](#dependent-options) pattern above in an important way: * *Dependent options*: Controls whether options are *available* based on a flag's presence * *Value dependencies*: Controls which *values are valid* based on another option's value ### Multiple dependencies When an option depends on multiple other options, use `deriveFrom()`: ```typescript twoslash import { dependency, deriveFrom } from "@optique/core/dependency"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { choice } from "@optique/core/valueparser"; import { run } from "@optique/run"; const envParser = dependency(choice(["local", "staging", "prod"] as const)); const regionParser = dependency(choice(["us", "eu", "asia"] as const)); // Server names depend on both environment and region const serverParser = deriveFrom({ metavar: "SERVER", mode: "sync", dependencies: [envParser, regionParser] as const, factory: (env, region) => choice(env === "local" ? ["localhost"] : [`${env}-${region}-1`, `${env}-${region}-2`]), defaultValues: () => ["local", "us"] as const, }); const parser = object({ env: option("--env", envParser), region: option("--region", regionParser), server: option("--server", serverParser), }); const config = run(parser); // --env prod --region eu --server prod-eu-1 ✓ // --env local --server localhost ✓ // --env local --server prod-us-1 ✗ (invalid for local) ``` The dependency system also integrates with shell completion—when users request completions for `--server`, they see suggestions appropriate for the current `--env` and `--region` values. For more details, see the [*Inter-option dependencies*](./concepts/dependencies.md) concept guide. ### Conditional options based on discriminator *This API is available since Optique 0.8.0.* When you have options that depend on a specific discriminator value (like a `--reporter` option determining which additional options are valid), the [`conditional()`](./concepts/constructs.md#conditional-parser) combinator provides a clean solution. ```typescript twoslash import { conditional, object } from "@optique/core/constructs"; import { optional } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { choice, integer, string } from "@optique/core/valueparser"; import { run } from "@optique/run"; // ---cut-before--- const reporterParser = conditional( option("--reporter", choice(["console", "junit", "html", "json"])), { console: object({ colors: optional(option("--colors")), }), junit: object({ outputFile: option("--output-file", string({ metavar: "FILE" })), }), html: object({ outputDir: option("--output-dir", string({ metavar: "DIR" })), title: optional(option("--title", string())), }), json: object({ pretty: optional(option("--pretty")), indent: optional(option("--indent", integer({ min: 0, max: 8 }))), }), } ); const result = run(reporterParser); // The result type is a tuple union based on the discriminator value. ``` This pattern is different from using [`or()`](./concepts/constructs.md#or-parser) with [`constant()`](./concepts/primitives.md#constant-parser) because: * *Explicit discriminator*: The user provides `--reporter junit` rather than inferring mode from which options are present * *Clear error messages*: If `--reporter junit` is provided but `--output-file` is missing, the error clearly states that `--output-file` is required when using the junit reporter * *Tuple result*: The result is `["junit", { outputFile: "..." }]` rather than a merged object, making the discriminator value easily accessible ### With default branch For CLIs where the discriminator is optional, provide a default branch: ```typescript twoslash import { conditional, object } from "@optique/core/constructs"; import { optional } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { choice, string } from "@optique/core/valueparser"; import { run } from "@optique/run"; // ---cut-before--- const outputParser = conditional( option("--format", choice(["json", "xml", "csv"])), { json: object({ pretty: optional(option("--pretty")) }), xml: object({ indent: optional(option("--indent", string())) }), csv: object({ delimiter: optional(option("--delimiter", string())) }), }, // Default: text output with optional color object({ color: optional(option("--color")) }) ); const [format, options] = run(outputParser); // ^? if (format === undefined) { // Default branch: text output console.log(`Text output, color: ${options.color ?? false}`); } else if (format === "json") { // JSON format with pretty option console.log(`JSON output, pretty: ${options.pretty ?? false}`); } ``` When no `--format` option is provided, the default branch is used and the format is `undefined`. ### Type-safe pattern matching The tuple result enables concise pattern matching: ```typescript twoslash import { conditional, object } from "@optique/core/constructs"; import { optional } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { choice, string, integer } from "@optique/core/valueparser"; import { run } from "@optique/run"; const reporterParser = conditional( option("--reporter", choice(["console", "junit", "html", "json"])), { console: object({ colors: optional(option("--colors")), }), junit: object({ outputFile: option("--output-file", string({ metavar: "FILE" })), }), html: object({ outputDir: option("--output-dir", string({ metavar: "DIR" })), title: optional(option("--title", string())), }), json: object({ pretty: optional(option("--pretty")), indent: optional(option("--indent", integer({ min: 0, max: 8 }))), }), } ); // ---cut-before--- const [reporter, config] = run(reporterParser); switch (reporter) { case "console": // TypeScript knows: config is { colors: boolean | undefined } console.log(`Console output with colors: ${config.colors ?? true}`); break; case "junit": // TypeScript knows: config is { outputFile: string } console.log(`Writing JUnit report to ${config.outputFile}`); break; case "html": // TypeScript knows: config is { outputDir: string, title: string | undefined } console.log(`Writing HTML report to ${config.outputDir}`); break; case "json": // TypeScript knows: config is { pretty: boolean | undefined, indent: number | undefined } console.log(`JSON output, pretty: ${config.pretty ?? false}`); break; } ``` The [`conditional()`](./concepts/constructs.md#conditional-parser) combinator is ideal when your CLI has a discriminator option that determines which set of additional options becomes valid. It provides better type inference and clearer error messages than manually building discriminated unions with `or()`. ### Key–value pair options Many CLI tools accept configuration as key–value pairs, similar to environment variables or configuration files. This pattern is common in containerization tools and configuration management systems. ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { map, multiple } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { message } from "@optique/core/message"; import { keyValue } from "@optique/core/valueparser"; import { print, run } from "@optique/run"; // ---cut-before--- // Docker-style environment variables const dockerParser = object({ env: map( multiple(option("-e", "--env", keyValue())), (pairs) => Object.fromEntries(pairs), ), labels: map( multiple(option("-l", "--label", keyValue({ separator: ":" }))), (pairs) => Object.fromEntries(pairs), ), }); // Kubernetes-style configuration const k8sParser = object({ set: map( multiple(option("--set", keyValue())), (pairs) => Object.fromEntries(pairs), ), values: map( multiple(option("--values", keyValue({ separator: ":" }))), (pairs) => Object.fromEntries(pairs), ), }); const parser = or(dockerParser, k8sParser); const config = run(parser); // ^? if ("env" in config) { // config.env and config.labels are now Record print(message`Environment: ${JSON.stringify(config.env, null, 2)}`); print(message`Labels: ${JSON.stringify(config.labels, null, 2)}`); } else { // config.set and config.values are now Record print(message`Set: ${JSON.stringify(config.set, null, 2)}`); print(message`Values: ${JSON.stringify(config.values, null, 2)}`); } ``` This pattern demonstrates several advanced techniques: ### Built-in key–value parser The built-in `keyValue()` parser: * *Validates format*: Ensures the input contains the separator * *Splits correctly*: Handles the separator appearing in values * *Allows empty values*: Accepts values such as `KEY=` by default, which is useful for environment variables and build defines * *Supports different separators*: Configurable for different use cases * *Narrows either side*: Accepts child `key` and `value` parsers for stricter validation and type inference ### Multiple collection Using [`multiple()`](./concepts/modifiers.md#multiple-parser) allows collecting many key–value pairs: ```bash myapp -e DATABASE_URL=postgres://... -e DEBUG=true -l app:web -l version:1.0 ``` ### Type transformation with `map()` The example uses [`map()`](./concepts/modifiers.md#map-parser) to transform the parsed `readonly [string, string][]` array directly into a `Record`. This transformation happens at parse time, so your application receives structured objects rather than arrays of tuples. The type system correctly infers `Record` for each field, providing better IDE support and type safety. This pattern is powerful because it bridges the gap between command-line interfaces and structured configuration data. For stricter domains, pass child value parsers to `keyValue()`: ```typescript twoslash import { choice, integer, keyValue } from "@optique/core/valueparser"; const portSetting = keyValue({ key: choice(["port"] as const), value: integer({ min: 1, max: 65535 }), }); const result = portSetting.parse("port=5432"); // ^? ``` Shell escaping and quotes are handled before Optique receives an argv token. For example, `--set name="hello world"` normally arrives as the single value `name=hello world`, and `keyValue()` splits only that final token. ### Verbosity levels Command-line tools often need different levels of output detail. The traditional Unix approach uses repeated flags: `-v` for verbose, `-vv` for very verbose, and so on. ```typescript twoslash import { object } from "@optique/core/constructs"; import { map, multiple } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { message } from "@optique/core/message"; import { print, run } from "@optique/run"; // ---cut-before--- const VERBOSITY_LEVELS = ["debug", "info", "warning", "error"] as const; const verbosityParser = object({ verbosity: map( multiple(option("-v", "--verbose")), (v) => VERBOSITY_LEVELS.at( -Math.min(v.length, VERBOSITY_LEVELS.length - 1) - 1, )!, ), }); const result = run(verbosityParser); // ^? print(message`Verbosity level: ${result.verbosity}.`); ``` This pattern combines several concepts: ### Repeated flag collection `multiple(option("-v", "--verbose"))` collects all instances of the flag, creating an array of boolean values. Each occurrence adds another `true` to the array. ### Length-based mapping The [`map()`](./concepts/modifiers.md#map-parser) transformation converts array length into verbosity levels: * `-v` → `["debug", "info", "warning", "error"].at(-1-1)` → `"error"` * `-vv` → `["debug", "info", "warning", "error"].at(-2-1)` → `"warning"` * `-vvv` → `["debug", "info", "warning", "error"].at(-3-1)` → `"info"` * `-vvvv` → `["debug", "info", "warning", "error"].at(-4-1)` → `"debug"` The negative indexing with [`Array.at()`] creates an inverse relationship: more flags mean more verbose output (lower threshold). The [`Math.min()`] prevents going beyond the available levels. This pattern is elegant because it: * *Matches user expectations*: More `-v` flags = more output * *Has natural limits*: Caps at maximum verbosity level * *Fails gracefully*: Extra flags don't cause errors [`Array.at()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/at [`Math.min()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/min ### Grouped mutually exclusive options When you have many mutually exclusive options, grouping them in help output improves usability while maintaining the same parsing logic. ```typescript twoslash import { group, or } from "@optique/core/constructs"; import { map, withDefault } from "@optique/core/modifiers"; import { flag } from "@optique/core/primitives"; import { message } from "@optique/core/message"; import { print, run } from "@optique/run"; // ---cut-before--- const formatParser = withDefault( group( "Formatting options", or( map(flag("--json", { description: message`Use JSON format.` }), () => "json" as const), map(flag("--yaml", { description: message`Use YAML format.` }), () => "yaml" as const), map(flag("--toml", { description: message`Use TOML format.` }), () => "toml" as const), map(flag("--xml", { description: message`Use XML format.` }), () => "xml" as const), ), ), "json" as const, ); const result = run(formatParser, { help: "option" }); // ^? print(message`Output format: ${result}.`); ``` This pattern introduces the `group()` combinator to organize related options in help output. The parsing logic is identical to the basic mutually exclusive flags pattern, but the help text is better organized: ```ansi Formatting options: --json Use JSON format. --yaml Use YAML format. --toml Use TOML format. --xml Use XML format. ``` The `group()` combinator is purely cosmetic for help generation—it doesn't change parsing behavior. This separation of concerns allows you to optimize for both code clarity and user experience independently. ### Negatable Boolean options Linux CLI tools commonly support positive and negative option pairs such as `--color` and `--no-color`. Use [`negatableFlag()`](./concepts/primitives.md#negatableflag-parser) when users should be able to override a Boolean setting in either direction. ```typescript twoslash import { object } from "@optique/core/constructs"; import { withDefault } from "@optique/core/modifiers"; import { negatableFlag, option } from "@optique/core/primitives"; import { message } from "@optique/core/message"; import { print, run } from "@optique/run"; declare function detectColorSupport(): boolean; // ---cut-before--- const configParser = object({ codeFence: withDefault( negatableFlag({ positive: "--code-fence", negative: "--no-code-fence", }, { description: message`Enable or disable Markdown code fences.`, }), true, ), lineNumbers: option("--line-numbers"), colors: withDefault( negatableFlag({ positive: "--colors", negative: "--no-colors", }, { description: message`Enable or disable colored output.`, }), () => detectColorSupport(), { message: message`auto` }, ), syntax: withDefault( negatableFlag({ positive: "--syntax", negative: "--no-syntax", }, { description: message`Enable or disable syntax highlighting.`, }), true, ), }); const result = run(configParser); // ^? console.debug(result); ``` `negatableFlag()` returns `true` for the positive flag and `false` for the negative flag. By itself it requires one of the two flags, so the example wraps each parser with [`withDefault()`](./concepts/modifiers.md#withdefault-parser) to keep the defaults explicit. The `message` option on `withDefault()` only changes the displayed default label; `detectColorSupport()` still returns the Boolean fallback value. When `--code-fence` is provided : `negatableFlag()` produces `true` When neither flag is provided : `withDefault()` uses the default value `true` When `--no-code-fence` is provided : `negatableFlag()` produces `false` If a CLI only supports a negative form, keep the simpler `option()` and `map()` pattern: ```typescript twoslash import { map } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; const codeFence = map(option("--no-code-fence"), (provided) => !provided); ``` ### Usage examples ```bash # All defaults: codeFence=true, lineNumbers=false, # colors follow auto-detection, syntax=true myapp # Disable colors and syntax, enable line numbers explicitly myapp --no-colors --no-syntax --line-numbers # Enable colors explicitly when auto-detection would disable them myapp --colors ``` This pattern is particularly useful for configuration-heavy tools where users need fine-grained control over defaults that may come from configuration files, environment variables, or runtime detection. ### Conditional defaults based on input consumption *This API is available since Optique 0.10.0.* Sometimes you need different behavior based on whether the user provided any options at all. For example, a CLI tool might show help when invoked with no arguments, but apply default values when at least one option is provided. The [`nonEmpty()`](./concepts/modifiers.md#nonempty-parser) modifier combined with [`longestMatch()`](./concepts/constructs.md#longestmatch-parser) enables this pattern: ```typescript twoslash import { longestMatch, object } from "@optique/core/constructs"; import { nonEmpty, optional, withDefault } from "@optique/core/modifiers"; import { constant, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { run } from "@optique/run"; // Active mode: requires at least one option to be provided const activeParser = nonEmpty(object({ mode: constant("active" as const), cwd: withDefault(option("--cwd", string()), "./"), key: optional(option("--key", string())), })); // Help mode: fallback when no options are given const helpParser = object({ mode: constant("help" as const), }); const parser = longestMatch(activeParser, helpParser); const result = run(parser); // ^? if (result.mode === "help") { console.log("No options provided. Showing help."); } else { console.log(`Running with cwd=${result.cwd}, key=${result.key ?? "none"}`); } ``` ### How it works Without `nonEmpty()`, the `activeParser` would always succeed even with no input, because all its options have defaults or are optional. This means it would consume 0 tokens and still produce a valid result, preventing the `helpParser` from ever being selected. The `nonEmpty()` modifier changes this behavior: 1. When no options are provided, `activeParser` succeeds but consumes 0 tokens 2. `nonEmpty()` detects this and converts the success into a failure 3. `longestMatch()` then falls back to `helpParser`, which also consumes 0 tokens but succeeds 4. The result is the help mode When at least one option is provided: 1. `activeParser` succeeds and consumes at least one token 2. `nonEmpty()` allows this success to pass through 3. `longestMatch()` selects `activeParser` because it consumed more tokens 4. Default values are applied to unprovided options ### Usage examples ```bash # No options: help mode myapp # → "No options provided. Showing help." # With --key: active mode with defaults myapp --key mykey # → "Running with cwd=./, key=mykey" # With --cwd: active mode myapp --cwd /tmp # → "Running with cwd=/tmp, key=none" # With both options: active mode myapp --cwd /tmp --key mykey # → "Running with cwd=/tmp, key=mykey" ``` This pattern is ideal for development tools, build systems, or any CLI where you want to guide users to provide at least some configuration while still supporting sensible defaults once they start configuring. ### Pass-through options for wrapper CLIs *This API is available since Optique 0.8.0.* When building wrapper tools that need to forward unknown options to an underlying command, the [`passThrough()`](./concepts/primitives.md#passthrough-parser) parser captures unrecognized options without validation errors. > \[!NOTE] > By default, `passThrough()` uses the `"equalsOnly"` format, which > only captures `--opt=val` style options. Options like `--foo bar` will fail. > See [Choosing the right format](#choosing-the-right-format) below for > alternatives. #### Basic wrapper pattern A common use case is wrapping another CLI tool while adding your own options: ```typescript twoslash import { object } from "@optique/core/constructs"; import { argument, option, passThrough } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { run } from "@optique/run"; const parser = object({ debug: option("--debug"), config: option("-c", "--config", string({ metavar: "FILE" })), // Default format is "equalsOnly", captures --opt=val only extraOpts: passThrough(), }); const result = run(parser); // ^? // Use result.extraOpts to pass through to the underlying tool ``` The key insight is that `passThrough()` has the *lowest priority* (−10), so your explicit options are always matched first. Only truly unrecognized options are captured in the pass-through array. #### Subcommand-specific pass-through For tools that delegate entire subcommands to other processes: ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { argument, command, constant, option, passThrough } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; import { run } from "@optique/run"; const parser = or( // Local command with known options command("local", object({ action: constant("local"), port: option("-p", "--port", integer()), host: option("-h", "--host", string()), })), // Exec command passes everything through command("exec", object({ action: constant("exec"), container: argument(string({ metavar: "CONTAINER" })), args: passThrough({ format: "greedy" }), })), ); const result = run(parser); if (result.action === "exec") { // result.args contains all remaining tokens // Pass them to the container: ["--verbose", "-it", "bash"] } ``` The `"greedy"` format is crucial here: once the container name is captured, all remaining tokens (including those that look like options) go into `args`. #### Choosing the right format The `passThrough()` parser supports three capture formats: `"equalsOnly"` (default) : Only captures `--opt=val` format. The safest choice when you need to distinguish between options and positional arguments: ``` ~~~~ typescript twoslash import { passThrough } from "@optique/core/primitives"; // ---cut-before--- const parser = passThrough({ format: "equalsOnly" }); // Captures: --foo=bar, --baz=123 // Rejects: --foo bar, --verbose ~~~~ ``` `"nextToken"` : Captures `--opt val` as two tokens when the value doesn't look like an option. Good for wrapping tools that use space-separated values: ``` ~~~~ typescript twoslash import { passThrough } from "@optique/core/primitives"; // ---cut-before--- const parser = passThrough({ format: "nextToken" }); // --foo bar → ["--foo", "bar"] // --foo --bar → ["--foo", "--bar"] (--bar is a separate option) ~~~~ ``` `"greedy"` : Captures *all* remaining tokens. Use for proxy/wrapper tools where everything after a certain point should pass through: ``` ~~~~ typescript twoslash import { passThrough } from "@optique/core/primitives"; // ---cut-before--- const parser = passThrough({ format: "greedy" }); // git commit -m "message" → ["git", "commit", "-m", "message"] ~~~~ ``` > \[!CAUTION] > The `"greedy"` format can shadow explicit parsers. Place it carefully, > typically as the last field in a subcommand-specific `object()`. ### Shell completion patterns *This API is available since Optique 0.6.0.* Modern CLI applications benefit from intelligent shell completion that helps users discover available options and reduces typing errors. Optique provides built-in completion support that integrates seamlessly with your parser definitions. #### Basic completion setup Enable completion for any CLI application by adding the `completion` option: ```typescript twoslash import { object } from "@optique/core/constructs"; import { argument, option } from "@optique/core/primitives"; import { string, choice } from "@optique/core/valueparser"; import { run } from "@optique/run"; const parser = object({ format: option("-f", "--format", choice(["json", "yaml", "xml"])), output: option("-o", "--output", string({ metavar: "FILE" })), verbose: option("-v", "--verbose"), input: argument(string({ metavar: "INPUT" })), }); const config = run(parser, { completion: "both" }); ``` This automatically provides intelligent completion for: * Option names: `--format`, `--output`, `--verbose` * Choice values: `--format json`, `--format yaml` * Help integration: `--help` is included in completions #### Custom value parser suggestions Create value parsers with domain-specific completion suggestions: ```typescript twoslash import type { ValueParser, ValueParserResult } from "@optique/core/valueparser"; import type { Suggestion } from "@optique/core/parser"; import { message } from "@optique/core/message"; // Custom parser for log levels with intelligent completion function logLevel(): ValueParser<"sync", string> { const levels = ["error", "warn", "info", "debug", "trace"]; return { mode: "sync", metavar: "LEVEL", placeholder: "", parse(input: string): ValueParserResult { if (levels.includes(input.toLowerCase())) { return { success: true, value: input.toLowerCase() }; } return { success: false, // Note: For proper formatting of choice lists, see the "Formatting choice lists" // section in the Concepts guide on Messages error: message`Invalid log level: ${input}. Valid levels: ${levels.join(", ")}.`, }; }, format(value: string): string { return value; }, *suggest(prefix: string): Iterable { for (const level of levels) { if (level.startsWith(prefix.toLowerCase())) { yield { kind: "literal", text: level, description: message`Set log level to ${level}` }; } } }, }; } ``` #### Multi-command CLI with rich completion Complex CLI tools with subcommands benefit greatly from completion: ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { optional } from "@optique/core/modifiers"; import { argument, command, constant, option } from "@optique/core/primitives"; import { string, choice } from "@optique/core/valueparser"; import { run } from "@optique/run"; const serverCommand = command("server", object({ action: constant("server"), port: optional(option("-p", "--port", string())), host: optional(option("-h", "--host", string())), env: optional(option("--env", choice(["dev", "staging", "prod"]))), })); const buildCommand = command("build", object({ action: constant("build"), target: argument(choice(["web", "mobile", "desktop"])), mode: optional(option("--mode", choice(["debug", "release"]))), output: optional(option("-o", "--output", string())), })); const parser = or(serverCommand, buildCommand); const config = run(parser, { completion: "both" }); ``` This provides completion for: * Command names: `server`, `build` * Command-specific options: `--port` only for server, `--mode` only for build * Enum values: `--env dev`, `--mode release` * Context-aware suggestions based on the current command #### File path completion integration For file and directory arguments, Optique delegates to native shell completion: ```typescript twoslash import { object } from "@optique/core/constructs"; import { argument, option } from "@optique/core/primitives"; import { path } from "@optique/run/valueparser"; import { run } from "@optique/run"; const parser = object({ config: option("-c", "--config", path({ extensions: [".json", ".yaml"], type: "file" })), outputDir: option("-o", "--output", path({ type: "directory" })), input: argument(path({ extensions: [".md", ".txt"], type: "file" })), }); const config = run(parser, { completion: "both" }); ``` The `path()` value parser automatically provides: * Native file system completion using shell built-ins * Extension filtering (*.json*, *.yaml* files only) * Type filtering (files vs directories) * Proper handling of spaces, special characters, and symlinks #### Installation and usage Once completion is enabled, users install it with simple commands: ::: code-group ```bash [Bash] # Generate and install Bash completion myapp completion bash > ~/.bashrc.d/myapp.bash source ~/.bashrc.d/myapp.bash ``` ```zsh [zsh] # Generate and install zsh completion myapp completion zsh > ~/.zsh/completions/_myapp ``` ```fish [fish] # Generate and install fish completion myapp completion fish > ~/.config/fish/completions/myapp.fish ``` ```powershell [PowerShell] # Generate and install PowerShell completion myapp completion pwsh > myapp-completion.ps1 ``` ::: The completion system leverages the same parser structure used for argument validation, ensuring suggestions always stay synchronized with your CLI's actual behavior without requiring separate maintenance. Users then benefit from intelligent completion: ```bash myapp # Shows: server, build, help myapp server -- # Shows: --port, --host, --env, --help myapp server --env # Shows: dev, staging, prod myapp build # Shows: web, mobile, desktop ``` ### Hidden and deprecated options As CLIs evolve, you may need to deprecate old options while maintaining backward compatibility, or add internal debugging options that shouldn't appear in user-facing documentation. The `hidden` option lets you keep parsers functional while controlling visibility. Hidden options still consume input normally, so they also still participate in duplicate option-name validation: * `hidden: true`: hide from usage, help entries, completions, and “Did you mean?” suggestions * `hidden: "usage"`: hide from usage only * `hidden: "doc"`: hide from help entries only * `hidden: "help"`: hide from usage and help entries, but keep completions and “Did you mean?” suggestions #### Deprecation pattern When renaming or replacing options, keep the old form working but hide it: ```typescript twoslash import { object } from "@optique/core/constructs"; import { message } from "@optique/core/message"; import { optional } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const parser = object({ // The new, preferred option name output: optional(option("-o", "--output", string(), { description: message`Output file path`, })), // Legacy option name - still works but hidden from help outputLegacy: optional(option("--out", string(), { hidden: true, })), }); // Later, merge the values: output ?? outputLegacy ``` This approach ensures existing scripts using `--out` continue to work while new users learn the preferred `--output` form. If you keep a hidden legacy option, it still reserves that option name inside the same combinator. A visible parser using the same name is still treated as a duplicate unless you explicitly opt out with `allowDuplicates: true`. #### Internal debugging options Add options for debugging or development that shouldn't clutter the help: ```typescript twoslash import { object } from "@optique/core/constructs"; import { message } from "@optique/core/message"; import { withDefault } from "@optique/core/modifiers"; import { flag, option } from "@optique/core/primitives"; import { integer } from "@optique/core/valueparser"; const parser = object({ verbose: flag("-v", "--verbose", { description: message`Enable verbose output`, }), // Developer-only options traceRequests: flag("--trace-requests", { hidden: true }), mockDelay: withDefault(option("--mock-delay", integer(), { hidden: true }), 0), }); ``` Developers who know about these options can use them, but they won't appear in `--help` output or shell completions. #### Undocumented but completion-discoverable flags Use `hidden: "help"` when you want to keep an option out of usage/help text without removing it from shell completion: ```typescript twoslash import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const parser = object({ profile: option("--profile", string()), // Not shown in usage/help, but still suggested by completion debugTransport: option("--debug-transport", string(), { hidden: "help" }), }); ``` #### Experimental features Hide features that aren't ready for general use: ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { argument, command, constant, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const commands = or( command("build", object({ type: constant("build"), target: option("--target", string()), })), command("test", object({ type: constant("test"), pattern: argument(string()), })), // Experimental - not yet documented command("experimental-watch", object({ type: constant("watch"), paths: argument(string()), }), { hidden: true }), ); ``` Hidden commands work normally but don't appear in command listings or get suggested in “Did you mean?” errors. ### Advanced patterns The cookbook patterns can be combined to create sophisticated CLI interfaces: ```typescript twoslash import { merge, object } from "@optique/core/constructs"; import { multiple, withDefault } from "@optique/core/modifiers"; import { argument, command, constant, flag, option } from "@optique/core/primitives"; import { keyValue, string } from "@optique/core/valueparser"; // ---cut-before--- // Combining subcommands with dependent options and key–value pairs const deployCommand = command("deploy", merge( object({ action: constant("deploy"), environment: argument(string()), }), withDefault( object({ dryRun: flag("--dry-run"), vars: multiple(option("--var", keyValue())), confirm: option("--confirm"), }), { dryRun: false } ) )); ``` This creates a deploy command that: * Requires an environment argument * Supports key–value variables * Has optional dry-run mode * Uses dependent confirmation when not in dry-run mode ### Custom value parser with `normalize()` *This API is available since Optique 1.0.0.* When a value has multiple valid representations, implement `normalize()` on your value parser so that `withDefault()` can canonicalize default values. This example creates a parser for hexadecimal color codes that normalizes case and optional `#` prefixes: ```typescript twoslash import type { ValueParser, ValueParserResult } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; function hexColor(): ValueParser<"sync", string> { const pattern = /^#?([0-9a-f]{6})$/i; return { mode: "sync", metavar: "COLOR", placeholder: "#000000", parse(input: string): ValueParserResult { const match = input.match(pattern); if (match) { return { success: true, value: `#${match[1].toLowerCase()}` }; } return { success: false, error: message`Expected a hex color like #ff0000, but got ${input}.`, }; }, format(value: string): string { return value; }, normalize(value: string): string { const match = value.match(pattern); return match ? `#${match[1].toLowerCase()}` : value; }, }; } ``` With `normalize()` in place, `withDefault()` automatically canonicalizes the default value: ```typescript twoslash import type { ValueParser, ValueParserResult } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; function hexColor(): ValueParser<"sync", string> { const pattern = /^#?([0-9a-f]{6})$/i; return { mode: "sync", metavar: "COLOR", placeholder: "#000000", parse(input: string): ValueParserResult { const match = input.match(pattern); if (match) return { success: true, value: `#${match[1].toLowerCase()}` }; return { success: false, error: message`Invalid.` }; }, format(v: string): string { return v; }, normalize(value: string): string { const match = value.match(pattern); return match ? `#${match[1].toLowerCase()}` : value; }, }; } // ---cut-before--- import { withDefault } from "@optique/core/modifiers"; import { parse } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; const parser = withDefault( option("--bg-color", hexColor()), "FF8800", // no "#" prefix, uppercase ); const result = parse(parser, []); // result.value is "#ff8800", normalized from "FF8800" ``` ### Design principles These patterns demonstrate several key principles for designing CLI parsers: #### Composition over configuration Instead of complex configuration objects, combine simple parsers using combinators like [`or()`](./concepts/constructs.md#or-parser), [`merge()`](./concepts/constructs.md#merge-parser), and [`multiple()`](./concepts/modifiers.md#multiple-parser). Each combinator has a single, well-defined purpose. #### Type-driven design Use TypeScript's type system to enforce correct usage. Discriminated unions, conditional types, and literal types prevent runtime errors by catching mistakes at compile time. #### Separation of concerns Separate parsing logic from presentation logic. Use [`group()`](./concepts/constructs.md#group-parser) for help organization, [`withDefault()`](./concepts/modifiers.md#withdefault-parser) for fallback behavior, and [`map()`](./concepts/modifiers.md#map-parser) for data transformation. #### Progressive disclosure Start with simple parsers and add complexity through composition. A basic flag becomes a mutually exclusive choice, which becomes a grouped set of options, which becomes part of a larger command structure. #### Fail-safe defaults Always consider what happens when optional inputs are missing. Use [`withDefault()`](./concepts/modifiers.md#withdefault-parser) to provide sensible fallbacks and [`optional()`](./concepts/modifiers.md#optional-parser) when absence is meaningful. ## Application structure patterns These recipes use packages that sit above individual parsers and help shape a larger CLI application. ### File-based command discovery When a command tree grows beyond a handful of branches, keeping every command inside one `or(command(...))` expression can make the entry point do too much. The *@optique/discover* package lets each command live in its own module with its parser, help metadata, and handler. > \[!WARNING] > This pattern discovers and imports command modules at runtime. It works best > when those command files are present beside the running CLI. For CLIs that > rely on tree shaking, static bundling, or single-file executable packaging, > import command modules manually and pass them to `runProgram()` with > `commands`. Put command modules under a directory: ```typescript twoslash // commands/build.ts import { object } from "@optique/core/constructs"; import { message } from "@optique/core/message"; import { withDefault } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { defineCommand } from "@optique/discover/command"; export default defineCommand({ parser: object({ target: withDefault(option("--target", string()), "app"), }), metadata: { brief: message`Build the project.`, }, handler(value) { console.log(`Building ${value.target}.`); }, }); ``` Then point `runProgram()` at the command directory: ```typescript twoslash import { message } from "@optique/core/message"; import { runProgram } from "@optique/discover"; await runProgram({ dir: new URL("./commands/", import.meta.url), metadata: { name: "tasks", version: "1.0.0", brief: message`Project task runner.`, }, }); ``` For a bundled CLI, add a path to each command definition and import the command modules manually: ```typescript twoslash import { object } from "@optique/core/constructs"; import { message } from "@optique/core/message"; import { withDefault } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { defineCommand, runProgram } from "@optique/discover"; const build = defineCommand({ path: ["build"], parser: object({ target: withDefault(option("--target", string()), "app"), }), metadata: { brief: message`Build the project.`, }, handler(value) { console.log(`Building ${value.target}.`); }, }); await runProgram({ commands: [build], metadata: { name: "tasks", version: "1.0.0", brief: message`Project task runner.`, }, }); ``` With this layout: ```text commands/ build.ts deploy.ts release/ notes.ts ``` the file paths become command paths: ```bash tasks build tasks deploy tasks release notes ``` Use this pattern when the command module is the natural unit of ownership. It keeps the root file focused on program metadata and runner configuration, while each command file owns the parser and the code that acts on its parsed value. The discovered program still gets the usual *@optique/run* help, version, and shell completion behavior. The repository also includes a runnable version of this pattern in `examples/patterns/command-discovery.ts`. For the full API details, see [command discovery](./concepts/discover.md). ## Integration patterns These recipes use integration packages like *@optique/env*, *@optique/config*, and *@optique/inquirer*. ### Environment variable fallbacks *This API is available since Optique 1.0.0.* When a CLI argument is not provided, `bindEnv()` from *@optique/env* checks the corresponding environment variable before falling back to a default. ```typescript twoslash import { bindEnv, bool, createEnvContext } from "@optique/env"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; import { runAsync } from "@optique/run"; const envContext = createEnvContext({ prefix: "MYAPP_" }); const parser = object({ host: bindEnv( option("-h", "--host", string()), { context: envContext, key: "HOST", parser: string(), default: "localhost" }, ), port: bindEnv( option("-p", "--port", integer()), { context: envContext, key: "PORT", parser: integer(), default: 3000 }, ), debug: bindEnv( option("-d", "--debug", bool()), { context: envContext, key: "DEBUG", parser: bool(), default: false }, ), }); const result = await runAsync(parser, { contexts: [envContext], }); ``` The priority order is: CLI argument > environment variable > default value. With the `MYAPP_` prefix, the parser reads `MYAPP_HOST`, `MYAPP_PORT`, and `MYAPP_DEBUG`. For more details, see the [environment variable guide](./integrations/env.md). ### Config file integration *This API is available since Optique 0.10.0.* Many CLI tools support configuration files that provide default values for options. The *@optique/config* package provides type-safe config file integration with automatic fallback handling. ### Basic setup with schema validation Use a Standard Schema-compatible library (Zod, Valibot, ArkType, etc.) to define your config structure: ```typescript twoslash import { z } from "zod"; import { createConfigContext, bindConfig } from "@optique/config"; import { object } from "@optique/core/constructs"; import { optional } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; import { runAsync } from "@optique/run"; // Define the config schema const configSchema = z.object({ host: z.string().optional(), port: z.number().int().min(1).max(65535).optional(), debug: z.boolean().optional(), }); // Create a config context const configContext = createConfigContext({ schema: configSchema }); // Build the parser with config bindings const parser = object({ config: optional(option("-c", "--config", string())), host: bindConfig(option("-h", "--host", string()), { context: configContext, key: "host", default: "localhost", }), port: bindConfig(option("-p", "--port", integer()), { context: configContext, key: "port", default: 3000, }), debug: bindConfig(option("-d", "--debug"), { context: configContext, key: "debug", default: false, }), }); // Run with config file support via contexts const result = await runAsync(parser, { contexts: [configContext], contextOptions: { getConfigPath: (parsed) => parsed.config, }, }); // result.host: CLI > config.json > "localhost" // result.port: CLI > config.json > 3000 ``` The `bindConfig()` function wraps a parser to provide fallback behavior: 1. *CLI argument* (highest priority): User-provided command-line value 2. *Config file value*: Loaded from config file if path was specified 3. *Default value*: Specified in `bindConfig()` options 4. *Error*: If none of the above and no default ### Type-safe config path extraction The `getConfigPath` option is type-checked against your parser's result type. TypeScript ensures you're accessing a field that actually exists: ```typescript twoslash import { z } from "zod"; import { createConfigContext } from "@optique/config"; import { object } from "@optique/core/constructs"; import { optional } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { runAsync } from "@optique/run"; const configSchema = z.object({ host: z.string().optional() }); const configContext = createConfigContext({ schema: configSchema }); const parser = object({ configFile: optional(option("--config-file", string())), host: option("--host", string()), }); const result = await runAsync(parser, { contexts: [configContext], contextOptions: { // `parsed` is typed as { configFile?: string; host: string } getConfigPath: (parsed) => parsed.configFile, }, }); ``` ### Nested config values For nested config structures, use a function instead of a key: ```typescript twoslash import { z } from "zod"; import { createConfigContext, bindConfig } from "@optique/config"; import { option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; const configSchema = z.object({ server: z.object({ host: z.string(), port: z.number(), }).optional(), database: z.object({ connectionString: z.string(), }).optional(), }); const configContext = createConfigContext({ schema: configSchema }); // Access nested values with a function const hostParser = bindConfig(option("--host", string()), { context: configContext, key: (config) => config.server?.host, default: "localhost", }); const dbParser = bindConfig(option("--db", string()), { context: configContext, key: (config) => config.database?.connectionString, }); ``` ### Custom config file formats By default, config files are parsed as JSON. For YAML, TOML, or other formats, provide a custom file parser to `createConfigContext()`: ```typescript twoslash import { z } from "zod"; import { createConfigContext, bindConfig } from "@optique/config"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; import { runAsync } from "@optique/run"; import { parse as parseYaml } from "yaml"; const configSchema = z.object({ host: z.string(), port: z.number(), }); // Pass fileParser when creating the context const configContext = createConfigContext({ schema: configSchema, fileParser: (contents) => parseYaml(new TextDecoder().decode(contents)), }); const parser = object({ config: option("--config", string()), host: bindConfig(option("--host", string()), { context: configContext, key: "host", default: "localhost", }), port: bindConfig(option("--port", integer()), { context: configContext, key: "port", default: 3000, }), }); const result = await runAsync(parser, { contexts: [configContext], contextOptions: { getConfigPath: (parsed) => parsed.config, }, }); ``` ### Combining with environment variables Use `bindEnv()` from *@optique/env* together with `bindConfig()` to create a four-level fallback chain. The nesting order determines priority: `bindEnv(bindConfig(option(...)))` gives CLI > env > config > default. ```typescript twoslash import { z } from "zod"; import { createConfigContext, bindConfig } from "@optique/config"; import { bindEnv, createEnvContext } from "@optique/env"; import { object } from "@optique/core/constructs"; import { optional } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; import { runAsync } from "@optique/run"; const configSchema = z.object({ host: z.string().optional(), port: z.number().int().min(1).max(65535).optional(), }); const envContext = createEnvContext({ prefix: "MYAPP_" }); const configContext = createConfigContext({ schema: configSchema }); const parser = object({ config: optional(option("-c", "--config", string())), // Priority: CLI > env var > config file > default host: bindEnv( bindConfig(option("-h", "--host", string()), { context: configContext, key: "host", default: "localhost", }), { context: envContext, key: "HOST", parser: string() }, ), port: bindEnv( bindConfig(option("-p", "--port", integer()), { context: configContext, key: "port", default: 3000, }), { context: envContext, key: "PORT", parser: integer() }, ), }); const result = await runAsync(parser, { contexts: [envContext, configContext], contextOptions: { getConfigPath: (parsed) => parsed.config, }, }); ``` ### Combining with interactive prompts Use `prompt()` from *@optique/inquirer* as the outermost wrapper when you want an interactive fallback *after* checking CLI arguments, environment variables, and config files. A practical approach is to preload config annotations once and expose them via a single-pass context. This keeps the fallback order predictable while still using `bindEnv()` and `bindConfig()` together: ```typescript twoslash import { z } from "zod"; import { bindConfig, createConfigContext } from "@optique/config"; import { object } from "@optique/core/constructs"; import { optional } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; import { bindEnv, createEnvContext } from "@optique/env"; import { prompt } from "@optique/inquirer"; import { runAsync } from "@optique/run"; function getConfigPathFromArgs(args: readonly string[]): string | undefined { for (let index = 0; index < args.length; index++) { const arg = args[index]; if (arg === "-c" || arg === "--config") { return args[index + 1]; } if (arg.startsWith("--config=")) { return arg.slice("--config=".length); } } return undefined; } const configSchema = z.object({ host: z.string().optional(), port: z.number().int().min(1).max(65535).optional(), }); const envContext = createEnvContext({ prefix: "MYAPP_" }); const configContext = createConfigContext({ schema: configSchema }); const args = ["--config", "./config.json"] as const; const configAnnotations = await configContext.getAnnotations( { phase: "phase2", parsed: { config: getConfigPathFromArgs(args) }, }, { getConfigPath: (parsed: { readonly config?: string }) => parsed.config }, ); const staticConfigContext = { id: configContext.id, phase: "single-pass" as const, getAnnotations() { return configAnnotations; }, }; const parser = object({ config: optional(option("-c", "--config", string())), host: prompt( bindEnv( bindConfig(option("--host", string()), { context: configContext, key: "host", }), { context: envContext, key: "HOST", parser: string() }, ), { type: "input", message: "Host:", default: "localhost" }, ), port: prompt( bindEnv( bindConfig(option("--port", integer()), { context: configContext, key: "port", }), { context: envContext, key: "PORT", parser: integer({ min: 1, max: 65535 }), }, ), { type: "number", message: "Port:", default: 3000, min: 1, max: 65535 }, ), }); const result = await runAsync(parser, { args, contexts: [envContext, staticConfigContext], }); ``` When you preload annotations manually like this, you still need to thread them back into parsing explicitly, either by wrapping them in a static context as shown here or by passing them directly to low-level APIs such as `parse()`, `parseAsync()`, or `parser.complete()`. The `getAnnotations()` call itself does not change later parses. This preserves the priority chain: CLI argument > environment variable > config file > interactive prompt For more details, see the [environment variable guide](./integrations/env.md), [config file integration guide](./integrations/config.md), and [interactive prompt guide](./integrations/inquirer.md). ### Config-only and env-only values with `fail()` Some configuration values should never be exposed as CLI flags. For example, an API secret might come only from an environment variable or config file. The [`fail()`](./concepts/primitives.md#fail-parser) parser always fails without consuming input, so wrapping it with `bindConfig()` or `bindEnv()` forces the value to come from the external source: ```typescript twoslash import { z } from "zod"; import { bindConfig, createConfigContext } from "@optique/config"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; import { bindEnv, createEnvContext } from "@optique/env"; import { fail } from "@optique/core/primitives"; import { runAsync } from "@optique/run"; import { withDefault } from "@optique/core/modifiers"; const configSchema = z.object({ timeout: z.number().optional(), apiSecret: z.string().optional(), }); const envContext = createEnvContext({ prefix: "MYAPP_" }); const configContext = createConfigContext({ schema: configSchema }); const parser = object({ config: withDefault(option("--config", string()), "config.json"), // Visible CLI option host: option("--host", string()), // Config-only: no CLI flag, falls back to config or default timeout: bindConfig(fail(), { context: configContext, key: "timeout", default: 30, }), // Env-only: no CLI flag, falls back to env or default apiSecret: bindEnv(fail(), { context: envContext, key: "API_SECRET", parser: string(), default: "", }), }); const result = await runAsync(parser, { contexts: [envContext, configContext], contextOptions: { getConfigPath: (parsed) => parsed.config, }, }); ``` Because `fail()` never succeeds on its own, `bindConfig()` and `bindEnv()` always use the external source (or fall back to the configured default). This keeps the help output clean while still allowing values to flow in from configuration files and environment variables. --- --- url: /concepts/discover.md description: >- Build command-oriented CLI applications by discovering Optique command modules from the file system and dispatching to command handlers. --- # Command discovery Optique's core command combinators work well when the whole command tree fits comfortably in one module. Larger applications often want a file layout where each command owns its parser, metadata, and handler. The *@optique/discover* package provides that layer: it scans a command directory, imports command modules, builds a nested parser tree, and dispatches to the matched command handler. The package is named *@optique/discover* because its main job is command module discovery. The alternative name *@optique/program* would overlap with `@optique/core/program`, which already provides parser-and-metadata objects for man pages and runner integration. > \[!WARNING] > Command discovery is a runtime feature, not a static registry. It reads the > command directory and imports matching modules dynamically, so bundlers cannot > reliably see which command files are used. If your CLI depends on tree > shaking, static bundling, or single-file executable packaging, import command > modules manually and pass them to `runProgram()` with `commands`. ## Command modules A command module default-exports a value created with `defineCommand()`. The parser describes the command-specific arguments and options, `metadata` feeds help and completion output, and `handler` receives the parsed value. ```typescript twoslash import { defineCommand } from "@optique/discover/command"; import { object } from "@optique/core/constructs"; import { message } from "@optique/core/message"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; export default defineCommand({ parser: object({ name: option("--name", string()), }), metadata: { brief: message`Add a user.`, }, handler(value) { console.log(`Adding ${value.name}.`); }, }); ``` `defineCommand()` preserves the parser's inferred value type for `handler`. If you change the parser, TypeScript checks the handler against the new shape. When commands are passed manually to `runProgram()`, add a `path` field to the command definition. File-based discovery can omit `path`; if it is present, it must match the path derived from the file name. ## Running a discovered program Point `runProgram()` at a command directory and provide root program metadata: ```typescript twoslash import { runProgram } from "@optique/discover"; import { message } from "@optique/core/message"; await runProgram({ dir: new URL("./commands/", import.meta.url), metadata: { name: "admin", version: "1.0.0", brief: message`Administrative command-line tools.`, }, }); ``` With this file layout: ```text commands/ build.ts user/ add.ts remove.ts ``` the discovered command paths are: ```bash admin build admin user add admin user remove ``` `runProgram()` uses *@optique/run* internally. Help and shell completion are enabled in both command and option forms by default, and version output is enabled when `metadata.version` is present: ```bash admin --help admin help admin --version admin completion bash admin --completion bash ``` You can disable or customize these runner features with the same option shapes accepted by `run()`: ```typescript twoslash import { runProgram } from "@optique/discover"; import { message } from "@optique/core/message"; await runProgram({ dir: new URL("./commands/", import.meta.url), metadata: { name: "admin", brief: message`Administrative command-line tools.`, }, help: { option: true }, version: false, completion: false, }); ``` ## Running statically imported commands When command modules need to be visible to a bundler or single-file packager, import them manually and pass them as `commands`. Each command declares its own path: ```typescript twoslash import { object } from "@optique/core/constructs"; import { message } from "@optique/core/message"; import { withDefault } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { defineCommand, runProgram } from "@optique/discover"; const build = defineCommand({ path: ["build"], parser: object({ target: withDefault(option("--target", string()), "app"), }), metadata: { brief: message`Build the project.`, }, handler(value) { console.log(`Building ${value.target}.`); }, }); await runProgram({ commands: [build], metadata: { name: "admin", version: "1.0.0", brief: message`Administrative command-line tools.`, }, }); ``` `commands` and `dir` are mutually exclusive. Use `commands` when static imports matter; use `dir` when the runtime file layout is the command registry. ## File names and extensions The relative file path becomes the command path after removing the configured suffix. Compound suffixes are supported, so `user/add.cmd.ts` can become `user add` when `.cmd.ts` is listed before `.ts`. By default, *@optique/discover* chooses extensions for the current runtime: | Runtime | Default extensions | | ------- | --------------------------------------------------- | | Deno | `.ts`, `.mts`, `.js`, `.mjs` | | Bun | `.ts`, `.mts`, `.js`, `.mjs` | | Node.js | `.js`, `.mjs`, `.cjs`, and sometimes TypeScript too | Node.js also includes `.ts`, `.mts`, and `.cts` when it appears to be running with native TypeScript support, a TypeScript loader such as `tsx`, `ts-node`, `tsimp`, or `jiti`, or Node's built-in type-stripping flags. TypeScript declaration files (`.d.ts`, `.d.mts`, and `.d.cts`) are ignored even when their suffix matches the configured extension list. Pass `extensions` when you want an explicit policy: ```typescript twoslash import { runProgram } from "@optique/discover"; await runProgram({ dir: new URL("./commands/", import.meta.url), metadata: { name: "admin" }, extensions: [".cmd.ts"], }); ``` > \[!NOTE] > Command modules are imported eagerly during startup. This keeps discovery > simple and makes help, errors, and completion aware of the full command tree. > Avoid side effects at module top level other than defining the command. ## Path conflicts Each discovered file must map to exactly one command path. Discovery rejects duplicate paths, such as `build.ts` and `build.cmd.ts` both becoming `build`. It also rejects file-vs-namespace conflicts such as: ```text commands/ user.ts user/ add.ts ``` In that layout, `user` would need to be both a leaf command and a namespace for `user add`. Move the shared behavior into a helper module or choose a deeper leaf command path instead. ## When to use command discovery Use *@optique/discover* when: * Your CLI has enough commands that a file-per-command layout is clearer * Each command should keep its parser, help metadata, and handler together * You want *@optique/run* help, version, and completion behavior without manually composing the whole command tree Use static `commands` with manually imported command modules when: * You need tree shaking, static bundling, or single-file executable packaging * You want command modules to be visible through ordinary static imports Use plain *@optique/core* and *@optique/run* when: * The command tree is small enough to define directly with `command()` and `or()` * You need lazy command loading or a custom plugin registry * You want to parse commands without coupling them to handlers --- --- url: /integrations/config.md description: >- Load configuration files with type-safe validation using Standard Schema compatible libraries like Zod, Valibot, and ArkType. --- # Config file support *This API is available since Optique 0.10.0.* The *@optique/config* package provides configuration file support for Optique, enabling CLI applications to load default values from configuration files with proper priority handling: CLI arguments > config file values > defaults. ::: code-group ```bash [Deno] deno add jsr:@optique/config npm:@standard-schema/spec npm:zod ``` ```bash [npm] npm add @optique/config @standard-schema/spec zod ``` ```bash [pnpm] pnpm add @optique/config @standard-schema/spec zod ``` ```bash [Yarn] yarn add @optique/config @standard-schema/spec zod ``` ```bash [Bun] bun add @optique/config @standard-schema/spec zod ``` ::: ## Why config files? Many CLI applications need configuration files for: * *Default values* that persist across invocations * *Environment-specific* settings (development, staging, production) * *Complex options* that are tedious to specify on the command line * *Shared settings* across team members (via version control) The *@optique/config* package handles this pattern with full type safety, automatic validation, and seamless integration with Optique parsers. ## Basic usage ### 1. Create a config context Define your configuration schema using any [Standard Schema]-compatible library: ```typescript twoslash import { z } from "zod"; import { createConfigContext } from "@optique/config"; const configSchema = z.object({ host: z.string(), port: z.number(), verbose: z.boolean().optional(), }); const configContext = createConfigContext({ schema: configSchema }); ``` [Standard Schema]: https://standardschema.dev/ ### 2. Bind parsers to config values Use `bindConfig()` to create parsers that fall back to configuration file values: ```typescript twoslash import { z } from "zod"; import { createConfigContext, bindConfig } from "@optique/config"; const configContext = createConfigContext({ schema: z.object({ host: z.string(), port: z.number() }) }); // ---cut-before--- import { option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; const hostParser = bindConfig(option("--host", string()), { context: configContext, key: "host", default: "localhost", }); const portParser = bindConfig(option("--port", integer()), { context: configContext, key: "port", default: 3000, }); ``` ### 3. Run with config support Pass the config context to `runAsync()` (or `run()`) via the `contexts` option: > \[!WARNING] > `bindConfig()` only reads configuration values when its context is registered > with the runner. Omitting `contexts: [configContext]` from the `run()` call > causes the config lookup to be silently skipped and the parser falls back to > the default or fails with an error indicating the context was not registered. ```typescript twoslash import { z } from "zod"; import { createConfigContext, bindConfig } from "@optique/config"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; import { withDefault } from "@optique/core/modifiers"; const configSchema = z.object({ host: z.string(), port: z.number(), }); const configContext = createConfigContext({ schema: configSchema }); const parser = object({ config: withDefault(option("--config", string()), "~/.myapp.json"), host: bindConfig(option("--host", string()), { context: configContext, key: "host", default: "localhost", }), port: bindConfig(option("--port", integer()), { context: configContext, key: "port", default: 3000, }), }); // ---cut-before--- import { runAsync } from "@optique/run"; const result = await runAsync(parser, { contexts: [configContext], contextOptions: { getConfigPath: (parsed) => parsed.config, }, }); console.log(`Connecting to ${result.host}:${result.port}`); ``` If the config file `~/.myapp.json` contains: ```json { "host": "api.example.com", "port": 8080 } ``` And the user runs: ```bash myapp --host localhost ``` The result will be: * `host`: `"localhost"` (from CLI, overrides config) * `port`: `8080` (from config file) ## Priority order Values are resolved in this priority order: 1. *CLI argument*: Highest priority, always used when provided 2. *Config file value*: Used when CLI argument not provided 3. *Default value*: Used when neither CLI nor config provides a value 4. *Error*: If no value is available and no default is specified ```typescript twoslash import { z } from "zod"; import { createConfigContext, bindConfig } from "@optique/config"; const configContext = createConfigContext({ schema: z.object({ port: z.number() }) }); // ---cut-before--- import { option } from "@optique/core/primitives"; import { integer } from "@optique/core/valueparser"; // With default: always succeeds const portWithDefault = bindConfig(option("--port", integer()), { context: configContext, key: "port", default: 3000, }); // Without default: requires CLI or config const portRequired = bindConfig(option("--port", integer()), { context: configContext, key: "port", // No default - will error if not in CLI or config }); ``` ## Help, version, and completion When using `run()` or `runAsync()` with config contexts, help messages, version display, and shell completion generation all work seamlessly. Genuine help, version, and completion requests work even when configuration files are missing or invalid, ensuring users can still access those features unless the user parser already consumes the same token sequence as ordinary data: ```typescript twoslash import { z } from "zod"; import { createConfigContext, bindConfig } from "@optique/config"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; import { withDefault } from "@optique/core/modifiers"; const configSchema = z.object({ host: z.string(), port: z.number(), }); const configContext = createConfigContext({ schema: configSchema }); const parser = object({ config: withDefault(option("--config", string()), "~/.myapp.json"), host: bindConfig(option("--host", string()), { context: configContext, key: "host", default: "localhost", }), port: bindConfig(option("--port", integer()), { context: configContext, key: "port", default: 3000, }), }); // ---cut-before--- import { runAsync } from "@optique/run"; const result = await runAsync(parser, { contexts: [configContext], contextOptions: { getConfigPath: (parsed) => parsed.config, }, help: "option", version: "1.0.0", completion: "option", }); ``` Now users can use: ```bash # Show help (even if config file is missing) myapp --help # Show version myapp --version # Generate shell completion myapp --completion bash > myapp-completion.sh ``` The key benefit is that genuine help, version, and completion requests work *before* config file loading, so they succeed even when the config file is invalid or missing. If the parser accepts the same tokens as ordinary input, parsing takes precedence and config loading proceeds as usual. ## Nested config values Use accessor functions to extract nested configuration values: ```typescript twoslash import { z } from "zod"; import { createConfigContext, bindConfig } from "@optique/config"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const configSchema = z.object({ server: z.object({ host: z.string(), port: z.number(), }), database: z.object({ host: z.string(), port: z.number(), }), }); const configContext = createConfigContext({ schema: configSchema }); const serverHost = bindConfig(option("--server-host", string()), { context: configContext, key: (config) => config.server.host, default: "localhost", }); const dbHost = bindConfig(option("--db-host", string()), { context: configContext, key: (config) => config.database.host, default: "localhost", }); ``` With a config file: ```json { "server": { "host": "api.example.com", "port": 8080 }, "database": { "host": "db.example.com", "port": 5432 } } ``` ## Resolving paths relative to config files For path-like options, CLI values and config values often need different base directories: * CLI values are usually interpreted relative to the current working directory * Config values are usually interpreted relative to the config file location `bindConfig()` key callbacks receive metadata as a second argument, so you can resolve config-relative paths reliably. ```typescript twoslash import { resolve } from "node:path"; import { z } from "zod"; import { bindConfig, createConfigContext } from "@optique/config"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { map } from "@optique/core/modifiers"; const configContext = createConfigContext({ schema: z.object({ outDir: z.string(), }), }); const parser = bindConfig( map(option("--out-dir", string()), (value) => resolve(process.cwd(), value)), { context: configContext, key: (config, meta) => { if (meta === undefined) { throw new TypeError("Config metadata is not available."); } return resolve(meta.configDir, config.outDir); }, }, ); ``` In single-file mode, Optique provides `meta.configPath` and `meta.configDir` automatically, so the guard above only matters when metadata may be absent. ## Config-only values Sometimes a configuration value should *never* come from a CLI flag—it lives entirely in the config file (or uses a default). In that case, use `fail()` as the inner parser for `bindConfig()`. `fail()` always fails, so `bindConfig()` always falls back to the config file or the supplied default. Compare this with `constant(value)`, which always succeeds and would prevent the config fallback from ever triggering. ```typescript twoslash import { z } from "zod"; import { bindConfig, createConfigContext } from "@optique/config"; import { object } from "@optique/core/constructs"; import { fail, option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; import { withDefault } from "@optique/core/modifiers"; import { runAsync } from "@optique/run"; const configSchema = z.object({ host: z.string(), port: z.number(), // timeout only lives in the config file, not exposed as a CLI flag timeout: z.number().optional(), }); const configContext = createConfigContext({ schema: configSchema }); const parser = object({ config: withDefault(option("--config", string()), "~/.myapp.json"), host: bindConfig(option("--host", string()), { context: configContext, key: "host", default: "localhost", }), port: bindConfig(option("--port", integer()), { context: configContext, key: "port", default: 3000, }), // No CLI flag — value comes only from config file or default timeout: bindConfig(fail(), { context: configContext, key: "timeout", default: 30, }), }); const result = await runAsync(parser, { contexts: [configContext], contextOptions: { getConfigPath: (parsed) => parsed.config, }, }); console.log(`Timeout: ${result.timeout}s`); ``` With a config file containing `"timeout": 60`, `result.timeout` will be `60`. Without a config file (or if `timeout` is absent), it falls back to `30`. ## Custom file formats By default, *@optique/config* parses JSON files. You can provide a custom file parser when creating the config context: ```typescript twoslash import { z } from "zod"; import { createConfigContext, bindConfig } from "@optique/config"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; import { runAsync } from "@optique/run"; const configSchema = z.object({ host: z.string(), port: z.number(), }); // Custom parser for KEY=VALUE format const customParser = (contents: Uint8Array): unknown => { const text = new TextDecoder().decode(contents); const lines = text.split("\n"); const result: Record = {}; for (const line of lines) { const [key, value] = line.split("="); if (key && value) { result[key] = key === "port" ? parseInt(value, 10) : value; } } return result; }; // Pass fileParser to createConfigContext const configContext = createConfigContext({ schema: configSchema, fileParser: customParser, }); const parser = object({ config: option("--config", string()), host: bindConfig(option("--host", string()), { context: configContext, key: "host", default: "localhost", }), port: bindConfig(option("--port", integer()), { context: configContext, key: "port", default: 3000, }), }); const result = await runAsync(parser, { contexts: [configContext], contextOptions: { getConfigPath: (parsed) => parsed.config, }, }); ``` Now your application can read files in the custom KEY=VALUE format: ``` host=api.example.com port=8080 ``` ## Multi-file configuration For advanced scenarios like hierarchical config merging (system → user → project), use the `load` callback in the runtime options: ```typescript twoslash // @noErrors import { z } from "zod"; import { createConfigContext, bindConfig } from "@optique/config"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; import { runAsync } from "@optique/run"; import { readFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; declare function deepMerge(...objects: any[]): any; const configSchema = z.object({ host: z.string(), port: z.number(), timeout: z.number().optional(), }); const configContext = createConfigContext({ schema: configSchema }); const parser = object({ config: option("--config", string()).optional(), host: bindConfig(option("--host", string()), { context: configContext, key: "host", default: "localhost", }), port: bindConfig(option("--port", integer()), { context: configContext, key: "port", default: 3000, }), }); const result = await runAsync(parser, { contexts: [configContext], contextOptions: { load: async (parsed) => { // Load multiple config files with different error handling const tryLoad = async (path: string) => { try { return JSON.parse(await readFile(path, "utf-8")); } catch { return {}; // Silent skip on error } }; const system = await tryLoad("/etc/myapp/config.json"); const user = await tryLoad(`${process.env.HOME}/.config/myapp/config.json`); const project = await tryLoad("./.myapp.json"); // Load custom config file if specified (throws on error) const custom = parsed.config ? JSON.parse(await readFile(parsed.config, "utf-8")) : {}; const customPath = resolve(parsed.config ?? "./.myapp.json"); // Merge with priority: custom > project > user > system return { config: deepMerge(system, user, project, custom), meta: { configPath: customPath, configDir: dirname(customPath), }, }; }, }, }); ``` This approach gives you full control over: * File discovery and loading order * Error handling policies (silent skip vs. hard error) * Merging strategies (deep merge, shallow merge, array concatenation, etc.) * File formats (JSON, TOML, YAML, etc.) If no config data is available, return `undefined` or `null` directly from `load()` (not wrapped in a `ConfigLoadResult`). This signals “no config found” and `bindConfig()` falls back to its defaults, just like `getConfigPath` mode when the path is `undefined` or the file is missing. You'll need to provide your own merge utility (e.g., from [lodash] or [es-toolkit]). [lodash]: https://lodash.com/docs#merge [es-toolkit]: https://es-toolkit.slash.page/reference/object/merge.html ## Standard Schema support The *@optique/config* package uses [Standard Schema], which means it works with any compatible validation library: ### Zod ```typescript twoslash import { z } from "zod"; import { createConfigContext } from "@optique/config"; const configContext = createConfigContext({ schema: z.object({ apiKey: z.string().min(32), timeout: z.number().positive(), }), }); ``` ### Valibot ```typescript twoslash import * as v from "valibot"; import { createConfigContext } from "@optique/config"; const configContext = createConfigContext({ schema: v.object({ apiKey: v.pipe(v.string(), v.minLength(32)), timeout: v.pipe(v.number(), v.minValue(1)), }), }); ``` ### ArkType ```typescript twoslash import { type } from "arktype"; import { createConfigContext } from "@optique/config"; const configContext = createConfigContext({ schema: type({ apiKey: "string>=32", timeout: "number>0", }), }); ``` ## Composable with other sources Config contexts implement the `SourceContext` interface, allowing composition with other data sources. When using `run()` or `runAsync()` with multiple contexts, you can pass them all in the `contexts` array. Earlier contexts override later ones, enabling natural priority chains like CLI > environment variables > config file > defaults: ```typescript twoslash import { z } from "zod"; import { createConfigContext, bindConfig } from "@optique/config"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const configContext = createConfigContext({ schema: z.object({ host: z.string() }) }); const parser = object({ config: option("--config", string()), host: bindConfig(option("--host", string()), { context: configContext, key: "host", default: "localhost", }), }); // ---cut-before--- import { runAsync } from "@optique/run"; // Combine config with other sources (e.g., environment variables) const result = await runAsync(parser, { contexts: [configContext], contextOptions: { getConfigPath: (parsed) => parsed.config, // Typed from parser result! }, }); ``` The `getConfigPath` callback is fully typed based on the parser's result type, providing type safety without manual type assertions. You can also use `runWith()` from `@optique/core/facade` directly for process-agnostic environments: ```typescript twoslash import { z } from "zod"; import { createConfigContext, bindConfig } from "@optique/config"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const configContext = createConfigContext({ schema: z.object({ host: z.string() }) }); const parser = object({ config: option("--config", string()), host: bindConfig(option("--host", string()), { context: configContext, key: "host", default: "localhost", }), }); // ---cut-before--- import { runWith } from "@optique/core/facade"; const result = await runWith(parser, "myapp", [configContext], { args: process.argv.slice(2), contextOptions: { getConfigPath: (parsed) => parsed.config, }, }); ``` ## Error handling ### Config file not found If the config file is not found, *@optique/config* continues with default values: ```typescript twoslash import { z } from "zod"; import { createConfigContext, bindConfig } from "@optique/config"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { runAsync } from "@optique/run"; const configContext = createConfigContext({ schema: z.object({ host: z.string() }), }); const parser = object({ config: option("--config", string()), host: bindConfig(option("--host", string()), { context: configContext, key: "host", default: "localhost", }), }); // Config file not found or not specified - uses default const result = await runAsync(parser, { contexts: [configContext], contextOptions: { getConfigPath: (parsed) => parsed.config, }, args: [], }); console.log(result.host); // "localhost" (default) ``` ### Invalid config file If the config file fails validation, an error is thrown: ```typescript twoslash import { z } from "zod"; import { createConfigContext, bindConfig } from "@optique/config"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const configContext = createConfigContext({ schema: z.object({ host: z.string() }) }); const parser = object({ config: option("--config", string()), host: bindConfig(option("--host", string()), { context: configContext, key: "host" }), }); // ---cut-before--- import { runAsync } from "@optique/run"; try { const result = await runAsync(parser, { contexts: [configContext], contextOptions: { getConfigPath: (parsed) => "/path/to/invalid-config.json", }, args: [], }); } catch (error) { console.error("Config validation failed:", error); } ``` ### Fallback validation Since Optique 1.0.0, fallback values produced by `bindConfig()` are re-validated against the inner CLI parser's constraints (regex patterns, numeric bounds, `choice()` values, etc.). This applies to both values loaded from the config file and to the configured `default`. For example, the following parser rejects the default `80` because it is below the inner CLI parser's `min: 1024` bound: ```typescript twoslash import { z } from "zod"; import { option } from "@optique/core/primitives"; import { integer } from "@optique/core/valueparser"; import { bindConfig, createConfigContext } from "@optique/config"; const configContext = createConfigContext({ schema: z.object({ port: z.number().optional() }), }); // ---cut-before--- bindConfig(option("--port", integer({ min: 1024 })), { context: configContext, key: "port", default: 80, // rejected at runtime: must be >= 1024 }); ``` Validation is forwarded through standard combinators (`optional()`, `withDefault()`, `group()`, `command()`) and through wrapping `bindEnv()`/`bindConfig()` layers, so a constraint defined on a deeply nested primitive is still enforced against a fallback value. `multiple()` attaches its own `validateValue`: it enforces the configured `min`/`max` arity against the fallback array length and, *if* the inner parser exposes a `validateValue` hook, walks each element through it. Arity enforcement is unconditional—it kicks in even when the inner parser has no `validateValue`—and a non-array fallback (for example a mis-typed default escaped through `as never`) is rejected outright because `multiple()` can never produce a non-array shape from CLI input. `nonEmpty()` is a pure pass-through: it does not add an extra non-empty check on the fallback path. On the CLI path `nonEmpty()` still enforces that at least one token was consumed, but on fallback values `nonEmpty(multiple(...))` delegates entirely to the inner `multiple()`'s arity rules. If you need a “must have at least one element” guarantee against fallback arrays, use `multiple(..., { min: 1 })` directly. `map()`, `derive()`, and `deriveFrom()` intentionally *strip* the inner parser's `validateValue`: the mapping function is one-way, so the mapped output type no longer corresponds to the inner parser's constraints, and derived value parsers rebuild from *default* dependency values rather than the live-resolved ones. Wrapping an inner parser in any of these suppresses revalidation of the wrapped primitive's constraints—but outer combinators layered above (notably `multiple()`) still enforce their own checks. ## API reference ### `createConfigContext(options)` Creates a configuration context. Parameters : - `options.schema`: Standard Schema validator for the config file \- `options.fileParser`: Optional custom parser for file contents (defaults to `JSON.parse`) Returns : `ConfigContext` implementing `SourceContext` interface > \[!IMPORTANT] > If you call `configContext.getAnnotations()` manually, omit the request for > a phase-1 snapshot or pass `{ phase: "phase2", parsed }` for a phase-two > snapshot, then pass the returned object to low-level APIs such as > `parse()`, `parseAsync()`, `parser.complete()`, `suggest()`, or > `getDocPage()`. Calling `getAnnotations()` alone does not affect later > parses. ### `bindConfig(parser, options)` Binds a parser to configuration values with fallback priority. Fallback values—values loaded from the config file and the configured `default`—are re-validated against the inner CLI parser's constraints, so constraints like `integer({ min })`, `string({ pattern })`, and `choice([...])` cannot be bypassed through a config file or default. See *Fallback validation* under “Error handling” for details. Parameters : - `parser`: The parser to bind ``` - `options.context`: Config context to use - `options.key`: Property key or accessor function to extract value from config. Accessor functions receive two arguments: 1) `config`: validated config data 2) `meta`: config metadata if available (`ConfigMeta | undefined` by default) - `options.default`: Optional default value ``` Returns : A new parser with config fallback behavior ### Runtime options When using a config context with `run()`, `runAsync()`, or `runWith()`, the following context-specific options are passed alongside the standard runner options: `getConfigPath` : Function to extract config file path from parsed result. Optional when using the `load` callback. `load` : Function that receives parsed result and returns `ConfigLoadResult` (or Promise of it). `meta` may be `undefined`. Return `undefined` or `null` directly (not wrapped in a `ConfigLoadResult`) to signal that no config data is available. Use this for multi-file merging scenarios. Optional when using `getConfigPath`. At least one of `getConfigPath` or `load` must be provided. Each `run()`, `runAsync()`, or `runWith()` call snapshots config annotations per run, so reusing the same `ConfigContext` instance across independent or concurrent runs is safe. When calling `configContext.getAnnotations()` manually, remember that the call only returns annotations. Use `{ phase: "phase2", parsed }` when you need a manual phase-two snapshot. It does not mutate global state or affect later parses by itself. To use those values with low-level APIs such as `parse()` or `suggestSync()`, pass the returned annotations explicitly. ### `ConfigMeta` Default config metadata shape: * `configPath`: Absolute path to the config file * `configDir`: Directory containing the config file ## Limitations * *File I/O is async* — config loading always returns a Promise due to file reading, so use `runAsync()` or `run()` (which returns a Promise when contexts are provided) * *JSON only by default* — Other formats require the `fileParser` option on `createConfigContext()` or a custom `load` callback * *Two-pass parsing* — Parsing happens twice (once to extract config path, once with config data), which has a performance cost * *Standard Schema required* — You must use a Standard Schema-compatible validation library * *No built-in merge utilities* — Multi-file merging requires bringing your own merge function (e.g., from lodash or es-toolkit) ## Example application Here's a complete example of a CLI application with config file support: ```typescript twoslash import { z } from "zod"; import { createConfigContext, bindConfig } from "@optique/config"; import { object } from "@optique/core/constructs"; import { option, flag } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; import { withDefault } from "@optique/core/modifiers"; import { runAsync } from "@optique/run"; // Define config schema const configSchema = z.object({ host: z.string(), port: z.number(), verbose: z.boolean().optional(), apiKey: z.string(), }); const configContext = createConfigContext({ schema: configSchema }); // Build parser const parser = object({ config: withDefault(option("--config", string()), "~/.myapp.json"), host: bindConfig(option("--host", string()), { context: configContext, key: "host", default: "localhost", }), port: bindConfig(option("--port", integer()), { context: configContext, key: "port", default: 3000, }), verbose: bindConfig(flag("--verbose"), { context: configContext, key: "verbose", default: false, }), apiKey: bindConfig(option("--api-key", string()), { context: configContext, key: "apiKey", // No default - required from CLI or config }), }); // Run with config support const config = await runAsync(parser, { contexts: [configContext], contextOptions: { getConfigPath: (parsed) => parsed.config, }, }); if (config.verbose) { console.log("Configuration:", config); } // Use the configuration console.log(`Connecting to ${config.host}:${config.port}`); console.log(`API Key: ${config.apiKey.substring(0, 8)}...`); ``` With a config file `~/.myapp.json`: ```json { "host": "api.example.com", "port": 8080, "apiKey": "secret-key-12345678" } ``` Running the application: ```bash # Uses all config values myapp # Override host from CLI myapp --host localhost # Enable verbose mode myapp --verbose ``` The *@optique/config* package provides a clean, type-safe way to manage configuration files in your CLI applications while maintaining the flexibility of command-line arguments. > \[!TIP] > See the [cookbook](../cookbook.md#config-file-integration) for additional > patterns, including > [combining config with env variables](../cookbook.md#combining-with-environment-variables), > [combining with interactive prompts](../cookbook.md#combining-with-interactive-prompts), > and > [config-only values with `fail()`](../cookbook.md#config-only-and-env-only-values-with-fail). --- --- url: /concepts/constructs.md description: >- Construct combinators compose multiple parsers into complex structures using object(), tuple(), seq(), or(), and merge() to build sophisticated CLI interfaces with full type inference. --- # Construct combinators Construct combinators are the high-level combinators that compose multiple parsers into complex, structured CLI interfaces. While primitive parsers handle individual options and arguments, construct combinators orchestrate them into cohesive applications with sophisticated behavior patterns like mutually exclusive commands, structured configuration, and modular option groups. Understanding construct combinators is essential for building real-world CLI applications. They provide the architectural patterns you need to create maintainable, user-friendly interfaces that can grow in complexity without becoming unwieldy. Each construct combinator follows Optique's compositional philosophy: complex behavior emerges from combining simple, well-understood pieces. The power of construct combinators lies in their ability to preserve full type safety while enabling complex composition. TypeScript automatically infers the result types of even deeply nested parser structures, ensuring that your parsed CLI data is fully typed without manual type annotations. ## `object()` parser The `object()` parser combines multiple named parsers into a single parser that produces a structured object. This is the primary way to group related options and arguments into logical units, creating the foundation for most CLI applications. ```typescript twoslash import { object } from "@optique/core/constructs"; import type { InferValue } from "@optique/core/parser"; import { argument, option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; const serverConfig = object({ name: argument(string({ metavar: "NAME" })), port: option("-p", "--port", integer({ min: 1, max: 0xffff })), host: option("-h", "--host", string()), verbose: option("-v", "--verbose") }); type ServerConfig = InferValue; // ^? // Type automatically inferred as above. ``` ### Labeled objects You can provide a label for documentation and error reporting purposes. This label appears in help text to group related options: ```typescript twoslash import { object } from "@optique/core/constructs"; import { optional } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { choice, integer, string } from "@optique/core/valueparser"; // ---cut-before--- const networkOptions = object("Network Configuration", { host: option("--host", string()), port: option("--port", integer()), ssl: option("--ssl") }); const loggingOptions = object("Logging Options", { logLevel: option("--log-level", choice(["debug", "info", "warn", "error"])), logFile: optional(option("--log-file", string())) }); ``` ### Parser priority The `object()` parser uses the highest priority among its constituent parsers. This ensures that higher-priority parsers (like commands) within the object are tried before lower-priority parsers in other parts of the CLI structure. ## `tuple()` parser The `tuple()` parser combines multiple parsers into a sequential parser that produces a tuple (ordered heterogenous array) of results. Unlike `object()` which uses named fields, `tuple()` preserves the order and positional nature of its components. ```typescript twoslash import { tuple } from "@optique/core/constructs"; import type { InferValue } from "@optique/core/parser"; import { argument, option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; const connectionTuple = tuple([ option("--host", string()), option("--port", integer()), argument(string({ metavar: "DATABASE" })) ] as const); type Connection = InferValue; // ^? // Type automatically inferred as above. ``` ### Sequential parsing The `tuple()` parser processes its component parsers in priority order (not array order), which means parsers with higher priority are tried first regardless of their position in the tuple: ```typescript twoslash import { tuple } from "@optique/core/constructs"; import { argument, command, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; // ---cut-before--- // Even though argument is last in the array, it might be parsed first // depending on the current parsing context and available input const mixedTuple = tuple([ option("-v", "--verbose"), // Priority 10 command("start", argument(string())), // Priority 15 - tried first argument(string()) // Priority 5 ] as const); ``` ### Labeled tuples Like `object()`, `tuple()` supports labels for documentation: ```typescript twoslash import { tuple } from "@optique/core/constructs"; import { optional } from "@optique/core/modifiers"; import type { InferValue } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { float, string } from "@optique/core/valueparser"; // ---cut-before--- const coordinates = tuple("Location", [ option("--lat", float({ metavar: "LATITUDE" })), option("--lon", float({ metavar: "LONGITUDE" })), optional(option("--alt", float({ metavar: "ALTITUDE" }))) ] as const); type Coordinate = InferValue; // ^? // Type automatically inferred as above. ``` ### Usage patterns Tuples are useful when you need: * Ordered results where position matters * Integration with APIs that expect arrays * Processing of homogeneous but positionally significant data ```typescript twoslash import { tuple } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; import { argument } from "@optique/core/primitives"; import { integer } from "@optique/core/valueparser"; // ---cut-before--- const rangeParser = tuple([ argument(integer({ metavar: "START" })), argument(integer({ metavar: "END" })) ]); // Usage: myapp 10 20 // Result: [10, 20] const config = parse(rangeParser, ["10", "20"]); if (config.success) { const [start, end] = config.value; console.log(`Processing range ${start} to ${end}.`); } ``` ## `seq()` parser The `seq()` parser combines multiple parsers into an ordered parser that produces a tuple of results. Unlike [`tuple()`](#tuple-parser), which lets child parsers compete by priority, `seq()` keeps a cursor and applies child parsers in declaration order. This is useful when the grammar itself is ordered, especially when an optional positional value appears before a later command: ```typescript twoslash import { object, or, seq } from "@optique/core/constructs"; import { optional } from "@optique/core/modifiers"; import type { InferValue } from "@optique/core/parser"; import { argument, command, constant, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const parser = seq( optional(argument(string({ metavar: "PROFILE" }))), or( command( "build", object({ action: constant("build"), target: argument(string({ metavar: "TARGET" })), }), ), command( "deploy", object({ action: constant("deploy"), environment: argument(string({ metavar: "ENV" })), force: option("--force"), }), ), ), ); type Parsed = InferValue; // ^? // Type automatically inferred as above. ``` With this parser, both of these forms are valid: ```bash tool build app tool staging deploy production --force ``` The first command leaves the optional profile as `undefined`; the second command sets it to `"staging"`. When the current child parser can finish without consuming more input, `seq()` can advance to a later command name. ### Ordered usage output Because `seq()` preserves declaration order, generated usage follows the grammar you wrote instead of priority order: ```typescript twoslash import { object, or, seq } from "@optique/core/constructs"; import { formatUsage } from "@optique/core/usage"; import { argument, command, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; // ---cut-before--- const copyParser = seq( argument(string({ metavar: "SOURCE" })), or(command("local", object({})), command("remote", object({}))), option("--force"), ); const usage = formatUsage("copy", copyParser.usage); // ^? // "copy SOURCE (local | remote) [--force]" ``` ### Duplicate options `seq()` rejects duplicate option names only when the same option can be active at the same cursor position. Duplicate options separated by a required sequential boundary are allowed, but ambiguous duplicates can be enabled explicitly with `{ allowDuplicates: true }`. ### No backtracking `seq()` does not backtrack after a later parser succeeds. It can skip fixed optional parsers before a later command name, but ambiguous variadic positionals still need an explicit grammar boundary such as a command name, an option, or `--`. ## `or()` parser The `or()` parser creates mutually exclusive alternatives, trying each alternative in order until one succeeds. This is fundamental for building CLIs with different modes of operation, subcommands, or mutually exclusive option sets. > \[!NOTE] > `or()` can use a definitive non-consuming branch as a fallback. For example, > `or(constant("fallback"), option("-o", string()))` succeeds on empty input > and still prefers the consuming branch when `-o` is present. Wrap the whole > parser with [`optional()`](./modifiers.md#optional-parser) when you want > “no selection” to produce `undefined` instead of an explicit fallback value. > \[!IMPORTANT] > TypeScript overload inference for `or()` supports up to 15 parser > arguments. When you need 16 or more branches, split them into nested > groups so each `or()` call stays at 15 or fewer arguments. > > ```typescript twoslash > import { or } from "@optique/core/constructs"; > import { command, constant } from "@optique/core/primitives"; > // ---cut-before--- > const commandGroupA = or( > command("a1", constant("a1")), > command("a2", constant("a2")), > command("a3", constant("a3")), > command("a4", constant("a4")), > command("a5", constant("a5")), > command("a6", constant("a6")), > command("a7", constant("a7")), > command("a8", constant("a8")), > ); > > const commandGroupB = or( > command("b1", constant("b1")), > command("b2", constant("b2")), > command("b3", constant("b3")), > command("b4", constant("b4")), > command("b5", constant("b5")), > command("b6", constant("b6")), > command("b7", constant("b7")), > command("b8", constant("b8")), > ); > > const parser = or(commandGroupA, commandGroupB); > ``` ```typescript twoslash import { object, or } from "@optique/core/constructs"; import type { InferValue } from "@optique/core/parser"; import { command, constant, option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; const parser = or( // Server mode object({ mode: constant("server"), port: option("-p", "--port", integer()), host: option("-h", "--host", string()) }), // Client mode object({ mode: constant("client"), connect: option("-c", "--connect", string()), timeout: option("-t", "--timeout", integer()) }) ); type Result = InferValue; // ^? // Type automatically inferred as discriminated union. ``` ### Discriminated unions The `or()` parser creates TypeScript discriminated unions when used with `constant()` parsers. This enables type-safe pattern matching: ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; import { command, constant, option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; const parser = or( // Server mode object({ mode: constant("server"), port: option("-p", "--port", integer()), host: option("-h", "--host", string()) }), // Client mode object({ mode: constant("client"), connect: option("-c", "--connect", string()), timeout: option("-t", "--timeout", integer()) }) ); // ---cut-before--- const config = parse(parser, ["-p", "8080", "-h", "localhost"]); if (config.success) { switch (config.value.mode) { case "server": // TypeScript knows this is server config console.log(`Server running on ${config.value.host}:${config.value.port}.`); break; case "client": // TypeScript knows this is client config console.log(`Connecting to ${config.value.connect} with timeout ${config.value.timeout}.`); break; } } ``` ### Command alternatives The most common use of `or()` is for subcommand dispatch: ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { multiple, optional, withDefault } from "@optique/core/modifiers"; import { argument, command, constant, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; // ---cut-before--- const gitLike = or( command("add", object({ type: constant("add"), files: multiple(argument(string())), all: optional(option("-A", "--all")) })), command("commit", object({ type: constant("commit"), message: option("-m", "--message", string()), amend: optional(option("--amend")) })), command("push", object({ type: constant("push"), remote: withDefault(option("-r", "--remote", string()), "origin"), force: optional(option("-f", "--force")) })) ); // Usage examples: // myapp add file1.txt file2.txt --all // myapp commit -m "Fix parser bug" --amend // myapp push --remote upstream --force ``` ### Alternative parsing strategies You can also use `or()` for different parsing approaches of the same logical concept: ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { constant, option } from "@optique/core/primitives"; import { integer, string, url } from "@optique/core/valueparser"; // ---cut-before--- const flexibleConfig = or( // Configuration via individual options object({ source: constant("options"), host: option("--host", string()), port: option("--port", integer()), ssl: option("--ssl") }), // Configuration via config file object({ source: constant("file"), configFile: option("-c", "--config", string()) }), // Configuration via URL object({ source: constant("url"), connectionString: option("--url", url()) }) ); ``` ### Optional alternatives with `optional(or(...))` When you want to allow *none* of the alternatives to be provided, wrap the `or()` with [`optional()`](./modifiers.md#optional-parser). This is useful when you have mutually exclusive options that are all optional: ```typescript twoslash import { or } from "@optique/core/constructs"; import { map, optional } from "@optique/core/modifiers"; import { parse } from "@optique/core/parser"; import { flag } from "@optique/core/primitives"; // Without optional(): at least one flag MUST be provided const requiredChoice = or( map(flag("--verbose", "-v"), () => "verbose" as const), map(flag("--quiet", "-q"), () => "quiet" as const), ); // With optional(): none of the flags need to be provided const optionalChoice = optional( or( map(flag("--verbose", "-v"), () => "verbose" as const), map(flag("--quiet", "-q"), () => "quiet" as const), ), ); // No arguments: requiredChoice fails, optionalChoice succeeds const result1 = parse(requiredChoice, []); console.log(result1.success); // false - "No matching option found" const result2 = parse(optionalChoice, []); console.log(result2.success); // true if (result2.success) { console.log(result2.value); // undefined } // With --verbose: both succeed const result3 = parse(optionalChoice, ["--verbose"]); if (result3.success) { console.log(result3.value); // "verbose" } ``` This pattern is common when implementing verbosity levels or output format selection where a default behavior should apply when no explicit choice is made. You can combine it with [`withDefault()`](./modifiers.md#withdefault-parser) to provide a fallback value instead of `undefined`: ```typescript twoslash import { or } from "@optique/core/constructs"; import { map, withDefault } from "@optique/core/modifiers"; import { flag } from "@optique/core/primitives"; // ---cut-before--- const verbosityLevel = withDefault( or( map(flag("--verbose", "-v"), () => "verbose" as const), map(flag("--quiet", "-q"), () => "quiet" as const), ), "normal" as const, // Default when neither flag is provided ); // No flags → "normal" // --verbose → "verbose" // --quiet → "quiet" ``` ### Error message customization *This feature is available since Optique 0.9.0.* The `or()` parser generates contextual error messages by analyzing what types of inputs are expected (options, commands, or arguments). You can customize these messages using the `errors.noMatch` option, which supports both static messages and dynamic functions for advanced use cases like internationalization: ```typescript twoslash import { message, or } from "@optique/core"; import { command, constant } from "@optique/core/primitives"; // ---cut-before--- // Static custom error message const parser1 = or( command("add", constant("add")), command("remove", constant("remove")), { errors: { noMatch: message`Invalid command. Please use 'add' or 'remove'.` } } ); // Dynamic error message for internationalization const parser2 = or( command("add", constant("add")), command("remove", constant("remove")), { errors: { noMatch: ({ hasOptions, hasCommands, hasArguments }) => { if (hasCommands && !hasOptions && !hasArguments) { return message`일치하는 명령을 찾을 수 없습니다.`; // Korean } return message`잘못된 입력입니다.`; } } } ); ``` The function form receives a `NoMatchContext` object with three boolean flags: * `hasOptions`: Whether any parsers expect options * `hasCommands`: Whether any parsers expect commands * `hasArguments`: Whether any parsers expect arguments This enables precise, context-aware error messages. For example, if all parsers expect only commands, you can show “No matching command found” instead of the generic “No matching option or command found.” **Default behavior** (when no custom error is provided): | Context | Default error message | | ------------------------------- | ------------------------------------------------- | | Only arguments expected | `Missing required argument.` | | Only commands expected | `No matching command found.` | | Only options expected | `No matching option found.` | | Commands and options expected | `No matching option or command found.` | | Arguments and options expected | `No matching option or argument found.` | | Arguments and commands expected | `No matching command or argument found.` | | All three types expected | `No matching option, command, or argument found.` | The default messages automatically adapt to your parser structure, but you can override them for custom formatting or localization needs. ### Zero-consumed fallback branches When all input has been consumed and no branch matched, `or()` can fall back to a branch that succeeds without consuming any input, such as `constant()`. A branch qualifies as a fallback candidate only when *all* of the following hold: * The result is not `provisional` (tentative zero-consumed matches from nested constructs like `conditional()` are excluded). * The branch has no `leadingNames` and does not accept arbitrary tokens—i.e., it can *never* match an input token. * Exactly one branch qualifies (ambiguous fallbacks are rejected). * No other branch consumed tokens before failing. * The input buffer is empty. In practice, this means: * Annotation-backed parsers like `bindEnv(option(...))` or `bindConfig(option(...))` are *not* eligible, because they inherit `leadingNames` from the inner option. * Positional parsers like `argument(...)` are *not* eligible either, because they accept arbitrary tokens even though they have no `leadingNames`. To provide a fallback value for an env/config-backed option, use the parser's own default mechanism instead of wrapping it in `or()`: ```typescript twoslash import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; declare function bindEnv(p: any, o: any): any; declare const envContext: any; // ---cut-before--- // Instead of or(bindEnv(option(...)), constant("fallback")): bindEnv(option("--mode", string()), { context: envContext, key: "APP_MODE", parser: string(), default: "fallback", // built-in fallback }) ``` ## `merge()` parser The `merge()` parser combines multiple object-generating parsers into a single unified parser, enabling modular CLI design through reusable option groups. While originally designed for `object()` parsers, it now accepts any parser that produces object-like values, including `withDefault()`, `map()`, and other transformative parsers. This is essential for building maintainable applications where related options can be shared across different commands or modes. > \[!IMPORTANT] > TypeScript overload inference for `merge()` supports up to 15 parser > arguments. Calls beyond 15 now fail at compile time with an actionable > message. For 16+ parsers, split into nested groups so each `merge()` call > stays at 15 or fewer arguments. ```typescript twoslash import { merge, object, or } from "@optique/core/constructs"; import { optional } from "@optique/core/modifiers"; import { constant, option } from "@optique/core/primitives"; import { string, integer, choice } from "@optique/core/valueparser"; // Define reusable option groups const networkOptions = object("Network", { host: option("--host", string()), port: option("--port", integer({ min: 1, max: 0xffff })) }); const authOptions = object("Authentication", { username: option("-u", "--user", string()), password: optional(option("-p", "--pass", string())), token: optional(option("-t", "--token", string())) }); const loggingOptions = object("Logging", { logLevel: option("--log-level", choice(["debug", "info", "warn", "error"])), logFile: optional(option("--log-file", string())) }); // Combine groups for different application modes const devMode = merge( object({ mode: constant("development") }), networkOptions, loggingOptions ); const prodMode = merge( object({ mode: constant("production") }), networkOptions, authOptions, loggingOptions, object("Production", { workers: option("-w", "--workers", integer({ min: 1 })), configFile: option("-c", "--config", string()) }) ); const applicationConfig = or(devMode, prodMode); ``` ### Labeled merge groups *This feature is available since Optique 0.4.0.* Like `object()`, the `merge()` parser can accept an optional label as its first parameter. This label appears in help text to organize the combined options into a logical group, making documentation clearer when merging parsers that don't already have their own labels: ```typescript twoslash import { merge, object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string, integer, choice } from "@optique/core/valueparser"; // Define simple option groups without labels const connectionOptions = object({ host: option("--host", string()), port: option("--port", integer()) }); const performanceOptions = object({ workers: option("-w", "--workers", integer({ min: 1 })), timeout: option("-t", "--timeout", integer()), cache: option("--cache", choice(["none", "memory", "disk"])) }); // Combine with a label for organized help text const serverConfig = merge( "Server Configuration", // Label for the merged group connectionOptions, performanceOptions ); // The label "Server Configuration" will appear in help text, // grouping all options from both parsers under this section ``` This is particularly useful when combining parsers from different modules or when the constituent parsers don't have their own labels. It ensures that the merged options appear as a cohesive group in help documentation rather than scattered individual options. ### Advanced parser combinations *This feature is available since Optique 0.3.0.* The `merge()` parser can now combine various types of object-generating parsers, not just `object()` parsers. This enables sophisticated patterns like dependent options and conditional configurations: ```typescript twoslash import { merge, object } from "@optique/core/constructs"; import { map, withDefault } from "@optique/core/modifiers"; import type { InferValue } from "@optique/core/parser"; import { flag, option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; import { run } from "@optique/run"; // Dependent options pattern: options that are only available when a flag is set const dependentOptions = withDefault( object({ feature: flag("-f", "--feature"), config: option("-c", "--config", string()), level: option("-l", "--level", integer()) }), { feature: false as const } as const ); // Transform parser results const transformedConfig = map( object({ host: option("--host", string()), port: option("--port", integer()) }), ({ host, port }) => ({ endpoint: `${host}:${port}` }) ); // Combine different parser types const advancedParser = merge( dependentOptions, // withDefault() parser transformedConfig, // map() result object({ // traditional object() parser verbose: option("-v", "--verbose") }) ); type Result = InferValue; // ^? const result: Result = run(advancedParser); ``` ### Dependent options pattern A common use case is creating options that are only relevant when certain conditions are met: ```typescript twoslash import { merge, object } from "@optique/core/constructs"; import { withDefault } from "@optique/core/modifiers"; import { flag, option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; // ---cut-before--- const serverMode = withDefault( object({ server: flag("-s", "--server"), port: option("-p", "--port", integer()), host: option("-h", "--host", string()), workers: option("-w", "--workers", integer()) }), { server: false as const } as const ); const globalOptions = object({ verbose: option("-v", "--verbose"), config: option("-c", "--config", string()) }); const appConfig = merge(serverMode, globalOptions); // Usage examples: // myapp -v -c config.json → server mode disabled // myapp -s -p 8080 -h localhost -v → server mode with port and host // myapp -s -p 3000 -w 4 -c prod.json → full server configuration ``` ### Type inference and merging The `merge()` parser intelligently combines the types of all merged parsers, regardless of their original parser type: ```typescript twoslash import { merge, object } from "@optique/core/constructs"; import type { InferValue } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { integer } from "@optique/core/valueparser"; // ---cut-before--- const basicOptions = object({ verbose: option("-v", "--verbose"), quiet: option("-q", "--quiet") }); const advancedOptions = object({ timeout: option("--timeout", integer()), retries: option("--retries", integer()) }); const allOptions = merge(basicOptions, advancedOptions); type Options = InferValue; // ^? // Type automatically inferred as above. ``` When combining different parser types, the merge result maintains full type safety while accounting for the unique characteristics of each parser: ```typescript twoslash import { merge, object } from "@optique/core/constructs"; import { map, withDefault } from "@optique/core/modifiers"; import type { InferValue } from "@optique/core/parser"; import { flag, option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; // ---cut-before--- const conditionalFeatures = withDefault( object({ experimental: flag("--experimental"), debugLevel: option("--debug-level", integer()) }), { experimental: false as const } as const ); const transformedSettings = map( object({ theme: option("--theme", string()), lang: option("--language", string()) }), ({ theme, lang }) => ({ locale: `${lang}_${theme.toUpperCase()}`, settings: { theme, lang } }) ); const complexConfig = merge( conditionalFeatures, transformedSettings, object({ version: option("--version", string()) }) ); type ComplexConfig = InferValue; // ^? // Type automatically inferred with conditional fields and transformations. ``` ### Optional parser groups *This feature is available since Optique 0.9.0.* The `merge()` parser supports `optional()` wrappers around object-generating parsers. This allows you to define groups of mutually exclusive options that may or may not be provided: ```typescript twoslash import { merge, object, or } from "@optique/core/constructs"; import { map, multiple, optional } from "@optique/core/modifiers"; import { argument, flag } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; // Mutually exclusive verbosity flags wrapped in optional() const verbosityOptions = optional( or( object({ verbosity: optional( map(multiple(flag("--verbose", "-v")), (v) => v.length), ), }), object({ verbosity: optional(map(flag("--quiet", "-q"), () => 0)), }), ), ); // Combine with required positional argument const parser = merge( verbosityOptions, object({ file: argument(string()) }), ); // Usage examples: // myapp file.txt → { file: "file.txt" } // myapp --verbose file.txt → { verbosity: 1, file: "file.txt" } // myapp -v -v -v file.txt → { verbosity: 3, file: "file.txt" } // myapp --quiet file.txt → { verbosity: 0, file: "file.txt" } ``` When using `optional()` in `merge()`: * If the inner parser matches, its result is merged into the final object * If the inner parser doesn't match (no input consumed), parsing continues with the next parser in the merge * The state from matched parsers is preserved across parse iterations The same behavior applies to `withDefault()`, which provides a fallback value when the inner parser doesn't match. This pattern is particularly useful when you have optional feature groups that should not interfere with other required arguments: ```typescript twoslash import { merge, object, or } from "@optique/core/constructs"; import { map, optional } from "@optique/core/modifiers"; import { argument, constant, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; // Optional output format selection const outputOptions = optional( or( object({ format: map(option("--json", string()), () => "json" as const), pretty: constant(undefined), }), object({ format: constant("text" as const), pretty: option("--pretty"), }), ), ); // Main command structure const command = merge( outputOptions, object({ input: argument(string({ metavar: "FILE" })), }), ); // Usage: // myapp data.txt → { input: "data.txt" } // myapp --json schema data.txt → { format: "json", input: "data.txt" } // myapp --pretty data.txt → { format: "text", pretty: true, input: "data.txt" } ``` ## `concat()` parser *This API is available since Optique 0.2.0.* The `concat()` parser combines multiple `tuple()` parsers into a single unified parser, enabling modular tuple design through reusable tuple groups. This is the tuple equivalent of `merge()` for objects, providing compositional flexibility for sequential, positional argument structures. > \[!IMPORTANT] > TypeScript overload inference for `concat()` supports up to 15 parser > arguments. Calls beyond 15 now fail at compile time with an actionable > message. For 16+ parsers, split into nested groups so each `concat()` call > stays at 15 or fewer arguments. ```typescript twoslash import { concat, tuple } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; // Define reusable tuple groups const basicFlags = tuple([ option("-v", "--verbose"), option("-q", "--quiet"), ] as const); const serverConfig = tuple([ option("-p", "--port", integer({ min: 1, max: 0xffff })), option("-h", "--host", string()), ] as const); const logConfig = tuple([ option("--log-level", string()), option("--log-file", string()), ] as const); // Combine tuples for different application modes const webServer = concat(basicFlags, serverConfig); const fullServer = concat(basicFlags, serverConfig, logConfig); const result = parse(fullServer, [ "-v", "-p", "8080", "-h", "localhost", "--log-level", "info", "--log-file", "app.log" ]); if (result.success) { const [verbose, quiet, port, host, logLevel, logFile] = result.value; console.log(`Server: ${host}:${port}, verbose: ${verbose}.`); console.log(`Logging: ${logLevel} to ${logFile}.`); } ``` ### Type concatenation The `concat()` parser intelligently flattens the types of all concatenated tuple parsers into a single tuple type: ```typescript twoslash import { concat, tuple } from "@optique/core/constructs"; import type { InferValue } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; // ---cut-before--- const userInfo = tuple([ option("-n", "--name", string()), option("-a", "--age", integer()), ] as const); const preferences = tuple([ option("-t", "--theme", string()), option("-v", "--verbose"), ] as const); const combined = concat(userInfo, preferences); type CombinedType = InferValue; // ^? // Type automatically inferred as above. ``` ### Usage patterns Concatenation is useful when you need: * Modular tuple construction from reusable components * Sequential argument processing with clear grouping * Building complex CLIs from simpler tuple building blocks * Maintaining positional semantics across grouped options ```typescript twoslash import { concat, tuple } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; import { argument, option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; // ---cut-before--- // Build authentication tuple const authTuple = tuple([ option("-u", "--user", string()), option("-p", "--pass", string()), ] as const); // Build connection tuple const connectionTuple = tuple([ option("--host", string()), option("--port", integer()), option("--ssl"), ] as const); // Build command arguments tuple const commandTuple = tuple([ argument(string()), // command name argument(string()), // target file ] as const); // Combine all for a database client const dbClient = concat(authTuple, connectionTuple, commandTuple); // Usage: myapp -u admin -p secret --host db.example.com --port 5432 --ssl backup users.sql const config = parse(dbClient, [ "-u", "admin", "-p", "secret", "--host", "db.example.com", "--port", "5432", "--ssl", "backup", "users.sql" ]); if (config.success) { const [user, pass, host, port, ssl, command, file] = config.value; console.log(`Connecting as ${user} to ${host}:${port}.`); console.log(`Running ${command} on ${file}.`); } ``` ### Relationship to `merge()` While `merge()` combines object parsers by merging their properties, `concat()` combines tuple parsers by flattening their positional elements: | Combinator | Input parsers | Result type | | ---------- | ------------------ | --------------------------------- | | `merge()` | `object()` parsers | Merged object with all properties | | `concat()` | `tuple()` parsers | Flattened tuple with all elements | Both provide compositional design patterns but serve different structural needs in CLI applications. ## `longestMatch()` parser *This API is available since Optique 0.3.0.* The `longestMatch()` parser combines multiple mutually exclusive parsers by selecting the parser that consumes the most input tokens. Unlike `or()` which returns the first successful match, `longestMatch()` tries all parsers and selects the one with the longest match. This enables context-aware parsing where more specific patterns take precedence over general ones. > \[!IMPORTANT] > TypeScript overload inference for `longestMatch()` supports up to 15 parser > arguments. Calls beyond 15 now fail at compile time with an actionable > message. For 16+ parsers, split into nested groups so each > `longestMatch()` call stays at 15 or fewer arguments. ```typescript twoslash import { longestMatch, object } from "@optique/core/constructs"; import type { InferValue } from "@optique/core/parser"; import { argument, constant, flag } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const globalHelp = object({ type: constant("global"), help: flag("--help"), }); const contextualHelp = object({ type: constant("contextual"), command: argument(string({ metavar: "COMMAND" })), help: flag("--help"), }); const parser = longestMatch(globalHelp, contextualHelp); type Result = InferValue; // ^? // Usage examples: // myapp --help → globalHelp (1 token: --help) // myapp list --help → contextualHelp (2 tokens: list --help) ``` ### Longest match selection The key behavior of `longestMatch()` is selecting the parser that consumes the most tokens from the input: ```typescript twoslash import { longestMatch, object } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; import { argument, constant, flag } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; // ---cut-before--- const shortPattern = object({ type: constant("short"), help: flag("--help"), }); const longPattern = object({ type: constant("long"), command: argument(string()), help: flag("--help"), }); const parser = longestMatch(shortPattern, longPattern); // Short pattern matches: consumes 1 token (--help) const shortResult = parse(parser, ["--help"]); if (shortResult.success) { console.log(shortResult.value.type); // "short" } // Long pattern matches: consumes 2 tokens (list --help) const longResult = parse(parser, ["list", "--help"]); if (longResult.success) { console.log(longResult.value.type); // "long" } ``` ### Context-aware help systems The most common use case for `longestMatch()` is implementing context-aware help systems where `command --help` shows help for that specific command: ```typescript twoslash import { longestMatch, object, or } from "@optique/core/constructs"; import { multiple, optional } from "@optique/core/modifiers"; import { argument, command, constant, flag, option, } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; // ---cut-before--- // Define your application commands const addCommand = command( "add", object({ action: constant("add"), key: argument(string({ metavar: "KEY" })), value: argument(string({ metavar: "VALUE" })), }), ); const listCommand = command( "list", object({ action: constant("list"), pattern: optional( option("-p", "--pattern", string({ metavar: "PATTERN" })), ), }), ); // Normal command parsing const normalParser = object({ help: constant(false), result: or(addCommand, listCommand), }); // Context-aware help parsing const contextualHelpParser = object({ help: constant(true), commands: multiple(argument(string({ metavar: "COMMAND" }))), flag: flag("--help"), }); // Combine with longestMatch for intelligent help selection const parser = longestMatch(normalParser, contextualHelpParser); // Normal usage works as expected: // myapp add key1 value1 → normalParser (3 tokens) // myapp list -p "*.txt" → normalParser (3 tokens) // Context-aware help automatically activates: // myapp list --help → contextualHelpParser (2 tokens: ["list"]) // myapp add --help → contextualHelpParser (2 tokens: ["add"]) ``` ### Type inference and unions Like `or()`, `longestMatch()` creates discriminated union types when used with parsers that produce different shaped objects: ```typescript twoslash import { longestMatch, object } from "@optique/core/constructs"; import { type InferValue, parse } from "@optique/core/parser"; import { constant, option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; // ---cut-before--- const stringMode = object({ mode: constant("string" as const), value: option("-s", "--string", string()), }); const numberMode = object({ mode: constant("number" as const), value: option("-n", "--number", integer()), }); const parser = longestMatch(stringMode, numberMode); type Config = InferValue; // ^? // Type-safe pattern matching const result = parse(parser, ["-s", "hello"]); if (result.success) { switch (result.value.mode) { case "string": // TypeScript knows this is string config console.log(`String value: ${result.value.value}.`); break; case "number": // TypeScript knows this is number config console.log(`Number value: ${result.value.value}.`); break; } } ``` ### Allowed duplicates in `longestMatch()` Like `or()`, `longestMatch()` allows duplicate option names across branches. This is intentional: the branches are still mutually exclusive, and only the selected branch contributes to the final result. When multiple branches share the same option names, `longestMatch()` selects the branch that consumes the most input tokens. If two branches consume the same number of tokens, the first matching branch wins. ```typescript twoslash import { longestMatch, object } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; import { constant, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; // ---cut-before--- const parser = longestMatch( object({ mode: constant("basic" as const), format: option("--format", string()), }), object({ mode: constant("advanced" as const), format: option("--format", string()), profile: option("--profile", string()), }), ); const result = parse(parser, [ "--format", "json", "--profile", "prod", ]); // Chooses the second branch because it consumes more tokens. // If both branches consumed the same number of tokens, // the first matching branch would win. ``` ### Usage patterns and best practices Use `longestMatch()` when you need: * *Context-aware behavior*: Different parsing based on how much input is consumed * *Precedence by specificity*: More specific patterns should win over general ones * *Greedy matching*: Always prefer the parser that consumes the most tokens * *Help system integration*: Context-sensitive help that shows relevant information ```typescript twoslash import { longestMatch, object, or } from "@optique/core/constructs"; import { multiple, optional } from "@optique/core/modifiers"; import { argument, command, constant, flag, option, } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; // ---cut-before--- // Example: Git-like CLI with context-aware help const gitAdd = command("add", object({ action: constant("add"), files: multiple(argument(string())), all: optional(option("-A", "--all")), })); const gitCommit = command("commit", object({ action: constant("commit"), message: option("-m", "--message", string()), })); const normalCommands = object({ help: constant(false), command: or(gitAdd, gitCommit), }); const helpParser = object({ help: constant(true), commands: multiple(argument(string())), helpFlag: flag("--help"), }); const cli = longestMatch(normalCommands, helpParser); // Usage patterns: // git add file.txt --all → normalCommands (gitAdd) // git commit -m "message" → normalCommands (gitCommit) // git add --help → helpParser (commands: ["add"]) // git commit --help → helpParser (commands: ["commit"]) // git --help → helpParser (commands: []) ``` ### Relationship to other combinators | Combinator | Selection strategy | Use case | | ---------------- | ---------------------- | -------------------------------- | | `or()` | First successful match | Mutually exclusive alternatives | | `longestMatch()` | Most tokens consumed | Context-aware and greedy parsing | | `merge()` | Combines all parsers | Composing complementary parsers | The `longestMatch()` combinator bridges the gap between simple alternatives (`or()`) and complex composition (`merge()`) by providing intelligent selection based on input consumption. ## `conditional()` parser *This API is available since Optique 0.8.0.* The `conditional()` parser creates a discriminated union based on a discriminator option value. This enables context-dependent parsing where certain options are only valid when a specific discriminator value is selected. ```typescript twoslash import { conditional, object } from "@optique/core/constructs"; import type { InferValue } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { choice, string } from "@optique/core/valueparser"; const parser = conditional( option("--reporter", choice(["console", "junit", "html"])), { console: object({}), junit: object({ outputFile: option("--output-file", string()) }), html: object({ outputFile: option("--output-file", string()) }), } ); type Result = InferValue; // ^? // Type automatically inferred as discriminated tuple union. ``` ### Discriminator-based branching The key behavior of `conditional()` is selecting the appropriate branch parser based on the discriminator value. The result is a tuple containing the discriminator value and the branch result: ```typescript twoslash import { conditional, object } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { choice, string } from "@optique/core/valueparser"; // ---cut-before--- const parser = conditional( option("--format", choice(["json", "xml"])), { json: object({ pretty: option("--pretty") }), xml: object({ indent: option("--indent", string()) }), } ); // When --format json is provided const jsonResult = parse(parser, ["--format", "json", "--pretty"]); if (jsonResult.success) { const [format, options] = jsonResult.value; if (format === "json") { // TypeScript knows options is { pretty: boolean } console.log(`Format: ${format}, Pretty: ${options.pretty}.`); } } // When --format xml is provided const xmlResult = parse(parser, ["--format", "xml", "--indent", " "]); if (xmlResult.success) { const [format, options] = xmlResult.value; if (format === "xml") { // TypeScript knows options is { indent: string } console.log(`Format: ${format}, Indent: "${options.indent}".`); } } ``` ### Default branch When a default branch is provided, the discriminator becomes optional. If the discriminator is not specified, the default branch is used: ```typescript twoslash import { conditional, object } from "@optique/core/constructs"; import type { InferValue } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { choice, string } from "@optique/core/valueparser"; // ---cut-before--- const parser = conditional( option("--reporter", choice(["junit", "html"])), { junit: object({ outputFile: option("--output-file", string()) }), html: object({ outputFile: option("--output-file", string()) }), }, object({ format: option("--format", string()) }) // Default branch ); type Result = InferValue; // Default branch result has undefined as discriminator value. ``` When the discriminator is not provided, the result tuple has `undefined` as the first element: ```typescript twoslash import { conditional, object } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { choice, string } from "@optique/core/valueparser"; const parser = conditional( option("--reporter", choice(["junit", "html"])), { junit: object({ outputFile: option("--output-file", string()) }), html: object({ outputFile: option("--output-file", string()) }), }, object({ format: option("--format", string()) }) ); // ---cut-before--- // Without --reporter, default branch is used const result = parse(parser, ["--format", "text"]); if (result.success) { const [reporter, options] = result.value; if (reporter === undefined) { // Default branch - options is { format: string } console.log(`Using default format: ${options.format}.`); } } ``` ### Required discriminator (no default) When no default branch is provided, the discriminator is required. If it's missing, an error is returned: ```typescript twoslash import { conditional, object } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { choice, string } from "@optique/core/valueparser"; // ---cut-before--- const parser = conditional( option("--reporter", choice(["junit", "html"])), { junit: object({ outputFile: option("--output-file", string()) }), html: object({ outputFile: option("--output-file", string()) }), } // No default branch - discriminator is required ); const result = parse(parser, ["--output-file", "report.xml"]); // Error: Missing required option --reporter. ``` ### Type-safe pattern matching The `conditional()` parser enables type-safe pattern matching based on the discriminator value: ```typescript twoslash import { conditional, object } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { choice, string, integer } from "@optique/core/valueparser"; const parser = conditional( option("--db", choice(["sqlite", "postgres", "mysql"])), { sqlite: object({ file: option("--file", string()) }), postgres: object({ host: option("--host", string()), port: option("--port", integer()), }), mysql: object({ host: option("--host", string()), port: option("--port", integer()), }), } ); // ---cut-before--- const result = parse(parser, ["--db", "postgres", "--host", "localhost", "--port", "5432"]); if (result.success) { const [db, config] = result.value; if (db === "sqlite") { // TypeScript knows config is { file: string } console.log(`SQLite database file: ${config.file}.`); } else if (db === "postgres") { // TypeScript knows config is { host: string, port: number } console.log(`PostgreSQL server at ${config.host}:${config.port}.`); } else if (db === "mysql") { // TypeScript knows config is { host: string, port: number } console.log(`MySQL server at ${config.host}:${config.port}.`); } } ``` ### Relationship to `or()` The `conditional()` parser provides a more structured alternative to `or()` for discriminated union patterns: | Feature | `or()` | `conditional()` | | ---------------- | ------------------------ | ----------------------------------- | | Discriminator | Manual with `constant()` | Explicit discriminator option | | Branch selection | First matching parser | Based on discriminator value | | Result type | Union of branch types | Tuple `[discriminator, branchType]` | | Default handling | Via parser ordering | Explicit default branch | | Type narrowing | Via discriminator field | Via tuple first element | Use `conditional()` when you have an explicit discriminator option that determines which set of options is valid. Use `or()` for more general mutually exclusive alternatives. ### Speculative branch parsing When the discriminator is an async parser that succeeds without consuming input (e.g., `prompt(option(...))` with no CLI input), branch selection is normally deferred to the complete phase. To allow branch-specific tokens to be consumed, `conditional()` speculatively tries all named branches during parse. If exactly one branch can consume tokens, it is tentatively selected and verified against the resolved discriminator during the complete phase. If the discriminator resolves to a different branch than the one that consumed tokens (contradictory input), the parse fails. When multiple branches can consume the same tokens (ambiguous), speculation is skipped entirely to keep branch selection order-independent. Speculation works best when named branches have distinct leading options (e.g., `--threads` vs `--timeout`). When a default branch accepts the same tokens as a named branch, or when the parser is nested inside `longestMatch()`, the speculative choice may conflict with the discriminator. To avoid this, ensure named branch options are distinct from the default branch, or wrap the discriminator with `withDefault()` or `bindEnv()` so it can resolve synchronously. ## `group()` parser *This API is available since Optique 0.4.0.* The `group()` parser is a documentation-only wrapper that applies a group label to any parser for help text organization. This allows you to maintain clean code structure with combinators like `or()`, `flag()`, or `multiple()` while providing well-organized help output through group labeling. Unlike `merge()` and `object()` which have built-in label support, many parsers don't natively support labeling. The `group()` parser fills this gap by wrapping any parser with a labeled section that appears in help documentation. ```typescript twoslash import { group, or } from "@optique/core/constructs"; import { map, multiple } from "@optique/core/modifiers"; import { argument, flag } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; // Group mutually exclusive output format options const outputFormat = group( "Output Format", or( map(flag("--json"), () => "json" as const), map(flag("--yaml"), () => "yaml" as const), map(flag("--xml"), () => "xml" as const), ), ); // Group multiple file inputs const inputFiles = group( "Input Files", multiple(argument(string({ metavar: "FILE" })), { min: 1 }), ); // The labels "Output Format" and "Input Files" will appear as // section headers in help text, organizing related options ``` ### Documentation-only wrapper The `group()` parser has identical parsing behavior to its wrapped parser. All parsing operations, state management, and type information are preserved unchanged: ```typescript twoslash import { group, or } from "@optique/core/constructs"; import { map } from "@optique/core/modifiers"; import { type InferValue, parse } from "@optique/core/parser"; import { flag } from "@optique/core/primitives"; const formatParser = or( map(flag("--json"), () => "json" as const), map(flag("--yaml"), () => "yaml" as const), ); const groupedParser = group("Format Options", formatParser); // Type inference is preserved type FormatType = InferValue; // ^? // Parsing behavior is identical const result1 = parse(formatParser, ["--json"]); const result2 = parse(groupedParser, ["--json"]); if (result1.success && result2.success) { // Both produce the same result console.assert(result1.value === result2.value); // "json" } ``` ### Realistic usage patterns The `group()` parser is most useful with parsers that don't have built-in labeling support: ```typescript twoslash import { group, or, object } from "@optique/core/constructs"; import { map, multiple, optional, withDefault } from "@optique/core/modifiers"; import { argument, flag, option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; // ---cut-before--- // Logging level selection (mutually exclusive) const loggingOptions = group( "Logging Options", or( map(flag("--debug"), () => "debug" as const), map(flag("--verbose"), () => "verbose" as const), map(flag("--quiet"), () => "quiet" as const), ), ); // Multiple input files const inputSources = group( "Input Sources", multiple(argument(string({ metavar: "FILE" })), { min: 1 }), ); // Optional output configuration const outputConfig = group( "Output Configuration", optional(option("--output", string({ metavar: "PATH" }))), ); // Single debug flag const debugMode = group( "Debug Mode", flag("--debug-mode"), ); // Default server configuration const serverSettings = group( "Server Configuration", withDefault( object({ port: option("--port", integer()), host: option("--host", string()), }), { port: 3000, host: "localhost" }, ), ); ``` ### Help text organization The primary benefit of `group()` is organizing help output into logical sections: ```ansi Usage: myapp (--debug | --verbose | --quiet) [--output PATH] --debug-mode [--port INTEGER --host STRING] FILE... Logging Options: --debug --verbose --quiet Output Configuration: --output PATH Debug Mode: --debug-mode Server Configuration: --port INTEGER --host STRING Input Sources: FILE ``` Without `group()`, these options would appear as a flat list without clear organization, making it harder for users to understand the relationship between related options. ### Nested groups The `group()` parser supports nesting, allowing you to create hierarchical documentation structures: ```typescript twoslash import { group, object } from "@optique/core/constructs"; import { flag, option } from "@optique/core/primitives"; import { integer } from "@optique/core/valueparser"; // ---cut-before--- const debugOptions = object({ verbose: flag("--verbose"), trace: flag("--trace"), }); const serverOptions = object({ port: option("--port", integer()), workers: option("--workers", integer()), }); // First level grouping const innerDebugGroup = group("Debug Options", debugOptions); const innerServerGroup = group("Server Options", serverOptions); // Second level grouping const applicationConfig = group("Application Configuration", object({ debug: innerDebugGroup, server: innerServerGroup, })); // This creates a nested structure in help documentation ``` ### Best practices Use `group()` when you need: * *Section organization*: Grouping related options under meaningful headers * *Parser flexibility*: Labeling parsers that don't have built-in label support * *Help text clarity*: Making complex CLIs more user-friendly * *Clean code structure*: Maintaining modular parser composition ```typescript twoslash import { group, object, or } from "@optique/core/constructs"; import { map, multiple, withDefault } from "@optique/core/modifiers"; import { argument, flag, option } from "@optique/core/primitives"; import { string, integer, choice } from "@optique/core/valueparser"; // ---cut-before--- // Example: File processing tool with organized help const processingMode = group( "Processing Mode", or( map(flag("--compress"), () => "compress" as const), map(flag("--extract"), () => "extract" as const), map(flag("--list"), () => "list" as const), ), ); const compressionSettings = group( "Compression Settings", object({ level: withDefault( option("--level", integer({ min: 1, max: 9 })), 6, ), algorithm: withDefault( option("--algorithm", choice(["gzip", "bzip2", "xz"])), "gzip", ), }), ); const inputOutput = group( "Input/Output", object({ input: multiple(argument(string({ metavar: "INPUT_FILE" })), { min: 1 }), output: option("-o", "--output", string({ metavar: "OUTPUT_FILE" })), }), ); // Each group appears as a distinct section in help text, // making the CLI interface much more approachable ``` ## Duplicate option detection Optique automatically detects and prevents duplicate option names within parser combinators to avoid ambiguous behavior. When the same option name appears in multiple fields or parsers, parser construction throws an error before parsing begins. This check includes `hidden: true` options because hidden options still consume the same command-line syntax. ### Detected duplicates The `object()`, `tuple()`, and `merge()` combinators validate that option names are unique across their child parsers: ```typescript twoslash import { object, option } from "@optique/core"; // ---cut-before--- // ❌ This throws while constructing the parser const parser = object({ verbose: option("-v", "--verbose"), version: option("-v", "--version"), // Duplicate: -v }); // Throws DuplicateOptionError because both fields use -v. ``` This applies to nested structures as well: ```typescript twoslash import { object, option } from "@optique/core"; // ---cut-before--- // ❌ Nested duplicate detected const parser = object({ opts: object({ verbose: option("-v"), }), flags: object({ version: option("-v"), // Duplicate across nested objects }), }); ``` Hidden options are checked the same way: ```typescript twoslash import { object, option } from "@optique/core"; import { string } from "@optique/core/valueparser"; // ---cut-before--- const parser = object({ legacy: option("--output", string(), { hidden: true }), current: option("--output", string()), }); // Throws DuplicateOptionError because both fields use --output. // Hidden options still participate in parsing. ``` ### Allowed duplicates in `or()` The `or()` combinator allows duplicate option names because branches are mutually exclusive—only one branch can match: ```typescript twoslash import { or, option } from "@optique/core"; import { parse } from "@optique/core/parser"; // ---cut-before--- // ✅ This is valid - branches are mutually exclusive const parser = or( option("-v", "--verbose"), option("-v", "--version"), ); const result = parse(parser, ["-v"]); // First matching branch wins ``` ### Opting out with `allowDuplicates` For advanced use cases, you can disable duplicate detection using the `allowDuplicates` option: ```typescript twoslash import { object, option } from "@optique/core"; import { parse } from "@optique/core/parser"; // ---cut-before--- const parser = object({ verbose: option("-v", "--verbose"), version: option("-v", "--version"), }, { allowDuplicates: true }); const result = parse(parser, ["-v"]); // Succeeds - first parser wins ``` > \[!CAUTION] > Using `allowDuplicates` can lead to unpredictable behavior where the first > matching parser consumes the option and subsequent parsers receive their > default values. Only use this option when you fully understand the > implications. --- --- url: /integrations/env.md description: >- Bind parser fields to environment variables with type-safe parsing and fallback behavior. --- # Environment variable support *This API is available since Optique 1.0.0.* The *@optique/env* package lets you bind parser values to environment variables while preserving Optique's type safety and parser composition model. The fallback priority is: 1. CLI argument 2. Environment variable 3. *.env* file value 4. Default value 5. Error ::: code-group ```bash [Deno] deno add jsr:@optique/env ``` ```bash [npm] npm add @optique/env ``` ```bash [pnpm] pnpm add @optique/env ``` ```bash [Yarn] yarn add @optique/env ``` ```bash [Bun] bun add @optique/env ``` ::: ## Basic usage ### 1. Create an environment context ```typescript twoslash import { createEnvContext } from "@optique/env"; const envContext = createEnvContext({ prefix: "MYAPP_", }); ``` You can also provide a custom source function for tests or custom runtimes: ```typescript twoslash import { createEnvContext } from "@optique/env"; const mockEnv: Record = { MYAPP_HOST: "test.example.com", }; const envContext = createEnvContext({ prefix: "MYAPP_", source: (key) => mockEnv[key], }); ``` To load *.env* files as an internal fallback layer, pass `envFile`: ```typescript twoslash import { createEnvContext } from "@optique/env"; const envContext = createEnvContext({ prefix: "MYAPP_", envFile: [".env", ".env.local"], }); ``` Values from *.env* files are read by `bindEnv()` but are not written to `process.env` or `Deno.env`. Real environment variables still take priority over file values. ### 2. Bind parsers to environment keys ```typescript twoslash import { bindEnv, bool, createEnvContext } from "@optique/env"; import { option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; const envContext = createEnvContext({ prefix: "MYAPP_" }); const host = bindEnv(option("--host", string()), { context: envContext, key: "HOST", parser: string(), default: "localhost", }); const port = bindEnv(option("--port", integer()), { context: envContext, key: "PORT", parser: integer(), default: 3000, }); const verbose = bindEnv(option("--verbose"), { context: envContext, key: "VERBOSE", parser: bool(), default: false, }); ``` ### 3. Run with contexts Use `run()`, `runSync()`, or `runAsync()` from *@optique/run* with `contexts: [envContext]`. > \[!WARNING] > `bindEnv()` only reads environment variables when its context is registered > with the runner. If you omit `contexts: [envContext]` from the `run()` call, > the env lookup is silently skipped and the parser falls back to the default > or fails with an error indicating the context was not registered. ```typescript twoslash import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; import { bindEnv, bool, createEnvContext } from "@optique/env"; import { runAsync } from "@optique/run"; const envContext = createEnvContext({ prefix: "MYAPP_" }); const parser = object({ host: bindEnv(option("--host", string()), { context: envContext, key: "HOST", parser: string(), default: "localhost", }), port: bindEnv(option("--port", integer()), { context: envContext, key: "PORT", parser: integer(), default: 3000, }), verbose: bindEnv(option("--verbose"), { context: envContext, key: "VERBOSE", parser: bool(), default: false, }), }); const result = await runAsync(parser, { contexts: [envContext], }); ``` ## Boolean values `bool()` parses common environment Boolean literals (case-insensitive): * true values: `"true"`, `"1"`, `"yes"`, `"on"` * false values: `"false"`, `"0"`, `"no"`, `"off"` ```typescript twoslash import { bool } from "@optique/env"; const parser = bool(); ``` ## *.env* files `envFile` accepts `true`, a path string, an array of paths, or an options object: ```typescript twoslash import { createEnvContext } from "@optique/env"; const envContext = createEnvContext({ prefix: "MYAPP_", envFile: { paths: [".env", ".env.local"], substitute: (command) => command === "whoami" ? "developer" : undefined, }, }); ``` Passing `true` loads only *.env* from the current working directory. When several paths are provided, files are loaded in order and later files override earlier file values. Missing files are skipped. The parser supports common dotenv syntax: * blank lines and comments * optional `export` prefixes * `KEY=VALUE` assignments * single-quoted, double-quoted, and unquoted values * multiline quoted values * inline comments after unquoted values * `$VAR` and `${VAR}` expansion Single-quoted values are literal. Optique does not expand variables, perform command substitution, or interpret escape sequences inside single quotes. Double-quoted and unquoted values expand variables in a single left-to-right pass. Expansion reads from the configured `source` first, then from values already loaded from *.env* files. Missing variables expand to the empty string. Optique recognizes `$(...)` and backtick command-substitution forms, but it never executes commands by itself. If `substitute` is provided, Optique passes the command text to that hook and inserts the returned string. If the hook is absent or returns `undefined`, the substitution becomes the empty string. Encrypted dotenvx values are not decrypted; they are treated as ordinary string values. ## Env-only values If a value should come only from environment (or default), pair `bindEnv()` with `fail()`: ```typescript twoslash import { bindEnv, createEnvContext } from "@optique/env"; import { fail } from "@optique/core/primitives"; import { integer } from "@optique/core/valueparser"; const envContext = createEnvContext({ prefix: "MYAPP_" }); const timeout = bindEnv(fail(), { context: envContext, key: "TIMEOUT", parser: integer(), default: 30, }); ``` ## Composing with other contexts Environment context is a regular `SourceContext`, so it composes naturally with configuration contexts. The *outermost* wrapper is checked first during completion, so nesting order determines fallback priority. Wrapping as `bindEnv(bindConfig(option(...)))` gives: CLI argument > Environment variable > Config file > Default value ```typescript twoslash import { z } from "zod"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { createConfigContext, bindConfig } from "@optique/config"; import { bindEnv, createEnvContext } from "@optique/env"; import { runAsync } from "@optique/run"; const envContext = createEnvContext({ prefix: "MYAPP_" }); const configContext = createConfigContext({ schema: z.object({ host: z.string() }), }); const parser = object({ config: option("--config", string()), host: bindEnv( bindConfig(option("--host", string()), { context: configContext, key: "host", default: "localhost", }), { context: envContext, key: "HOST", parser: string(), }, ), }); await runAsync(parser, { contexts: [envContext, configContext], contextOptions: { getConfigPath: (parsed) => parsed.config, }, }); ``` ## Prefix and key resolution When `bindEnv()` looks up an environment variable, it concatenates the context's `prefix` with the `key` you pass. For example: | `prefix` | `key` | Looked-up variable | | ---------- | ---------- | ------------------ | | `"MYAPP_"` | `"HOST"` | `MYAPP_HOST` | | `"MYAPP_"` | `"PORT"` | `MYAPP_PORT` | | `""` | `"EDITOR"` | `EDITOR` | If you omit `prefix` (or pass `""`), the key is used as-is. This is useful when binding to well-known variables like `EDITOR` or `HOME` that have no application-specific prefix: ```typescript twoslash import { bindEnv, createEnvContext } from "@optique/env"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const envContext = createEnvContext(); // no prefix const editor = bindEnv(option("--editor", string()), { context: envContext, key: "EDITOR", parser: string(), default: "vi", }); ``` ## Using other value parsers The `parser` option in `bindEnv()` accepts any Optique `ValueParser`. Because environment variables are always strings, the value parser converts the raw string into the target type. All built-in value parsers from *@optique/core* work here: ```typescript twoslash import { bindEnv, createEnvContext } from "@optique/env"; import { option } from "@optique/core/primitives"; import { port, url, string } from "@optique/core/valueparser"; const envContext = createEnvContext({ prefix: "MYAPP_" }); // Parse as a URL const apiUrl = bindEnv(option("--api-url", url()), { context: envContext, key: "API_URL", parser: url(), }); // Parse as a port number (validated range 0–65535) const listenPort = bindEnv(option("--port", port()), { context: envContext, key: "PORT", parser: port(), default: 8080, }); ``` You can also use value parsers from integration packages such as *@optique/zod* or *@optique/valibot* if you need richer validation: ```typescript twoslash import { z } from "zod"; import { bindEnv, createEnvContext } from "@optique/env"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { zod } from "@optique/zod"; const envContext = createEnvContext({ prefix: "MYAPP_" }); const logLevel = bindEnv( option( "--log-level", zod( z.enum(["debug", "info", "warn", "error"]), { placeholder: "debug" }, ), ), { context: envContext, key: "LOG_LEVEL", parser: zod( z.enum(["debug", "info", "warn", "error"]), { placeholder: "debug" }, ), default: "info" as const, }, ); ``` ## Error handling ### Missing environment variable When the environment variable is not set and no `default` is provided, `bindEnv()` falls through to the wrapped parser's `complete()` result. This means the final error message depends on the wrapped parser (or other wrappers such as `bindConfig()`), rather than always being an environment- specific error. If a `default` is provided, the default is used silently. ### Invalid value When the environment variable is set but the value parser rejects it, the error from the value parser propagates directly. For example, if `MYAPP_PORT` is set to `"abc"` and the parser is `integer()`: ```text Expected an integer, but received "abc". ``` Similarly, `bool()` rejects unrecognized literals: ```text Invalid Boolean value: "maybe". Expected one of "true", "1", "yes", "on", "false", "0", "no", or "off" ``` ### Fallback validation Since Optique 1.0.0, fallback values produced by `bindEnv()` are re-validated against the inner CLI parser's constraints (regex patterns, numeric bounds, `choice()` values, etc.). This applies to both environment variable values—which may have been parsed by a looser env-level `parser` option—and to the configured `default`. For example, the following parser rejects the default `"abc"` because it does not match the inner CLI pattern `/^[A-Z]+$/`: ```typescript twoslash import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { bindEnv, createEnvContext } from "@optique/env"; const envContext = createEnvContext({ prefix: "MYAPP_" }); // ---cut-before--- bindEnv(option("--name", string({ pattern: /^[A-Z]+$/ })), { context: envContext, key: "NAME", parser: string(), // looser than the inner parser default: "abc", // rejected at runtime: must match /^[A-Z]+$/ }); ``` Validation is forwarded through standard combinators (`optional()`, `withDefault()`, `group()`, `command()`) and through wrapping `bindEnv()`/`bindConfig()` layers, so a constraint defined on a deeply nested primitive is still enforced against a fallback value. `multiple()` attaches its own `validateValue`: it enforces the configured `min`/`max` arity against the fallback array length and, *if* the inner parser exposes a `validateValue` hook, walks each element through it. Arity enforcement is unconditional—it kicks in even when the inner parser has no `validateValue`—and a non-array fallback (for example a mis-typed default escaped through `as never`) is rejected outright because `multiple()` can never produce a non-array shape from CLI input. `nonEmpty()` is a pure pass-through: it does not add an extra non-empty check on the fallback path. On the CLI path `nonEmpty()` still enforces that at least one token was consumed, but on fallback values `nonEmpty(multiple(...))` delegates entirely to the inner `multiple()`'s arity rules. If you need a “must have at least one element” guarantee against fallback arrays, use `multiple(..., { min: 1 })` directly. `map()`, `derive()`, and `deriveFrom()` intentionally *strip* the inner parser's `validateValue`: the mapping function is one-way, so the mapped output type no longer corresponds to the inner parser's constraints, and derived value parsers rebuild from *default* dependency values rather than the live-resolved ones. Wrapping an inner parser in any of these suppresses revalidation of the wrapped primitive's constraints—but outer combinators layered above (notably `multiple()`) still enforce their own checks. ### Help, version, and completion Like config contexts, environment contexts work seamlessly with help, version, and completion features. Genuine help, version, and completion requests are handled before environment variable lookup, so `--help` still works even when required environment variables are missing, unless the user parser already consumes that same token sequence as ordinary data: ```typescript twoslash import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { bindEnv, createEnvContext } from "@optique/env"; import { runAsync } from "@optique/run"; const envContext = createEnvContext({ prefix: "MYAPP_" }); const parser = object({ apiKey: bindEnv(option("--api-key", string()), { context: envContext, key: "API_KEY", parser: string(), // No default — required from CLI or env }), }); await runAsync(parser, { contexts: [envContext], help: "option", version: "1.0.0", }); ``` ## API reference ### `createEnvContext(options?)` Creates an environment context for use with Optique runners. Parameters : - `options.prefix`: String prefix prepended to all keys when looking up environment variables. Defaults to `""`. \- `options.source`: Custom function `(key: string) => string | undefined` for reading environment values. Defaults to `Deno.env.get` on Deno and `process.env` on Node.js/Bun. \- `options.envFile`: Optional *.env* file fallback layer. Pass `true` to load *.env*, a path string, an array of paths, or an object with `paths` and `substitute`. Returns : `EnvContext` implementing `SourceContext` and `Disposable`. > \[!IMPORTANT] > If you call `envContext.getAnnotations()` manually, pass the returned > object to low-level APIs such as `parse()`, `parseAsync()`, > `parser.complete()`, `suggest()`, or `getDocPage()`. Environment contexts > are single-pass, so calling `getAnnotations()` without a request still > reads the final snapshot. Calling it alone does not affect later parses. ### `bindEnv(parser, options)` Binds a parser to environment variables with fallback priority (CLI > environment > default > error). Fallback values—environment variable values and the configured `default`—are re-validated against the inner CLI parser's constraints, so constraints like `integer({ min })`, `string({ pattern })`, and `choice([...])` cannot be bypassed through an environment variable or default. See *Fallback validation* under “Error handling” for details. Parameters : - `parser`: The inner parser to wrap. \- `options.context`: `EnvContext` to read from. \- `options.key`: Environment variable key *without* the prefix. The actual variable looked up is `prefix + key`. \- `options.parser`: A `ValueParser` used to parse the raw string value from the environment. \- `options.default`: Optional default value used when neither CLI nor environment provides a value. Returns : A new parser with environment fallback behavior. ### `bool(options?)` Creates a synchronous `ValueParser<"sync", boolean>` that accepts common Boolean literals (case-insensitive). Parameters : - `options.metavar`: Metavariable name shown in help text. Defaults to `"BOOLEAN"`. \- `options.errors.invalidFormat`: Custom error message or function for unrecognized input. Returns : `ValueParser<"sync", boolean>` ### `EnvContext` Interface extending `SourceContext` with two additional properties: * `prefix`: The prefix string passed to `createEnvContext()` * `source`: The `EnvSource` function used to read variables ## Limitations * *String-only input* — Environment variables are always strings, so a `parser` is required in every `bindEnv()` call to convert the raw string into the target type. Unlike `bindConfig()`, there is no way to skip the parser. * *Flat keys only* — Environment variables have no native nesting structure. Unlike config files, you cannot use accessor functions to navigate nested objects. Use naming conventions (e.g., `DB_HOST`, `DB_PORT`) to represent structure. * *No schema validation* — Unlike *@optique/config*, there is no schema that validates the set of environment variables as a whole. Each binding is validated independently. * *No automatic dotenv conventions* — `envFile: true` loads only *.env*. Optique does not automatically load *.env.local*, *.env.development*, or framework-specific file sets. * *No built-in command execution* — Command-substitution syntax is recognized only so applications can opt in with `envFile.substitute`. Without that hook, command substitutions become empty strings. * *No dotenvx decryption* — Encrypted values remain ordinary strings. * *Synchronous reads*: `createEnvContext()` reads environment variables and *.env* files synchronously. The context itself does not add async overhead, but if the `parser` used in `bindEnv()` is async, the overall parsing becomes async. > \[!TIP] > See the [cookbook](../cookbook.md#environment-variable-fallbacks) for > additional patterns, including > [combining env with config files](../cookbook.md#combining-with-environment-variables) > and > [env-only values with `fail()`](../cookbook.md#config-only-and-env-only-values-with-fail). --- --- url: /concepts/extend.md --- # Extending Optique with runtime context > \[!NOTE] > Most users do not need this page. If you are using > *[@optique/env](../integrations/env.md)*, > *[@optique/config](../integrations/config.md)*, or > *[@optique/inquirer](../integrations/inquirer.md)*, those packages handle > source context integration for you. This page is for developers who want to > build custom integrations or understand the internals. This guide explains how to extend Optique parsers with runtime context. Optique provides two complementary systems for this purpose: * *Annotations*: A low-level primitive for passing runtime data to parsers * *Source context*: A high-level system for composing multiple data sources with automatic priority handling Together, these systems enable advanced use cases like config file fallbacks, environment variable integration, and shared context across parsers. ## Introduction Optique parsers typically operate in isolation, processing only command-line arguments. However, real-world CLI applications often need access to external runtime data that is not part of the command-line arguments: * *Configuration files*: A parser might fall back to values from a config file whose path is determined by another option * *Environment variables*: Options might have defaults from environment variables (e.g., `MYAPP_HOST` for `--host`) * *Shared context*: Multiple parsers might need access to common runtime data such as user preferences or locale settings The challenge is that this external data often becomes available only during parsing (for example, the config file path is only known after parsing `--config`), but parsers need access to it during their execution. Optique solves this with two systems: * Use *annotations* when you need direct, low-level control over how runtime data flows through parsers * Use *source context* and `runWith()` when you need to compose multiple data sources with clear priority ordering ### Which system to use If you are building a new data source (for example, a remote key–value store or a secrets manager), start with *source contexts*. They handle two-phase parsing, priority ordering, and cleanup automatically. Use *annotations* directly only if you need to inject runtime data into a parser without the source context lifecycle, or if you are implementing a source context yourself. ## The annotations system Optique's *annotations system* allows you to attach runtime data to a parsing session. This data flows through the parser state and can be accessed during both `parse()` and `complete()` phases. ### Key design principles * *Non-invasive*: Fully backward compatible, existing parsers work unchanged * *Symbol-keyed*: Packages use unique symbols to avoid naming conflicts * *Type-safe*: Full TypeScript support for accessing typed annotation data * *Opt-in*: Only parsers that need annotations access them * *Low-level primitive*: Exposed only in low-level APIs (`parse()`, `parseSync()`, `parseAsync()`), not in high-level APIs like `runParser()` or `run()` ## Basic usage ### Defining annotation keys Each package should define its own annotation key using a unique symbol: ```typescript // In your package const configDataKey = Symbol.for("@myapp/config"); ``` > \[!TIP] > Use the `Symbol.for()` constructor with a namespaced string to create > globally unique symbols. The namespace should match your package name to > avoid conflicts with other packages. ### Passing annotations Annotations are passed to parsing functions via the `ParseOptions` parameter. The third argument to `parse()` accepts an `annotations` object where each key is a symbol and each value is the data you want to make available during parsing: ```typescript twoslash import { parse } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const parser = option("--name", string()); const configDataKey = Symbol.for("@myapp/config"); const configData = { defaultName: "Alice" }; const result = parse(parser, ["--name", "Bob"], { annotations: { [configDataKey]: configData, }, }); ``` In this example, we attach a `configData` object containing default values. Custom parsers can later retrieve this data and use it as a fallback when command-line arguments are not provided. > \[!NOTE] > An annotations object with no own symbol keys (e.g., the empty record > `{}`) is treated as equivalent to omitting the `annotations` option > entirely. Parser state is left untouched in that case, so callers can > safely pass a computed annotations object without worrying about paying > a state-cloning cost when it happens to be empty. ### Accessing annotations Inside a parser, you can retrieve annotations using the `getAnnotations()` helper function. This function extracts the annotations object from the parser state, which is passed to the parser's `parse()` and `complete()` methods: ```typescript twoslash import { getAnnotations } from "@optique/core/annotations"; const configDataKey = Symbol.for("@myapp/config"); // ---cut-before--- function myCustomComplete(state: unknown) { const annotations = getAnnotations(state); const configData = annotations?.[configDataKey] as | { defaultName: string } | undefined; // Use config data for fallback values const name = configData?.defaultName ?? "Unknown"; return { success: true, value: name }; } ``` The function returns `undefined` if no annotations were provided, so always use optional chaining (`?.`) when accessing annotation data. Since annotations are typed as `unknown`, you need to cast them to your expected type. ## Creating custom parsers with annotations Custom parsers can access annotations during both `parse()` and `complete()` phases. The `complete()` phase is particularly useful for annotations because it runs after all command-line arguments have been processed, allowing you to provide fallback values for missing options. The following example creates a `configOption()` function that returns a parser looking up values from a configuration object passed via annotations. If the user doesn't provide the option on the command line, the parser falls back to the config file value: ```typescript twoslash import type { Parser } from "@optique/core/parser"; import { getAnnotations } from "@optique/core/annotations"; import { type Message, message } from "@optique/core/message"; const configDataKey = Symbol.for("@myapp/config"); // ---cut-before--- function configOption( name: string, configKey: string, required: boolean = false, ): Parser<"sync", string | undefined> { return { $valueType: [] as const, $stateType: [] as const, mode: "sync", priority: 0, usage: [], leadingNames: new Set(), acceptingAnyToken: false, initialState: undefined, parse: (context) => ({ success: true, next: { ...context, buffer: [] }, consumed: [], }), complete: (state) => { // Try to get value from annotations const annotations = getAnnotations(state); const configData = annotations?.[configDataKey] as | Record | undefined; const value = configData?.[configKey]; if (value === undefined && required) { return { success: false, error: message`Missing required option ${name} and no config fallback.`, }; } return { success: true, value }; }, suggest: () => [], getDocFragments: () => ({ fragments: [] }), }; } ``` Here, `mode` is a real runtime field that Optique reads to decide whether the parser runs synchronously or asynchronously. By contrast, `$valueType` and `$stateType` are type markers for inference and do not carry meaningful runtime information. The key insight here is that `complete()` receives the accumulated parser state, which includes any annotations passed to `parse()`. By calling `getAnnotations(state)`, the parser can access the configuration data and use it as a fallback. The `required` parameter controls whether a missing value (both from CLI and config) should be treated as an error. Custom parsers must implement two metadata properties. `leadingNames` enables `runParser()` to detect collisions with built-in meta features (help, version, completion), while `acceptingAnyToken` controls parser composition semantics: `leadingNames` : The set of fixed tokens that this parser accepts at `argv[0]`. Include every command name, option name, or literal value that the parser matches as its first token. If the parser accepts *any positional* token (like `argument()`), use an empty set and set `acceptingAnyToken` instead. Built-in combinators compute this automatically. `acceptingAnyToken` : Set to `true` when the parser unconditionally consumes the first positional token regardless of its value. This tells shared-buffer compositions (`tuple()`, `object()`, etc.) that sibling parsers with equal or lower priority cannot match at the same position for positional (non-option) tokens. Most custom parsers should set this to `false`. ### Helper modules for custom parsers Optique exposes a few low-level helper modules for custom parser authors: * `@optique/core/context`: Implement `SourceContext` when runtime data should be collected outside the parser itself and injected through `runWith()`. * `@optique/core/annotations`: Read annotation-bearing parser state with `getAnnotations()`. * `@optique/core/extension`: Coordinate parser traits and source-aware wrapper behavior with `defineTraits()`, `getTraits()`, `injectAnnotations()`, `inheritAnnotations()`, `isInjectedAnnotationState()`, `unwrapInjectedAnnotationState()`, `withAnnotationView()`, `dispatchByMode()`, `mapModeValue()`, `wrapForMode()`, `delegateSuggestNodes()`, and `mapSourceMetadata()`. Most custom parsers only need `annotations` and `context`. The `extension` helpers are intended for parser wrappers that need to preserve annotation state or participate in source-backed completion and suggestion flows. `inheritsAnnotations` : Use this trait when your parser rebuilds child state and needs parent annotations injected into that rebuilt state. `completesFromSource` : Use this trait when your parser can still produce a completion result from a non-CLI source value even when it consumed no CLI input. `requiresSourceBinding` : Use this trait when annotation-only primitive states should count as completable only when they came from a nested source-bound parser. ### Provisional results When a custom parser succeeds in `parse()` without consuming any input (returning `consumed: []`), but is not yet fully resolved (e.g., it resolved a discriminator to select a branch, but the branch itself still needs input), set `provisional: true` on the parse result. This signals to outer combinators like `or()` that the success is tentative and should not be treated as a definitive zero-consumed fallback: ```typescript return { success: true, provisional: true, next: context, consumed: [], }; ``` Most custom parsers do *not* need this flag. It is primarily relevant for construct-level parsers that compose multiple sub-parsers and need to distinguish between “matched without input” (like `constant()`) and “partially resolved, pending more input.” ## Use cases ### Config file fallback pattern A common pattern is *two-pass parsing*: first parse to extract the config file path, then parse again with the loaded config data as annotations. This is necessary because the config file path itself comes from command-line arguments, creating a chicken-and-egg problem that two-pass parsing solves. ```typescript twoslash import { parse } from "@optique/core/parser"; import { object } from "@optique/core/constructs"; import { option, argument } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { optional } from "@optique/core/modifiers"; const configDataKey = Symbol.for("@myapp/config"); // Placeholder for config loading function declare function loadConfig(path: string): Promise>; // ---cut-before--- // First pass: extract config path const firstPassParser = object({ config: optional(option("--config", string())), // ... other options }); const firstResult = parse(firstPassParser, process.argv.slice(2)); if (!firstResult.success) { console.error(firstResult.error); process.exit(1); } // Load config file if provided const configData = firstResult.value.config ? await loadConfig(firstResult.value.config) : {}; // Second pass: parse with config annotations const finalParser = object({ config: optional(option("--config", string())), name: option("--name", string()), port: option("--port", string()), }); const finalResult = parse(finalParser, process.argv.slice(2), { annotations: { [configDataKey]: configData, }, }); ``` The first pass extracts just the `--config` option to determine which file to load. After loading the config file, the second pass runs with the full parser, now with config data available via annotations. Custom parsers for `--name` and `--port` can then fall back to config values when the user doesn't provide them on the command line. ### Environment-based validation Annotations can provide runtime context that affects validation behavior. For example, you might want stricter validation in production but more permissive rules during development: ```typescript twoslash import { getAnnotations } from "@optique/core/annotations"; const envKey = Symbol.for("@myapp/env"); const isDevelopment = true; // ---cut-before--- // Custom validator that checks against environment function createEnvironmentValidator() { return { // ... parser implementation complete: (state: unknown) => { const annotations = getAnnotations(state); const env = annotations?.[envKey] as { isDevelopment: boolean } | undefined; // Different validation rules based on environment if (env?.isDevelopment) { // Allow more permissive values in development return { success: true, value: "debug" }; } else { // Stricter validation in production return { success: true, value: "error" }; } }, }; } ``` The caller passes environment information via annotations, and the validator adapts its behavior accordingly. This keeps the validation logic decoupled from how the environment is determined. ### Shared context across parsers When building complex CLI applications with multiple subcommands or composed parsers, you often need to share common data across all of them. Annotations provide a clean way to inject this shared context once at the top level: ```typescript twoslash import { parse } from "@optique/core/parser"; import { object } from "@optique/core/constructs"; import { constant } from "@optique/core/primitives"; const contextKey = Symbol.for("@myapp/context"); const parser = object({ cmd: constant("test"), }); // ---cut-before--- const sharedContext = { userId: "user123", apiUrl: "https://api.example.com", features: ["feature-a", "feature-b"], }; const result = parse(parser, process.argv.slice(2), { annotations: { [contextKey]: sharedContext, // Multiple packages can add their own keys here }, }); ``` Any parser in the tree can access this shared context without explicit parameter passing. This is particularly useful for feature flags, user session data, or API configuration that many parts of your CLI might need. ## Type safety Since annotations are stored as `Record`, you need to cast them to your expected types when accessing. Define interfaces for your annotation data and use type assertions: ```typescript twoslash import { getAnnotations } from "@optique/core/annotations"; // Define typed annotation key const configKey = Symbol.for("@myapp/config"); interface ConfigData { readonly apiUrl: string; readonly timeout: number; readonly retries?: number; } // ---cut-before--- function parseWithConfig(state: unknown) { const annotations = getAnnotations(state); // Type assertion for your specific data const config = annotations?.[configKey] as ConfigData | undefined; if (config) { // TypeScript knows the shape of config const url: string = config.apiUrl; const timeout: number = config.timeout; const retries: number | undefined = config.retries; } } ``` The `as ConfigData | undefined` cast tells TypeScript what shape to expect. Always include `undefined` in the union since the annotation might not be present. For better type safety and to avoid repeating the cast, create a typed helper function: ```typescript twoslash import { getAnnotations, type Annotations } from "@optique/core/annotations"; const configKey = Symbol.for("@myapp/config"); interface ConfigData { readonly apiUrl: string; readonly timeout: number; } // ---cut-before--- function getConfigAnnotation(state: unknown): ConfigData | undefined { const annotations = getAnnotations(state); return annotations?.[configKey] as ConfigData | undefined; } // Usage in parser function myParser(state: unknown) { const config = getConfigAnnotation(state); // config is typed as ConfigData | undefined } ``` ## Best practices ### Annotation key naming Use the `Symbol.for()` constructor with a namespaced string that matches your package name: ```typescript // Good: package-namespaced const myKey = Symbol.for("@myapp/config"); const dataKey = Symbol.for("@mycompany/user-data"); // Avoid: generic names that might conflict const key = Symbol.for("config"); const data = Symbol.for("data"); ``` ### Type definitions Define clear TypeScript interfaces for your annotation data: ```typescript // Define the shape of your annotation data interface MyAppConfig { readonly apiUrl: string; readonly timeout: number; readonly retries?: number; } // Export the key and type together export const configKey = Symbol.for("@myapp/config"); export type { MyAppConfig }; ``` ### Error handling Always handle the case where annotations might not be present: ```typescript twoslash import { getAnnotations } from "@optique/core/annotations"; const configKey = Symbol.for("@myapp/config"); interface ConfigData { defaultValue: string; } // ---cut-before--- function safelyAccessAnnotations(state: unknown) { const annotations = getAnnotations(state); if (!annotations) { // No annotations provided - use sensible defaults return "default"; } const config = annotations[configKey] as ConfigData | undefined; if (!config) { // Annotation key not present - use fallback return "fallback"; } // Safe to use config data return config.defaultValue; } ``` ### Documentation Document that your parsers use annotations and which annotation keys they expect: ````typescript /** * Creates a parser that validates against API endpoints. * * This parser requires runtime context via annotations: * - `@myapp/api-client`: An API client instance for validation * * @example * ```typescript * import { parse } from "@optique/core/parser"; * * const apiKey = Symbol.for("@myapp/api-client"); * const result = parse(myParser, args, { * annotations: { [apiKey]: apiClient } * }); * ``` */ export function createApiParser() { // ... } ```` ## Advanced patterns ### Two-pass parsing The most common pattern is two-pass parsing for config file loading: ```typescript twoslash import { parse } from "@optique/core/parser"; import { object } from "@optique/core/constructs"; import { option, argument } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; import { optional, withDefault } from "@optique/core/modifiers"; declare function loadConfigFile(path: string): Promise<{ host?: string; port?: number; }>; const configKey = Symbol.for("@myapp/config"); // ---cut-before--- // Define the main parser const parser = object({ config: optional(option("--config", string())), host: withDefault(option("--host", string()), "localhost"), port: withDefault(option("--port", integer()), 3000), input: argument(string()), }); // First pass: extract config path const firstPass = parse(parser, process.argv.slice(2)); if (!firstPass.success) { console.error(firstPass.error); process.exit(1); } // Load config file if specified const configData = firstPass.value.config ? await loadConfigFile(firstPass.value.config) : {}; // Second pass: parse with config annotations // CLI args override config file values const finalResult = parse(parser, process.argv.slice(2), { annotations: { [configKey]: configData, }, }); if (!finalResult.success) { console.error(finalResult.error); process.exit(1); } // finalResult.value contains merged CLI + config values console.log(`Connecting to ${finalResult.value.host}:${finalResult.value.port}`); ``` ### Conditional validation based on runtime state Annotations enable validators that adapt to runtime conditions. This example shows a port validator that enforces different rules based on the deployment environment. In production, only specific whitelisted ports are allowed, while development mode is more permissive: ```typescript twoslash import type { ValueParser } from "@optique/core/valueparser"; import { getAnnotations } from "@optique/core/annotations"; import { message } from "@optique/core/message"; const envKey = Symbol.for("@myapp/env"); interface EnvironmentData { readonly mode: "development" | "production"; readonly allowedPorts: readonly number[]; } // ---cut-before--- function portValidator(): ValueParser<"sync", number> { return { mode: "sync", metavar: "PORT", placeholder: 1, // Validation using runtime environment data from annotations parse: (input: string, state?: unknown) => { const port = parseInt(input, 10); if (isNaN(port)) { return { success: false, error: message`Invalid port number.` }; } const annotations = getAnnotations(state); const env = annotations?.[envKey] as EnvironmentData | undefined; if (!env) { // No environment data - allow any valid port return port >= 1 && port <= 65535 ? { success: true, value: port } : { success: false, error: message`Port must be between 1 and 65535.` }; } // Validate against environment-specific allowed ports if (!env.allowedPorts.includes(port)) { return { success: false, error: message`Port ${String(port)} is not allowed in ${env.mode} mode.`, }; } return { success: true, value: port }; }, format: (port: number) => port.toString(), }; } ``` The validator first checks for basic validity (is it a number?), then applies environment-specific rules if available. When no environment data is provided, it falls back to accepting any valid port number. This graceful degradation ensures the validator works both with and without annotations. ### Multiple annotation sources Different packages can use different annotation keys simultaneously: ```typescript twoslash import { parse } from "@optique/core/parser"; import { constant } from "@optique/core/primitives"; const configKey = Symbol.for("@myapp/config"); const userKey = Symbol.for("@myapp/user"); const featureKey = Symbol.for("@myapp/features"); const parser = constant("test"); // ---cut-before--- const result = parse(parser, process.argv.slice(2), { annotations: { [configKey]: { apiUrl: "https://api.example.com" }, [userKey]: { id: "user123", name: "Alice" }, [featureKey]: { experimental: true }, }, }); ``` Each package can independently access its own annotation data without interfering with others. ## API reference ### Types from `@optique/core/annotations` `Annotations` : Type alias for `Record`. Represents the annotation data structure where each key is a symbol and each value can be any type. `ParseOptions` : Interface containing options for parse functions. Currently has one field: `annotations?: Annotations`. ### Types from `@optique/core/context` `SourceContextPhase` : Type alias for `"single-pass" | "two-pass"`. Indicates whether a context contributes only its phase-1 annotations or is recollected after a usable first parse pass to refine them. `SourceContext` : Interface for data sources that provide annotations. The `TRequiredOptions` type parameter specifies additional options that `runWith()` must provide via `contextOptions` when this context is used. ``` Members: - `id: symbol` — Unique identifier for the context - `phase: SourceContextPhase` — Required policy declaring whether this context is `"single-pass"` or `"two-pass"` - `getAnnotations(request?, options?): Promise | Annotations` — Returns annotations to inject into parsing - `getInternalAnnotations?(request, annotations): Annotations | undefined` — Optional hook called after `getAnnotations()` to inject additional internal annotations (e.g., phase-specific markers). Returns additional annotations to merge, or `undefined` to add nothing. ``` `SourceContextRequest` : Request object passed to `getAnnotations()` and `getInternalAnnotations()`. A discriminated union based on the `phase` field: ``` - `phase: "phase1"` — Initial annotation collection before the first parse pass. - `phase: "phase2"` — Second annotation collection after a usable first pass. The `parsed` field holds the first-pass value, which may itself be `undefined`. Use `ParserValuePlaceholder` in `TRequiredOptions` when the options depend on the parser's result type. ``` `ParserValuePlaceholder` : A placeholder type representing the parser's result value type. Use this in `SourceContext` when the required options depend on the parser's result type. The `runWith()` function substitutes this placeholder with the actual parser type at the call site. ``` ~~~~ typescript import type { SourceContext, ParserValuePlaceholder } from "@optique/core/context"; // Context that requires getConfigPath with typed parser result interface MyContext extends SourceContext<{ getConfigPath: (parsed: ParserValuePlaceholder) => string | undefined; }> { // ... } ~~~~ ``` ### Functions from `@optique/core/annotations` `getAnnotations(state: unknown): Annotations | undefined` : Extracts annotations from parser state. Returns `undefined` if the state does not contain annotations or if the state is not an object. ``` ~~~~ typescript twoslash import { getAnnotations } from "@optique/core/annotations"; const myKey = Symbol.for("@myapp/data"); declare const state: unknown; // ---cut-before--- const annotations = getAnnotations(state); const myData = annotations?.[myKey]; ~~~~ The raw storage protocol for annotations is internal. Custom parsers should read annotations through `getAnnotations()` instead of relying on symbol-keyed state properties directly. ``` ### Functions from `@optique/core/extension` `injectAnnotations(state, annotations)` : Attaches annotations to parser state while preserving Optique's supported state-shape semantics for primitives, arrays, plain objects, and common built-in objects. `inheritAnnotations(source, target)` : Propagates annotations from an existing state into a replacement state. Useful for wrappers that rebuild child state during `parse()` or `complete()`. `isInjectedAnnotationState(value)` : Returns whether a value is Optique's injected primitive-state annotation wrapper. `unwrapInjectedAnnotationState(value)` : Removes that primitive-state wrapper when present. `withAnnotationView(state, annotations)` : Creates an annotation-aware proxy for non-plain objects whose identity or internal slots should be preserved instead of cloning them. `dispatchByMode(mode, syncFn, asyncFn)` : Runs sync or async implementations while preserving Optique's mode typing. `mapModeValue(mode, value, mapFn)` : Maps a mode-wrapped value without changing whether it stays sync or async. `wrapForMode(mode, value)` : Wraps a plain value or Promise so it matches the parser's execution mode. `defineTraits(parser, traits)` : Declares stable extension traits on a parser so wrappers can opt into annotation inheritance, source-backed completion, or source-binding-only primitive wrappers without reaching into parser internals. `getTraits(parser)` : Reads those stable extension traits back from a parser. `delegateSuggestNodes(innerParser, outerParser, state, path, innerState, position?)` : Reuses the wrapped parser's suggest-time runtime nodes while keeping the outer parser's own source metadata node in the final list. `mapSourceMetadata(parser, mapSource)` : Rewrites only the source capability inside dependency metadata while leaving derived and transform capabilities unchanged. ### Functions from `@optique/core/facade` `runWith(parser, programName, contexts, options): Promise` : Runs a parser with multiple source contexts. Automatically handles single-pass and two-pass contexts with two-phase parsing when needed. ``` The `options` parameter accepts a `contextOptions` property for any options required by the contexts. For example, if a context specifies `TRequiredOptions` with `ParserValuePlaceholder`, you must provide that option inside `contextOptions`: ~~~~ typescript // If configContext requires getConfigPath const result = await runWith(parser, "myapp", [configContext], { args: process.argv.slice(2), contextOptions: { getConfigPath: (parsed) => parsed.config, // typed from parser! }, }); ~~~~ ``` `runWithSync(parser, programName, contexts, options): T` : Synchronous variant of `runWith()`. All contexts must return annotations synchronously (not Promises). `runWithAsync(parser, programName, contexts, options): Promise` : Explicit async variant of `runWith()`. Equivalent to `runWith()`. `SubstituteParserValue` : Type utility that substitutes `ParserValuePlaceholder` with the actual parser value type. Used internally by `runWith()` to compute the required options type. `ExtractRequiredOptions` : Type utility that extracts and merges required options from an array of source contexts. Returns the intersection of all contexts' required options with `ParserValuePlaceholder` substituted by `TValue`. ## The source context system While annotations provide low-level control, the *source context* system offers a higher-level abstraction for composing multiple data sources. This is particularly useful when you need to implement priority-based fallback chains like CLI > environment variables > configuration file > default values. ### What is a source context? A `SourceContext` is an interface that represents a data source capable of providing annotations to parsers. Each context has: * A unique `id` symbol for identification * A `getAnnotations()` method that returns annotations Here's a simple context that provides environment variables to parsers: ```typescript import type { SourceContext, SourceContextRequest, } from "@optique/core/context"; const envContext: SourceContext = { id: Symbol.for("@myapp/env"), phase: "single-pass", getAnnotations() { return { [Symbol.for("@myapp/env")]: { HOST: process.env.MYAPP_HOST, PORT: process.env.MYAPP_PORT, } }; } }; ``` The `id` symbol identifies this context for debugging and priority resolution. The required `phase` field declares whether the context is `"single-pass"` or `"two-pass"`. This removes the old inference ambiguity between “this context already has some phase-1 data” and “this context still needs phase-2 refinement.” The `getAnnotations()` method returns an object mapping annotation keys to their values. Parsers can then access these values using `getAnnotations()`. ### Single-pass vs two-pass contexts Contexts can be either *single-pass* or *two-pass*: * *Single-pass contexts* contribute their final annotations before parsing starts (e.g., environment variables) * *Two-pass contexts* may contribute phase-1 annotations and then refine them after a usable first parse pass (e.g., config files whose path is determined by a CLI option) The difference lies in whether `getAnnotations()` needs the parsed result to do its work: ```typescript twoslash declare const process: { readonly env: Record; }; declare function loadConfigFile(path: string): Promise; // ---cut-before--- import type { SourceContext, SourceContextRequest, } from "@optique/core/context"; // Single-pass context: data is always available const envContext: SourceContext = { id: Symbol.for("@myapp/env"), phase: "single-pass", getAnnotations() { // Returns immediately - no need for parsing results return { [Symbol.for("@myapp/env")]: process.env }; } }; // Two-pass context: needs parsed result to load config const configContext: SourceContext = { id: Symbol.for("@myapp/config"), phase: "two-pass", async getAnnotations(request?: SourceContextRequest) { if (request == null || request.phase === "phase1") return {}; const parsed = request.parsed as { config?: string } | undefined; if (!parsed?.config) return {}; // Load config file asynchronously const data = await loadConfigFile(parsed.config); return { [Symbol.for("@myapp/config")]: data }; } }; ``` The single-pass `envContext` reads environment variables directly and doesn't need any parsed values. The two-pass `configContext`, however, needs to know the config file path from the parsed `--config` option before it can load the file. Phase 1 and phase 2 are explicit through `request.phase`, so a real phase-two parsed value of `undefined` is no longer ambiguous. ## Using `runWith()` The `runWith()` function orchestrates multiple source contexts with automatic priority handling and smart two-phase parsing. ### Basic usage The `runWith()` function takes a parser, program name, array of contexts, and options. It automatically collects annotations from all contexts, merges them with proper priority handling, and runs the parser: ```typescript import { runWith } from "@optique/core/facade"; import { object } from "@optique/core/constructs"; import { option, argument } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { withDefault } from "@optique/core/modifiers"; import type { SourceContext } from "@optique/core/context"; const envContext: SourceContext = { id: Symbol.for("@myapp/env"), phase: "single-pass", getAnnotations() { return { [Symbol.for("@myapp/env")]: { HOST: process.env.MYAPP_HOST ?? "localhost", } }; } }; const parser = object({ host: withDefault(option("--host", string()), "localhost"), name: argument(string()), }); const result = await runWith(parser, "myapp", [envContext], { args: process.argv.slice(2), help: { option: true }, version: { option: true, value: "1.0.0" }, }); ``` The function handles all the complexity of collecting annotations from multiple sources and injecting them into the parsing process. It also supports the same help and version options as `runParser()`. If a context implements `Symbol.dispose` or `Symbol.asyncDispose`, cleanup does not begin until the full `runWith()` operation has settled. For async parsers, that includes later asynchronous `complete()` work, so contexts remain live for the entire parse/completion lifecycle. ### Priority handling When using multiple contexts, *earlier contexts have priority over later ones*. This allows you to implement fallback chains naturally: ```typescript // Priority: envContext > configContext // Environment variables override config file values const result = await runWith( parser, "myapp", [envContext, configContext], // env has higher priority { args: process.argv.slice(2) } ); ``` ### Two-phase parsing When two-pass contexts are present, `runWith()` automatically performs two-phase parsing: 1. *Phase 1*: Parse with phase-1 annotations from all contexts 2. *Phase 2*: Call `getAnnotations({ phase: "phase2", parsed })` on two-pass contexts with the parsed result, then parse again with the merged final annotations For each two-pass context, the phase-2 return value replaces that context's phase-1 annotation set for the final parse. This means returning an empty object from phase-two `getAnnotations()` clears any annotations the same context contributed during phase 1. Single-pass contexts keep their phase-1 snapshot. This ensures that: * Single-pass contexts (like environment variables) are available immediately * Two-pass contexts (like config files) can extract information from the first parse pass without being misclassified when phase 1 already returns non-empty annotations During phase 1, parsers whose values are not yet resolved (e.g., deferred interactive prompts) use the `ValueParser.placeholder` property to produce a type-appropriate stand-in value. Because the placeholder is a valid inhabitant of the result type, `map()` transforms run normally on it and deferred parts are tracked via `deferredKeys` and selectively hidden as `undefined` before phase-two context collection. No sentinel symbols or runtime checks are needed. ### Help, version, and completion remain available The `runWith()` family keeps help, version, and completion available without requiring valid configuration files or contexts, but it checks the user parser first. These features are treated as runner-level meta requests only when the parser leaves the token sequence unconsumed: * `--help` displays help even if later parse phases would fail * `--version` shows version information without running phase 2 * Completion scripts still skip phase-two context refinement If the parser accepts the same tokens as ordinary data, such as a positional `help` value or an option value `--help`, the parse result wins and normal context collection continues. Genuine meta requests still stop after phase 1, so users can access documentation and basic information without needing a successful second parse pass. ```typescript // Help works even if config file is missing or invalid const result = await runWith(parser, "myapp", [configContext], { args: ["--help"], help: { option: true, onShow: () => process.exit(0) }, }); // → Shows help immediately without errors ``` ### Sync and async variants Three function variants are available: `runWith()` : Always returns a Promise. Handles both sync and async contexts. `runWithSync()` : Returns the result directly. All contexts must be synchronous. `runWithAsync()` : Same as `runWith()`. Explicit async variant for clarity. ```typescript import { runWith, runWithSync, runWithAsync } from "@optique/core/facade"; // Async (recommended for most cases) const result1 = await runWith(parser, "myapp", contexts, options); // Sync (only for sync contexts and parsers) const result2 = runWithSync(parser, "myapp", syncContexts, options); // Explicit async const result3 = await runWithAsync(parser, "myapp", contexts, options); ``` ## Building custom source contexts ### Creating a simple environment context A reusable environment context can be created with a factory function that accepts a prefix. This pattern allows different applications to use their own environment variable naming conventions: ```typescript import type { Annotations, SourceContext, SourceContextRequest, } from "@optique/core/context"; const envKey = Symbol.for("@myapp/env"); interface EnvData { readonly HOST?: string; readonly PORT?: string; readonly DEBUG?: string; } export function createEnvContext(prefix: string = ""): SourceContext { return { id: envKey, phase: "single-pass", getAnnotations(): Annotations { const data: EnvData = { HOST: process.env[`${prefix}HOST`], PORT: process.env[`${prefix}PORT`], DEBUG: process.env[`${prefix}DEBUG`], }; return { [envKey]: data }; } }; } // Usage const envContext = createEnvContext("MYAPP_"); ``` When called with `"MYAPP_"`, the context reads `MYAPP_HOST`, `MYAPP_PORT`, and `MYAPP_DEBUG` from the environment. This is a single-pass context since it doesn't need any parsed values. ### Creating a config file context A config file context is two-pass because it needs to know the file path from parsed arguments. The `getAnnotations()` method receives the parsed result and uses it to load the configuration: ```typescript twoslash import type { Annotations, SourceContext, SourceContextRequest, } from "@optique/core/context"; import { readFile } from "node:fs/promises"; const configKey = Symbol.for("@myapp/config"); interface ConfigData { readonly host?: string; readonly port?: number; readonly debug?: boolean; } export function createConfigContext(): SourceContext { return { id: configKey, phase: "two-pass", async getAnnotations( request?: SourceContextRequest, ): Promise { if (request == null || request.phase === "phase1") return {}; const parsed = request.parsed as { config?: string } | undefined; if (!parsed?.config) return {}; // No config file specified try { const content = await readFile(parsed.config, "utf8"); const data: ConfigData = JSON.parse(content); return { [configKey]: data }; } catch { return {}; // Config file not found or invalid } } }; } // Usage const configContext = createConfigContext(); ``` Note the defensive checks: when `request.phase` is `"phase1"`, return empty. When the user didn't specify `--config`, return empty. When the file can't be read or parsed, return empty. This ensures the context never throws and gracefully degrades when config isn't available. ### Creating a type-safe config context The example above hardcodes how to extract the config path from parsed results (`result.config`). This couples the context to a specific parser structure. For a more reusable approach, use `ParserValuePlaceholder` to declare that the caller must provide a `getConfigPath` function: ```typescript twoslash import type { Annotations, ParserValuePlaceholder, SourceContext, SourceContextRequest, } from "@optique/core/context"; import { readFile } from "node:fs/promises"; const configKey = Symbol.for("@myapp/config"); interface ConfigData { readonly host?: string; readonly port?: number; readonly debug?: boolean; } // Declare required options using ParserValuePlaceholder interface ConfigContextOptions { getConfigPath: (parsed: ParserValuePlaceholder) => string | undefined; } // The context type includes required options type ConfigContext = SourceContext; export function createConfigContext(): ConfigContext { return { id: configKey, phase: "two-pass", async getAnnotations( request?: SourceContextRequest, options?: ConfigContextOptions, ): Promise { if (request == null || request.phase === "phase1") return {}; const parsed = request.parsed as ParserValuePlaceholder | undefined; // Use the injected getConfigPath function const configPath = parsed == null ? undefined : options?.getConfigPath(parsed); if (!configPath) return {}; try { const content = await readFile(configPath, "utf8"); const data: ConfigData = JSON.parse(content); return { [configKey]: data }; } catch { return {}; } } }; } ``` Now when using `runWith()`, TypeScript *requires* `contextOptions` with `getConfigPath` and infers the correct type for the `parsed` parameter: ```typescript import { runWith } from "@optique/core/facade"; import { object } from "@optique/core/constructs"; import { option, argument } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { optional } from "@optique/core/modifiers"; const parser = object({ config: optional(option("--config", string())), host: option("--host", string()), input: argument(string()), }); const configContext = createConfigContext(); // TypeScript requires contextOptions with getConfigPath const result = await runWith(parser, "myapp", [configContext], { args: process.argv.slice(2), contextOptions: { getConfigPath: (parsed) => parsed.config, // parsed is typed! }, }); ``` If you omit `contextOptions`, TypeScript reports an error. The `parsed` parameter is automatically typed as `{ config?: string; host: string; input: string }`, providing full type safety and autocompletion. This pattern is used by *@optique/config* to provide type-safe config file integration. See the [config file integration](../integrations/config.md) guide for a complete implementation. ### Best practices for custom contexts * *Use unique symbols*: Always use `Symbol.for()` with a namespaced string matching your package name * *Declare `phase` explicitly*: Set `phase: "single-pass"` or `phase: "two-pass"` on every context. This makes refinement intent explicit and prevents the runner from guessing based on the shape of the phase-1 annotations * *Handle missing data gracefully*: Return empty objects instead of throwing errors * *Keep contexts focused*: Each context should handle one data source * *Document the annotation key*: Make it clear what data your context provides ## Limitations and considerations ### Low-level API only Annotations are only available in low-level parsing functions: * ✅ Available: `parse()`, `parseSync()`, `parseAsync()` * ✅ Available via contexts: `runWith()`, `runWithSync()`, `runWithAsync()` * ❌ Not directly available: `runParser()`, `run()` from *@optique/run* This is intentional. Annotations are a low-level primitive for building advanced parsers. For high-level usage, use the SourceContext system with `runWith()`. ### State immutability Annotations are injected into the initial state and should be treated as read-only. Do not modify annotation data during parsing: ```typescript // Good: read-only access const annotations = getAnnotations(state); const value = annotations?.[myKey]; // Bad: modifying annotations (undefined behavior) const annotations = getAnnotations(state); if (annotations) { annotations[myKey] = newValue; // Don't do this } ``` ### Performance considerations Annotation injection creates a shallow copy of the initial state. This has minimal performance impact, but be aware that it happens on every call to `parse()`, `suggest()`, or `getDocPage()` when annotations are provided. For `runWith()` with two-pass contexts, two parse passes are performed. This is necessary for the two-phase approach but doubles the parsing overhead. For performance-critical applications: * Use only single-pass contexts when possible (single pass) * Cache parsed results rather than re-parsing multiple times * Consider using `runWithSync()` for sync-only contexts to avoid Promise overhead --- --- url: /integrations/git.md description: >- Git value parsers for validating branches, tags, commits, and remotes using isomorphic-git. --- # Git integration *This API is available since Optique 0.9.0.* The *@optique/git* package provides async value parsers for validating Git references (branches, tags, commits, remotes) using [isomorphic-git]. These parsers validate input against an actual Git repository, ensuring that users can only specify existing references. ```typescript twoslash import { gitBranch, gitTag, gitCommit, gitRef, gitRemote, gitRemoteBranch } from "@optique/git"; import { option, argument } from "@optique/core/primitives"; ``` [isomorphic-git]: https://isomorphic-git.org/ ## Installation ::: code-group ```bash [Deno] deno add jsr:@optique/git ``` ```bash [npm] npm add @optique/git ``` ```bash [pnpm] pnpm add @optique/git ``` ```bash [Yarn] yarn add @optique/git ``` ```bash [Bun] bun add @optique/git ``` ::: ## Getting started Import the parsers from `@optique/git` and use them with Optique primitives: ```typescript twoslash import { gitBranch } from "@optique/git"; import { argument } from "@optique/core/primitives"; const parser = argument(gitBranch()); // Accepts: "main", "develop", "feature/my-feature" // Rejects: "nonexistent-branch" ``` ## `gitBranch()` Validates that input matches an existing local branch name in the repository. ```typescript twoslash import { gitBranch } from "@optique/git"; import { argument } from "@optique/core/primitives"; const branchParser = argument(gitBranch()); ``` ### Options ```typescript twoslash import type { GitParserOptions } from "@optique/git"; ``` ### Example ```typescript twoslash import { gitBranch } from "@optique/git"; import { option } from "@optique/core/primitives"; // Branch argument for git checkout-like command const checkoutParser = option("-b", "--branch", gitBranch()); // Usage: myapp checkout --branch feature/my-feature ``` ## `gitTag()` Validates that input matches an existing tag name in the repository. ```typescript twoslash import { gitTag } from "@optique/git"; import { option } from "@optique/core/primitives"; const tagParser = option("-t", "--tag", gitTag()); // Usage: myapp release --tag v1.0.0 ``` ### Example with custom options ```typescript twoslash import { gitTag } from "@optique/git"; import { option } from "@optique/core/primitives"; const versionParser = option("--release", gitTag({ metavar: "VERSION", })); ``` ## `gitRemote()` Validates that input matches an existing remote name in the repository. ```typescript twoslash import { gitRemote } from "@optique/git"; import { option } from "@optique/core/primitives"; const remoteParser = option("--remote", gitRemote()); // Usage: myapp fetch --remote origin ``` ## `gitRemoteBranch()` Validates that input matches an existing branch on a specific remote. ```typescript twoslash import { gitRemoteBranch } from "@optique/git"; import { option } from "@optique/core/primitives"; const remoteBranchParser = option("--branch", gitRemoteBranch("origin")); // Usage: myapp pull --branch main ``` ### Dynamic remote with dependencies Use the [dependency system](../concepts/dependencies.md) to validate branches against a user-specified remote. The `gitRemoteBranch()` parser works with async factory support in derived parsers: ```typescript twoslash import { gitRemote, gitRemoteBranch } from "@optique/git"; import { dependency } from "@optique/core/dependency"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; // Wrap gitRemote() as a dependency source const remoteParser = dependency(gitRemote()); // Create a derived parser that validates branches against the selected remote const branchParser = remoteParser.derive({ metavar: "BRANCH", mode: "async", factory: (remote) => gitRemoteBranch(remote), defaultValue: () => "origin", }); const pullCommand = object({ remote: option("--remote", remoteParser), branch: option("--branch", branchParser), }); // Now --branch validates against the remote specified by --remote: // myapp pull --remote upstream --branch feature/new // → validates that "feature/new" exists on "upstream" ``` Since `gitRemoteBranch()` returns an async parser, the derived parser automatically becomes async. The dependency system handles the mode combination seamlessly. ## `gitCommit()` Validates that input is a valid commit SHA (full or shortened) that exists in the repository. Returns the resolved full OID. ```typescript twoslash import { gitCommit } from "@optique/git"; import { option } from "@optique/core/primitives"; const commitParser = option("--commit", gitCommit()); // Usage: myapp revert --commit abc1234 ``` ### SHA formats supported The parser accepts various SHA formats: * Full 40-character SHA: `d670460b4b4aece5915caf5c68d12f560a9fe3e4` * Shortened SHAs (4+ characters): `d670460`, `d670460b4b4a` ```typescript twoslash import { parseAsync } from "@optique/core/parser"; import { gitCommit } from "@optique/git"; import { option } from "@optique/core/primitives"; const parser = option("-c", "--commit", gitCommit()); // ---cut-before--- const result = await parseAsync(parser, ["-c", "550e840"]); if (result.success) { console.log(result.value); // Full 40-char OID } ``` ## `gitRef()` A flexible parser that accepts any valid Git reference: branches, tags, or commits. Returns the resolved commit OID. ```typescript twoslash import { gitRef } from "@optique/git"; import { argument } from "@optique/core/primitives"; const refParser = argument(gitRef()); // Accepts: branch names, tags, or commit SHAs // Returns: resolved commit OID ``` ## `createGitParsers()` A factory function for creating multiple Git parsers with shared configuration. ```typescript twoslash import { createGitParsers } from "@optique/git"; import { option } from "@optique/core/primitives"; const git = createGitParsers({ dir: "/path/to/repo", }); const parser = option("--branch", git.branch()); const tagParser = option("--tag", git.tag()); const commitParser = option("--commit", git.commit()); ``` ### Example with custom options per parser ```typescript twoslash import { createGitParsers } from "@optique/git"; const git = createGitParsers({ dir: "/path/to/repo", metavar: "REF", }); // Override metavar for specific parser const branchParser = git.branch({ metavar: "BRANCH_NAME" }); ``` ## Async mode All Git parsers operate in async mode because they perform I/O operations to read the Git repository: ```typescript twoslash import { gitBranch } from "@optique/git"; import { argument } from "@optique/core/primitives"; const parser = argument(gitBranch()); // parser.mode === "async" ``` Use `parseAsync()` with async parsers: ```typescript twoslash import { parseAsync } from "@optique/core/parser"; import { gitBranch } from "@optique/git"; import { argument } from "@optique/core/primitives"; const parser = argument(gitBranch()); // ---cut-before--- const result = await parseAsync(parser, ["main"]); if (result.success) { console.log(result.value); // "main" } ``` ## Suggestions Git parsers provide intelligent completion suggestions: ```typescript twoslash import { gitBranch, gitTag, gitRef } from "@optique/git"; import { argument } from "@optique/core/primitives"; // Suggests existing branches const branchParser = argument(gitBranch()); // Completing "fe" suggests: "feature/*", "fix/*", etc. // Suggests existing tags const tagParser = argument(gitTag()); // Completing "v1" suggests: "v1.0.0", "v1.1.0", etc. // Suggests both branches and tags const refParser = argument(gitRef()); // Completing "v" suggests tags; completing "fe" suggests branches ``` ## Error handling Git parsers provide clear error messages: ```bash $ myapp --branch nonexistent Error: Branch nonexistent does not exist. Available branches: main, develop, feature/*. $ myapp --tag v999.0.0 Error: Tag v999.0.0 does not exist. Available tags: v1.0.0, v2.0.0. $ myapp --commit abc Error: Invalid commit SHA: abc. Provide an abbreviated (4+) or full (40) hexadecimal commit SHA. $ myapp --ref nonexistent-ref Error: Reference nonexistent-ref does not exist. Provide a valid branch, tag, or commit SHA. ``` ## Custom error messages You can customize error messages using the `errors` option with the `Message` type from `@optique/core/message`: ```typescript twoslash import { gitBranch } from "@optique/git"; import { message, valueSet } from "@optique/core/message"; const parser = gitBranch({ errors: { notFound: (input, available) => message`Branch ${input} not found. Available: ${ valueSet(available ?? [], "none") }`, listFailed: (dir) => message`Cannot read git repository at ${dir}`, } }); ``` ### Error types The `errors` option supports the following error types: * `notFound(input, available?)` — Called when the git reference is not found. Provides the invalid input and optionally a list of available references. * `listFailed(dir)` — Called when listing git references fails, typically when the directory is not a valid git repository. * `invalidFormat(input)` — Called for commit SHA validation failures when the input format is invalid (e.g., too short). * `remoteNotFound(remote, availableRemotes)` — Called by `gitRemoteBranch()` when the named remote does not exist. ### Example with gitCommit ```typescript twoslash import { gitCommit } from "@optique/git"; import { message } from "@optique/core/message"; const parser = gitCommit({ errors: { invalidFormat: (input) => message`${input} must be 4-40 characters.`, notFound: (input) => message`Commit ${input} not found in repository.`, } }); ``` ### Example with createGitParsers ```typescript twoslash import { createGitParsers } from "@optique/git"; import { message } from "@optique/core/message"; const git = createGitParsers({ errors: { notFound: (input, available) => message`${input} is not a valid reference.`, } }); const branchParser = git.branch(); const tagParser = git.tag(); const commitParser = git.commit(); ``` ## Metavar defaults Each parser uses an appropriate default metavar for help text: | Parser | Default metavar | | ------------------- | --------------- | | `gitBranch()` | `"BRANCH"` | | `gitTag()` | `"TAG"` | | `gitRemote()` | `"REMOTE"` | | `gitRemoteBranch()` | `"BRANCH"` | | `gitCommit()` | `"COMMIT"` | | `gitRef()` | `"REF"` | Override with the `metavar` option: ```typescript twoslash import { gitBranch, gitTag } from "@optique/git"; const branchParser = gitBranch({ metavar: "BRANCH_NAME" }); const tagParser = gitTag({ metavar: "RELEASE_VERSION" }); ``` ## Exported utilities The package also re-exports several utilities from isomorphic-git for advanced use cases: ```typescript twoslash import { expandOid, listBranches, listTags, listRemotes, readObject, resolveRef } from "@optique/git"; ``` * `expandOid()` — Expand short SHAs to full OIDs * `listBranches()` — List all local branches * `listTags()` — List all tags * `listRemotes()` — List all remotes * `readObject()` — Read a Git object * `resolveRef()` — Resolve a ref to its OID ## Complete example A Git-like CLI application using Git parsers: ```typescript twoslash import { createGitParsers } from "@optique/git"; import { object, or } from "@optique/core/constructs"; import { argument, command, constant, option } from "@optique/core/primitives"; import { parseAsync } from "@optique/core/parser"; const git = createGitParsers(); const checkoutCmd = command("checkout", object({ type: constant("checkout"), branch: option("-b", "--branch", git.branch()), startPoint: argument(git.ref()), })); const logCmd = command("log", object({ type: constant("log"), ref: argument(git.ref()), })); const app = or(checkoutCmd, logCmd); // ---cut-before--- const result = await parseAsync(app, ["checkout", "-b", "develop", "main"]); if (result.success) { console.log(result.value); } ``` --- --- url: /install.md description: Instructions for installing Optique. --- # Installation Optique is a family of packages. Most users start with the two foundational packages, each of which is available on both JSR and npm: ::: code-group ```bash [Deno] deno add --jsr @optique/core @optique/run ``` ```bash [npm] npm add @optique/core @optique/run ``` ```bash [pnpm] pnpm add @optique/core @optique/run ``` ```bash [Yarn] yarn add @optique/core @optique/run ``` ```bash [Bun] bun add @optique/core @optique/run ``` ::: Additional packages provide integrations for config files, environment variables, prompts, Temporal, Git, schema validators, LogTape, and man page generation. You may want to use Optique in a web browser, in which case you would only install the *@optique/core* package: ::: code-group ```bash [Deno] deno add jsr:@optique/core ``` ```bash [npm] npm add @optique/core ``` ```bash [pnpm] pnpm add @optique/core ``` ```bash [Yarn] yarn add @optique/core ``` ```bash [Bun] bun add @optique/core ``` ::: --- --- url: /concepts/dependencies.md description: >- Inter-option dependencies allow one option's valid values to depend on another option's value, enabling dynamic validation and context-aware shell completion. --- # Inter-option dependencies *This API is available since Optique 0.10.0.* Sometimes the valid values for one command-line option depend on the value of another option. For example, a `--log-level` option might accept different values depending on whether `--mode` is set to `dev` or `prod`. Optique's dependency system provides type-safe support for these inter-option relationships. The dependency system works by deferring the final validation of dependent options until all options have been parsed. During parsing, dependent options record their raw input and preliminary result in a shared input trace. After parsing, Optique builds a shared dependency runtime, resolves dependency source values, and replays dependent parsers with the actual dependency values. ## Creating a dependency source To create a dependency relationship, first wrap an existing value parser with `dependency()` to create a *dependency source*. A dependency source is a value parser that can be referenced by other parsers: ```typescript twoslash import { dependency } from "@optique/core/dependency"; import { choice } from "@optique/core/valueparser"; // Create a dependency source from a choice parser const modeParser = dependency(choice(["dev", "prod"] as const)); ``` The `dependency()` function returns a `DependencySource` that behaves exactly like the wrapped parser but can be used to create derived parsers. ## Creating a derived parser Once you have a dependency source, use its `derive()` method to create a *derived parser*. The derived parser's behavior depends on the source's value: ```typescript twoslash import { dependency } from "@optique/core/dependency"; import { choice } from "@optique/core/valueparser"; const modeParser = dependency(choice(["dev", "prod"] as const)); // ---cut-before--- // Create a derived parser that depends on the mode const logLevelParser = modeParser.derive({ metavar: "LEVEL", mode: "sync", factory: (mode) => choice( mode === "dev" ? ["debug", "info", "warn", "error"] : ["warn", "error"] ), defaultValue: () => "dev" as const, }); ``` The `derive()` method takes an options object with four properties: `metavar` : The metavariable name shown in help text (e.g., `"LEVEL"`). `mode` : The mode of the parser returned by the factory: `"sync"` or `"async"`. This determines whether the derived parser is synchronous or asynchronous, without calling the factory at construction time. `factory` : A function that receives the dependency's value and returns a value parser. This function is called during dependency resolution with the actual dependency value. `defaultValue` : A function that returns the default value to use when the dependency is not provided. This allows the derived parser to work even when the dependency option is omitted. ## Async factory support The `factory` function can return either a sync or async value parser. When the factory returns an async parser, the resulting derived parser will also be async: ```typescript twoslash import type { ValueParser } from "@optique/core/valueparser"; declare function gitRemoteBranch(options: { remote: string }): ValueParser<"async", string>; // ---cut-before--- import { dependency } from "@optique/core/dependency"; import { string } from "@optique/core/valueparser"; const remoteParser = dependency(string({ metavar: "REMOTE" })); // Factory returns an async parser - derived parser is also async const branchParser = remoteParser.derive({ metavar: "BRANCH", mode: "async", factory: (remote) => gitRemoteBranch({ remote }), defaultValue: () => "origin", }); // branchParser.mode is "async" ``` For explicit control over the factory mode, use `deriveSync()` or `deriveAsync()` instead of `derive()`: ```typescript twoslash import { dependency } from "@optique/core/dependency"; import { choice, string } from "@optique/core/valueparser"; const modeParser = dependency(choice(["dev", "prod"] as const)); // Explicitly sync factory const logLevelParser = modeParser.deriveSync({ metavar: "LEVEL", factory: (mode) => choice(mode === "dev" ? ["debug", "info", "warn", "error"] : ["warn", "error"]), defaultValue: () => "dev" as const, }); ``` The mode of the resulting derived parser is determined by combining the source parser's mode and the factory's return mode: | Source mode | Factory returns | Result mode | | ----------- | --------------- | ----------- | | sync | sync parser | sync | | sync | async parser | async | | async | sync parser | async | | async | async parser | async | ## Using dependencies in parsers Use the dependency source and derived parser as regular value parsers in your option definitions: ```typescript twoslash import { dependency } from "@optique/core/dependency"; import { object } from "@optique/core/constructs"; import { parseSync } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { choice } from "@optique/core/valueparser"; const modeParser = dependency(choice(["dev", "prod"] as const)); const logLevelParser = modeParser.derive({ metavar: "LEVEL", mode: "sync", factory: (mode) => choice(mode === "dev" ? ["debug", "info", "warn", "error"] : ["warn", "error"]), defaultValue: () => "dev" as const, }); // ---cut-before--- const parser = object({ mode: option("--mode", modeParser), logLevel: option("--log-level", logLevelParser), }); // In dev mode, debug and info are valid const result1 = parseSync(parser, ["--mode", "dev", "--log-level", "debug"]); // result1.value = { mode: "dev", logLevel: "debug" } // In prod mode, only warn and error are valid const result2 = parseSync(parser, ["--mode", "prod", "--log-level", "warn"]); // result2.value = { mode: "prod", logLevel: "warn" } ``` This replay happens automatically during normal `parse*()` and `suggest*()` flows, whether the parsers appear at the top level or inside combinators like `object()`, `tuple()`, `merge()`, and `concat()`. You do not need any special handling beyond using the dependency source and derived parser together. Dependencies also work across parser combinators like `merge()` and `concat()`. For example, you can have the dependency source in one `object()` and the derived parser in another, then combine them with `merge()`: ```typescript twoslash import { dependency } from "@optique/core/dependency"; import { merge, object } from "@optique/core/constructs"; import { parseSync } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { choice, string } from "@optique/core/valueparser"; const modeParser = dependency(choice(["dev", "prod"] as const)); const logLevelParser = modeParser.derive({ metavar: "LEVEL", mode: "sync", factory: (mode) => choice(mode === "dev" ? ["debug", "info", "warn", "error"] : ["warn", "error"]), defaultValue: () => "dev" as const, }); // ---cut-before--- // Dependency source and derived parser in separate objects const parser = merge( object({ mode: option("--mode", modeParser) }), object({ logLevel: option("--log-level", logLevelParser), name: option("--name", string()), }), ); // Dependencies are resolved across merged objects const result = parseSync(parser, [ "--mode", "prod", "--log-level", "warn", "--name", "app" ]); // result.value = { mode: "prod", logLevel: "warn", name: "app" } ``` ## Option ordering independence The dependency system handles options in any order. Even if the dependent option appears before its dependency on the command line, the resolution works correctly: ```typescript twoslash import { dependency } from "@optique/core/dependency"; import { object } from "@optique/core/constructs"; import { parseSync } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { choice } from "@optique/core/valueparser"; const modeParser = dependency(choice(["dev", "prod"] as const)); const logLevelParser = modeParser.derive({ metavar: "LEVEL", mode: "sync", factory: (mode) => choice(mode === "dev" ? ["debug", "info", "warn", "error"] : ["warn", "error"]), defaultValue: () => "dev" as const, }); const parser = object({ mode: option("--mode", modeParser), logLevel: option("--log-level", logLevelParser), }); // ---cut-before--- // --log-level appears before --mode, but resolution still works const result = parseSync(parser, [ "--log-level", "error", "--mode", "prod" ]); // result.value = { mode: "prod", logLevel: "error" } ``` ## Default value behavior When the dependency option is not provided, the derived parser uses its `defaultValue` function to determine the dependency value: ```typescript twoslash import { dependency } from "@optique/core/dependency"; import { object } from "@optique/core/constructs"; import { optional } from "@optique/core/modifiers"; import { parseSync } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { choice } from "@optique/core/valueparser"; const modeParser = dependency(choice(["dev", "prod"] as const)); const logLevelParser = modeParser.derive({ metavar: "LEVEL", mode: "sync", factory: (mode) => choice(mode === "dev" ? ["debug", "info", "warn", "error"] : ["warn", "error"]), defaultValue: () => "dev" as const, // Default to dev mode }); const parser = object({ mode: optional(option("--mode", modeParser)), logLevel: option("--log-level", logLevelParser), }); // ---cut-before--- // Without --mode, defaultValue() returns "dev" // So "debug" is valid (it's in the dev mode choices) const result = parseSync(parser, ["--log-level", "debug"]); // result.value = { mode: undefined, logLevel: "debug" } ``` ## Multiple dependencies with `deriveFrom()` For parsers that depend on multiple options, use the `deriveFrom()` function instead of the `derive()` method: ```typescript twoslash import { dependency, deriveFrom } from "@optique/core/dependency"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { choice, string } from "@optique/core/valueparser"; // Create multiple dependency sources const envParser = dependency(choice(["local", "staging", "production"] as const)); const regionParser = dependency(choice(["us", "eu", "asia"] as const)); // Create a parser that depends on both const serverParser = deriveFrom({ metavar: "SERVER", mode: "sync", dependencies: [envParser, regionParser] as const, factory: (env, region) => { // Generate valid servers based on both environment and region const servers = []; if (env === "local") { servers.push("localhost"); } else { servers.push(`${env}-${region}-1`, `${env}-${region}-2`); } return choice(servers); }, defaultValues: () => ["local", "us"] as const, }); const parser = object({ env: option("--env", envParser), region: option("--region", regionParser), server: option("--server", serverParser), }); ``` Like `derive()`, `deriveFrom()` also supports async factories. Use `deriveFromSync()` or `deriveFromAsync()` for explicit mode control: ```typescript twoslash import { dependency, deriveFromSync } from "@optique/core/dependency"; import { choice } from "@optique/core/valueparser"; const envParser = dependency(choice(["local", "staging", "production"] as const)); const regionParser = dependency(choice(["us", "eu", "asia"] as const)); // Explicitly sync factory const serverParser = deriveFromSync({ metavar: "SERVER", dependencies: [envParser, regionParser] as const, factory: (env, region) => choice(env === "local" ? ["localhost"] : [`${env}-${region}-1`, `${env}-${region}-2`]), defaultValues: () => ["local", "us"] as const, }); ``` ## Shell completion support The dependency system integrates with Optique's shell completion. When generating completions for a derived parser, the system is context-aware: * If the dependency option has already been specified on the command line, completions are generated based on that actual value. * If the dependency option hasn't been specified yet, the system uses the `defaultValue` to generate reasonable suggestions. This means users get accurate completions that reflect the current state of their command line: ```typescript twoslash import { dependency } from "@optique/core/dependency"; import { object } from "@optique/core/constructs"; import { suggestAsync } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { choice } from "@optique/core/valueparser"; const modeParser = dependency(choice(["dev", "prod"] as const)); const portParser = modeParser.derive({ metavar: "PORT", mode: "sync", factory: (mode) => choice(mode === "dev" ? ["3000", "8080"] : ["80", "443"]), defaultValue: () => "dev" as const, }); const parser = object({ mode: option("--mode", modeParser), port: option("--port", portParser), }); // ---cut-before--- // With --mode prod already specified, completions show prod ports const suggestions = await suggestAsync(parser, ["--mode", "prod", "--port", ""]); // suggestions include "80" and "443" (prod mode ports) // Without --mode, completions use defaultValue ("dev") const defaultSuggestions = await suggestAsync(parser, ["--port", ""]); // suggestions include "3000" and "8080" (dev mode ports) ``` ## Practical example: Git-like CLI Here's a more realistic example showing how dependencies can be used in a Git-like CLI where the valid branches depend on the remote: ```typescript twoslash declare function fetchRemotes(): string[]; declare function fetchBranches(remote: string): string[]; // ---cut-before--- import { dependency } from "@optique/core/dependency"; import { object } from "@optique/core/constructs"; import { option, argument } from "@optique/core/primitives"; import { choice, string } from "@optique/core/valueparser"; // Remote is a dependency source const remoteParser = dependency(choice(fetchRemotes())); // Branch depends on which remote is selected const branchParser = remoteParser.derive({ metavar: "BRANCH", mode: "sync", factory: (remote) => choice(fetchBranches(remote)), defaultValue: () => "origin", }); const pushCommand = object({ remote: argument(remoteParser), branch: argument(branchParser), force: option("-f", "--force"), }); ``` ## Limitations The current dependency implementation has some limitations to be aware of: * *No nested dependencies*: A derived parser cannot itself be used as a dependency source. Dependencies form a single level of relationships. However, you can have multiple derived parsers that depend on the same source, or use `deriveFrom()` to depend on multiple sources simultaneously. * *`deriveFrom()` requires dependency sources*: The `dependencies` array in `deriveFrom()` must contain `DependencySource` objects created with `dependency()`, not derived parsers. If you need a parser that depends on both a source and a derived value, consider restructuring to have multiple sources instead. --- --- url: /integrations/inquirer.md description: >- Add interactive prompts as fallback for missing CLI arguments using Inquirer.js. --- # Interactive prompts *This API is available since Optique 1.0.0.* The *@optique/inquirer* package wraps any Optique parser with an interactive [Inquirer.js] prompt. When the user provides a value via CLI, that value is used directly. When the argument is absent, an interactive prompt is shown instead of failing. The fallback priority is: 1. *CLI argument* 2. *Interactive prompt* Because interactive prompts are inherently asynchronous, the returned parser always has `mode: "async"`. ::: code-group ```bash [Deno] deno add jsr:@optique/inquirer ``` ```bash [npm] npm add @optique/inquirer ``` ```bash [pnpm] pnpm add @optique/inquirer ``` ```bash [Yarn] yarn add @optique/inquirer ``` ```bash [Bun] bun add @optique/inquirer ``` ::: [Inquirer.js]: https://github.com/SBoudrias/Inquirer.js ## Basic usage Wrap any parser with `prompt()` and provide a prompt configuration object: ```typescript twoslash import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; import { prompt } from "@optique/inquirer"; import { run } from "@optique/run"; const parser = object({ name: prompt(option("--name", string()), { type: "input", message: "Enter your name:", }), port: prompt(option("--port", integer()), { type: "number", message: "Enter the port number:", default: 3000, }), }); await run(parser); ``` When `--name` and `--port` are provided on the command line, the prompts are skipped. When they are absent, the user sees interactive prompts. ## Prompt types ### `input` — free-text string Prompts the user for an arbitrary string value: ```typescript twoslash import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { prompt } from "@optique/inquirer"; const name = prompt(option("--name", string()), { type: "input", message: "Enter your name:", default: "World", validate: (value) => value.length > 0 || "Name cannot be empty.", }); ``` `input` properties `message` : *(required)* The question to display. `default` : Pre-filled text shown in the input field. `validate` : Function called when the user submits. Return `true` to accept or a string error message to reject and re-prompt. ### `confirm` — Boolean yes/no Prompts the user with a yes/no question: ```typescript twoslash import { flag } from "@optique/core/primitives"; import { prompt } from "@optique/inquirer"; const verbose = prompt(flag("--verbose"), { type: "confirm", message: "Enable verbose output?", default: false, }); ``` `confirm` properties `message` : *(required)* The question to display. `default` : Default answer when the user presses Enter without typing. ### `number` — numeric input Prompts the user for a number: ```typescript twoslash import { option } from "@optique/core/primitives"; import { integer } from "@optique/core/valueparser"; import { prompt } from "@optique/inquirer"; const port = prompt(option("--port", integer()), { type: "number", message: "Enter the port:", default: 8080, min: 1, max: 65535, }); ``` `number` properties `message` : *(required)* The question to display. `default` : Default number shown to the user. `min`, `max` : Accepted value range. `step` : Granularity of valid values. Use `"any"` for arbitrary decimals. > \[!NOTE] > If the user submits the prompt without entering a number (leaving it blank), > the result is a parse failure rather than `undefined`. ### `password` — masked input Prompts for a secret value without displaying the characters: ```typescript twoslash import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { prompt } from "@optique/inquirer"; const apiKey = prompt(option("--api-key", string()), { type: "password", message: "Enter your API key:", mask: true, }); ``` `password` properties `message` : *(required)* The question to display. `mask` : When `true`, show `*` for each keystroke. When `false` or omitted, input is completely hidden. `validate` : Same as `input`. ### `editor` — multi-line text Opens the user's `$VISUAL` or `$EDITOR` for multi-line input: ```typescript twoslash import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { prompt } from "@optique/inquirer"; const message = prompt(option("--message", string()), { type: "editor", message: "Write your commit message:", default: "", validate: (value) => value.trim().length > 0 || "Message cannot be empty.", }); ``` `editor` properties `message` : *(required)* The question to display. `default` : Content pre-filled in the editor buffer. `validate` : Same as `input`. ### `select` — arrow-key single-select Shows a scrollable list where the user selects one option using arrow keys: ```typescript twoslash import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { prompt } from "@optique/inquirer"; const env = prompt(option("--env", string()), { type: "select", message: "Choose the deployment environment:", choices: ["development", "staging", "production"], default: "development", }); ``` Choices can also be objects with display names and descriptions: ```typescript twoslash import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { prompt, Separator } from "@optique/inquirer"; const color = prompt(option("--color", string()), { type: "select", message: "Choose a color:", choices: [ { value: "red", name: "Red", description: "A warm primary color" }, { value: "green", name: "Green", description: "A cool secondary color" }, new Separator("──────────"), { value: "custom", name: "Custom…", disabled: "Coming soon" }, ], }); ``` `select` properties `message` : *(required)* The question to display. `choices` : *(required)* Array of strings, [`Choice`] objects, or `Separator` instances. `default` : Initially highlighted choice value. [`Choice`]: #choice ### `rawlist` — numbered list Shows a numbered list and prompts the user to type a number: ```typescript twoslash import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { prompt } from "@optique/inquirer"; const format = prompt(option("--format", string()), { type: "rawlist", message: "Choose the output format:", choices: ["json", "yaml", "toml"], }); ``` `rawlist` properties `message` : *(required)* The question to display. `choices` : *(required)* Array of strings or [`Choice`] objects. `default` : Pre-selected choice value. ### `expand` — keyboard shortcut single-select Prompts the user to press a single key to select an option: ```typescript twoslash import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { prompt } from "@optique/inquirer"; const action = prompt(option("--action", string()), { type: "expand", message: "What do you want to do?", choices: [ { value: "overwrite", name: "Overwrite", key: "o" }, { value: "skip", name: "Skip", key: "s" }, { value: "abort", name: "Abort", key: "a" }, ], }); ``` `expand` properties `message` : *(required)* The question to display. `choices` : *(required)* Array of [`ExpandChoice`] objects, each with a single lowercase alphanumeric `key`. `default` : Default choice key. [`ExpandChoice`]: #expandchoice ### `checkbox` — multi-select Shows a scrollable list where the user toggles multiple options with Space: ```typescript twoslash import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { multiple } from "@optique/core/modifiers"; import { prompt } from "@optique/inquirer"; const tags = prompt(multiple(option("--tag", string())), { type: "checkbox", message: "Select tags:", choices: ["typescript", "deno", "node", "bun"], }); ``` The inner parser must produce `readonly string[]`, so use `multiple()` around an option or argument parser. `checkbox` properties `message` : *(required)* The question to display. `choices` : *(required)* Array of strings, [`Choice`] objects, or `Separator` instances. ## Prompt-only values When a value should *only* come from a prompt (no CLI flag at all), pair `prompt()` with `fail()`: ```typescript twoslash import { object } from "@optique/core/constructs"; import { fail } from "@optique/core/primitives"; import { prompt } from "@optique/inquirer"; const parser = object({ name: prompt(fail(), { type: "input", message: "Enter your name:", }), confirm: prompt(fail(), { type: "confirm", message: "Are you sure?", default: false, }), }); ``` `fail()` always fails the CLI parse, so the prompt runs unconditionally. ## Optional prompts Wrap the inner parser with `optional()` to allow the user to skip the prompt via CLI while still showing a prompt when the flag is absent. This is equivalent to any other `prompt()` usage—`optional()` is handled transparently: ```typescript twoslash import { option } from "@optique/core/primitives"; import { optional } from "@optique/core/modifiers"; import { string } from "@optique/core/valueparser"; import { prompt } from "@optique/inquirer"; const description = prompt(optional(option("--description", string())), { type: "input", message: "Enter a description (or press Enter to skip):", }); ``` > \[!NOTE] > In this case, if the user just presses Enter at the prompt, the returned > value is an empty string `""`, not `undefined`. To get `undefined` when > the user leaves the field blank, use `validate` to reject empty input or > handle the empty string in your application. ## Composing with other integrations `prompt()` composes naturally with `bindEnv()` and `bindConfig()`. The innermost wrapper is evaluated first, so nesting order determines fallback priority. This works the same inside `object()`, `tuple()`, `merge()`, and `concat()`, including dependency-aware `suggest*()` flows. For example, to fall back to an environment variable before prompting: ```typescript twoslash import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { bindEnv, createEnvContext } from "@optique/env"; import { prompt } from "@optique/inquirer"; import { run } from "@optique/run"; const envContext = createEnvContext({ prefix: "MYAPP_" }); const parser = object({ apiKey: prompt( bindEnv(option("--api-key", string()), { context: envContext, key: "API_KEY", parser: string(), }), { type: "password", message: "Enter your API key:", mask: true, }, ), }); await run(parser, { contexts: [envContext] }); ``` This gives the priority: CLI argument > Environment variable > Interactive prompt ## Testing All prompt configuration types accept an optional `prompter` property for testing. When provided, the function is called instead of launching an interactive Inquirer.js prompt: ```typescript twoslash import { parseAsync } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { prompt } from "@optique/inquirer"; const parser = prompt(option("--name", string()), { type: "input", message: "Enter your name:", prompter: () => Promise.resolve("Alice"), // used in tests }); const result = await parseAsync(parser, []); // result.value === "Alice" ``` ## API reference ### `prompt(parser, config)` Wraps a parser with an interactive prompt fallback. Parameters : - `parser`: The inner parser. CLI tokens consumed by this parser suppress the prompt. \- `config`: A [`PromptConfig`] object specifying the prompt type and its options. Returns : A new parser with `mode: "async"` and interactive prompt fallback. The `usage` is wrapped in an `optional` term since the prompt handles the missing-value case. [`PromptConfig`]: #promptconfigt ### `PromptConfig` A conditional type that maps a parser's value type `T` to the appropriate prompt configuration union: | Value type | Accepted config type | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `boolean` | [`ConfirmConfig`] | | `number` | [`NumberPromptConfig`] | | `string` | [`InputConfig`] | [`PasswordConfig`](#password--masked-input) | [`EditorConfig`](#editor--multi-line-text) | [`SelectConfig`](#select--arrow-key-single-select) | [`RawlistConfig`](#rawlist--numbered-list) | [`ExpandConfig`](#expand--keyboard-shortcut-single-select) | | `readonly string[]` | [`CheckboxConfig`] | Optional variants (`boolean | undefined`, `string | undefined`, etc.) map to the same config types as their non-optional counterparts. [`ConfirmConfig`]: #confirm--boolean-yesno [`NumberPromptConfig`]: #number--numeric-input [`InputConfig`]: #input--free-text-string [`CheckboxConfig`]: #checkbox--multi-select ### `Choice` An object with `value`, optional `name`, `description`, `short`, and `disabled` fields. Used in `select`, `rawlist`, and `checkbox` prompts. ### `ExpandChoice` Like [`Choice`] but requires a `key` field (single lowercase alphanumeric character). Used in `expand` prompts. ### `Separator` Re-exported from Inquirer.js. Use `new Separator(text?)` to add visual dividers in `select` and `checkbox` choice lists. ## Prompt and inner parser independence The CLI path and the prompt path are *independent value sources*. When a value comes from the CLI, the inner parser's full constraint pipeline (value parsing, `choice()` domain checks, `integer({ min, max })`, etc.) is applied. When a value comes from a prompt, it is used as-is—the inner parser's constraints are *not* re-applied. This design is intentional: combinators like `map()` can transform the value domain, making the prompted value incompatible with the inner parser's input path. Treating the two paths independently avoids false rejections and keeps the architecture sound. As a consequence, any runtime validation you need on prompted values must be configured in the prompt config itself. Some prompt types provide a `validate` option for this purpose. ### Matching constraints between CLI and prompt When the inner parser carries constraints, you should mirror them in the prompt config. `number` prompt with `integer()` semantics : Use `step: 1` to restrict the prompt to integers, and `min`/`max` to match the inner parser's range. ``` ~~~~ typescript twoslash import { option } from "@optique/core/primitives"; import { integer } from "@optique/core/valueparser"; import { prompt } from "@optique/inquirer"; const port = prompt(option("--port", integer({ min: 1024, max: 65535 })), { type: "number", message: "Enter the port:", min: 1024, max: 65535, step: 1, }); ~~~~ ``` `input` prompt with `string({ pattern })` semantics : Use `validate` to enforce the same pattern. ``` ~~~~ typescript twoslash import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { prompt } from "@optique/inquirer"; const id = prompt(option("--id", string({ pattern: /^[A-Z]{3}-\d+$/ })), { type: "input", message: "Enter the ID:", validate: (value) => /^[A-Z]{3}-\d+$/.test(value) || "Must match AAA-123 format.", }); ~~~~ ``` `select`/`rawlist`/`expand` with `choice()` values : Keep the prompt `choices` array consistent with the inner parser's `choice()` domain. Ensuring this consistency is the caller's responsibility. ``` ~~~~ typescript twoslash import { option } from "@optique/core/primitives"; import { choice } from "@optique/core/valueparser"; import { prompt } from "@optique/inquirer"; const env = prompt(option("--env", choice(["dev", "staging", "prod"])), { type: "select", message: "Choose environment:", choices: ["dev", "staging", "prod"], // must match choice() values }); ~~~~ ``` `checkbox` with `multiple()` cardinality : The `checkbox` prompt type does not currently support a `validate` callback, so cardinality constraints from `multiple(..., { min, max })` cannot be enforced at the prompt level. This is a known limitation; prompted checkbox values may violate the inner parser's cardinality bounds. > \[!IMPORTANT] > `select`, `rawlist`, `expand`, and `checkbox` prompt types do not > expose a `validate` callback. For these types, there is currently no > way to add custom runtime validation on prompted values. This is a > known limitation that may be addressed in a future release. ## Limitations * *Always async* — `prompt()` always returns an async parser because Inquirer.js prompts are inherently asynchronous. This means any `object()` or other combinator containing a `prompt()` parser also becomes async. * *No shell completion* — Interactive prompts do not contribute to shell tab-completion suggestions. Only the wrapped inner parser's suggestions are used. * *Single prompt per field* — Each `prompt()` call runs the prompter exactly once per parse, even when used inside `object()`. * *TTY required*: Inquirer.js requires an interactive terminal (TTY). In non-interactive environments (CI pipelines, piped input), prompts will error. Use the `prompter` override for non-interactive testing. > \[!TIP] > See the [cookbook](../cookbook.md#combining-with-interactive-prompts) for > a complete example combining interactive prompts with environment variables > and configuration files. --- --- url: /integrations/logtape.md description: >- Integrate LogTape logging with Optique CLI applications using various parsing strategies for log levels, verbosity flags, and output destinations. --- # LogTape integration *This API is available since Optique 0.8.0.* The *@optique/logtape* package provides seamless integration with [LogTape], enabling you to configure logging through command-line arguments. This package offers multiple approaches to control log verbosity and output destinations, from simple debug flags to sophisticated verbosity accumulation. ::: code-group ```bash [Deno] deno add --jsr @optique/logtape @logtape/logtape ``` ```bash [npm] npm add @optique/logtape @logtape/logtape ``` ```bash [pnpm] pnpm add @optique/logtape @logtape/logtape ``` ```bash [Yarn] yarn add @optique/logtape @logtape/logtape ``` ```bash [Bun] bun add @optique/logtape @logtape/logtape ``` ::: [LogTape]: https://logtape.org/ ## Quick start The fastest way to add logging configuration to your CLI is using the `loggingOptions()` preset: ```typescript twoslash import { loggingOptions, createLoggingConfig } from "@optique/logtape"; import { object } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; import { configure } from "@logtape/logtape"; const parser = object({ logging: loggingOptions({ level: "verbosity" }), }); const args = ["-vv", "--log-output=-"]; const result = parse(parser, args); if (result.success) { const config = await createLoggingConfig(result.value.logging); await configure(config); } ``` This example enables: * `-v`, `-vv`, `-vvv` flags for increasing verbosity * `--log-output` option for directing logs to console (`-`) or a file ## Log level value parser The `logLevel()` function creates a value parser for LogTape's `LogLevel` type. It accepts log level strings and validates them against LogTape's supported levels. ```typescript twoslash import { logLevel } from "@optique/logtape"; import { option } from "@optique/core/primitives"; import { withDefault } from "@optique/core/modifiers"; import { object, merge } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; import type { LogLevel } from "@logtape/logtape"; const parser = object({ level: withDefault( option("--log-level", "-l", logLevel()), "info" as LogLevel ), }); // Accepts: "trace", "debug", "info", "warning", "error", "fatal" const result = parse(parser, ["--log-level=debug"]); ``` ### Features * *Case-insensitive parsing*: Accepts `"DEBUG"`, `"Debug"`, `"debug"` * *Shell completion*: Provides suggestions for all valid log levels * *Custom metavar*: Default is `"LEVEL"`, customizable via options ### Options ```typescript twoslash import { logLevel } from "@optique/logtape"; // ---cut-before--- const level = logLevel({ metavar: "LOG_LEVEL", // Custom metavar for help text }); ``` ## Verbosity parser The `verbosity()` parser implements the common `-v`/`-vv`/`-vvv` pattern for controlling log verbosity. Each additional `-v` flag increases the verbosity (decreases the log level severity). ```typescript twoslash import { verbosity } from "@optique/logtape"; import { object } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; const parser = object({ logLevel: verbosity(), }); // No flags → "warning" parse(parser, []); // -v → "info" parse(parser, ["-v"]); // -vv → "debug" parse(parser, ["-v", "-v"]); // -vvv → "trace" parse(parser, ["-v", "-v", "-v"]); // Additional flags beyond -vvv stay at "trace" parse(parser, ["-v", "-v", "-v", "-v"]); ``` ### Level mapping With the default `baseLevel: "warning"`: | Flags | Level | | ------- | ----------- | | (none) | `"warning"` | | `-v` | `"info"` | | `-vv` | `"debug"` | | `-vvv`+ | `"trace"` | ### Options ```typescript twoslash import { verbosity } from "@optique/logtape"; // ---cut-before--- const level = verbosity({ short: "-v", // Short option (default: "-v") long: "--verbose", // Long option (default: "--verbose") baseLevel: "error", // Starting level (default: "warning") }); ``` With `baseLevel: "error"`: | Flags | Level | | -------- | ----------- | | (none) | `"error"` | | `-v` | `"warning"` | | `-vv` | `"info"` | | `-vvv` | `"debug"` | | `-vvvv`+ | `"trace"` | ## Debug flag parser The `debug()` parser provides a simple boolean toggle for enabling debug logging. This is ideal for applications that don't need fine-grained verbosity control. ```typescript twoslash import { debug } from "@optique/logtape"; import { object } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; const parser = object({ logLevel: debug(), }); // No flag → "info" const normal = parse(parser, []); // --debug or -d → "debug" const debugging = parse(parser, ["--debug"]); ``` ### Options ```typescript twoslash import { debug } from "@optique/logtape"; // ---cut-before--- const level = debug({ short: "-d", // Short option (default: "-d") long: "--debug", // Long option (default: "--debug") debugLevel: "trace", // Level when flag is present (default: "debug") normalLevel: "warning", // Level when flag is absent (default: "info") }); ``` ## Log output parser The `logOutput()` parser handles log output destinations. Following CLI conventions, it accepts `-` for console output or a file path for file output. ```typescript twoslash import { logOutput } from "@optique/logtape"; import { object } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; const parser = object({ output: logOutput(), }); // Console output const console = parse(parser, ["--log-output=-"]); // → { type: "console" } // File output const file = parse(parser, ["--log-output=/var/log/app.log"]); // → { type: "file", path: "/var/log/app.log" } // Optional: undefined when not specified const none = parse(parser, []); // → undefined ``` ### Options ```typescript twoslash import { logOutput } from "@optique/logtape"; // ---cut-before--- const output = logOutput({ long: "--log-output", // Long option (default: "--log-output") short: "-o", // Optional short option metavar: "FILE", // Metavar for help text (default: "FILE") }); ``` ## Logging options preset The `loggingOptions()` function creates a complete logging configuration parser that combines log level and output options into a single group. It uses a discriminated union configuration to enforce mutually exclusive level selection methods. ### Using explicit log level option ```typescript twoslash import { loggingOptions } from "@optique/logtape"; import { object } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; const parser = object({ logging: loggingOptions({ level: "option" }), }); // --log-level=debug --log-output=/var/log/app.log const result = parse(parser, ["--log-level=debug", "--log-output=-"]); ``` Configuration options for `level: "option"`: | Option | Default | Description | | --------- | --------------- | ----------------- | | `long` | `"--log-level"` | Long option name | | `short` | `"-l"` | Short option name | | `default` | `"info"` | Default log level | ### Using verbosity flags ```typescript twoslash import { loggingOptions } from "@optique/logtape"; import { object } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; const parser = object({ logging: loggingOptions({ level: "verbosity" }), }); // -vv --log-output=- const result = parse(parser, ["-v", "-v", "--log-output=-"]); ``` Configuration options for `level: "verbosity"`: | Option | Default | Description | | ----------- | ------------- | ----------------- | | `short` | `"-v"` | Short option name | | `long` | `"--verbose"` | Long option name | | `baseLevel` | `"warning"` | Base log level | ### Using debug flag ```typescript twoslash import { loggingOptions } from "@optique/logtape"; import { object } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; const parser = object({ logging: loggingOptions({ level: "debug" }), }); // --debug const result = parse(parser, ["--debug"]); ``` Configuration options for `level: "debug"`: | Option | Default | Description | | ------------- | ----------- | -------------------------- | | `short` | `"-d"` | Short option name | | `long` | `"--debug"` | Long option name | | `debugLevel` | `"debug"` | Level when flag is present | | `normalLevel` | `"info"` | Level when flag is absent | ### Common options All three modes share these options: | Option | Default | Description | | ---------------- | ------------------- | ---------------------------- | | `output.enabled` | `true` | Enable `--log-output` option | | `output.long` | `"--log-output"` | Long option name for output | | `groupLabel` | `"Logging options"` | Help text group label | ## Creating LogTape configuration The `createLoggingConfig()` function converts parsed logging options into a LogTape configuration object that can be passed directly to `configure()`. ```typescript twoslash import { loggingOptions, createLoggingConfig } from "@optique/logtape"; import { object } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; import { configure } from "@logtape/logtape"; const parser = object({ logging: loggingOptions({ level: "verbosity" }), }); const result = parse(parser, ["-vv"]); if (result.success) { const config = await createLoggingConfig(result.value.logging); await configure(config); } ``` ### Console sink options Customize how console output is handled: ```typescript twoslash import { loggingOptions, createLoggingConfig } from "@optique/logtape"; import { object } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; const parser = object({ logging: loggingOptions({ level: "option" }), }); const result = parse(parser, ["--log-level=debug"]); if (!result.success) throw new Error(); // ---cut-before--- // Write all logs to stderr const config1 = await createLoggingConfig(result.value.logging, { stream: "stderr", }); // Dynamic stream selection based on log level const config2 = await createLoggingConfig(result.value.logging, { streamResolver: (level) => level === "error" || level === "fatal" ? "stderr" : "stdout", }); ``` ### Additional LogTape configuration Extend the generated configuration with custom loggers, filters, or sinks: ```typescript twoslash import { loggingOptions, createLoggingConfig } from "@optique/logtape"; import { object } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; import { configure } from "@logtape/logtape"; const parser = object({ logging: loggingOptions({ level: "option" }), }); const result = parse(parser, ["--log-level=debug"]); if (!result.success) throw new Error(); // ---cut-before--- const config = await createLoggingConfig(result.value.logging, {}, { loggers: [ // Add category-specific logging { category: ["database"], lowestLevel: "warning", sinks: ["default"] }, { category: ["http"], lowestLevel: "info", sinks: ["default"] }, ], }); await configure(config); ``` ## Creating sinks ### Console sink The `createConsoleSink()` function creates a console sink with configurable stream selection: ```typescript twoslash import { createConsoleSink } from "@optique/logtape"; // Default: write to stderr const sink1 = createConsoleSink(); // Write to stdout const sink2 = createConsoleSink({ stream: "stdout" }); // Dynamic stream based on log level const sink3 = createConsoleSink({ streamResolver: (level) => level === "error" || level === "fatal" ? "stderr" : "stdout", }); ``` ### Creating sink from LogOutput The `createSink()` function creates a LogTape sink from a `LogOutput` value: ```typescript twoslash import { createSink, type LogOutput } from "@optique/logtape"; // Console sink const consoleSink = await createSink({ type: "console" }); // File sink (requires @logtape/file package) const fileSink = await createSink({ type: "file", path: "/var/log/app.log" }); ``` > \[!NOTE] > File output requires the `@logtape/file` package: > > ::: code-group > > ```bash [Deno] > deno add jsr:@logtape/file > ``` > > ```bash [npm] > npm add @logtape/file > ``` > > ```bash [pnpm] > pnpm add @logtape/file > ``` > > ```bash [Yarn] > yarn add @logtape/file > ``` > > ```bash [Bun] > bun add @logtape/file > ``` > > ::: ## Complete example Here's a complete example showing a CLI application with full logging configuration: ```typescript twoslash import { loggingOptions, createLoggingConfig } from "@optique/logtape"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; import { withDefault } from "@optique/core/modifiers"; import { parse } from "@optique/core/parser"; import { configure, getLogger } from "@logtape/logtape"; const parser = object({ // Application options host: withDefault(option("--host", "-h", string()), "localhost"), port: withDefault(option("--port", "-p", integer({ min: 1, max: 65535 })), 3000), // Logging configuration logging: loggingOptions({ level: "verbosity" }), }); const result = parse(parser, ["--host", "0.0.0.0", "-vv"]); if (result.success) { // Configure LogTape const config = await createLoggingConfig(result.value.logging, { streamResolver: (level) => level === "error" || level === "fatal" ? "stderr" : "stdout", }); await configure(config); // Use LogTape const logger = getLogger(["myapp"]); logger.info`Starting server on ${result.value.host}:${result.value.port}`; logger.debug`Debug logging enabled`; } ``` Usage examples: ```bash # Default logging (warning level) myapp --host 0.0.0.0 # Info level myapp --host 0.0.0.0 -v # Debug level myapp --host 0.0.0.0 -vv # Trace level with file output myapp --host 0.0.0.0 -vvv --log-output=/var/log/myapp.log ``` --- --- url: /concepts/man.md description: >- Learn how to generate Unix man pages from your CLI application using Optique's man page generator. Covers man page format, customization options, and integration patterns. --- # Man pages *This API is available since Optique 0.10.0.* Man pages are the traditional Unix documentation format, providing offline help that users can access with the `man` command. Optique can generate man pages directly from your parser definitions, ensuring documentation stays synchronized with your CLI's actual behavior. The *@optique/man* package generates man pages in the standard man(7) roff format, compatible with `groff`, `mandoc`, and other man page processors. ## Installation ::: code-group ```bash [Deno] deno add jsr:@optique/man ``` ```bash [npm] npm add @optique/man ``` ```bash [pnpm] pnpm add @optique/man ``` ```bash [Yarn] yarn add @optique/man ``` ```bash [Bun] bun add @optique/man ``` ::: ## Basic usage The `generateManPage()` function creates a man page from a parser: ```typescript twoslash import { object } from "@optique/core/constructs"; import { message } from "@optique/core/message"; import { option, argument } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { generateManPage } from "@optique/man"; const parser = object({ output: option("-o", "--output", string(), { description: message`Output file path.`, }), verbose: option("-v", "--verbose", { description: message`Enable verbose output.`, }), input: argument(string({ metavar: "FILE" }), { description: message`Input file to process.`, }), }); const manPage = generateManPage(parser, { name: "myapp", section: 1, date: "January 2026", }); console.log(manPage); ``` This generates a complete man page with NAME, SYNOPSIS, DESCRIPTION, and OPTIONS sections. ## Using with Program *This feature is available since Optique 0.10.0.* If you're using the [`Program`](./runners.md#program-interface) interface, `generateManPage()` can extract metadata directly from your program: ```typescript twoslash import { defineProgram } from "@optique/core/program"; import { object } from "@optique/core/constructs"; import { message } from "@optique/core/message"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { generateManPage } from "@optique/man"; const prog = defineProgram({ parser: object({ config: option("-c", "--config", string(), { description: message`Path to configuration file.`, }), }), metadata: { name: "myapp", version: "1.0.0", author: message`Hong Minhee `, bugs: message`https://github.com/dahlia/optique/issues`, }, }); // Metadata is automatically extracted from the program const manPage = generateManPage(prog, { section: 1 }); ``` The following metadata fields are shared between `Program` and man pages: * `name`: Program name * `version`: Version string * `brief`: Brief description for the NAME section * `description`: Detailed description for the DESCRIPTION section * `author`: Author information * `bugs`: Bug reporting URL * `examples`: Usage examples * `footer`: Footer text You can still override any of these by passing them in the options: ```typescript twoslash import { defineProgram } from "@optique/core/program"; import { object } from "@optique/core/constructs"; import { message } from "@optique/core/message"; import { generateManPage } from "@optique/man"; const prog = defineProgram({ parser: object({}), metadata: { name: "myapp", version: "1.0.0", }, }); // ---cut-before--- const manPage = generateManPage(prog, { section: 1, version: "2.0.0", // Overrides the version from metadata date: new Date(), manual: "User Commands", }); ``` ## Man page options The `generateManPage()` function accepts several options to customize the generated man page: `name` : The program name displayed in the header and NAME section. This is required. `section` : The manual section number (1-8). Section 1 is for user commands, which is typically what CLI applications use. `date` : The date shown in the footer. Can be a string (e.g., `"January 2026"`) or a `Date` object. If omitted, no date is shown. `version` : The version string shown in the footer (e.g., `"1.0.0"`). `manual` : The manual title shown centered in the header. Defaults to `"User Commands"` for section 1. `author` : Author information as a `Message` for the AUTHOR section. `seeAlso` : An array of related manual page references for the SEE ALSO section. Each entry is an object with `name` and `section` properties. `bugs` : A `Message` describing how to report bugs, shown in the BUGS section. `examples` : A `Message` with usage examples, shown in the EXAMPLES section. `brief` : A `Message` for the NAME section's brief description. When provided, this overrides the brief from the parser's documentation. `description` : A `Message` for the DESCRIPTION section. When provided, this overrides the description from the parser's documentation. `footer` : A `Message` appended at the end of the man page as footer text. When provided, this overrides the footer from the parser's documentation. ## Complete example Here's a more complete example showing all available options: ```typescript twoslash import { object, or, merge } from "@optique/core/constructs"; import { message } from "@optique/core/message"; import { option, command, constant } from "@optique/core/primitives"; import { string, choice } from "@optique/core/valueparser"; import { generateManPage } from "@optique/man"; const globalOptions = object({ config: option("-c", "--config", string(), { description: message`Path to configuration file.`, }), verbose: option("-v", "--verbose", { description: message`Enable verbose output.`, }), }); const buildCmd = command( "build", object({ mode: constant("build" as const), target: option("--target", choice(["dev", "prod"]), { description: message`Build target environment.`, }), }), { description: message`Build the project.`, } ); const testCmd = command( "test", object({ mode: constant("test" as const), watch: option("-w", "--watch", { description: message`Watch mode for continuous testing.`, }), }), { description: message`Run tests.`, } ); const parser = merge(globalOptions, or(buildCmd, testCmd)); const manPage = generateManPage(parser, { name: "myapp", section: 1, date: new Date(2026, 0, 22), version: "2.0.0", manual: "MyApp Manual", author: message`Jane Developer `, seeAlso: [ { name: "git", section: 1 }, { name: "npm", section: 1 }, ], bugs: message`Report bugs at https://github.com/example/myapp/issues`, }); ``` ## Generated man page structure The generated man page follows the standard structure: ### Header The header line contains the program name, section number, date, source, and manual title: ``` .TH MYAPP 1 "January 2026" "MyApp 2.0.0" "MyApp Manual" ``` ### NAME section Shows the program name and brief description: ``` .SH NAME myapp \- A powerful project management tool ``` ### SYNOPSIS section Shows the usage pattern derived from your parser: ``` .SH SYNOPSIS .B myapp [\fB\-c\fR \fICONFIG\fR] [\fB\-v\fR] .I COMMAND ``` ### DESCRIPTION section If your parser has a description, it appears here. For commands with subcommands, the available commands are listed. ### OPTIONS section Lists all options with their descriptions: ``` .SH OPTIONS .TP \fB\-c\fR, \fB\-\-config\fR \fICONFIG\fR Path to configuration file .TP \fB\-v\fR, \fB\-\-verbose\fR Enable verbose output ``` ### COMMANDS section For parsers with subcommands, lists each command: ``` .SH COMMANDS .TP \fBbuild\fR Build the project .TP \fBtest\fR Run tests ``` ## Async parsers For parsers that use async value parsers (like those from *@optique/git*), use `generateManPageAsync()`: ```typescript twoslash import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { generateManPageAsync } from "@optique/man"; // Example with an async parser const parser = object({ name: option("--name", string()), }); const manPage = await generateManPageAsync(parser, { name: "myapp", section: 1, }); ``` ## Integration patterns ### Writing to a file Man pages are typically installed in system directories like */usr/local/share/man/man1/*: ```typescript twoslash import { writeFileSync } from "node:fs"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { generateManPage } from "@optique/man"; const parser = object({ name: option("--name", string()), }); const manPage = generateManPage(parser, { name: "myapp", section: 1, }); writeFileSync("myapp.1", manPage); ``` ### Build-time generation You can generate man pages as part of your build process: ```typescript twoslash // @filename: scripts/generate-man.ts import { writeFileSync, mkdirSync } from "node:fs"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { generateManPage } from "@optique/man"; const parser = object({ name: option("--name", string()), }); mkdirSync("dist/man", { recursive: true }); const manPage = generateManPage(parser, { name: "myapp", section: 1, date: new Date(), version: process.env.npm_package_version, }); writeFileSync("dist/man/myapp.1", manPage); ``` ### Runtime generation via subcommand You can add a `man` subcommand to generate man pages on demand: ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { message } from "@optique/core/message"; import { command, constant, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { run } from "@optique/run"; import { generateManPage } from "@optique/man"; const mainCmd = command( "main", object({ mode: constant("main" as const), name: option("--name", string()), }), { description: message`Main functionality.` } ); const manCmd = command( "man", object({ mode: constant("man" as const) }), { description: message`Generate man page.` } ); const parser = or(mainCmd, manCmd); const result = run(parser); if (result.mode === "man") { const manPage = generateManPage(parser, { name: "myapp", section: 1, }); console.log(manPage); } ``` ## Roff formatting The *@optique/man* package also exports low-level functions for working with roff format if you need more control: `escapeRoff(text)` : Escapes special roff characters in text (backslashes, line-initial `.` and `'`). `escapeHyphens(text)` : Escapes hyphens to `\-` for option names, ensuring they render correctly and are copyable. `formatMessageAsRoff(message)` : Converts an Optique `Message` to roff markup, with proper formatting for option names (bold), metavars (italic), and other components. `formatDocPageAsMan(docPage, options)` : Converts a `DocPage` object to a complete man page. This is the mid-level API used by `generateManPage()`. ## Viewing generated man pages To preview a generated man page: ```bash # Generate and view directly myapp man | man -l - # Or save and view myapp man > myapp.1 man ./myapp.1 # View with groff (shows raw output) groff -man -Tutf8 myapp.1 ``` ## CLI tool *This feature is available since Optique 0.10.0.* The *@optique/man* package includes a CLI tool called `optique-man` for generating man pages from TypeScript or JavaScript files that export a `Program` or `Parser`. ### Installation ::: code-group ```bash [npm] npm install -g @optique/man ``` ```bash [Deno] deno install -gAn optique-man jsr:@optique/man/cli ``` ::: After installation, you can use `optique-man` directly from the command line. ### Basic usage ```bash # Generate man page from a file with a default export optique-man ./src/cli.ts -s 1 # Use a named export instead of default optique-man ./src/cli.ts -s 1 -e myProgram # Write output to a file optique-man ./src/cli.ts -s 1 -o myapp.1 ``` ### Options `FILE` (required) : Path to a TypeScript or JavaScript file that exports a `Program` or `Parser`. `-s`, `--section` (required) : Man page section number (1–8). Section 1 is for user commands. `-e`, `--export` : Name of the export to use. Defaults to the default export. `-o`, `--output` : Output file path. If not specified, output goes to stdout. `--name` : Override the program name in the man page header. `--date` : Override the date in the man page footer. `--version-string` : Override the version string in the footer. `--manual` : Override the manual title in the header. ### TypeScript support The CLI tool can load TypeScript files directly: * *Deno*: Native TypeScript support. * *Bun*: Native TypeScript support. * *Node.js 25.2.0+*: Native type stripping enabled by default. * *Node.js < 25.2.0*: Requires `tsx` to be installed (`npm install -D tsx`). ### Build integration You can integrate `optique-man` into your build process: ::: code-group ```json [npm] { "scripts": { "build:man": "optique-man ./src/cli.ts -s 1 -o dist/myapp.1" } } ``` ```json [Deno] { "tasks": { "build:man": "deno run --allow-read --allow-write jsr:@optique/man/cli ./src/cli.ts -s 1 -o dist/myapp.1" } } ``` ::: --- --- url: /concepts/modifiers.md description: >- Modifying combinators enhance existing parsers by making them optional, providing defaults, transforming results, or allowing multiple occurrences while preserving type safety. --- # Modifying combinators Modifying combinators enhance and transform existing parsers without changing their core parsing logic. They act as decorators or wrappers that add new capabilities: making parsers optional, providing default values, transforming results, or allowing multiple occurrences. This compositional approach allows you to build exactly the CLI behavior you need by combining simple, focused modifiers. The power of modifying combinators lies in their composability. You can chain them together to create sophisticated parsing behavior while maintaining full type safety. TypeScript automatically infers how each modifier affects the result type, so you get complete type information without manual annotations. Each modifier preserves the original parser's essential characteristics—like priority and usage information—while extending its behavior. This ensures that modified parsers integrate seamlessly with Optique's priority system and help text generation. ## `optional()` parser The `optional()` modifier makes any parser optional, allowing parsing to succeed even when the wrapped parser fails to match. If the wrapped parser succeeds, `optional()` returns its value. If it fails, `optional()` returns `undefined` without consuming any input or reporting an error. When `optional()` wraps a dependency source, Optique still tracks that source through the shared dependency runtime. A missing optional source therefore remains visible to derived parsers as “not provided” rather than becoming a special wrapper-specific state. When the wrapped parser produces its value during completion rather than argument matching—for example, `constant()`, or `bindEnv()`/`bindConfig()` wrappers that resolve from environment variables or configuration files—`optional()` preserves that completed value. It only yields `undefined` when the wrapped parser is genuinely absent (no CLI match and no fallback source could produce a value). ```typescript twoslash import { object } from "@optique/core/constructs"; import { optional } from "@optique/core/modifiers"; import type { InferValue } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const parser = object({ name: option("-n", "--name", string()), // Required email: optional(option("-e", "--email", string())), // Optional // ^? verbose: option("-v", "--verbose") // Required boolean }); ``` ### Type transformation The `optional()` modifier transforms the result type from `T` to `T | undefined`. This forces you to handle the case where the value might not be present, preventing runtime errors from assuming values exist: ```typescript twoslash import { object } from "@optique/core/constructs"; import { optional } from "@optique/core/modifiers"; import { type InferValue, parse } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const parser = object({ name: option("-n", "--name", string()), // Required email: optional(option("-e", "--email", string())), // Optional verbose: option("-v", "--verbose") // Required boolean }); // ---cut-before--- const config = parse(parser, ["--name", "Alice", "--verbose"]); if (config.success) { console.log(`Name: ${config.value.name}.`); // Safe: always present console.log(`Verbose: ${config.value.verbose}.`); // Safe: always present // Must check for undefined if (config.value.email) { console.log(`Email: ${config.value.email}.`); // Safe: checked first } else { console.log("No email provided."); } } ``` ### Usage patterns The `optional()` modifier is ideal when: * A parameter might or might not be provided * You want to explicitly handle the “not provided” case * The absence of a value has semantic meaning in your application ```typescript twoslash import { object } from "@optique/core/constructs"; import { optional } from "@optique/core/modifiers"; import { parse } from "@optique/core/parser"; import { argument, option } from "@optique/core/primitives"; import { choice, string } from "@optique/core/valueparser"; // ---cut-before--- const backupConfig = object({ source: argument(string({ metavar: "SRC" })), // Required source destination: argument(string({ metavar: "DEST" })), // Required destination compression: optional(option("-c", "--compress", choice(["gzip", "bzip2"]))), encrypt: optional(option("--encrypt", string({ metavar: "KEY_FILE" }))) }); const config = parse(backupConfig, ["-c", "src", "dest"]); // Handle optional parameters explicitly if (config.success) { const { source, destination, compression, encrypt } = config.value; console.log(`Backing up ${source} to ${destination}.`); if (compression) { console.log(`Using ${compression} compression.`); } if (encrypt) { console.log(`Encrypting with key from ${encrypt}.`); } } ``` ## `withDefault()` parser The `withDefault()` modifier provides a default value when the wrapped parser fails to match. The result type is a union of the parser's result type and the default value's type (`T | TDefault`). This allows for flexible default values that can be different types from what the parser produces, enabling patterns like conditional option structures. ```typescript twoslash import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; // ---cut-before--- import { withDefault } from "@optique/core/modifiers"; import { cpus } from "node:os"; const parser = object({ host: withDefault(option("-h", "--host", string()), "localhost"), // ^? port: withDefault(option("-p", "--port", integer()), 8080), // ^? workers: withDefault(option("-w", "--workers", integer()), () => cpus().length) // ^? }); ``` ### Union type patterns When the default value is a different type from the parser result, `withDefault()` creates a union type. This is particularly useful for conditional CLI structures: ```typescript twoslash import { object } from "@optique/core/constructs"; import { withDefault } from "@optique/core/modifiers"; import type { InferValue } from "@optique/core/parser"; import { flag, option } from "@optique/core/primitives"; import { run } from "@optique/run"; // Parser that produces complex object when flag is present const complexParser = object({ flag: flag("-f", "--flag"), dependentOption: option("-d", "--dependent", { /* ... */ }) }); // Default value with different structure const conditionalParser = withDefault( complexParser, { flag: false as const } ); // Result is a union type that handles both cases type Result = InferValue; // ^? const result: Result = run(conditionalParser); ``` ### Static vs dynamic defaults The `withDefault()` modifier supports both static values and factory functions: ```typescript twoslash import { withDefault } from "@optique/core/modifiers"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { choice, integer, string } from "@optique/core/valueparser"; import os from "node:os"; // ---cut-before--- // Static defaults const staticDefaults = object({ timeout: withDefault(option("--timeout", integer()), 30), format: withDefault(option("--format", choice(["json", "yaml"])), "json") }); // Dynamic defaults (computed when needed) const dynamicDefaults = object({ timestamp: withDefault(option("--time", string()), () => new Date().toISOString()), tempDir: withDefault(option("--temp", string()), () => os.tmpdir()), cores: withDefault(option("--cores", integer()), () => os.cpus().length) }); ``` Dynamic defaults are useful when: * The default value depends on runtime conditions * You want to compute expensive defaults only when needed * The default value might change between invocations When `withDefault()` wraps a dependency source, the default also participates in dependency resolution. Derived parsers see the same fallback value that the user-facing parser returns, even through larger compositions such as `object()` or `merge()`. One exception is `map()`: once a source value has been transformed anywhere in the wrapper chain, `withDefault()` only supplies a fallback for the mapped output. This applies both to `withDefault(map(source), ...)` and to `map(withDefault(source), ...)`. In either form, the default does not invent a dependency-source value for downstream derived parsers. The configured default applies only when the wrapped parser produces no value at all. If the wrapped parser resolves a value during completion—from `constant()`, from an environment variable through `bindEnv()`, or from a configuration file through `bindConfig()`—that value wins over the configured default. The default is the fallback for the wrapped parser's entire resolution chain, not just for the CLI argument path. ### Default normalization When the underlying value parser implements `normalize()`, `withDefault()` automatically normalizes default values so they match the representation that `parse()` would produce. For example, a `macAddress()` parser configured with `case: "lower"` will lowercase a default MAC address: ```typescript twoslash import { withDefault } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { macAddress } from "@optique/core/valueparser"; import { parse } from "@optique/core/parser"; // ---cut-before--- const parser = withDefault( option("--mac", macAddress({ case: "lower", outputSeparator: ":" })), "AA-BB-CC-DD-EE-FF", ); const result = parse(parser, []); // When --mac is omitted, the default is normalized to "aa:bb:cc:dd:ee:ff" ``` Built-in parsers that implement `normalize()` include `macAddress()` (case and separator normalization) and `domain()` (lowercase normalization). Custom value parsers can implement `normalize()` to opt into this behavior. ### Error handling *This API is available since Optique 0.5.0.* When using function-based defaults, the `withDefault()` modifier automatically catches any errors thrown in the callback and converts them to parser-level errors. This allows you to handle validation failures (like missing environment variables) directly at the parser level: ```typescript twoslash import { object } from "@optique/core/constructs"; import { envVar, message } from "@optique/core/message"; import { withDefault, WithDefaultError } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { string, url } from "@optique/core/valueparser"; // ---cut-before--- const parser = object({ // Regular error handling - converted to plain text apiUrl: withDefault(option("--url", url()), () => { if (!process.env.API_URL) { throw new Error("Environment variable API_URL is not set."); } return new URL(process.env.API_URL); }), // Rich formatting with WithDefaultError configPath: withDefault(option("--config", string()), () => { throw new WithDefaultError( message`Environment variable ${envVar("CONFIG_PATH")} is not set.` ); }) }); ``` For structured error messages with rich formatting, use the `WithDefaultError` class which accepts a `Message` object instead of a plain string: ```typescript twoslash import { object } from "@optique/core/constructs"; import { WithDefaultError, withDefault } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { envVar, message } from "@optique/core/message"; import { string } from "@optique/core/valueparser"; // ---cut-before--- const configParser = withDefault(option("--database-url", string()), () => { const envValue = process.env.DATABASE_URL; if (!envValue) { throw new WithDefaultError( message`Environment variable ${envVar("DATABASE_URL")} is required but not set.` ); } return envValue; }); ``` This approach provides several benefits: * *Parser-level validation*: Errors are caught and reported as parsing failures rather than runtime exceptions * *Consistent error formatting*: Errors are displayed using Optique's standard error formatting and colors * *Rich error messages*: Use `WithDefaultError` with `Message` objects for structured error content with highlighting and formatting ### Custom help display messages *This API is available since Optique 0.5.0.* The `withDefault()` modifier accepts an optional third parameter to customize how default values are displayed in help text. This allows you to show descriptive text instead of actual default values, which is particularly useful for environment variables or computed defaults: ```typescript twoslash import { object } from "@optique/core/constructs"; import { message, envVar } from "@optique/core/message"; import { withDefault } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { integer, string, url } from "@optique/core/valueparser"; // ---cut-before--- const parser = object({ // Show custom help text instead of actual URL apiUrl: withDefault( option("--api-url", url()), new URL("https://api.example.com"), { message: message`Default API endpoint` } ), // Show environment variable name in help token: withDefault( option("--token", string()), () => process.env.API_TOKEN || "", { message: message`${envVar("API_TOKEN")}` } ), // Show descriptive text for computed defaults workers: withDefault( option("--workers", integer()), () => require("os").cpus().length, { message: message`Number of CPU cores` } ) }); ``` When the custom `message` is provided, it will be displayed in the help output instead of the actual default value: ``` Options: --api-url URL API endpoint [Default API endpoint] --token STRING Authentication token [API_TOKEN] --workers INT Number of workers [Number of CPU cores] ``` The `message` parameter accepts a [`Message`](./messages.md) object, which supports rich formatting with colors, environment variables, option names, and other structured elements. This ensures consistent styling with the rest of Optique's help output. ### Usage patterns The `withDefault()` modifier is ideal when: * You want to provide sensible defaults for optional parameters * You need different default types than the parser produces (union types) * You're building conditional CLI structures with dependent options * The default value is meaningful and commonly used * You want to customize how defaults are displayed in help text * You need to show descriptive text for environment variables or computed defaults ```typescript twoslash import { object } from "@optique/core/constructs"; import { message } from "@optique/core/message"; import { withDefault } from "@optique/core/modifiers"; import { parse } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { choice, integer, string } from "@optique/core/valueparser"; class Server { constructor(config: { name: string; host: string; port: number; logLevel: "debug" | "info" | "warn" | "error"; maxConnections: number; }) { } } // ---cut-before--- const serverConfig = object({ // Required parameters name: option("-n", "--name", string()), // Optional with defaults - no undefined handling needed host: withDefault(option("-h", "--host", string()), "0.0.0.0"), port: withDefault( option("-p", "--port", integer({ min: 1, max: 0xffff })), 3000 ), logLevel: withDefault( option("--log-level", choice(["debug", "info", "warn", "error"])), "info" as const, ), maxConnections: withDefault(option("--max-conn", integer({ min: 1 })), 100) }); // Clean usage without undefined checks const config = parse(serverConfig, ["--name", "my-server", "--port", "8080"]); if (config.success) { const server = new Server({ name: config.value.name, host: config.value.host, // Always "0.0.0.0" if not specified port: config.value.port, // 8080 from input logLevel: config.value.logLevel, // Always "info" if not specified maxConnections: config.value.maxConnections // Always 100 if not specified }); } // Help output will show actual default values: // Options: // -h, --host STRING Server host [0.0.0.0] // -p, --port INTEGER Server port [3000] // --log-level LEVEL Log level [info] // --max-conn INTEGER Max connections [100] ``` ### Dependent options with union types A powerful pattern uses `withDefault()` with different types to create conditional CLI structures where options depend on flags: ```typescript twoslash import { object } from "@optique/core/constructs"; import { withDefault } from "@optique/core/modifiers"; import { parse } from "@optique/core/parser"; import { flag, option } from "@optique/core/primitives"; // Define conditional configuration const parser = withDefault( object({ flag: flag("-f", "--flag"), dependentFlag: option("-d", "--dependent-flag"), dependentFlag2: option("-d2", "--dependent-flag-2"), }), { flag: false as const } as const, ); // Result type is automatically inferred as a union type Config = | { readonly flag: false } | { readonly flag: true; readonly dependentFlag: boolean; readonly dependentFlag2?: boolean; }; // Usage handles both cases cleanly const result = parse(parser, []); if (result.success) { if (result.value.flag) { // TypeScript knows dependent flags are available console.log(`Dependent flag: ${result.value.dependentFlag}.`); if (result.value.dependentFlag2) { console.log(`Second dependent flag: ${result.value.dependentFlag2}.`); } } else { // TypeScript knows this is the simple case console.log("Flag is disabled."); } } ``` ## `map()` parser The `map()` modifier transforms the parsed result using a mapping function while preserving the original parser's logic. This allows you to convert values to different types, apply formatting, or compute derived values without changing how the parsing itself works. ```typescript twoslash import { object } from "@optique/core/constructs"; import { map, multiple } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; const parser = object({ // Transform boolean flag to its inverse disallow: map(option("--allow"), allowFlag => !allowFlag), // ^? // Transform string to uppercase upperName: map(option("-n", "--name", string()), name => name.toUpperCase()), // ^? // Transform integer to formatted string portDisplay: map(option("-p", "--port", integer()), port => `port:${port}`), // ^? // Transform multiple values tags: map( // ^? multiple(option("-t", "--tag", string())), tags => new Set(tags.map(tag => tag.toLowerCase())) ) }); ``` ### Transformation patterns The `map()` modifier supports various transformation patterns: ```typescript twoslash import { map, multiple } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; // ---cut-before--- // Type conversions const convertedValue = map(option("--count", integer()), count => BigInt(count)); // Data structure transformations const keyValuePairs = map( multiple(option("-D", string())), pairs => Object.fromEntries(pairs.map(pair => pair.split('='))) ); // Validation transformations const validatedEmail = map( option("--email", string()), email => { if (!email.includes('@')) throw new Error(`Invalid email: ${email}`); return email.toLowerCase(); } ); // Computed values const expiryTime = map( option("--ttl", integer()), ttlSeconds => new Date(Date.now() + ttlSeconds * 1000) ); ``` > \[!IMPORTANT] > The `transform` function must not mutate its input. During deferred > prompt resolution, object and array values may be shared placeholder > references, and in-place mutations would corrupt the placeholder for > subsequent parses. Always return a new value: > > ```typescript > // ✅ Correct — creates a new object > map(parser, v => ({ ...v, host: "override" })) > > // ❌ Wrong — mutates the input in place > map(parser, v => { v.host = "override"; return v; }) > ``` ## `multiple()` parser The `multiple()` modifier allows a parser to match multiple times, collecting all results into an array. This is essential for CLI options that can be repeated, such as multiple input files, include paths, or environment variables. ```typescript twoslash import { object } from "@optique/core/constructs"; import { multiple } from "@optique/core/modifiers"; import { argument, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const parser = object({ // Multiple files (at least 1 required) files: multiple(argument(string()), { min: 1, max: 5 }), // ^? // Multiple include paths (optional) includes: multiple(option("-I", "--include", string())), // ^? // Multiple environment variables (optional) env: multiple(option("-e", "--env", string())) // ^? }); ``` ### Constraint options The `multiple()` modifier accepts constraint options—`min` and `max`—to control how many occurrences are required: ```typescript twoslash import { multiple } from "@optique/core/modifiers"; import { argument, option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; // ---cut-before--- // Exactly 2-4 files required const requiredFiles = multiple(argument(string()), { min: 2, max: 4 }); // At least 1 server required const servers = multiple(option("--server", string()), { min: 1 }); // At most 3 retries allowed const retries = multiple(option("--retry", integer()), { max: 3 }); ``` ### Default behavior When no matches are found, `multiple()` returns an empty array rather than failing. This makes repeated options truly optional: ```typescript twoslash import { object } from "@optique/core/constructs"; import { multiple } from "@optique/core/modifiers"; import { parse } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; // ---cut-before--- const parser = object({ // These all return empty arrays when not provided headers: multiple(option("-H", "--header", string())), excludes: multiple(option("-x", "--exclude", string())), defines: multiple(option("-D", "--define", string())) }); const config = parse(parser, ["-H", "Accept: text/plain"]); // Safe to use without checking - arrays are always present if (config.success) { config.value.headers.forEach(header => console.log(`Header: ${header}.`)); console.log(`Found ${config.value.excludes.length} exclusions.`); } ``` ## `nonEmpty()` parser *This API is available since Optique 0.10.0.* The `nonEmpty()` modifier requires the wrapped parser to consume at least one input token to succeed. If the wrapped parser succeeds without consuming any tokens, `nonEmpty()` fails with an error. This is particularly useful with [`longestMatch()`](./constructs.md#longestmatch-parser) for implementing conditional default values. ```typescript twoslash import { longestMatch, object } from "@optique/core/constructs"; import { nonEmpty, optional, withDefault } from "@optique/core/modifiers"; import { constant, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; // Without nonEmpty(): activeParser always wins (consumes 0 tokens) // With nonEmpty(): helpParser wins when no options are provided const activeParser = nonEmpty(object({ mode: constant("active" as const), cwd: withDefault(option("--cwd", string()), "./default"), key: optional(option("--key", string())), })); const helpParser = object({ mode: constant("help" as const), }); const parser = longestMatch(activeParser, helpParser); // cli → helpParser matches (activeParser fails with nonEmpty) // cli --key foo → activeParser matches (consumes tokens) ``` ### Type transformation The `nonEmpty()` modifier does not change the result type. It simply adds a constraint that prevents parsers with only default values from matching when no input is provided: ```typescript twoslash import { nonEmpty } from "@optique/core/modifiers"; import type { InferValue } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; const baseParser = option("-v", "--verbose"); const nonEmptyParser = nonEmpty(baseParser); type BaseResult = InferValue; // ^? type NonEmptyResult = InferValue; // ^? // Both types are `boolean` - nonEmpty() does not change the type ``` ### Usage patterns The `nonEmpty()` modifier is ideal when: * You want to distinguish between “no input” and “input with defaults” * You're using `longestMatch()` to provide different behaviors based on whether options were explicitly provided * You need a fallback branch (like help or default mode) when no options are given ```typescript twoslash import { longestMatch, object } from "@optique/core/constructs"; import { nonEmpty, optional, withDefault } from "@optique/core/modifiers"; import { parse } from "@optique/core/parser"; import { constant, option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; // ---cut-before--- const serverConfig = nonEmpty(object({ mode: constant("server" as const), host: withDefault(option("--host", string()), "localhost"), port: withDefault(option("--port", integer()), 3000), })); const helpConfig = object({ mode: constant("help" as const), }); const parser = longestMatch(serverConfig, helpConfig); // No options: help mode const helpResult = parse(parser, []); if (helpResult.success && helpResult.value.mode === "help") { console.log("No options provided. Showing help."); } // With options: server mode with defaults applied const serverResult = parse(parser, ["--port", "8080"]); if (serverResult.success && serverResult.value.mode === "server") { console.log(`Starting server on ${serverResult.value.host}:${serverResult.value.port}.`); } ``` ### Combining with other modifiers The `nonEmpty()` modifier works well with other modifiers. You can wrap complex parsers built with `object()`, `withDefault()`, and `optional()`: ```typescript twoslash import { object } from "@optique/core/constructs"; import { multiple, nonEmpty, optional, withDefault } from "@optique/core/modifiers"; import { parse } from "@optique/core/parser"; import { argument, option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; // ---cut-before--- const configParser = nonEmpty(object({ // Accepts zero or more files, but nonEmpty ensures at least one token overall files: multiple(argument(string({ metavar: "FILE" }))), // Optional with default timeout: withDefault(option("--timeout", integer()), 30), // Pure optional verbose: optional(option("-v", "--verbose")), })); // Fails: nonEmpty requires at least one consumed token const emptyResult = parse(configParser, []); console.log(emptyResult.success); // false // Succeeds: file argument is provided const validResult = parse(configParser, ["input.txt"]); console.log(validResult.success); // true ``` --- --- url: /tutorial.md description: >- This tutorial walks you through building type-safe command-line applications using Optique's parser combinators, starting from simple options to complex subcommands. --- # Optique tutorial: Build type-safe CLIs step by step Optique is a type-safe combinatorial CLI parser that makes building command-line interfaces both powerful and predictable. Unlike traditional CLI parsers that rely on configuration objects, Optique uses composable functions that automatically infer TypeScript types. ## How Optique works Instead of describing your CLI with configuration objects, you *build* it using small, composable functions called *parser combinators*. TypeScript automatically infers the exact type of data your parser will produce. For a deeper look at this approach and how it compares to other CLI libraries, see [Why Optique?](./why.md). The core building blocks are: * [`option()`](./concepts/primitives.md#option-parser) and [`argument()`](./concepts/primitives.md#argument-parser) for individual CLI elements * [`object()`](./concepts/constructs.md#object-parser) to group parsers into structured results * [`or()`](./concepts/constructs.md#or-parser) for mutually exclusive alternatives * [`optional()`](./concepts/modifiers.md#optional-parser), [`multiple()`](./concepts/modifiers.md#multiple-parser), and [`merge()`](./concepts/constructs.md#merge-parser) for flexible composition In this tutorial, we'll build progressively more complex CLI applications, starting with simple options and building up to production-ready tools with integration support. ## Getting started The journey into parser combinators begins with understanding the fundamental building blocks. In this section, we'll explore the most basic parsers and see how TypeScript's type inference makes CLI development both safer and more enjoyable. Every CLI parser in Optique is a function that takes command-line arguments and produces either a successfully parsed value or an error. The key insight is that these parsers can be composed and combined to create more sophisticated argument handling without losing type information. ### Your first CLI: Single option Let's start with the simplest possible CLI—a greeting program that accepts a name. This example demonstrates the core concepts of value parsers and type inference. ```typescript twoslash import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { run, print } from "@optique/run"; import { message } from "@optique/core/message"; // Create a parser for --name option const nameParser = option("--name", string()); // ^? // Run the parser with some example arguments const result = run(nameParser, { // ^? args: ["--name", "Alice"] }); print(message`Hello, ${result}!`); // Output: Hello, Alice! ``` This simple example demonstrates several important concepts: [Value parsers](./concepts/valueparsers.md) : The [`string()`](./concepts/valueparsers.md#string-parser) function is a [*value parser*](./concepts/valueparsers.md)—it knows how to convert a raw command-line argument (which is always a string) into a typed value. Optique provides many built-in value parsers for common data types. Type inference : Notice how TypeScript automatically infers that `nameParser` returns a `Parser`. You don't need to write any type annotations—the compiler figures out the types based on how you compose the parsers. Result handling : The *@optique/run* version of `run()` never returns on errors—it displays error messages and exits the process automatically. This makes CLI applications simpler since you only need to handle the success case. Boolean flags work differently—they don't take values and simply indicate presence or absence: ```typescript twoslash import { option } from "@optique/core/primitives"; import { run } from "@optique/run"; // Boolean flag (no value parser needed) const verboseParser = option("-v", "--verbose"); // ^? const result = run(verboseParser); // ^? // This returns true when present, false when absent ``` ### Working with positional arguments While options use flags like `--name` or `-v`, positional arguments are values that appear in specific positions on the command line. Think of commands like `cp source.txt destination.txt`—the filenames are positional arguments because their meaning depends on their position, not on any flag. Positional arguments are essential for creating intuitive CLIs. Users expect to type `git commit message.txt` rather than `git commit --file message.txt`. Let's create a file processor that demonstrates this pattern: ```typescript twoslash import { argument } from "@optique/core/primitives"; import { run, print } from "@optique/run"; import { path } from "@optique/run/valueparser"; import { message } from "@optique/core/message"; // Create a parser for a required file argument const fileParser = argument(path({ metavar: "FILE" })); // ^? const result = run(fileParser, { // ^? args: ["input.txt"] }); print(message`Processing file: ${result}`); // Output: Processing file: input.txt ``` The [`argument()`](./concepts/primitives.md#argument-parser) function creates a parser that consumes the next positional argument from the command line. The [`path()`](./concepts/valueparsers.md#path-parser) value parser is perfect for file and directory paths, and we'll explore its validation capabilities later in the tutorial. The `metavar: "FILE"` parameter is used in help text generation. Instead of showing a generic placeholder, help messages will display `FILE` to indicate what kind of argument is expected. ### Combining options and arguments Real CLI programs usually need both options and arguments working together. This is where Optique's compositional nature shines—the [`object()`](./concepts/constructs.md#object-parser) combinator lets us group multiple parsers into a single, structured result. The `object()` combinator is one of the most important tools in Optique. It takes multiple named parsers and combines them into a single parser that produces an object with all the parsed values. The beauty is that TypeScript automatically infers the exact shape of this object, including which fields are optional and what types they contain. ```typescript twoslash import { object } from "@optique/core/constructs"; import type { InferValue } from "@optique/core/parser"; import { argument, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { run, print } from "@optique/run"; import { path } from "@optique/run/valueparser"; import { message } from "@optique/core/message"; const parser = object({ file: argument(path({ metavar: "FILE" })), output: option("-o", "--output", path({ metavar: "OUTPUT" })), verbose: option("-v", "--verbose") }); // TypeScript automatically infers the complete type! type Config = InferValue; // ^? const config: Config = run(parser, { args: [ "input.txt", "--output", "output.txt", "--verbose" ] }); print(message`Converting ${config.file} to ${config.output}.`); if (config.verbose) { print(message`Verbose mode enabled.`); } ``` This example showcases the power of parser composition. We've created a parser that handles both positional arguments and options, and TypeScript automatically infers the complete result type. The `config` object is fully typed—the compiler knows that `file` and `output` are strings, while `verbose` is a boolean. Notice how natural the composition feels. Each parser handles one concern: * `argument(path(...))` handles the required input file * `option("-o", "--output", path(...))` handles the optional output location * `option("-v", "--verbose")` handles the verbose flag The `object()` combinator weaves them together into a cohesive whole, and the type system ensures everything fits together correctly. > \[!NOTE] > The `InferValue` utility type extracts the TypeScript type that a parser > will produce. This is useful for type annotations and ensuring type safety > throughout your application. However, in most cases you won't need > it—TypeScript's inference handles everything automatically. ## Value parsers and validation [Value parsers](./concepts/valueparsers.md) are the foundation of type-safe CLI parsing. While command-line arguments are always strings, your application needs them as numbers, URLs, file paths, or other typed values. Value parsers handle this conversion and provide validation at parse time, catching errors before they can cause problems in your application logic. The philosophy behind Optique's value parsers is “fail fast, fail clearly.” Instead of letting invalid data flow through your application and cause mysterious errors later, value parsers validate input immediately and provide clear error messages that help users fix their mistakes. ### Rich value types with built-in validation Optique provides powerful value parsers that go beyond simple strings. Each parser not only handles type conversion but also provides meaningful validation rules. Let's explore the most commonly used ones, with special attention to the versatile `path()` parser: ```typescript twoslash import { defineProgram } from "@optique/core/program"; import { object } from "@optique/core/constructs"; import { option, argument } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; import { run, print } from "@optique/run"; import { optional, withDefault, map } from "@optique/core/modifiers"; const parser = object({ name: option("-n", "--name", string()), config: optional(option("-c", "--config", string())), debug: option("--debug"), upperName: map( argument(string({ metavar: "NAME" })), (s) => s.toUpperCase(), ), host: withDefault(option("-h", "--host", string()), "localhost"), port: withDefault(option("-p", "--port", string()), "8080"), portDescription: map( withDefault(option("-p", "--port", string()), "8080"), (port) => `Server will run on port ${port}`, ), }); const prog = defineProgram({ parser, metadata: { name: "server" }, }); const config = run(prog, { args: ["--name", "test"] }); // Optional properties need checking if (config.config) { print(message`Using config: ${config.config}.`); } // Default values are always available print(message`Starting ${config.upperName} on ${config.host}:${config.port.toString()}.`); print(message`${config.portDescription}`); ``` *Value transformation with `map()`*: The `map()` combinator deserves special attention. It allows you to transform parsed values while preserving the original parsing logic. This is incredibly useful for normalizing data, computing derived values, or adapting to different data formats your application expects. ### Repeatable values with `multiple()` Command-line interfaces often need to accept multiple values for the same option. Consider `gcc -I include1 -I include2 -I include3` or `curl -H "Accept: application/json" -H "Authorization: Bearer token"`. The [`multiple()`](./concepts/modifiers.md#multiple-parser) combinator handles these patterns naturally. What makes `multiple()` special is how it handles the common case gracefully. When no matches are found, it returns an empty array rather than failing to parse. This means you can make repeated options truly optional—if the user doesn't provide any, your application gets an empty array and can continue normally: ```typescript twoslash import { object } from "@optique/core/constructs"; import { message, values } from "@optique/core/message"; import { multiple } from "@optique/core/modifiers"; import { argument, option } from "@optique/core/primitives"; import { path, print, run } from "@optique/run"; import { string } from "@optique/core/valueparser"; const parser = object({ // Multiple files with constraints files: multiple(argument(path()), { min: 1, max: 5 }), // ^? // Multiple options (can be empty) headers: multiple(option("-H", "--header", string())), // ^? // Multiple with no constraints tags: multiple(option("-t", "--tag", string())), // ^? // Boolean flag (single occurrence) verbose: option("-v", "--verbose") // ^? }); // Usage: myapp file1.txt file2.txt -H "Accept: application/json" -H "User-Agent: myapp" -t web -t api -v const config = run(parser, { args: [ "file1.txt", "file2.txt", "-H", "Accept: application/json", "-H", "User-Agent: myapp", "-t", "web", "-t", "api", "-v" ] }); print(message`Processing ${config.files.length.toString()} files:`); // ^? config.files.forEach((file, index) => { // ^? print(message` ${(index + 1).toString()}. ${file}`); }); if (config.headers.length > 0) { // ^? print(message`Custom headers:`); config.headers.forEach(header => { print(message` ${header}`); }); } print(message`Tags: ${values(config.tags)}.`); // ^? ``` *Constraints and validation*: The `{ min: 1, max: 5 }` constraint in the files example demonstrates another powerful feature. You can specify minimum and maximum bounds for repeated values, ensuring your application receives a reasonable number of arguments. This prevents both user error (forgetting to specify required files) and potential abuse (specifying thousands of files that might overwhelm your system). The `multiple()` combinator automatically provides empty arrays as defaults when no matches are found, making it safe to use without additional null checking. Your code can always assume arrays exist, simplifying the logic considerably. ## Building subcommands Subcommands are the hallmark of sophisticated CLI tools. They allow you to group related functionality under a single program while keeping individual commands focused and easy to understand. Think of `git add`, `docker run`, or `npm install`—each subcommand is essentially a mini-program with its own options and behavior. The [`command()`](./concepts/primitives.md#command-parser) combinator makes subcommands natural to express in Optique. Unlike some CLI libraries that require complex routing logic, Optique treats subcommands as just another form of parser composition. This means you can combine subcommands with all the other patterns you've learned—they can have optional parameters, repeated arguments, discriminated unions, and more. ### Git-style CLI Let's build a `git`-like CLI that demonstrates how subcommands work in practice. Each subcommand will have its own unique options, but they'll all be part of a single, type-safe parser: ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { multiple } from "@optique/core/modifiers"; import type { InferValue } from "@optique/core/parser"; import { argument, command, constant, option} from "@optique/core/primitives"; import { path, run } from "@optique/run"; import { string } from "@optique/core/valueparser"; const parser = or( command("add", object({ // [!code highlight] type: constant("add"), files: multiple(argument(path())), all: option("-A", "--all"), force: option("-f", "--force") })), command("commit", object({ // [!code highlight] type: constant("commit"), message: option("-m", "--message", string()), amend: option("--amend"), all: option("-a", "--all") })), command("push", object({ // [!code highlight] type: constant("push"), remote: option("-r", "--remote", string()), force: option("-f", "--force"), setUpstream: option("-u", "--set-upstream") })) ); // TypeScript creates a perfect discriminated union type GitCommand = InferValue; // ^? const result = run(parser, { args: ["commit", "-m", "Fix parsing bug", "--amend"] }); ``` ### Nested subcommands For more complex tools, you can nest subcommands multiple levels deep: ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { argument, command, constant, option } from "@optique/core/primitives"; import { run } from "@optique/run"; import { choice, string } from "@optique/core/valueparser"; // Second-level commands for "app config" const configCommands = or( command("get", object({ action: constant("get"), key: argument(string({ metavar: "KEY" })), format: option("-f", "--format", choice(["json", "yaml", "plain"])) })), command("set", object({ action: constant("set"), key: argument(string({ metavar: "KEY" })), value: argument(string({ metavar: "VALUE" })), global: option("-g", "--global") })), command("list", object({ action: constant("list"), format: option("-f", "--format", choice(["json", "yaml", "table"])) })) ); // Top-level commands const parser = or( // Nested: app config get/set/list command("config", object({ command: constant("config"), subcommand: configCommands })), // Simple: app init command("init", object({ command: constant("init"), template: option("-t", "--template", string()), force: option("-f", "--force") })), // Simple: app build command("build", object({ command: constant("build"), watch: option("-w", "--watch"), minify: option("-m", "--minify") })) ); // Usage examples: // app config get database.url --format json // app config set database.url "postgres://localhost/mydb" --global // app init --template react --force // app build --watch --minify const result = run(parser, { // ^? args: ["config", "set", "api.url", "https://api.example.com", "--global"] }); ``` *The power of nested parsing*: Notice how the nested structure mirrors the command structure itself. The `config` command contains its own subparser that handles `get`, `set`, and `list`. This compositional approach scales naturally—you can nest commands as deeply as needed without losing type safety or clarity. *Global vs. local options*: This pattern also demonstrates how to handle global options (like `--global-config`) that apply to all commands, while still providing command-specific options. The type system ensures that you can only access the options that are actually available for each command. This pattern scales well for complex CLI tools with multiple levels of subcommands, each with their own options and behaviors. The type system tracks the structure automatically, so you never have to worry about accessing the wrong properties or forgetting to handle a case. ### When commands outgrow one file The examples above define the command tree directly with `command()` and `or()`. That is the clearest way to learn the model, and it works well for small and medium CLIs. When each command starts to feel like its own module, *@optique/discover* can build the same command tree from files instead. > \[!WARNING] > `runProgram()` discovers command modules from the runtime file system and > imports them dynamically. This is useful for source-layout CLIs, but it is > not a good default when the CLI must be aggressively tree-shaken, statically > bundled, or packaged as a single executable. In those cases, import command > modules manually and pass them as `commands`. Each command module default-exports a command created with `defineCommand()`. The entry point then points `runProgram()` at the command directory: ```typescript twoslash import { message } from "@optique/core/message"; import { runProgram } from "@optique/discover"; await runProgram({ dir: new URL("./commands/", import.meta.url), metadata: { name: "app", brief: message`Project tools.`, }, }); ``` For a bundled CLI, declare each command's path and pass the imported commands directly: ```typescript twoslash import { object } from "@optique/core/constructs"; import { message } from "@optique/core/message"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { defineCommand, runProgram } from "@optique/discover"; const build = defineCommand({ path: ["build"], parser: object({ target: option("--target", string()), }), metadata: { brief: message`Build the project.`, }, handler(value) { console.log(`Building ${value.target}.`); }, }); await runProgram({ commands: [build], metadata: { name: "app", brief: message`Project tools.`, }, }); ``` With this layout: ```text commands/ build.ts deploy.ts config/ set.ts ``` the file paths become command paths: ```bash app build app deploy app config set ``` Use this pattern when the command file is the natural place to keep the parser, help metadata, and handler together. See the [command discovery guide](./concepts/discover.md) for the full API and the [cookbook recipe](./cookbook.md#file-based-command-discovery) for a complete example. ## Modularization and reusability As CLI applications grow in complexity, you'll find yourself repeating similar patterns across different commands. Database connection options, logging configuration, and authentication settings tend to appear in multiple places. Rather than duplicating this logic, Optique provides powerful tools for creating reusable, composable option groups. The [`merge()`](./concepts/constructs.md#merge-parser) combinator is the key to building modular CLI applications. It allows you to define option groups once and reuse them across different commands, while maintaining complete type safety. This approach promotes consistency across your CLI—users learn the database options once and can apply that knowledge to any command that needs database access. ### Reusable option groups with `merge()` The philosophy behind option groups is separation of concerns. Instead of monolithic parsers that handle everything, you create focused parsers that handle specific areas of functionality. Then you compose these focused parsers in different combinations depending on what each command needs: ```typescript twoslash import { merge, object, or } from "@optique/core/constructs"; import { optional } from "@optique/core/modifiers"; import { constant, option } from "@optique/core/primitives"; import { path, run } from "@optique/run"; import { choice, integer, string } from "@optique/core/valueparser"; // Define reusable option groups const networkOptions = object("Network", { host: option("--host", string({ metavar: "HOST" })), port: option("--port", integer({ min: 1, max: 0xffff })) }); const authOptions = object("Authentication", { username: option("-u", "--user", string({ metavar: "USER" })), password: optional(option("-p", "--password", string({ metavar: "PASS" }))), token: optional(option("-t", "--token", string({ metavar: "TOKEN" }))) }); const loggingOptions = object("Logging", { logLevel: option("--log-level", choice(["debug", "info", "warn", "error"])), logFile: optional(option("--log-file", path({ metavar: "FILE" }))) }); // Combine groups differently for different modes const parser = or( // Development mode: minimal required options merge( object({ mode: constant("dev") }), networkOptions, object({ debug: option("--debug") }) ), // Production mode: full configuration required merge( object({ mode: constant("prod") }), networkOptions, authOptions, loggingOptions, object({ configFile: option("-c", "--config", path({ mustExist: true })), workers: option("-w", "--workers", integer({ min: 1, max: 16 })) }) ) ); const config = run(parser, { // ^? args: [ "--host", "0.0.0.0", "--port", "8080", "--user", "admin", "--log-level", "info", "--config", "prod.json", "--workers", "4" ] }); ``` ### Real-world example: Deployment tool CLI Let's build a comprehensive deployment tool that demonstrates all the features we've learned: ```typescript twoslash import { merge, object, or } from "@optique/core/constructs"; import { multiple, optional, withDefault } from "@optique/core/modifiers"; import type { InferValue } from "@optique/core/parser"; import { argument, command, constant, option } from "@optique/core/primitives"; import { path, run } from "@optique/run"; import { choice, integer, string, url } from "@optique/core/valueparser"; // Reusable option groups const commonOptions = object("Common", { verbose: optional(option("-v", "--verbose")), config: optional(option("-c", "--config", path({ mustExist: true }))), dryRun: optional(option("--dry-run")) }); const environmentOptions = object("Environment", { environment: argument(choice(["dev", "staging", "prod"])), region: option("-r", "--region", string()), timeout: withDefault(option("-t", "--timeout", integer({ min: 0 })), 300) }); const deployOptions = object("Deploy", { image: option("-i", "--image", string({ metavar: "IMAGE:TAG" })), replicas: withDefault(option("--replicas", integer({ min: 1, max: 50 })), 1), healthCheck: option("--health-check", url()), secrets: multiple(option("-s", "--secret", string())) }); // Main CLI parser const deploymentTool = object({ // Global options available to all commands globalConfig: optional(option("--global-config", path())), quiet: optional(option("-q", "--quiet")), // Command with rich subcommand structure command: or( // Deploy command: merge multiple option groups command("deploy", merge( object({ action: constant("deploy") }), commonOptions, environmentOptions, deployOptions, object({ // Deploy-specific options force: optional(option("-f", "--force")), rollback: optional(option("--rollback-on-failure")) }) )), // Status command: simpler option set command("status", merge( object({ action: constant("status") }), commonOptions, object({ environment: argument(choice(["dev", "staging", "prod"])), watch: optional(option("-w", "--watch")), format: withDefault( option("--format", choice(["table", "json", "yaml"])), "table" ) }) )), // Rollback command: targeted options command("rollback", merge( object({ action: constant("rollback") }), commonOptions, environmentOptions, object({ revision: option("--revision", string({ metavar: "REV" })), confirm: optional(option("--confirm")) }) )), // Logs command: streaming options command("logs", merge( object({ action: constant("logs") }), commonOptions, object({ environment: argument(choice(["dev", "staging", "prod"])), service: argument(string({ metavar: "SERVICE" })), follow: optional(option("-f", "--follow")), lines: withDefault(option("-n", "--lines", integer({ min: 1 })), 100), since: optional(option("--since", string({ metavar: "TIME" }))) }) )) ) }); // The complete inferred type - look how rich this is! type DeployConfig = InferValue; // ^? // Example usage scenarios: // deploy-tool deploy prod -i myapp:v1.2.3 --replicas 5 --health-check https://api.example.com/health -v // deploy-tool status staging --watch --format json // deploy-tool rollback prod --revision v1.2.2 --confirm // deploy-tool logs prod api-service --follow --lines 1000 const config = run(deploymentTool, { args: [ "deploy", "prod", "--image", "myapp:v1.2.3", "--replicas", "3", "--health-check", "https://api.example.com/health", "--secret", "DB_PASSWORD", "--secret", "API_KEY", "--region", "us-east-1", "--verbose", "--force" ] }); ``` This example showcases: * *Modular design* with reusable option groups (`commonOptions`, `environmentOptions`, `deployOptions`) * *Rich type inference* with complex discriminated unions * *Flexible composition* using `merge()` to combine option groups differently per command * *Real-world validation* with path checking, URL validation, integer bounds, and choice constraints The `merge()` combinator is particularly powerful here—it lets us define option groups once and reuse them across different commands, while TypeScript automatically combines the types correctly. ## Production CLI applications Throughout this tutorial, we've been using *@optique/run* which provides a batteries-included experience for building CLI applications. This is the recommended approach for most use cases, as it handles all the common concerns automatically: reading from [`process.argv`] (or [`Deno.args`] on Deno), detecting terminal capabilities, displaying help text, and exiting with appropriate status codes. However, it's worth understanding the difference between *@optique/run* and *@optique/core*, and when you might choose one over the other. [`process.argv`]: https://nodejs.org/api/process.html#processargv [`Deno.args`]: https://docs.deno.com/api/deno/~/Deno.args ### *@optique/run* vs. *@optique/core* The difference between *@optique/core* and *@optique/run* is primarily about convenience and control: Use *@optique/run* when: * Building standalone CLI applications * You want automatic [`process.argv`] (or [`Deno.args`] on Deno) handling and error display * You need terminal capability detection (colors, width) * You prefer convention over configuration Use *@optique/core* when: * Building libraries that need to parse CLI-like arguments * Working in web applications or environments without [`node:process`] * You need full control over error handling and result processing * You want to integrate parsing into larger application logic Here's how the same parser would work with *@optique/core*: ```typescript twoslash import type { Program } from "@optique/core/program"; import type { InferValue } from "@optique/core/parser"; import { object } from "@optique/core/constructs"; import { runParser } from "@optique/core/facade"; import { optional } from "@optique/core/modifiers"; import { argument, option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; import process from "node:process"; const parser = object({ input: argument(string({ metavar: "FILE" })), output: option("-o", "--output", string({ metavar: "FILE" })), port: optional(option("-p", "--port", integer({ min: 1, max: 0xffff }))), verbose: option("-v", "--verbose") }); const prog: Program<"sync", InferValue> = { parser, metadata: { name: "myapp" }, }; // @optique/core requires explicit argument handling const config = runParser(prog, process.argv.slice(2), { // ^? onError: process.exit, help: { option: true, onShow: process.exit }, }); console.log(`Processing ${config.input} -> ${config.output}.`); if (config.port) { console.log(`Server will run on port ${config.port}.`); } ``` Compare this to the *@optique/run* version we've been using throughout this tutorial: ```typescript twoslash import { object } from "@optique/core/constructs"; import { optional } from "@optique/core/modifiers"; import { argument, option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; import { path, run, print } from "@optique/run"; import { message } from "@optique/core/message"; const parser = object({ input: argument(path({ mustExist: true, metavar: "FILE" })), output: option("-o", "--output", path({ metavar: "FILE" })), port: optional(option("-p", "--port", integer({ min: 1, max: 0xffff }))), verbose: option("-v", "--verbose") }); // @optique/run handles everything automatically const config = run(parser); // ^? print(message`Processing ${config.input} -> ${config.output}.`); if (config.port) { print(message`Server will run on port ${config.port.toString()}.`); } ``` The *@optique/run* version is much more concise and handles all error cases automatically. [`node:process`]: https://nodejs.org/api/process.html ### Configuration options *@optique/run* provides several configuration options for fine-tuning behavior: ```typescript twoslash import type { Program } from "@optique/core/program"; import type { InferValue } from "@optique/core/parser"; import { object } from "@optique/core/constructs"; import { map, optional, withDefault } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { path, print, run } from "@optique/run"; import { integer, string } from "@optique/core/valueparser"; const parser = object({ name: option("-n", "--name", string()), config: optional(option("-c", "--config", path())), port: withDefault(option("-p", "--port", integer()), 3000), portDescription: map( // ^? withDefault(option("--port", integer()), 3000), port => `Server will run on port ${port}` ) }); const prog: Program<"sync", InferValue> = { parser, metadata: { name: "my-tool" }, }; const config = run(prog, { help: "both", // Enable --help option AND help subcommand // ^? aboveError: "usage", // Show usage information above errors // ^? colors: true, // Force colored output (auto-detected by default) maxWidth: 100, // Set help text width (terminal width by default) errorExitCode: 2 // Custom exit code for errors (default: 1) }); // The help system automatically generates comprehensive help text: // $ my-tool --help // $ my-tool help ``` ### Complete CLI application Here's a complete, production-ready CLI application using everything we've learned: ```typescript twoslash #!/usr/bin/env node import { merge, object, or } from "@optique/core/constructs"; import { multiple, optional, withDefault } from "@optique/core/modifiers"; import type { InferValue } from "@optique/core/parser"; import type { Program } from "@optique/core/program"; import { argument, command, constant, option } from "@optique/core/primitives"; import { choice, integer, string } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; import { path, run } from "@optique/run"; // Reusable option groups const globalOptions = object("Global Options", { config: optional(option("-c", "--config", path({ mustExist: true }))), verbose: optional(option("-v", "--verbose")), quiet: optional(option("-q", "--quiet")) }); const buildOptions = object("Build Options", { watch: optional(option("-w", "--watch")), minify: optional(option("--minify")), sourcemap: withDefault(option("--sourcemap", choice(["inline", "external", "none"])), "external"), outDir: withDefault(option("--out-dir", path()), "./dist") }); // Complete CLI parser const cli = merge( globalOptions, object({ command: or( // Build command command("build", merge( object({ action: constant("build") }), buildOptions, object({ entry: multiple(argument(path({ mustExist: true })), { min: 1 }), target: withDefault(option("--target", choice(["es2015", "es2018", "es2022", "esnext"])), "es2018") }) )), // Dev command command("dev", merge( object({ action: constant("dev") }), buildOptions, object({ port: withDefault( option("-p", "--port", integer({ min: 1, max: 0xffff })), 3000 ), host: withDefault(option("--host", string()), "localhost"), open: optional(option("--open")) }) )), // Test command command("test", object({ action: constant("test"), watch: optional(option("-w", "--watch")), coverage: optional(option("--coverage")), pattern: optional(option("--pattern", string())), timeout: withDefault(option("--timeout", integer({ min: 1 })), 5000) })) ) }) ); type Config = InferValue; // ^? const prog: Program<"sync", Config> = { parser: cli, metadata: { name: "build-tool", version: "1.0.0", brief: message`A modern build tool for JavaScript projects`, }, }; // Run with comprehensive configuration const config: Config = run(prog, { help: "both", // Both --help and help command version: prog.metadata.version, // Enable version display aboveError: "usage", // Show usage on errors colors: true, // Colored output }); ``` This complete example demonstrates: * *Process integration* with automatic `process.argv` handling * *Comprehensive help system* with both `--help` and `help` command * *Error handling* with custom exit codes and error formatting * *Type safety* throughout the entire application * *Modular design* with reusable option groups * *Real-world patterns* commonly used in build tools and CLI applications Usage examples: ```bash # Build command $ build-tool build src/index.ts --target es2022 --minify -v # Dev server $ build-tool dev --port 8080 --open --watch # Testing $ build-tool test --coverage --pattern "*.spec.ts" --timeout 10000 # Help system $ build-tool --help $ build-tool help $ build-tool help build ``` ## Integrating external data sources Real CLI applications often get values from multiple sources beyond command-line arguments. Optique's integration packages let you layer these sources with a clear priority order, all while preserving the same composition model. ### Environment variables with *@optique/env* Use `bindEnv()` to fall back to an environment variable when a CLI option is not provided: ```typescript twoslash import { bindEnv, createEnvContext } from "@optique/env"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; import { runAsync } from "@optique/run"; const envContext = createEnvContext({ prefix: "MYAPP_" }); const parser = object({ host: bindEnv(option("--host", string()), { context: envContext, key: "HOST", parser: string(), default: "localhost", }), port: bindEnv(option("--port", integer()), { context: envContext, key: "PORT", parser: integer(), default: 3000, }), }); // Pass the context to the runner const result = await runAsync(parser, { contexts: [envContext], }); ``` Priority order: CLI argument > environment variable > default value. With the `MYAPP_` prefix, the parser reads `MYAPP_HOST` and `MYAPP_PORT`. See the [environment variable guide](./integrations/env.md) for more details. ### Config files with *@optique/config* Use `bindConfig()` to fall back to a configuration file. The schema is validated using any [Standard Schema]-compatible library (Zod, Valibot, ArkType): ```typescript twoslash import { z } from "zod"; import { bindConfig, createConfigContext } from "@optique/config"; import { object } from "@optique/core/constructs"; import { withDefault } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; import { runAsync } from "@optique/run"; const configSchema = z.object({ host: z.string().optional(), port: z.number().optional(), }); const configContext = createConfigContext({ schema: configSchema }); const parser = object({ config: withDefault(option("--config", string()), "config.json"), host: bindConfig(option("--host", string()), { context: configContext, key: "host", default: "localhost", }), port: bindConfig(option("--port", integer()), { context: configContext, key: "port", default: 3000, }), }); const result = await runAsync(parser, { contexts: [configContext], contextOptions: { getConfigPath: (parsed) => parsed.config, }, }); ``` Priority order: CLI argument > config file value > default value. See the [config file guide](./integrations/config.md) for more details. [Standard Schema]: https://standardschema.dev/ ### Interactive prompts with *@optique/inquirer* Use `prompt()` to show an interactive prompt when a value is not provided on the command line: ```typescript twoslash import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; import { prompt } from "@optique/inquirer"; import { run } from "@optique/run"; const parser = object({ name: prompt(option("--name", string()), { type: "input", message: "Project name:", }), port: prompt(option("--port", integer()), { type: "number", message: "Port number:", default: 3000, }), }); await run(parser); ``` When `--name` and `--port` are provided, the prompts are skipped. Otherwise, the user sees interactive prompts. See the [interactive prompt guide](./integrations/inquirer.md) for more details. ### Composing multiple sources These integrations compose naturally. Wrapping order determines fallback priority: ```typescript // CLI > environment > config > interactive prompt prompt(bindEnv(bindConfig(option("--host", string()), { ... }), { ... }), { ... }) ``` See the [cookbook](./cookbook.md#combining-with-interactive-prompts) for a complete example. ## Next steps You now have a solid foundation for building CLI applications with Optique. Here are some directions to explore next: * [Cookbook](./cookbook.md): Practical recipes for common patterns like mutually exclusive options, dependent flags, and integration examples * [Command discovery](./concepts/discover.md): File-based command modules and static `runProgram({ commands })` registration for CLIs that have outgrown a single parser file * [Concept guides](./concepts/primitives.md): Deep dives into primitives, value parsers, combinators, shell completion, and man page generation * Integration packages: [environment variables](./integrations/env.md), [config files](./integrations/config.md), [interactive prompts](./integrations/inquirer.md), [Zod](./integrations/zod.md)/[Valibot](./integrations/valibot.md), [Git references](./integrations/git.md), [Temporal dates](./integrations/temporal.md) * [Why Optique?](./why.md): The design philosophy behind parser combinators --- --- url: /concepts/primitives.md description: >- Primitive parsers are the foundational building blocks that handle basic CLI elements like options, arguments, commands, and constants with full type safety and clear error messages. --- # Primitive parsers Primitive parsers are the foundational building blocks of Optique. They handle the most basic elements of command-line interfaces: flags, options, positional arguments, and subcommands. Unlike higher-level combinators that compose multiple parsers together, primitives interact directly with the command-line input, consuming and validating individual pieces. Understanding primitive parsers is essential because they form the core of every CLI parser you'll build. Whether you're creating a simple utility with a single flag or a complex multi-command application, you'll combine these primitives to express your CLI's structure and behavior. Each primitive parser follows Optique's consistent design principles: they are type-safe, composable, and provide clear error messages when parsing fails. The type system automatically infers the result types, so you get full type safety without manual type annotations. ## `constant()` parser The `constant()` parser always succeeds without consuming any input and produces a fixed value. While this might seem trivial, it plays a crucial role in creating discriminated unions that allow TypeScript to distinguish between different parsing alternatives. ```typescript twoslash import { constant } from "@optique/core/primitives"; // Always produces the string "add" without consuming input const addCommand = constant("add"); // Can produce any type of constant value const defaultPort = constant(8080); const defaultConfig = constant({ debug: false, verbose: true }); ``` The `constant()` parser is particularly important when building subcommands or mutually exclusive options. It provides the discriminator field that enables type-safe pattern matching: ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; import { command, constant, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const parser = or( command("add", object({ type: constant("add"), file: option("-f", "--file", string()) })), command("remove", object({ type: constant("remove"), force: option("--force") })) ); // TypeScript can now distinguish between the two commands const result = parse(parser, ["add", "--file", "example.txt"]); if (result.success && result.value.type === "add") { // TypeScript knows this is the "add" command result console.log(`Adding file: ${result.value.file}.`); } else if (result.success && result.value.type === "remove") { // TypeScript knows this is the "remove" command result console.log(`Force remove: ${result.value.force}.`); } ``` The `constant()` parser has the lowest priority (0), meaning it never interferes with other parsers that need to consume input. ## `fail()` parser The `fail()` parser always fails without consuming any input. It is the counterpart to `constant()`: while `constant(value)` always succeeds and produces a value, `fail()` always fails. At the type level, `fail()` is declared to produce a value of type `T`, so it composes naturally with any parser that expects `Parser<"sync", T, …>`. At runtime, however, it never succeeds on its own. ```typescript twoslash import { fail } from "@optique/core/primitives"; // Declared to produce string, but always fails at runtime const alwaysFails = fail(); ``` ### Relationship to `constant()` | Trait | `constant(value)` | `fail()` | | -------------- | ------------------- | ----------------- | | Succeeds? | Always | Never | | Input consumed | 0 tokens | 0 tokens | | Priority | 0 | 0 | | Primary use | Discriminator field | Config-only value | ### Use with `bindConfig()` The primary use case for `fail()` is as the inner parser for `bindConfig()` when a value should come *only* from a config file—never from a CLI flag or positional argument. Because `fail()` always fails, `bindConfig()` always falls back to the config file (or the supplied default): ```typescript twoslash import { z } from "zod"; import { bindConfig, createConfigContext } from "@optique/config"; import { fail } from "@optique/core/primitives"; const configSchema = z.object({ timeout: z.number(), }); const configContext = createConfigContext({ schema: configSchema }); // No CLI flag for timeout — it only comes from the config file or default const timeoutParser = bindConfig(fail(), { context: configContext, key: "timeout", default: 30, }); ``` See [*Config file support*](../integrations/config.md#config-only-values) for a complete example. ### Why not `constant()` instead? `constant(value)` cannot be used for this purpose because it always *succeeds*, causing `bindConfig()` to treat it as a provided CLI value and skip the config file fallback entirely. `fail()` must always fail so that `bindConfig()` knows to look up the value from the config. ## `option()` parser The `option()` parser handles command-line options in various formats: long options (`--verbose`), short options (`-v`), combined short options (`-abc`), and options with values (`--port=8080` or `--port 8080`). ### Boolean flags When no [value parser](./valueparsers.md) is provided, `option()` creates a Boolean flag that returns `true` when present and `false` when absent: ```typescript twoslash import { option } from "@optique/core/primitives"; // Boolean flag with short and long form const verbose = option("-v", "--verbose"); // Multiple option names are supported const help = option("-h", "--help", "-?"); ``` ### Options with values When a [value parser](./valueparsers.md) is provided, the option expects and validates a value: ```typescript twoslash import { option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; // String option const name = option("-n", "--name", string()); // Integer option with validation const port = option("-p", "--port", integer({ min: 1, max: 0xffff })); // Option with custom metavar for help text const config = option("-c", "--config", string({ metavar: "FILE" })); ``` ### Supported option formats The `option()` parser recognizes multiple input formats: Space-separated : `-p 8080`, `--port 8080` Equals-separated : `--port=8080`, `-port=8080` Java-style : `-port 8080` DOS-style : `/port:8080` Bundled short options : `-abc` (equivalent to `-a -b -c` for boolean flags) ### Option ordering The `option()` parser has high priority (10) to ensure options are matched before positional arguments. ### Option descriptions You can provide descriptions for help text generation: ```typescript twoslash import { message } from "@optique/core/message"; import { option } from "@optique/core/primitives"; // ---cut-before--- const parser = option("-v", "--verbose", { description: message`Enable verbose output for debugging` }); ``` > \[!TIP] > Descriptions use Optique's [structured message system](./messages.md) rather > than plain strings. This provides consistent formatting and enables rich text > with semantic components like option names and metavariables. ## `flag()` parser *This API is available since Optique 0.3.0.* The `flag()` parser creates required Boolean flags that must be explicitly provided on the command line. Unlike `option()` which defaults to `false` when absent, `flag()` fails parsing entirely when not provided. This makes it ideal for scenarios where a flag's presence fundamentally changes the CLI's behavior or when implementing dependent options. ```typescript twoslash import { flag } from "@optique/core/primitives"; // A flag that must be explicitly provided const force = flag("-f", "--force"); // Multiple names are supported const confirm = flag("-y", "--yes", "--confirm"); ``` ### Key differences from `option()` While both `flag()` and `option()` can create Boolean flags, they differ in how they handle absence: ```typescript twoslash import { parse } from "@optique/core/parser"; import { flag, option } from "@optique/core/primitives"; const optionParser = option("-v", "--verbose"); const flagParser = flag("-f", "--force"); // option() succeeds with false when not provided const optionResult = parse(optionParser, []); // => { success: true, value: false } // flag() fails when not provided const flagResult = parse(flagParser, []); // => { success: false, error: "Expected an option, but got end of input." } ``` ### Use cases for `flag()` The `flag()` parser is particularly useful for: Required confirmation flags : Operations that need explicit user confirmation ``` ~~~~ typescript twoslash import { object } from "@optique/core/constructs"; import { argument, flag } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; // ---cut-before--- const deleteParser = object({ confirm: flag("--yes-i-am-sure"), // User must explicitly confirm target: argument(string()), }); ~~~~ ``` Dependent options : When a flag's presence enables additional options ``` ~~~~ typescript twoslash import { object } from "@optique/core/constructs"; import { withDefault } from "@optique/core/modifiers"; import { flag, option } from "@optique/core/primitives"; // When --advanced is not provided, parser fails and defaults are used const parser = withDefault( object({ advanced: flag("--advanced"), maxThreads: option("--threads"), // Only meaningful with --advanced cacheSize: option("--cache") // Only meaningful with --advanced }), { advanced: false, maxThreads: false, cacheSize: false } ); ~~~~ ``` Mode selection : When different flags trigger different parsing modes ``` ~~~~ typescript twoslash import { object } from "@optique/core/constructs"; import { optional } from "@optique/core/modifiers"; import { flag } from "@optique/core/primitives"; // ---cut-before--- const parser = object({ interactive: optional(flag("-i", "--interactive")), batch: optional(flag("-b", "--batch")), daemon: optional(flag("-d", "--daemon")) }); // At most one mode can be selected, enforced by application logic ~~~~ ``` ### Flag descriptions Like other parsers, `flag()` supports descriptions for help text: ```typescript twoslash import { message } from "@optique/core/message"; import { flag } from "@optique/core/primitives"; // ---cut-before--- const parser = flag("-f", "--force", { description: message`Skip all confirmation prompts` }); ``` The `flag()` parser has the same priority (10) as `option()` to ensure consistent option handling. ## `negatableFlag()` parser *This API is available since Optique 1.1.0.* The `negatableFlag()` parser creates a required Boolean choice between a positive flag and a negative flag. It returns `true` when the positive flag is provided and `false` when the negative flag is provided. If neither flag is present, parsing fails unless you wrap it with `optional()` or `withDefault()`. This is useful for settings whose default can vary at runtime, but where users still need an explicit command-line override in either direction: ```typescript twoslash import { withDefault } from "@optique/core/modifiers"; import { message } from "@optique/core/message"; import { negatableFlag } from "@optique/core/primitives"; declare function detectColorSupport(): boolean; // ---cut-before--- const color = withDefault( negatableFlag({ positive: "--color", negative: "--no-color", }, { description: message`Force-enable or force-disable colored output.`, }), () => detectColorSupport(), { message: message`auto` }, ); ``` When used without a wrapper, one of the two flags is required: ```typescript twoslash import { parse } from "@optique/core/parser"; import { negatableFlag } from "@optique/core/primitives"; const parser = negatableFlag({ positive: "--color", negative: "--no-color", }); parse(parser, ["--color"]); // => { success: true, value: true } parse(parser, ["--no-color"]); // => { success: true, value: false } parse(parser, []); // => { success: false, ... } ``` ### Aliases and help output Each side can have aliases. In help output, Optique renders the positive and negative names in one entry because they control the same Boolean value: ```typescript twoslash import { message } from "@optique/core/message"; import { negatableFlag } from "@optique/core/primitives"; // ---cut-before--- const color = negatableFlag({ positive: ["-c", "--color"], negative: "--no-color", }, { description: message`Control colored output.`, }); ``` The same option name cannot appear twice, or appear on both sides. Repeating the same side on the command line is treated as a duplicate, while using both the positive and negative flags is treated as a conflict. ### Optional and defaulted negatable flags Wrap `negatableFlag()` with `optional()` when absence should mean “no explicit override”: ```typescript twoslash import { optional } from "@optique/core/modifiers"; import { negatableFlag } from "@optique/core/primitives"; const colorOverride = optional(negatableFlag({ positive: "--color", negative: "--no-color", })); ``` Use `withDefault()` when you always want a concrete Boolean. The default value can be computed lazily, which makes runtime-dependent defaults explicit: ```typescript twoslash import { withDefault } from "@optique/core/modifiers"; import { negatableFlag } from "@optique/core/primitives"; declare function shouldUseColor(): boolean; // ---cut-before--- const color = withDefault( negatableFlag({ positive: "--color", negative: "--no-color", }), () => shouldUseColor(), ); ``` ### Custom errors `negatableFlag()` accepts custom error messages for missing flags, duplicate uses, conflicts, unexpected joined values, and no-match suggestions: ```typescript twoslash import { message, text } from "@optique/core/message"; import { negatableFlag } from "@optique/core/primitives"; // ---cut-before--- const color = negatableFlag({ positive: "--color", negative: "--no-color", }, { errors: { missing: (positive, negative) => message`Pass ${text(positive[0])} or ${text(negative[0])}.`, conflict: (previous, next) => message`${text(previous)} and ${text(next)} cannot be used together.`, }, }); ``` ## `argument()` parser The `argument()` parser handles positional arguments—values that appear in specific positions on the command line without option flags. Positional arguments are essential for intuitive CLI design, as users expect commands like `cp source.txt dest.txt` rather than `cp --source source.txt --dest dest.txt`. ```typescript twoslash import { argument } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; // Single positional argument const filename = argument(string({ metavar: "FILE" })); // Argument with validation const port = argument(integer({ min: 1, max: 0xffff, metavar: "PORT" })); ``` The `argument()` parser automatically handles the `--` separator, which conventionally signals the end of options. Arguments after `--` are treated as positional arguments even if they look like options: ```bash # Both "file" arguments are treated as positional arguments $ myapp --verbose -- --file1 --file2 ``` ### Argument ordering Arguments are consumed in the order they appear, and the parser will fail if it encounters an option where it expects a positional argument: ```typescript twoslash import { object } from "@optique/core/constructs"; import { option, argument } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const parser = object({ input: argument(string({ metavar: "INPUT" })), output: argument(string({ metavar: "OUTPUT" })), verbose: option("-v", "--verbose") }); // Valid: myapp input.txt output.txt -v // Invalid: myapp -v input.txt (expects INPUT but got option) ``` The `argument()` parser has medium priority (5) to ensure it runs after options but before lower-priority parsers. ### Argument descriptions You can provide descriptions for help text generation: ```typescript twoslash import { message } from "@optique/core/message"; import { argument } from "@optique/core/primitives"; import { path } from "@optique/run/valueparser" // ---cut-before--- const parser = argument(path(), { description: message`The file where data are read from.` }); ``` > \[!TIP] > Like option descriptions, argument descriptions use the [structured message > system](./messages.md) for consistent formatting and rich text capabilities. ## `command()` parser The `command()` parser enables building `git`-like CLI interfaces with subcommands. It matches a specific command name and then applies an inner parser to the remaining arguments. ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { command, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const addCommand = command("add", object({ file: option("-f", "--file", string()), all: option("-A", "--all") })); const removeCommand = command("remove", object({ force: option("--force"), recursive: option("-r", "--recursive") })); const parser = or(addCommand, removeCommand); ``` ### Command priority and matching The `command()` parser has the highest priority (15) to ensure subcommands are matched before other parsers attempt to process the input. This prevents conflicts where option parsers might try to interpret command names as invalid options. ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; import { command, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const addCommand = command("add", object({ file: option("-f", "--file", string()), all: option("-A", "--all") })); const removeCommand = command("remove", object({ force: option("--force"), recursive: option("-r", "--recursive") })); const parser = or(addCommand, removeCommand); // ---cut-before--- // Command matching happens first const result = parse(parser, ["add", "--file", "example.txt"]); // 1. "add" matches the command name // 2. Remaining ["--file", "example.txt"] is passed to the inner parser ``` ### Command descriptions Commands support descriptions for help text generation: ```typescript twoslash import { message } from "@optique/core/message"; import { command, constant } from "@optique/core/primitives"; const innerParser = constant(1); // ---cut-before--- const addCommand = command("add", innerParser, { description: message`Add files to the project` // [!code highlight] }); ``` > \[!TIP] > Command descriptions also use the [structured message system](./messages.md), > enabling rich descriptions with semantic components for better help text > formatting. ### Command aliases Use `aliases` when a command should accept shorter or legacy names while keeping one canonical display name: ```typescript twoslash import { object } from "@optique/core/constructs"; import { command, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const installCommand = command("install", object({ packageName: option("--package", string()) }), { aliases: ["i"] // [!code highlight] }); ``` Aliases parse exactly like the canonical command name. They are suggested by shell completion, but usage and help output continue to show only the canonical name. This keeps the public help text focused on the preferred command name without requiring a duplicate hidden command branch. Command aliases share the command namespace with sibling commands in alternative-style compositions. Optique throws a `TypeError` when a command name or alias collides with another command name or alias among active siblings in `or()`, `longestMatch()`, `object()`, or `merge()`. Ordered compositions such as `tuple()` and `seq()` are different: their child parsers are positional parts of the same command line, not alternative commands at one position. In those parsers, the same command name or alias can appear in more than one child when the grammar intentionally expects the command token more than once. ### Command usage lines For command help pages, you can override the usage tail with `usageLine`. This is useful for large nested command trees where you want a compact usage line like `myapp config ...`. ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { command } from "@optique/core/primitives"; // ---cut-before--- const configCommands = or( command("get", object({})), command("set", object({})), command("list", object({})), ); const config = command("config", configCommands, { usageLine: [{ type: "ellipsis" }], }); ``` You can also use a callback to derive the usage line from the default tail: ```typescript twoslash import type { Usage } from "@optique/core/usage"; import { object, or } from "@optique/core/constructs"; import { command } from "@optique/core/primitives"; // ---cut-before--- const configCommands = or( command("get", object({})), command("set", object({})), ); const config = command("config", configCommands, { usageLine: (defaultUsageLine: Usage) => { // Keep this command concise in help output return [{ type: "ellipsis" }]; }, }); ``` The `ellipsis` term is display-only. It does not change parsing behavior or shell completion. ### Nested subcommands You can nest commands multiple levels deep by using `command()` parsers as inner parsers: ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { argument, command, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; // ---cut-before--- const configCommands = or( command("get", object({ key: argument(string({ metavar: "KEY" })) })), command("set", object({ key: argument(string({ metavar: "KEY" })), value: argument(string({ metavar: "VALUE" })) })) ); const parser = or( command("config", configCommands), command("init", object({ template: option("-t", "--template", string()) })) ); // Usage: myapp config get database.url // Usage: myapp config set database.url "postgres://localhost/mydb" // Usage: myapp init --template react ``` ## `passThrough()` parser *This API is available since Optique 0.8.0.* The `passThrough()` parser collects unrecognized options and passes them through without validation. This is useful for building wrapper CLI tools that need to forward unknown options to an underlying tool or command. > \[!CAUTION] > *Consider alternatives before using `passThrough()`.* This parser > intentionally weakens Optique's strict parsing philosophy where “all input > must be recognized.” While it enables legitimate wrapper/proxy tool > patterns, it comes with significant trade-offs: > > * Typos in pass-through options won't be caught > * No type safety for forwarded options > * No shell completion support for pass-through options > * Error messages become less helpful for users > > Before reaching for `passThrough()`, consider whether: > > * You can use the standard `--` separator to explicitly mark pass-through > arguments (e.g., `mycli --debug -- --forwarded-opt`) > * You can define the forwarded options explicitly for better type safety > * Your use case truly requires capturing arbitrary unknown options ```typescript twoslash import { object } from "@optique/core/constructs"; import { option, passThrough } from "@optique/core/primitives"; const parser = object({ debug: option("--debug"), extra: passThrough(), }); // mycli --debug --foo=bar --baz=qux // → { debug: true, extra: ["--foo=bar", "--baz=qux"] } ``` ### Capture formats The `passThrough()` parser supports three different capture formats, each with different trade-offs: #### `"equalsOnly"` (default) The safest and most predictable format. Only captures options in `--opt=val` format where the value is explicitly attached to the option name: ```typescript twoslash import { passThrough } from "@optique/core/primitives"; // ---cut-before--- const parser = passThrough({ format: "equalsOnly" }); // Captures: --foo=bar, --baz=qux // Does NOT capture: --foo bar, --verbose ``` This format has no ambiguity because the value is explicitly attached to the option name. Non-option arguments and space-separated values are not captured. #### `"nextToken"` A balanced choice that handles space-separated option values. When an unrecognized option starting with `-` is encountered, the parser also consumes the next token if it doesn't start with `-`: ```typescript twoslash import { passThrough } from "@optique/core/primitives"; // ---cut-before--- const parser = passThrough({ format: "nextToken" }); // mycli --foo bar --baz qux // → ["--foo", "bar", "--baz", "qux"] // mycli --foo --bar // → ["--foo", "--bar"] (--bar is a separate option, not a value) ``` This format covers most CLI styles while still being reasonably predictable. #### `"greedy"` Captures *all remaining tokens* from the first unrecognized token onwards, regardless of whether they would match other parsers: ```typescript twoslash import { object } from "@optique/core/constructs"; import { argument, command, passThrough } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const parser = command("exec", object({ container: argument(string()), args: passThrough({ format: "greedy" }), })); // myproxy exec mycontainer --verbose -it bash // → { container: "mycontainer", args: ["--verbose", "-it", "bash"] } ``` > \[!CAUTION] > The `"greedy"` format requires careful use because it can shadow explicit > parsers. Once greedy mode triggers, all remaining tokens are consumed. > Typically used only when you have *no other options to parse* after the > pass-through point, or in subcommand-specific contexts where the entire > subcommand is pass-through. ### Priority The `passThrough()` parser has the *lowest priority* (−10) among all parsers to ensure explicit parsers always match first: * *Priority 15*: `command()` parsers * *Priority 10*: `option()`, `flag()`, and `negatableFlag()` parsers * *Priority 5*: `argument()` parsers * *Priority 0*: `constant()` and `fail()` parsers * *Priority −10*: `passThrough()` parsers This priority system ensures that your recognized options (like `--debug` in the example above) are always processed correctly, with only truly unrecognized options going to `passThrough()`. ### Options terminator The `passThrough()` parser respects the `--` options terminator in `"equalsOnly"` and `"nextToken"` modes. After `--`, options are no longer captured: ```typescript twoslash import { object } from "@optique/core/constructs"; import { multiple } from "@optique/core/modifiers"; import { argument, option, passThrough } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const parser = object({ debug: option("--debug"), extra: passThrough(), files: multiple(argument(string())), }); // mycli --debug --foo=bar -- --not-an-option file.txt // → { debug: true, extra: ["--foo=bar"], files: ["--not-an-option", "file.txt"] } ``` In `"greedy"` mode, the parser still captures tokens after `--` since its purpose is to pass everything through. ## Parser priority and state management ### Priority system Optique uses a priority system to determine the order in which parsers are applied when multiple parsers are available. This ensures that more specific parsers (like commands) are tried before more general ones (like arguments): * *Priority 15*: `command()` parsers * *Priority 10*: `option()`, `flag()`, and `negatableFlag()` parsers * *Priority 5*: `argument()` parsers * *Priority 0*: `constant()` and `fail()` parsers * *Priority −10*: `passThrough()` parsers Higher priority parsers are always tried first, which prevents ambiguous parsing situations and ensures predictable behavior. The `passThrough()` parser has the lowest priority to ensure it only captures truly unrecognized options. ### State management Each primitive parser manages its own internal state during the parsing process. The state tracks whether the parser has been invoked, what values have been consumed, and any validation results. For example, an `option()` parser's state might be: * `undefined`: Option not yet encountered * `{ success: true, value: "hello" }`: Option successfully parsed with value * `{ success: false, error: "Invalid value" }`: Option encountered but value parsing failed This state management enables features like preventing duplicate options, validating that required arguments are provided, and generating helpful error messages. ### Error handling When primitive parsers encounter invalid input, they return detailed error messages that help users understand what went wrong: ```typescript // Parsing ["--port", "invalid"] with integer value parser { success: false, error: "Expected a valid integer, but got invalid." } ``` ```typescript // Parsing ["--missing-option"] where no parser matches { success: false, error: "No matched option for --missing-option." } ``` The error messages are designed to be user-friendly while providing enough detail for developers to understand parsing failures. ## Working with primitive parsers ### Single primitive usage You can use primitive parsers directly for simple CLI applications: ```typescript twoslash import { parse } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const nameParser = option("--name", string()); const result = parse(nameParser, ["--name", "Alice"]); if (result.success) { console.log(`Hello, ${result.value}!`); } else { console.error(result.error); } ``` ### Combining primitives More commonly, you'll combine multiple primitive parsers using [structural combinators](./constructs.md) like `object()`: ```typescript twoslash import { object } from "@optique/core/constructs"; import type { InferValue } from "@optique/core/parser"; import { argument, option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; const parser = object({ // [!code highlight] input: argument(string({ metavar: "INPUT" })), output: option("-o", "--output", string({ metavar: "OUTPUT" })), port: option("-p", "--port", integer({ min: 1, max: 0xffff })), verbose: option("-v", "--verbose") }); type Result = InferValue; // ^? // TypeScript automatically infers the result type! ``` ### Common patterns #### Required vs optional By default, `option()` and `argument()` parsers are required—parsing fails if they're not provided. Use [modifying combinators](./modifiers.md) like `optional()` or `withDefault()` to make them optional: ```typescript twoslash import { object } from "@optique/core/constructs"; import { optional, withDefault } from "@optique/core/modifiers"; import { argument, option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; const parser = object({ input: argument(string()), // Required output: optional(option("-o", string())), // Optional (returns string | undefined) // [!code highlight] port: withDefault(option("-p", integer()), 8080) // Optional with default // [!code highlight] }); ``` #### Multiple occurrences Use the `multiple()` combinator to allow repeated options or arguments: ```typescript twoslash import { object } from "@optique/core/constructs"; import { multiple } from "@optique/core/modifiers"; import { argument, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const parser = object({ files: multiple(argument(string())), // Multiple files // [!code highlight] includes: multiple(option("-I", string())) // Multiple -I options // [!code highlight] }); ``` ## Hidden parsers All primitive parsers—`option()`, `flag()`, `negatableFlag()`, `argument()`, `command()`, and `passThrough()`—support a `hidden` option: * `true`: hide from usage, help entries, shell completions, and “Did you mean?” suggestions * `"usage"`: hide from usage only * `"doc"`: hide from help entries only * `"help"`: hide from usage and help entries, but keep shell completions and “Did you mean?” suggestions Hidden parsers remain fully functional for parsing. `group()`, `object()`, and `merge()` also support `hidden` with the same values. When both a wrapper parser and an inner primitive specify `hidden`, restrictions are combined as a union. ### When to use hidden parsers Hidden parsers are useful for: * *Deprecated options*: Keep old options working for backward compatibility while hiding them from new users * *Internal debugging flags*: Options that developers need but shouldn't be exposed in user-facing documentation * *Experimental features*: Try out new options without committing to documenting them * *Alias consolidation*: Hide less-preferred forms while keeping them functional ### Examples ```typescript twoslash import { group, object, or } from "@optique/core/constructs"; import { argument, command, flag, option, passThrough } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; // Hidden option (deprecated) const parser1 = object({ output: option("-o", "--output", string()), // Keep old --out working but hide it from all user discovery outputLegacy: option("--out", string(), { hidden: true }), }); // Hidden flag (debugging) const parser2 = object({ verbose: flag("-v", "--verbose"), // Internal debugging flag trace: flag("--trace-internal", { hidden: true }), }); // Hidden command (experimental) const commands = or( command("build", object({ mode: option("--mode", string()) })), command("test", object({ watch: flag("--watch") })), // Experimental command not yet documented command("experimental-deploy", object({ target: argument(string()), }), { hidden: true }), ); // Hidden argument (internal) const parser3 = object({ file: argument(string()), // Debug parameter not shown in usage debugLevel: argument(integer(), { hidden: true }), }); // Hide global options from usage lines, but keep them in help const parser4 = group("Global", object({ verbose: flag("-v", "--verbose"), config: option("--config", string()), }), { hidden: "usage" }); // Keep an option undocumented, but discoverable via completion const parser5 = object({ profile: option("--profile", string()), debugTransport: option("--debug-transport", string(), { hidden: "help" }), }); ``` Hidden parsers still parse input normally. Users who know about them can still use them: ```bash # These all work, even though they're hidden myapp --out output.txt # Hidden legacy option myapp --trace-internal # Hidden debug flag myapp experimental-deploy # Hidden command ``` These patterns demonstrate how primitive parsers serve as the foundation for more complex CLI structures, providing the building blocks that higher-level combinators orchestrate into complete parsing solutions. --- --- url: /concepts/runners.md description: >- Learn about the different ways to execute parsers in Optique: from low-level parsing to high-level process integration with automatic error handling and help text generation. --- # Runners and execution Once you've built a parser using combinators, you need to execute it against command-line arguments. Optique provides three different approaches with varying levels of automation and control: the low-level `parse()` function, the mid-level `runParser()` function from `@optique/core/facade`, and the high-level `run()` function from *@optique/run* with full process integration. Each approach serves different use cases, from fine-grained control over parsing results to completely automated CLI applications that handle everything from argument extraction to process exit codes. ## Bundling parsers with metadata Optique supports two approaches for providing program metadata (name, version, description, etc.): 1. *Bundled with `Program`*: Create a single object containing both parser and metadata 2. *Passed as options*: Provide metadata directly to `runParser()` or `run()` ### Using the `Program` interface The `Program` interface from `@optique/core/program` bundles your parser with metadata: ```typescript twoslash import { defineProgram } from "@optique/core/program"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; const parser = object({ name: option("-n", "--name", string()), port: option("-p", "--port", integer({ min: 1000 })), }); const prog = defineProgram({ parser, metadata: { name: "myserver", version: "1.0.0", brief: message`A powerful server application`, description: message`This server processes requests efficiently.`, author: message`Jane Doe `, bugs: message`Report bugs at https://github.com/user/repo/issues`, examples: message` ${message`myserver --name server1 --port 8080`} ${message`myserver --help`} `, footer: message`Visit https://example.com for more info.`, }, }); ``` *Benefits of using `Program`:* * Metadata is defined once and reused everywhere * `runParser()` and `run()` automatically reuse the program name and help-text metadata * Version display can still opt into `metadata.version` explicitly * Man page generation and related tooling can reuse the same metadata * Cleaner API with fewer parameters to pass *When to use `Program`:* * Production CLI applications with version numbers and help text * Projects where metadata needs to be shared across multiple entry points * When building reusable CLI components *When to pass metadata as options:* * Simple scripts or prototypes without versioning * One-off tools where metadata isn't reused * When metadata needs to be computed dynamically at runtime Both approaches are fully supported and you can choose based on your needs. ## Low-level parsing with `parse()` The `parse()` function from `@optique/core/parser` provides the most basic parsing operation. It takes a parser and an array of string arguments, returning a result object that you must handle manually. ```typescript twoslash import { object } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; import { formatMessage } from "@optique/core/message"; const parser = object({ name: option("-n", "--name", string()), port: option("-p", "--port", integer({ min: 1000 })), }); const result = parse(parser, ["--name", "server", "--port", "8080"]); // ^? if (result.success) { console.log(`Starting ${result.value.name} on port ${result.value.port}.`); } else { console.error(`Parse error: ${formatMessage(result.error)}.`); process.exit(1); } ``` The `parse()` function returns a discriminated union type that indicates success or failure: ```typescript twoslash import { object } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { formatMessage } from "@optique/core/message"; const parser = object({ name: option("-n", "--name", string()) }); const result = parse(parser, ["--name", "test"]); // Result type is: { success: true, value: { name: string } } | // { success: false, error: Message } if (result.success) { // TypeScript knows this is the success case result.value.name; // string } else { // TypeScript knows this is the error case formatMessage(result.error); // string } ``` Use `parse()` when you need complete control over error handling, want to integrate parsing into a larger application flow, or need to handle multiple parsing attempts. ## Mid-level execution with `@optique/core/facade` The `runParser()` function from `@optique/core/facade` adds automatic help generation and formatted error messages while still giving you control over program behavior through callbacks. It accepts either a `Program` object or a parser with metadata passed via options. ```typescript twoslash import { defineProgram } from "@optique/core/program"; import { object } from "@optique/core/constructs"; import { runParser } from "@optique/core/facade"; import { option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; const parser = object({ name: option("-n", "--name", string()), port: option("-p", "--port", integer({ min: 1000 })), }); const prog = defineProgram({ parser, metadata: { name: "myserver", version: "1.0.0", brief: message`A powerful server application`, }, }); // Program metadata provides the program name and documentation fields const config = runParser( prog, process.argv.slice(2), // arguments { help: { // Enable help functionality command: true, // Enable help command option: true, // Enable --help option onShow: process.exit, // Exit after showing help }, version: { // Enable version functionality option: true, // Enable --version flag value: prog.metadata.version!, // Use version from metadata onShow: process.exit, // Exit after showing version }, colors: process.stdout.isTTY, // Auto-detect color support onError: process.exit, // Exit with error code } ); config // Its result type is: // ^? console.log(`Starting ${config.name} on port ${config.port}.`); ``` Alternatively, you can pass metadata directly without using `Program`: ```typescript twoslash import { object } from "@optique/core/constructs"; import { runParser } from "@optique/core/facade"; import { option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; const parser = object({ name: option("-n", "--name", string()), port: option("-p", "--port", integer({ min: 1000 })), }); const config = runParser( parser, "myserver", // program name process.argv.slice(2), // arguments { brief: message`A powerful server application`, help: { command: true, option: true, onShow: process.exit, }, version: { option: true, value: "1.0.0", onShow: process.exit, }, colors: process.stdout.isTTY, onError: process.exit, } ); console.log(`Starting ${config.name} on port ${config.port}.`); ``` When configured, both approaches automatically handle: * *Help generation*: Creates formatted help text from parser structure * *Version display*: Shows version information via `--version` or `version` command * *Shell completion*: Generates completion scripts and handles completion requests * *Error formatting*: Shows clear error messages with usage information * *Meta request parsing*: Recognizes configured help/version/completion flags and subcommands * *Usage display*: Shows command syntax when errors occur Built-in help, version, and completion requests are parser-aware. The runner treats `help`, `version`, `completion`, `--help`, `--version`, `--completion`, and any configured aliases as meta requests only when the user parser leaves them unconsumed. If your parser accepts the same token sequence as ordinary data, such as a positional `help` value or an option value `--help`, the parse result wins and the runner does not intercept it. The `RunOptions` interface provides extensive customization: ```typescript twoslash import { defineProgram } from "@optique/core/program"; import { object } from "@optique/core/constructs"; import { runParser } from "@optique/core/facade"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; const parser = object({ name: option("-n", "--name", string()) }); const prog = defineProgram({ parser, metadata: { name: "myapp", version: "2.1.0", brief: message`A powerful CLI tool`, description: message`This tool processes data efficiently.`, footer: message`Visit https://example.com for more info`, }, }); const result = runParser(prog, ["--name", "test"], { colors: true, // Force colored output maxWidth: 80, // Wrap text at 80 columns showDefault: true, // Show default values in help text help: { // Grouped help API option: true, // Only --help option, no help command }, version: { // Version functionality command: true, // Both --version option and version command option: true, value: prog.metadata.version!, // Use version from metadata }, completion: { // Shell completion functionality command: { names: ["completions"] }, // Use plural command name option: true, }, aboveError: "help", // Show full help before error messages stderr: (text) => { // Custom error output handler console.error(`ERROR: ${text}`); }, stdout: console.log, // Custom help output handler }); ``` Use this approach when you need automatic help and error handling but want control over process behavior, or when integrating with frameworks that manage process lifecycle. ### Explicit sync/async variants *This API is available since Optique 0.9.0.* The `runParser()` function also has explicit sync/async variants for mode-aware execution: ```typescript twoslash import type { ValueParser, ValueParserResult } from "@optique/core/valueparser"; import { object } from "@optique/core/constructs"; import { runParserSync, runParserAsync } from "@optique/core/facade"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; function apiKey(): ValueParser<"async", string> { return { mode: "async", metavar: "KEY", placeholder: "", async parse(input: string): Promise> { return { success: true, value: input }; }, format: (v) => v, }; } // ---cut-before--- // Sync parser - returns directly const syncParser = object({ name: option("-n", "--name", string()), }); const syncResult = runParserSync(syncParser, "myapp", ["--name", "test"]); // Async parser - returns Promise const asyncParser = object({ key: option("--api-key", apiKey()), name: option("-n", "--name", string()), }); const asyncResult = await runParserAsync( asyncParser, "myapp", ["--api-key", "abc123", "-n", "test"], ); ``` `runParserSync()` : Only accepts sync parsers. Returns the parsed value directly. Provides a compile-time error if you pass an async parser. `runParserAsync()` : Accepts any parser (sync or async). Always returns a `Promise`. Use this when working with parsers that may contain async value parsers. `runParser()` : The generic function that automatically returns the appropriate type based on the parser's mode. ## High-level execution with *@optique/run* The `run()` function from *@optique/run* provides complete process integration with zero configuration required. It automatically handles argument extraction, terminal detection, and process exit. ```typescript twoslash import { defineProgram } from "@optique/core/program"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; import { run, print } from "@optique/run"; const parser = object({ name: option("-n", "--name", string()), port: option("-p", "--port", integer({ min: 1000 })), }); const prog = defineProgram({ parser, metadata: { name: "myserver", version: "1.0.0", brief: message`A powerful server application`, description: message`This server processes requests efficiently.`, footer: message`Visit https://example.com for more info.`, }, }); // Completely automated - just run the program const config = run(prog, { help: "both", // Enable both --help and help command version: prog.metadata.version, // Use version from metadata }); config // Its result type is: // ^? // If we reach this point, parsing succeeded print(message`Starting ${config.name} on port ${config.port.toString()}.`); ``` The function automatically: * *Extracts arguments* from `process.argv.slice(2)` * *Uses program name* from `Program` metadata * *Auto-detects colors* from `process.stdout.isTTY` * *Auto-detects width* from `process.stdout.columns` * *Exits on error* with code 1 by default When `help`, `version`, or `completion` is enabled, the same runner also handles those meta requests and exits with code 0. ### Configuration options *@optique/run*'s `run()` function provides several configuration options for fine-tuning behavior: ```typescript twoslash import { defineProgram } from "@optique/core/program"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; import { run } from "@optique/run"; const parser = object({ name: option("-n", "--name", string()), debug: option("--debug") }); const prog = defineProgram({ parser, metadata: { name: "my-tool", version: "2.0.0", brief: message`My CLI Tool`, description: message`Processes files efficiently`, footer: message`Visit example.com for help`, }, }); const config = run(prog, { args: ["custom", "args"], // Override process.argv colors: true, // Force colored output maxWidth: 100, // Set output width stdout: console.log, // Inject output writer for help/version/completion stderr: console.error, // Inject error writer onExit: process.exit, // Inject exit handler showDefault: true, // Show default values in help text help: "both", // Enable both --help and help command version: { // Advanced version configuration value: prog.metadata.version!, // Version from metadata command: true, // Only version command, no --version option }, aboveError: "usage", // Show usage on errors errorExitCode: 2 // Exit code for errors }); ``` ### Help system options Enable built-in help functionality with different modes: ```typescript twoslash import { defineProgram } from "@optique/core/program"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { run } from "@optique/run"; const parser = object({ name: option("-n", "--name", string()) }); const prog = defineProgram({ parser, metadata: { name: "mytool" }, }); // Simple string-based API const result1 = run(prog, { help: "option", // Adds --help option only }); const result2 = run(prog, { help: "command", // Adds help subcommand only }); const result3 = run(prog, { help: "both", // Adds both --help and help command }); // No help (default) - simply omit the help option const result4 = run(prog, {}); ``` ### Version system options Enable built-in version functionality with flexible configuration: ```typescript twoslash import { defineProgram } from "@optique/core/program"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { run } from "@optique/run"; const parser = object({ name: option("-n", "--name", string()) }); const prog = defineProgram({ parser, metadata: { name: "mytool", version: "1.0.0" }, }); // Simple string-based API (uses default "option" mode) const result1 = run(prog, { version: prog.metadata.version, // Adds --version option only }); // Advanced object-based API const result2 = run(prog, { version: { value: prog.metadata.version!, option: true, // Adds --version option only } }); const result3 = run(prog, { version: { value: prog.metadata.version!, command: true, // Adds version subcommand only } }); const result4 = run(prog, { version: { value: prog.metadata.version!, command: true, // Adds both --version and version command option: true, } }); // No version (default) - simply omit the version option const result5 = run(prog, {}); ``` ### Shell completion *This API is available since Optique 0.6.0.* Enable shell completion support for Bash, zsh, fish, PowerShell, and Nushell with simple configuration. When completion is enabled, the `run()` function automatically handles completion script generation and runtime completion requests: ```typescript twoslash import { object } from "@optique/core/constructs"; import { option, argument } from "@optique/core/primitives"; import { string, choice } from "@optique/core/valueparser"; import { run } from "@optique/run"; const parser = object({ format: option("-f", "--format", choice(["json", "yaml"])), input: argument(string()), }); const config = run(parser, { completion: "both", // "command" | "option" | "both" }); ``` ### Completion configuration The `command` and `option` properties control how completion is triggered: `command: true` : Completion via subcommand (`myapp completion bash`) `option: true` : Completion via option (`myapp --completion bash`) Both can be enabled simultaneously. ### Command name customization By default, the completion command is named `completion` and the option is `--completion`. You can customize the command name by passing a configuration object: ```typescript twoslash import { object } from "@optique/core/constructs"; import { run } from "@optique/run"; const parser = object({}); const config = run(parser, { completion: { command: { names: ["completions"] }, // Use "completions" command name option: true, } }); ``` To register multiple command names (e.g., both singular and plural), pass an array. Additional names after the first are hidden from help output by default: ```typescript twoslash import { object } from "@optique/core/constructs"; import { run } from "@optique/run"; const parser = object({}); const config = run(parser, { completion: { command: { names: ["completion", "completions"] }, option: true, } }); ``` Users can generate and install completion scripts: ::: code-group ```bash [Bash] myapp completion bash > ~/.bashrc.d/myapp.bash source ~/.bashrc.d/myapp.bash ``` ```zsh [zsh] myapp completion zsh > ~/.zsh/completions/_myapp ``` ```fish [fish] myapp completion fish > ~/.config/fish/completions/myapp.fish ``` ```powershell [PowerShell] myapp completion pwsh > myapp-completion.ps1 ``` ```nushell [Nushell] myapp completion nu | save myapp-completion.nu source myapp-completion.nu ``` ::: Shell completion works automatically with all parser types and value parsers, providing intelligent suggestions based on your parser structure. For detailed information, see the [*Shell completion* section](./completion.md). ### Meta-command grouping *This API is available since Optique 0.10.0.* By default, meta-commands (help, version, completion) appear alongside user-defined commands in help output. You can place them under titled sections by specifying a `group` option. Commands sharing the same group name are merged into a single section: ```typescript twoslash import { object } from "@optique/core/constructs"; import { command, constant, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { or } from "@optique/core/constructs"; import { run } from "@optique/run"; const parser = or( command("serve", object({ type: constant("serve"), port: option("-p", "--port", string()), })), command("build", object({ type: constant("build"), output: option("-o", "--output", string()), })), ); const config = run(parser, { help: { command: { group: "Other" }, option: true }, version: { value: "1.0.0", command: { group: "Other" }, option: true }, completion: { command: { group: "Other" }, option: true }, }); ``` This produces help output with a separated “Other:” section: ```ansi Usage: myapp Commands: serve build Other: help version completion SHELL ``` The `group` option is available on both `command` and `option` sub-configs. You can also group only some meta-commands while leaving others ungrouped. #### Section merging When a meta-command's `group` name matches an existing section in the user parser, the two sections are automatically merged into one. For example, if the user parser creates a “Commands” section and the help command is also assigned `group: "Commands"`, they appear together: ```typescript twoslash import { group } from "@optique/core/constructs"; import { command, constant } from "@optique/core/primitives"; import { or } from "@optique/core/constructs"; import { run } from "@optique/run"; const parser = group("Commands", or( command("serve", constant("serve")), command("build", constant("build")), )); const config = run(parser, { help: { command: { group: "Commands" }, option: true }, version: { value: "1.0.0", option: true }, }); ``` This produces a single “Commands:” section containing both user and meta commands. Similarly, multiple `group("X", …)` combinators in the user parser that share the same name are merged into a single “X:” section. ### Default value display Both runner functions support displaying default values in help text when options or arguments are created with `withDefault()`: ```typescript twoslash import { object } from "@optique/core/constructs"; import { withDefault } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; import { run } from "@optique/run"; const parser = object({ name: option("-n", "--name", string()), port: withDefault(option("-p", "--port", integer()), 3000), format: withDefault(option("-f", "--format", string()), "json"), }); const config = run(parser, { showDefault: true, // Shows: --port [3000], --format [json] }); // Custom formatting const config2 = run(parser, { showDefault: { prefix: " (default: ", suffix: ")" } // Shows: --port (default: 3000), --format (default: json) }); ``` Default values are automatically dimmed when colors are enabled, making them visually distinct from the main help text. ### Choice display *This API is available since Optique 0.10.0.* Both runner functions support displaying valid choices in help text when options or arguments use the `choice()` value parser: ```typescript twoslash import { object } from "@optique/core/constructs"; import { option, argument } from "@optique/core/primitives"; import { choice, string } from "@optique/core/valueparser"; import { run } from "@optique/run"; const parser = object({ name: option("-n", "--name", string()), format: option("-f", "--format", choice(["json", "yaml", "xml"])), level: argument(choice(["debug", "info", "warn", "error"])), }); const config = run(parser, { showChoices: true, // Shows: --format (choices: json, yaml, xml) }); // Custom formatting const config2 = run(parser, { showChoices: { prefix: " {", suffix: "}", label: "", } // Shows: --format {json, yaml, xml} }); // Limit displayed choices const config3 = run(parser, { showChoices: { maxItems: 3, // Shows first 3 choices, then "..." } }); ``` Choice values are automatically dimmed when colors are enabled, making them visually distinct from the main help text. Both `showDefault` and `showChoices` can be enabled simultaneously. ### Rich documentation support *This API is available since Optique 0.4.0.* Both runner functions support adding rich documentation to help text. The recommended approach is to bundle metadata with your parser using the `Program` interface: ```typescript twoslash import { defineProgram } from "@optique/core/program"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; import { run } from "@optique/run"; const parser = object("Options", { input: option("-i", "--input", string()), output: option("-o", "--output", string()), }); const prog = defineProgram({ parser, metadata: { name: "myapp", version: "1.0.0", brief: message`A powerful file processing tool`, description: message`This utility processes files with various transformations. Supports multiple input formats including JSON, YAML, and plain text. Output can be customized with different formatting options.`, footer: message`Examples: myapp -i data.json -o result.txt myapp --input config.yaml --output processed.json For more information, visit: https://example.com/docs Report bugs at: https://github.com/user/myapp/issues`, }, }); const config = run(prog, { help: "option", version: prog.metadata.version, }); ``` The documentation fields appear in the following order in help output: ```ansi A powerful file processing tool Usage: myapp -i/--input STRING -o/--output STRING This utility processes files with various transformations. Supports multiple input formats including JSON, YAML, and plain text. Output can be customized with different formatting options. Options: -i, --input STRING -o, --output STRING Examples: myapp -i data.json -o result.txt myapp --input config.yaml --output processed.json For more information, visit: https://example.com/docs Report bugs at: https://github.com/user/myapp/issues ``` These same fields also appear when errors are displayed with `aboveError: "help"`, providing context even when parsing fails. The user-provided documentation takes precedence over any documentation generated from parser structure. ### Section ordering *This feature is available since Optique 1.0.0.* By default, help output sections are sorted in a type-aware order: sections containing only commands appear first, followed by sections with a mix of commands and options, and finally sections containing only options, flags, and arguments. Within each group, the original relative order among sections is preserved (stable sort). This default order resolves the problem where titled command sections (e.g., `Other:` with subcommands) incorrectly appeared after titled option sections (e.g., `Global:` with `--verbose`) in help output. You can override the sort order by providing a `sectionOrder` comparator to `runParser()` or `run()`: ```typescript twoslash import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { run } from "@optique/run"; import type { DocSection } from "@optique/core/doc"; const parser = object("Options", { output: option("-o", "--output", string()), }); run(parser, { programName: "myapp", help: "option", // Sort sections alphabetically by title sectionOrder: (a: DocSection, b: DocSection) => (a.title ?? "").localeCompare(b.title ?? ""), }); ``` The comparator receives two `DocSection` objects and returns a number: negative to place `a` before `b`, positive to place `a` after `b`, or `0` to preserve their original relative order. ### Error handling behavior When the corresponding features are enabled, the *@optique/run* `run()` function automatically: * Prints usage information and error messages to stderr * Exits with code `0` for help, version, and completion requests * Exits with code `1` (or custom) for parse errors * Never returns on errors by default (calls `process.exit()`) You can override this process integration by injecting custom handlers: * `stdout`: controls where help/version/completion output is written * `stderr`: controls where parse/completion errors are written * `onExit`: controls how exits are handled (`0` for help/version, `errorExitCode` for errors) This is useful for embedding and testing, where calling `process.exit()` is undesirable. ```typescript twoslash import { object } from "@optique/core/constructs"; import { run } from "@optique/run"; const parser = object({}); let output = ""; let errorOutput = ""; let exitCode = -1; try { run(parser, { args: ["--help"], programName: "myapp", help: "option", stdout: (text) => { output += `${text}\n`; }, stderr: (text) => { errorOutput += `${text}\n`; }, onExit: (code) => { exitCode = code; throw new Error("EXIT"); }, }); } catch { // expected in tests } ``` ## Async parser execution *This API is available since Optique 0.9.0.* Parsers in Optique can be either synchronous or asynchronous. The mode is tracked at compile time through the `mode` property and the `Mode` type parameter. When any component of a parser (such as a value parser) is async, the entire composite parser becomes async. ### Using `parseAsync()` and `suggestAsync()` For parsers that may be async, use the explicit async functions: ```typescript twoslash import type { ValueParser, ValueParserResult } from "@optique/core/valueparser"; import { object } from "@optique/core/constructs"; import { parseAsync, suggestAsync } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; // A custom async value parser function apiKey(): ValueParser<"async", string> { return { mode: "async", metavar: "KEY", placeholder: "", async parse(input: string): Promise> { // Validate API key against remote service const response = await fetch(`https://api.example.com/validate?key=${encodeURIComponent(input)}`); if (!response.ok) { return { success: false, error: message`Invalid API key.` }; } return { success: true, value: input }; }, format: (v) => v, }; } const parser = object({ key: option("--api-key", apiKey()), name: option("-n", "--name", string()), }); // parseAsync() returns a Promise const result = await parseAsync(parser, ["--api-key", "abc123", "-n", "test"]); if (result.success) { console.log(`Using key for ${result.value.name}.`); } // suggestAsync() also returns a Promise const suggestions = await suggestAsync(parser, ["--"]); ``` ### Sync-only functions For parsers that are guaranteed to be sync, you can use the sync-only variants which provide direct return values without `Promise` wrappers: ```typescript twoslash import { object } from "@optique/core/constructs"; import { parseSync, suggestSync } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; // A parser using only sync value parsers const parser = object({ name: option("-n", "--name", string()), port: option("-p", "--port", integer()), }); // parseSync() returns directly (no Promise) const result = parseSync(parser, ["--name", "server", "--port", "8080"]); // suggestSync() also returns directly const suggestions = suggestSync(parser, ["--"]); ``` The generic `parse()` and `suggest()` functions automatically return the appropriate type based on the parser's mode: ```typescript twoslash import { object } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const syncParser = object({ name: option("-n", "--name", string()), }); // Returns Result directly for sync parsers const result = parse(syncParser, ["--name", "test"]); ``` For more details on creating async value parsers, see the [*Async value parsers*](./valueparsers.md#async-value-parsers) section. ### Documentation page generation The `getDocPage()` function extracts documentation information from a parser for generating help text. Like other functions, it has sync and async variants: ```typescript twoslash import type { ValueParser, ValueParserResult } from "@optique/core/valueparser"; import { object } from "@optique/core/constructs"; import { getDocPage, getDocPageSync, getDocPageAsync } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; function apiKey(): ValueParser<"async", string> { return { mode: "async", metavar: "KEY", placeholder: "", async parse(input: string): Promise> { return { success: true, value: input }; }, format: (v) => v, }; } // ---cut-before--- // Sync parser - use getDocPageSync() or getDocPage() const syncParser = object({ name: option("-n", "--name", string()), }); const syncDoc = getDocPageSync(syncParser); const syncDoc2 = getDocPage(syncParser); // Also returns directly // Async parser - use getDocPageAsync() or await getDocPage() const asyncParser = object({ key: option("--api-key", apiKey()), name: option("-n", "--name", string()), }); const asyncDoc = await getDocPageAsync(asyncParser); const asyncDoc2 = await getDocPage(asyncParser); // Returns Promise ``` `getDocPageSync()` : Only accepts sync parsers. Returns `DocPage | undefined` directly. `getDocPageAsync()` : Accepts any parser (sync or async). Always returns `Promise`. `getDocPage()` : The generic function that returns the appropriate type based on the parser's mode. ### Using `runSync()` and `runAsync()` *This API is available since Optique 0.9.0.* The *@optique/run* package also provides explicit sync/async variants of the `run()` function: ```typescript twoslash import type { ValueParser, ValueParserResult } from "@optique/core/valueparser"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; function apiKey(): ValueParser<"async", string> { return { mode: "async", metavar: "KEY", placeholder: "", async parse(input: string): Promise> { return { success: true, value: input }; }, format: (v) => v, }; } const args = ["--api-key", "abc123", "-n", "test"]; // ---cut-before--- import { run, runSync, runAsync } from "@optique/run"; // Sync parser with runSync() - returns directly const syncParser = object({ name: option("-n", "--name", string()), }); const syncResult = runSync(syncParser, { args }); // Async parser with runAsync() - returns Promise const asyncParser = object({ key: option("--api-key", apiKey()), name: option("-n", "--name", string()), }); const asyncResult = await runAsync(asyncParser, { args }); ``` `runSync()` : Only accepts sync parsers. Returns the parsed value directly. Provides a compile-time error if you pass an async parser. `runAsync()` : Accepts any parser (sync or async). Always returns a `Promise`. Use this when working with parsers that may contain async value parsers. `run()` : The generic function that automatically returns the appropriate type based on the parser's mode. For sync parsers it returns directly; for async parsers it returns a `Promise`. ### Source context support *This API is available since Optique 1.0.0.* The `run()`, `runSync()`, and `runAsync()` functions support source contexts for integrating external data sources like configuration files and environment variables. Pass a `contexts` array to enable automatic annotation collection, with two-phase parsing only when needed: ```typescript twoslash import { z } from "zod"; import { createConfigContext, bindConfig } from "@optique/config"; import { object } from "@optique/core/constructs"; import { optional } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; import { runAsync } from "@optique/run"; const configSchema = z.object({ host: z.string().optional(), port: z.number().optional(), }); const configContext = createConfigContext({ schema: configSchema }); const parser = object({ config: optional(option("-c", "--config", string())), host: bindConfig(option("--host", string()), { context: configContext, key: "host", default: "localhost", }), port: bindConfig(option("--port", integer()), { context: configContext, key: "port", default: 3000, }), }); const result = await runAsync(parser, { contexts: [configContext], contextOptions: { getConfigPath: (parsed) => parsed.config, }, }); ``` When `contexts` is provided, the runner delegates to `runWith()` (or `runWithSync()` for sync parsers) from `@optique/core/facade`, which handles single-pass and two-pass contexts automatically and performs two-phase parsing only when needed. In two-phase runs, each two-pass context's phase-two annotations replace that same context's phase-one contribution for the final parse, so returning an empty object from `getAnnotations({ phase: "phase2", parsed })` clears that context's earlier annotations. Context-specific options like `getConfigPath` are passed through to the contexts via the `contextOptions` property. For more details on config file integration, see the [config file integration guide](../integrations/config.md). ## Type inference with `InferValue` The `InferValue` utility type extracts the result type from any parser, enabling type-safe code when working with parser results programmatically. ```typescript twoslash import { object, or } from "@optique/core/constructs"; import type { InferValue, Parser } from "@optique/core/parser"; import { command, constant, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; // Complex parser with union types const parser = or( command("start", object({ type: constant("start"), port: option("-p", "--port", string()), })), command("stop", object({ type: constant("stop"), force: option("--force"), })) ); // InferValue extracts the union type automatically type Config = InferValue; // ^? function handleConfig(config: Config) { if (config.type === "start") { // TypeScript knows this is the start command console.log(`Starting on port ${config.port || "default"}.`); } else { // TypeScript knows this is the stop command console.log(`Stopping ${config.force ? "forcefully" : "gracefully"}.`); } } ``` `InferValue` is particularly useful when: * Creating functions that work with parser results * Building generic utilities around parsers * Extracting types for external APIs or storage ## When to use each approach Choose your execution strategy based on your application's needs. For guidance on whether to use `Program` objects or pass metadata directly, see [*Bundling parsers with metadata*](#bundling-parsers-with-metadata). ### Use `parse()` when: * *Testing parsers*: You need to inspect parsing results in tests * *Complex integration*: Parsing is part of a larger application flow * *Custom error handling*: You need application-specific error recovery * *Multiple attempts*: You want to try different parsers or arguments * *Reusable components*: Building parser components for use in libraries * *Environment constraints*: Running without `process` (browsers, web workers) ### Use `runParser()` from `@optique/core/facade` when: * *Framework integration*: Working with web frameworks or custom runtimes * *Library development*: Building CLI libraries for other applications * *Custom I/O*: You need non-standard input/output handling * *Controlled exit*: The application manages its own lifecycle * *Non-CLI contexts*: Building tools that embed a CLI interface in a larger app ### Use `run()` from `@optique/run` when: * *Standalone CLIs*: Building command-line applications for Node.js, Bun, or Deno * *Rapid prototyping*: You want to get a CLI running quickly * *Standard behavior*: Your application follows typical CLI conventions * *Batteries-included*: You want automatic argument extraction, terminal detection, and process exit handling The progression from `parse()` to *@optique/run*'s `run()` trades control for convenience. Start with the highest-level approach that meets your needs, then move to lower-level functions only when you need the additional control. --- --- url: /concepts/completion.md description: >- Learn how to add shell completion support to your CLI applications using Optique's built-in completion system. Covers Bash, zsh, fish, PowerShell, and Nushell integration, custom suggestions, and native file completion. --- # Shell completion *This API is available since Optique 0.6.0.* Shell completion enhances command-line user experience by providing intelligent suggestions for commands, options, and arguments as users type. Optique provides built-in completion support for Bash, zsh, fish, PowerShell, and Nushell that integrates seamlessly with the existing parser architecture. Unlike many CLI frameworks that require separate completion definitions, Optique's completion system leverages the same parser structure used for argument parsing. This eliminates code duplication and ensures completion suggestions stay synchronized with your CLI's actual behavior. ## How completion works Optique's completion system operates through three key components: `Parser.suggest()` methods : Each parser provides completion suggestions based on the current parsing context Shell script generation : Optique generates completion scripts for Bash, zsh, fish, PowerShell, and Nushell that integrate with shell completion systems Runtime completion : Your application automatically handles completion requests triggered by the generated scripts When a user presses Tab, the shell calls your application with special arguments that Optique intercepts. Your parsers provide suggestions for the current context, which are then filtered and displayed by the shell. ```typescript twoslash import { object } from "@optique/core/constructs"; import { argument, option } from "@optique/core/primitives"; import { string, choice } from "@optique/core/valueparser"; import { run } from "@optique/run"; const parser = object({ format: option("-f", "--format", choice(["json", "yaml", "xml"])), input: argument(string({ metavar: "FILE" })), }); // Enable completion with a single option const config = run(parser, { completion: "both" }); ``` Users can then generate and install completion scripts: ::: code-group ```bash [Bash] myapp completion bash > ~/.bashrc.d/myapp.bash source ~/.bashrc.d/myapp.bash ``` ```zsh [zsh] myapp completion zsh > ~/.zsh/completions/_myapp ``` ```fish [fish] myapp completion fish > ~/.config/fish/completions/myapp.fish ``` ```powershell [PowerShell] myapp completion pwsh > $PROFILE/../myapp-completion.ps1 Add-Content $PROFILE ". $PROFILE/../myapp-completion.ps1" ``` ```nushell [Nushell] myapp completion nu | save myapp-completion.nu source myapp-completion.nu ``` ::: ## The `Suggestion` type Optique uses a discriminated union to represent different types of completion suggestions: ```typescript twoslash import type { Message } from "@optique/core/message"; // ---cut-before--- export type Suggestion = | { readonly kind: "literal"; readonly text: string; readonly description?: Message; } | { readonly kind: "file"; readonly pattern?: string; readonly type: "file" | "directory" | "any"; readonly extensions?: readonly string[]; readonly includeHidden?: boolean; readonly description?: Message; }; ``` ### Literal suggestions Literal suggestions provide exact text completions for things like option names, subcommands, or predefined values: ```typescript twoslash import { type Parser, suggest } from "@optique/core/parser"; const parser = {} as unknown as Parser<"sync", unknown, unknown>; // ---cut-before--- // Suggests: ["--format", "--input", "--help"] const suggestions = suggest(parser, ["--"]); ``` ### File suggestions File suggestions delegate completion to the shell's native file system integration. This provides better performance and handles platform-specific behaviors like symlinks, permissions, and hidden files: ```typescript twoslash import { argument } from "@optique/core/primitives"; import { path } from "@optique/run/valueparser"; // ---cut-before--- // Uses shell's native file completion const fileParser = argument(path({ extensions: [".json", ".yaml"] })); ``` ## `Parser.suggest()` methods All Optique parsers implement an optional `suggest()` method that provides context-aware completion suggestions. Parser combinators automatically compose suggestions from their constituent parsers. ### Primitive parser suggestions Primitive parsers provide suggestions based on their specific roles: ```typescript twoslash import { type Parser, suggest } from "@optique/core/parser"; import { argument, command, option } from "@optique/core/primitives"; import { choice } from "@optique/core/valueparser"; const parser = {} as unknown as Parser<"sync", unknown, unknown>; // ---cut-before--- // Option parsers suggest their names suggest(option("-v", "--verbose"), ["--v"]); // Returns: [{ kind: "literal", text: "--verbose" }] // Command parsers suggest their command names suggest(command("build", parser), ["bu"]); // Returns: [{ kind: "literal", text: "build" }] // Command aliases are suggested too suggest(command("install", parser, { aliases: ["i"] }), [""]); // Returns: [{ kind: "literal", text: "install" }, // { kind: "literal", text: "i" }] // Argument parsers delegate to their value parsers suggest(argument(choice(["start", "stop"])), ["st"]); // Returns: [{ kind: "literal", text: "start" }, { kind: "literal", text: "stop" }] ``` ### Combinator composition Parser combinators automatically combine suggestions from their constituent parsers: ```typescript twoslash import { object } from "@optique/core/constructs"; import { suggest } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { choice } from "@optique/core/valueparser"; // ---cut-before--- const parser = object({ action: option("-a", "--action", choice(["start", "stop"])), verbose: option("-v", "--verbose"), }); // Suggests all available options suggest(parser, ["--"]); // Returns: ["--action", "--verbose", "--help"] // Suggests values for specific options suggest(parser, ["--action", "st"]); // Returns: ["start", "stop"] ``` ### Context-aware suggestions The `suggest()` method receives the current parsing context, allowing for sophisticated completion logic: ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { suggest } from "@optique/core/parser"; import { argument, constant } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { path } from "@optique/run/valueparser"; // ---cut-before--- // Suggests different options based on what's already parsed const parser = or( object({ command: constant("build"), target: argument(string()) }), object({ command: constant("test"), file: argument(path()) }) ); suggest(parser, ["build", ""]); // Suggests completions for 'target' argument suggest(parser, ["test", ""]); // Suggests completions for 'file' argument ``` ## `ValueParser.suggest()` methods Value parsers provide domain-specific completion suggestions for their respective data types. Optique includes several built-in value parsers with intelligent completion support. ### Built-in value parser suggestions ```typescript twoslash import { choice, locale, url } from "@optique/core/valueparser"; import { timeZone } from "@optique/temporal"; // ---cut-before--- // URL parser suggests protocol completions const urlParser = url({ allowedProtocols: ["https:", "http:", "ftp:"] }); urlParser.suggest?.("ht"); // Returns: ["http://", "https://"] // Choice parser suggests available options const formatParser = choice(["json", "yaml", "xml"]); formatParser.suggest?.("j"); // Returns: ["json"] // Locale parser suggests common locale identifiers const localeParser = locale(); localeParser.suggest?.("en"); // Returns: ["en", "en-US", "en-GB", "en-CA", ...] // Timezone parser uses Intl.supportedValuesOf for dynamic suggestions const timezoneParser = timeZone(); timezoneParser.suggest?.("America/"); // Returns: ["America/New_York", "America/Chicago", ...] ``` ### Custom value parser suggestions You can implement `suggest()` methods in custom value parsers: ```typescript twoslash // @noErrors: 2322 import type { ValueParser } from "@optique/core/valueparser"; // ---cut-before--- function customParser(): ValueParser<"sync", string> { return { mode: "sync", metavar: "CUSTOM", placeholder: "", parse(input) { // Parsing logic... }, format(value) { return value; }, *suggest(prefix) { const options = ["option1", "option2", "option3"]; for (const option of options) { if (option.startsWith(prefix.toLowerCase())) { yield { kind: "literal", text: option }; } } }, }; } ``` ## Shell script generation Optique generates completion scripts that integrate with each shell's native completion system. The generated scripts handle the complexity of shell integration while delegating suggestion logic to your application. ### Bash completion Bash completion scripts use the `complete` command to register completion functions. The generated script handles: * Option and command name completion * Value completion for options with `=` syntax * Native file completion using `compgen` * Proper handling of special characters and spaces ### zsh completion zsh completion scripts use the `compdef` system and `_describe` function for rich completion display. The generated script supports: * Completion descriptions displayed alongside suggestions * Native file completion using `_files` * Advanced completion contexts and filtering * Integration with zsh's completion styling system ### fish completion fish completion scripts use a function-based approach with the `complete` command. The generated script provides: * Tab-separated completion descriptions (`value\tdescription` format) * Automatic argument parsing using `commandline` utility * Native file completion using fish globbing and string matching * Extension filtering and hidden file handling * Auto-loading from *~/.config/fish/completions/* directory ### PowerShell completion PowerShell completion scripts use `Register-ArgumentCompleter` with the `-Native` parameter to integrate with PowerShell's completion system. The generated script provides: * AST-based argument extraction for robust parsing * `CompletionResult` objects with descriptions and tooltips * Native file completion using `Get-ChildItem` * Support for hidden files and extension filtering * Cross-platform compatibility (Windows, Linux, macOS) ### Nushell completion Nushell completion scripts use the `$env.config.completions.external.completer` system to provide completions for external commands. The generated script provides: * Custom completer registration that integrates with Nushell's completion system * Context-aware completion using custom argument parsing * Structured data return values with `value` and `description` fields * Native file completion using Nushell's `ls` command and `match` expressions * Tab-separated encoding format for CLI communication * Support for file type filtering and hidden file handling * Automatic preservation of existing completers for other commands ## Integration with `run()` The `run()` function from *@optique/run* provides seamless completion integration. Enable completion by adding the `completion` option to your `run()` configuration. ### Completion configuration ```typescript twoslash import { object } from "@optique/core/constructs"; import { argument, option } from "@optique/core/primitives"; import { string, choice } from "@optique/core/valueparser"; import { run } from "@optique/run"; const parser = object({ format: option("-f", "--format", choice(["json", "yaml"])), input: argument(string()), }); const config = run(parser, { completion: "both" }); ``` ### Completion configuration The `command` and `option` properties control how completion is triggered: `command: true` : Completion via subcommand (`myapp completion bash`) `option: true` : Completion via option (`myapp --completion bash`) Both can be enabled simultaneously. ### Command name customization By default, the completion command is named `completion` and the option is `--completion`. You can customize the command name by passing a configuration object instead of `true`: ```typescript twoslash import { object } from "@optique/core/constructs"; import { run } from "@optique/run"; const parser = object({}); const config = run(parser, { completion: { command: { names: ["completions"] }, // Use "completions" command name option: true, } }); ``` To register multiple command names (e.g., both singular and plural), pass an array. Additional names after the first are hidden from help output by default: ```typescript twoslash import { object } from "@optique/core/constructs"; import { run } from "@optique/run"; const parser = object({}); run(parser, { completion: { command: { names: ["completion", "completions"] }, option: true, }, }); ``` ### Automatic handling When completion is enabled, the `run()` function automatically: * Detects completion requests before normal argument parsing * Generates shell scripts when requested * Provides runtime completion suggestions * Handles output formatting for each shell * Exits with appropriate status codes ### Custom shell support By default, Optique provides completion for Bash, zsh, fish, PowerShell, and Nushell. You can add custom shell completions or override the defaults using the `shells` option: ```typescript twoslash import type { ShellCompletion } from "@optique/core/completion"; import { object } from "@optique/core/constructs"; import { argument } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { run } from "@optique/run"; const parser = object({ name: argument(string()), }); // Create a custom shell completion const customShell: ShellCompletion = { name: "custom", generateScript(programName: string, args: readonly string[] = []): string { return `# Custom shell completion script for ${programName}`; }, *encodeSuggestions(suggestions) { for (const suggestion of suggestions) { if (suggestion.kind === "literal") { yield `${suggestion.text}\n`; } } }, }; run(parser, { completion: { command: true, option: true, shells: { custom: customShell }, // Add custom shell }, }); ``` The custom shell completion will be merged with the default shells, making all shells available. You can also override default shells by using the same name (e.g., `bash`, `zsh`, `fish`, `pwsh`, or `nu`). ## Setup instructions Follow these steps to add shell completion to your CLI application: ### 1. Enable completion in your application ```typescript twoslash import type { Parser } from "@optique/core/parser"; const parser = {} as unknown as Parser<"sync", unknown, unknown>; // ---cut-before--- import { run } from "@optique/run"; const config = run(parser, { completion: "both" }); ``` ### 2. Generate completion scripts Users can generate completion scripts for their preferred shell: ::: code-group ```bash [Bash] myapp completion bash > ~/.bashrc.d/myapp.bash ``` ```zsh [zsh] myapp completion zsh > ~/.zsh/completions/_myapp # or myapp completion zsh > ~/.oh-my-zsh/completions/_myapp ``` ```fish [fish] myapp completion fish > ~/.config/fish/completions/myapp.fish ``` ```powershell [PowerShell] myapp completion pwsh > myapp-completion.ps1 ``` ```nushell [Nushell] myapp completion nu | save myapp-completion.nu ``` ::: ### 3. Source or install the completion script ::: code-group ```bash [Bash] # Bash: Source the script in your shell configuration echo "source ~/.bashrc.d/myapp.bash" >> ~/.bashrc ``` ```zsh [zsh] # zsh: Ensure completion directory is in fpath # (usually automatic with oh-my-zsh) ``` ```fish [fish] # fish: Completions are automatically loaded from ~/.config/fish/completions/ # No additional configuration needed - just restart fish or run: fish_update_completions ``` ```powershell [PowerShell] # PowerShell: Add to your profile Add-Content $PROFILE ". $PWD/myapp-completion.ps1" # Or load in current session . ./myapp-completion.ps1 ``` ```nushell [Nushell] # Nushell: Source the completion script in your config # The script automatically registers the completer when loaded source myapp-completion.nu # Or add to your config file to load on startup: # echo "source ~/myapp-completion.nu" | save --append $nu.config-path ``` ::: ### 4. Test completion ```bash # Test that completion is working myapp myapp --format myapp --format= ``` ### Distribution considerations For published CLI tools, consider: * Including completion installation instructions in your *README* * Providing install scripts that automatically set up completion * Supporting common completion directories (*/etc/bash\_completion.d/*, */usr/share/zsh/site-functions/*) * Documenting completion setup in your help text Completion significantly improves CLI usability and is expected by users of modern command-line tools. Optique's built-in support makes adding completion straightforward while maintaining type safety and consistency with your parser definitions. --- --- url: /concepts/messages.md description: >- Structured messages provide a type-safe way to create rich error messages and help text using template literals with semantic components for options, values, and metavariables. --- # Structured messages Optique provides a structured message system for creating rich, well-formatted error messages and help text. Instead of plain strings, messages are composed of typed components that ensure consistent presentation and help users distinguish between different types of CLI elements like option names, user values, and metavariables. The message system separates content from presentation—you focus on what the message should communicate, while Optique handles the formatting details. This approach ensures that all error messages and help text follow consistent conventions across your CLI application, making them easier for users to understand and for you to maintain. ## Template literal syntax The primary way to create messages is through the `message` template literal function, which allows natural embedding of different content types: ```typescript twoslash const minPort: string = ""; const maxPort: string = "" const actualPort: string = ""; // ---cut-before--- import { message, optionName } from "@optique/core/message"; // Simple text message const greeting = message`Welcome to the application!`; // Message with embedded values const error = message`Expected port between ${minPort} and ${maxPort}, got ${actualPort}.`; // Message with CLI-specific elements const optionError = message`Option ${optionName("--port")} requires a valid number.`; ``` ## Message components Messages can contain several types of components, each with specific semantic meaning and visual styling: ### Plain text Regular message content that provides context and explanation. Plain text appears as normal text without any special formatting. ### Values User-provided input that should be clearly distinguished from other text. Values are automatically styled with highlighting and quotes to make them stand out: ```typescript twoslash import { message } from "@optique/core/message"; // ---cut-before--- const userInput = "invalid-port"; const errorMsg = message`Invalid port ${userInput}.`; ``` With colors (no quotes): ```ansi Invalid port invalid-port. ``` Without colors (with quotes): ```ansi Invalid port "invalid-port". ``` ### Option names CLI option references like `--verbose` or `-p` that should be consistently styled. Option names are displayed in italics with backticks: ```typescript twoslash import { message, optionName } from "@optique/core/message"; // ---cut-before--- const helpMsg = message`Use ${optionName("--verbose")} for detailed output.`; ``` With colors (no quotes): ```ansi Use --verbose for detailed output. ``` Without colors (with quotes): ```ansi Use `--verbose` for detailed output. ``` For multiple option alternatives, use `optionNames` to display them with proper separation: ```typescript twoslash import { message, optionNames } from "@optique/core/message"; // ---cut-before--- const helpMsg = message`Use ${optionNames(["--help", "-h", "-?"])} for usage information.`; ``` With colors (no quotes): ```ansi Use --help/-h/-? for usage information. ``` Without colors (with quotes): ```ansi Use `--help`/`-h`/`-?` for usage information. ``` ### Metavariables Placeholder names like `FILE` or `PORT` used in help text and error messages. Metavariables are displayed in bold to indicate they represent user input: ```typescript twoslash import { message, metavar } from "@optique/core/message"; // ---cut-before--- const errorMsg = message`Expected ${metavar("NUMBER")}, got invalid input.`; ``` With colors (no quotes): ```ansi Expected NUMBER, got invalid input. ``` Without colors (with quotes): ```ansi Expected `NUMBER`, got invalid input. ``` ### Environment variables *Available since Optique 0.5.0.* Environment variable names that should be highlighted distinctly from other components. Environment variables are displayed in bold with underlines: ```typescript twoslash import { message, envVar } from "@optique/core/message"; // ---cut-before--- const configMsg = message`Set ${envVar("API_URL")} environment variable.`; ``` With colors (no quotes): ```ansi Set API_URL environment variable. ``` Without colors (with quotes): ```ansi Set `API_URL` environment variable. ``` ### Command-line examples *Available since Optique 0.6.0.* Command-line snippets and examples that should be visually distinct from other message components. Command-line examples are displayed in cyan color to clearly indicate executable commands: ```typescript twoslash import { message, commandLine } from "@optique/core/message"; // ---cut-before--- const helpMsg = message`Run ${commandLine("myapp --help")} to see all options.`; ``` With colors (no quotes): ```ansi Run myapp --help to see all options. ``` Without colors (with quotes): ```ansi Run `myapp --help` to see all options. ``` This is particularly useful for showing command examples in help text and footer sections: ```typescript twoslash import { message, commandLine } from "@optique/core/message"; // ---cut-before--- const examples = message`Examples: ${commandLine("myapp completion bash > myapp-completion.bash")} ${commandLine("myapp completion zsh > _myapp")} ${commandLine("myapp --config app.json --verbose")}`; ``` ### URLs *Available since Optique 0.10.0.* URLs that should be displayed as clickable hyperlinks in terminals that support [OSC 8 hyperlink sequences]. The URL can be provided as a string or a `URL` object: ```typescript twoslash import { message, url } from "@optique/core/message"; // ---cut-before--- const helpMsg = message`Visit ${url("https://example.com/docs")} for more information.`; ``` With colors (no quotes), the URL becomes a clickable hyperlink in terminals that support OSC 8 (such as iTerm2, GNOME Terminal 3.26+, Windows Terminal, etc.): ```ansi Visit https://example.com/docs for more information. ``` In the actual terminal, `https://example.com/docs` appears as an underlined, clickable link (the OSC 8 escape sequences are invisible). Without colors (with quotes): ```ansi Visit for more information. ``` The URL is validated when created. If an invalid URL string is provided, a `RangeError` is thrown: ```typescript twoslash import { url } from "@optique/core/message"; // ---cut-before--- // Valid URLs url("https://example.com"); url("http://localhost:8080"); url(new URL("https://example.com")); // @errors: 2345 // Invalid URL - throws RangeError url("not a valid url"); ``` > \[!NOTE] > The `link()` function is provided as an alias for `url()`. This can be useful > to avoid naming conflicts when importing both the message function and the > `url()` value parser from `@optique/core/valueparser`: > > ```typescript twoslash > import { message, link } from "@optique/core/message"; > import { url } from "@optique/core/valueparser"; > // ---cut-before--- > const helpMsg = message`Visit ${link("https://example.com")} for help.`; > ``` [OSC 8 hyperlink sequences]: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda ### Consecutive values Consecutive values that were provided together, such as multiple arguments or repeated option values. These are displayed as a sequence with consistent formatting: ```typescript twoslash import { message, values } from "@optique/core/message"; // ---cut-before--- const invalidArgs = ["file1.txt", "file2.txt", "file3.txt"]; const errorMsg = message`Invalid files: ${values(invalidArgs)}.`; ``` With colors (no quotes): ```ansi Invalid files: file1.txt file2.txt file3.txt. ``` Without colors (with quotes): ```ansi Invalid files: "file1.txt" "file2.txt" "file3.txt". ``` ### Value sets *Available since Optique 0.9.0.* Value sets are used for displaying a list of valid choices (such as in error messages for `choice()` value parsers) with proper locale-aware formatting. Unlike `values()` which is for consecutive user-provided values separated by spaces, `valueSet()` uses `Intl.ListFormat` to format lists according to locale conventions with appropriate conjunctions like “and” or “or”. ```typescript twoslash import { message, valueSet } from "@optique/core/message"; // ---cut-before--- const choices = ["error", "warn", "info", "debug"]; const input = "invalid"; // Format as conjunction: "error", "warn", "info" and "debug" const errorMsg = message`Invalid log level: ${input}. Valid levels: ${valueSet(choices, "")}.`; // Format as disjunction: "error", "warn", "info" or "debug" const altMsg = message`Expected ${valueSet(choices, { fallback: "", type: "disjunction" })}.`; ``` Each choice appears with proper formatting: With colors: ```ansi Invalid log level: invalid. Valid levels: error, warn, info and debug. ``` Without colors: ```ansi Invalid log level: "invalid". Valid levels: "error", "warn", "info" and "debug". ``` You can also specify a locale for proper internationalization: ```typescript twoslash import { message, valueSet } from "@optique/core/message"; // ---cut-before--- const choices = ["error", "warn", "info"]; // Korean disjunction: "error", "warn" 또는 "info" const koreanMsg = message`${valueSet(choices, { fallback: "", locale: "ko", type: "disjunction" })} 중 하나여야 합니다.`; // Japanese conjunction: "error"、"warn"、"info" const japaneseMsg = message`${valueSet(choices, { fallback: "", locale: "ja" })}のいずれかを指定してください。`; ``` The `valueSet()` function requires a second parameter that specifies what to display when the values array is empty. This can be a simple fallback string (e.g., `""` or `"(none)"`) or an options object with a `fallback` field and optional formatting options: `fallback` : The text to return when the values array is empty. An empty string produces an empty message (no output). `locale` : The locale(s) to use for formatting. It can be a string, array of strings, `Intl.Locale` object, or array of `Intl.Locale` objects. Defaults to the system locale. `type` : The type of list: `"conjunction"` for “and” lists (default), `"disjunction"` for “or” lists, or `"unit"` for simple comma-separated lists. `style` : The formatting style: `"long"` (default), `"short"`, or `"narrow"`. > \[!NOTE] > Do not use `.join(", ")` for choice lists, as this concatenates all choices > into a single value string, losing individual formatting: > `"error, warn, info, debug"` instead of `"error", "warn", "info", "debug"`. > \[!NOTE] > The exact formatting depends on the locale. If no locale is specified, > `valueSet()` uses the system default locale, which may vary across > environments. For consistent formatting, always specify a locale explicitly > (e.g., `{ locale: "en-US" }`). ### Combined examples ```typescript twoslash const userInput: string = ""; const userValue: string = ""; // ---cut-before--- import { commandLine, envVar, message, metavar, optionName, optionNames, url, values, } from "@optique/core/message"; const examples = { // Automatic value embedding simpleValue: message`Invalid value ${userInput}.`, // Single option name highlighting optionRef: message`Unknown option ${optionName("--invalid")}.`, // Multiple option alternatives helpOptions: message`Try ${optionNames(["--help", "-h"])} for usage.`, // Metavariable for documentation usage: message`Expected ${metavar("FILE")} argument.`, // Environment variable reference envError: message`Environment variable ${envVar("DATABASE_URL")} is not set.`, // Command-line example cmdExample: message`Run ${commandLine("myapp --config app.json")} to start.`, // URL reference helpUrl: message`For help, visit ${url("https://example.com/help")}.`, // Consecutive values invalidFiles: message`Cannot process files ${values(["missing.txt", "readonly.txt"])}.`, // Combined components complex: message`Option ${optionName("--port")} expects ${metavar("NUMBER")}, got ${userValue}.` }; ``` Here's how these examples appear in the terminal: ![Terminal output showing seven different message component examples, each displayed in both colored (no quotes) and non-colored (with quotes) formats, demonstrating values, option names, metavariables, environment variables, and complex combinations](/assets/combined-examples.BzN8FLWj.png) ## Value interpolation When you embed string values directly in a message template, they are automatically treated as user values: ```typescript twoslash import { message } from "@optique/core/message"; // ---cut-before--- const userInput: string = "invalid-port"; // Direct value interpolation - automatically quoted and styled const error = message`Invalid port ${userInput}.`; ``` ## Explicit component creation For dynamic message construction or when you need precise control: ```typescript twoslash const isLongForm: boolean = true; const args: readonly string[] = []; // ---cut-before--- import { message, optionName, optionNames, metavar, values } from "@optique/core/message"; // Dynamic option reference const option = isLongForm ? "--verbose" : "-v"; const helpMessage = message`Use ${optionName(option)} for detailed output.`; // Multiple option alternatives const optionsMessage = message`Try ${optionNames(["--help", "-h", "-?"])} for usage.`; // Consecutive values const valuesMessage = message`Invalid values: ${values(args)}.`; // Metavariable in error context const typeError = message`Expected ${metavar("STRING")}, got ${metavar("NUMBER")}.`; ``` ## Message composition You can compose complex messages by embedding existing `Message` objects within new message templates. When a `Message` object is interpolated, its components are automatically concatenated: ```typescript twoslash import { message, optionName, metavar } from "@optique/core/message"; // Create reusable message components const invalidInput = message`invalid input format.`; const missingOption = message`required option ${optionName("--config")} not found.`; // Compose messages by embedding existing ones const contextualError = message`Configuration error: ${invalidInput}`; const detailedError = message`Setup failed - ${missingOption}`; // Complex composition with multiple message parts const troubleshootingInfo = message`Check ${metavar("FILE")} permissions.`; const fullError = message`${detailedError} ${troubleshootingInfo}`; ``` This composition feature enables building structured error messages from reusable components: ```typescript twoslash import { message, optionName } from "@optique/core/message"; // Base error messages const errorMessages = { fileNotFound: (filename: string) => message`File ${filename} not found.`, permissionDenied: (action: string) => message`Permission denied for ${action}.`, invalidFormat: (format: string) => message`Invalid ${format} format.` }; // Compose complex errors from base messages function createFileError(filename: string, action: string) { const baseError = errorMessages.fileNotFound(filename); const permissionError = errorMessages.permissionDenied(action); return message`Operation failed: ${baseError} ${permissionError}`; } // Usage in parser error handling const configError = createFileError("config.json", "read"); const validationError = message`${errorMessages.invalidFormat("JSON")} in configuration.`; ``` ## Line break handling *Available since Optique 0.7.0.* The `formatMessage()` function handles line breaks in a way similar to Markdown, distinguishing between soft breaks (word wrap points) and hard breaks (actual paragraph separations): ### Explicit line break term (`lineBreak()`) *Available since Optique 0.10.0.* Use `lineBreak()` when you want an explicit single-line break between message parts. Unlike single newlines in `text()` terms, this is always rendered as an actual line break. ```typescript twoslash import { commandLine, lineBreak, message } from "@optique/core/message"; const examples = message`Examples:${lineBreak()} Bash: ${commandLine(`eval "$(mycli completion bash)"`)}${lineBreak()} zsh: ${commandLine(`eval "$(mycli completion zsh)"`)}`; ``` This renders as: ``` Examples: Bash: eval "$(mycli completion bash)" zsh: eval "$(mycli completion zsh)" ``` Note that `lineBreak()` absorbs the newline that immediately follows it in a template literal. In the example above, each `${lineBreak()}` is followed by a real newline character in the source, but that newline is dropped rather than being normalized to a space. This means you can write multi-line template literals naturally—breaking the source line right after `${lineBreak()}`—and the output will not gain a spurious leading space on the next line. ### Single newlines (`\n`) Single newlines in `text()` terms are treated as soft breaks and converted to spaces. This allows you to write long messages across multiple lines in source code while rendering them as continuous text: ```typescript twoslash import { message, text } from "@optique/core/message"; // Long message written across multiple lines const msg = message`This is a very long error message that\nspans multiple lines in the source code\nbut renders as continuous text.`; ``` This renders as: ``` This is a very long error message that spans multiple lines in the source code but renders as continuous text. ``` ### Double newlines (`\n\n`) Double or more consecutive newlines are treated as hard breaks, creating actual paragraph separations in the output: ```typescript twoslash import { message, text } from "@optique/core/message"; // Message with paragraph break const msg = [ text("First paragraph with important information."), text("\n\n"), text("Second paragraph with additional details.") ]; ``` This renders as: ``` First paragraph with important information. Second paragraph with additional details. ``` This distinction is particularly useful for multi-part error messages, such as those with suggestions or help text, ensuring proper spacing between the base error and additional information. > \[!NOTE] > Prior to Optique 1.0.0, double newlines were rendered as a single newline > (`\n`) in the output, making them visually indistinguishable from > `lineBreak()`. Since 1.0.0, they are rendered as a double newline (`\n\n`) > to clearly separate paragraphs. ## Terminal output Once you've created structured messages, you can output them to the terminal using the print functions provided by `@optique/run/print`. The `print()` function displays messages to stdout with automatic formatting: ```typescript twoslash import { print } from "@optique/run"; import { message, optionName } from "@optique/core/message"; const configFile = "app.config.json"; const port = 3000; // Simple informational output print(message`Starting application...`); // Output with embedded values print(message`Configuration loaded from ${configFile}.`); print(message`Server listening on port ${String(port)}.`); // Output with CLI elements print(message`Use ${optionName("--verbose")} for detailed logging.`); ``` By default, `print()` automatically detects whether your terminal supports colors and adjusts the formatting accordingly. Values are highlighted, option names are styled consistently, and the output width adapts to your terminal size. ## Error handling The `printError()` function is specifically designed for error messages, outputting to stderr with an `Error: ` prefix: ```typescript twoslash import { printError } from "@optique/run"; import { message, optionName } from "@optique/core/message"; const filename = "missing.txt"; const invalidValue = "not-a-number"; // Simple error message printError(message`File ${filename} not found.`); // Error with CLI context printError(message`Invalid value ${invalidValue} for ${optionName("--port")}.`); // Critical error that exits the process printError(message`Cannot connect to database.`, { exitCode: 1 }); ``` When you provide an `exitCode`, the function will terminate the process after displaying the error. This is useful for fatal errors that should stop execution immediately. ## Custom printers For specialized output needs, you can create custom printers with predefined formatting options: ```typescript twoslash import { createPrinter } from "@optique/run"; import { message, metavar, optionName } from "@optique/core/message"; // Create a printer for debugging output const debugPrint = createPrinter({ stream: "stderr", colors: true, // Force colors even in non-TTY quotes: false, // Disable quote marks around values }); // Create a printer for plain text logs const logPrint = createPrinter({ colors: false, // Disable all colors quotes: true, // Ensure values are clearly marked maxWidth: 80, // Wrap long lines at 80 characters }); // Use custom printers debugPrint(message`Debugging ${metavar("MODULE")} initialization.`); logPrint(message`Processing file ${metavar("FILENAME")}.`); ``` ## Output customization All output functions accept formatting options to override automatic detection: ```typescript twoslash import { print, printError } from "@optique/run"; import { message, optionName } from "@optique/core/message"; // Force specific formatting print(message`Status: ${optionName("--quiet")} mode enabled.`, { colors: false, // Disable colors quotes: true, // Force quote marks maxWidth: 60, // Wrap at 60 characters }); // Output to different stream print(message`Debug information.`, { stream: "stderr" }); // Error without automatic exit printError(message`Warning: deprecated ${optionName("--old-flag")}.`); ``` The formatting options give you fine-grained control while maintaining the structured nature of your messages across different output contexts. ## Customizing parser error messages *Available since Optique 0.5.0.* Optique allows you to customize error messages for all parser types through their `errors` option. This provides better user experience by giving context-specific feedback instead of generic error messages. ### Basic parser errors Most primitive parsers support customizing their core error conditions: ```typescript twoslash import { option, flag } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; import { message, optionName, metavar, type Message } from "@optique/core/message"; // Option parser with custom errors const portOption = option("--port", integer(), { errors: { missing: message`${optionName("--port")} is required for server startup.`, invalidValue: (error: Message) => message`Port validation failed: ${error}`, endOfInput: message`${optionName("--port")} requires a ${metavar("NUMBER")}.` } }); // Flag parser with custom error const verboseFlag = flag("--verbose", { errors: { duplicate: (token: string) => message`${optionName("--verbose")} was already specified: ${token}.` } }); ``` ### Function-based error messages Error messages can be functions that receive the problematic input and return a customized message. This allows for more specific and helpful feedback: ```typescript twoslash import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { message, optionName, type Message } from "@optique/core/message"; const formatOption = option("--format", string(), { errors: { // Static message missing: message`Output format must be specified.`, // Dynamic message based on original error invalidValue: (error: Message) => { return message`Invalid format specified: ${error}`; } } }); ``` ### Combinator error customization Parser combinators like `or()` and `longestMatch()` also support error customization for better failure reporting: ```typescript twoslash import { or } from "@optique/core/constructs"; import { constant, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { message, optionName } from "@optique/core/message"; // Custom error when no alternative matches const configOption = option("--config", string()); const helpOption = option("--help"); const configOrHelp = or(configOption, helpOption, { errors: { noMatch: message`Either provide ${optionName("--config")} or use help option.`, unexpectedInput: (token: string) => message`Unexpected input ${token}. Expected configuration or help option.` } }); ``` ### Object parser error customization Object parsers can customize errors for missing required fields and unexpected properties: ```typescript twoslash import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; import { message, optionName } from "@optique/core/message"; const serverConfig = object({ host: option("--host", string()), port: option("--port", integer()) }, { errors: { unexpectedInput: (token: string) => message`Unknown server option ${optionName(token)}.`, endOfInput: message`Server configuration incomplete. Expected more options.` } }); ``` ### Multiple parser error customization Multiple parsers can provide custom messages for count validation: ```typescript twoslash import { multiple } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { message, optionName, metavar } from "@optique/core/message"; const inputFiles = multiple(option("--input", string()), { min: 1, max: 5, errors: { tooFew: (count: number, min: number) => message`At least ${String(min)} input file(s) required, got ${String(count)}.`, tooMany: (count: number, max: number) => message`Maximum ${String(max)} input files allowed, got ${String(count)}.` } }); ``` ### Value parser error customization Value parsers can also provide custom error messages for validation failures. This allows you to give more specific feedback when user input doesn't meet the expected format or constraints. ```typescript twoslash import { option } from "@optique/core/primitives"; import { string, integer, choice, url } from "@optique/core/valueparser"; import { message, optionName, values, text } from "@optique/core/message"; // String parser with pattern validation const codeOption = option("--code", string({ pattern: /^[A-Z]{3}-\d{4}$/, errors: { patternMismatch: (input, pattern) => message`Code ${input} must follow format ABC-1234.` } })); // Integer parser with range validation const portOption = option("--port", integer({ min: 1024, max: 65535, errors: { invalidInteger: message`Port must be a whole number.`, belowMinimum: (value, min) => message`Port ${text(value.toString())} too low. Use ${text(min.toString())} or higher.`, aboveMaximum: (value, max) => message`Port ${text(value.toString())} too high. Maximum is ${text(max.toString())}.` } })); // Choice parser with custom suggestions const formatOption = option("--format", choice(["json", "yaml", "xml"], { errors: { invalidChoice: (input, choices) => message`Format ${input} not supported. Available: ${values(choices)}.` } })); // URL parser with protocol restrictions const endpointOption = option("--endpoint", url({ allowedProtocols: ["https:"], errors: { invalidUrl: message`Please provide a valid web address.`, disallowedProtocol: (protocol, allowedProtocols) => message`Only secure connections allowed. Use ${values(allowedProtocols)} instead of ${protocol}.` } })); ``` Value parser error customization works with all built-in parsers: `string()` : Custom `patternMismatch` errors for regex validation `integer()` and `float()` : Custom `invalidInteger`/`invalidNumber`, `belowMinimum`, and `aboveMaximum` errors `choice()` : Custom `invalidChoice` errors with available options `url()` : Custom `invalidUrl` and `disallowedProtocol` errors `locale()` : Custom `invalidLocale` errors for malformed locale identifiers `uuid()` : Custom `invalidUuid` and `disallowedVersion` errors ### Additional packages The error customization system also extends to additional Optique packages: #### `@optique/run` package ```typescript twoslash import { option } from "@optique/core/primitives"; import { path } from "@optique/run/valueparser"; import { message, text, values } from "@optique/core/message"; // File path parser with custom validation errors const configFile = option("--config", path({ mustExist: true, type: "file", extensions: [".json", ".yaml", ".yml"], errors: { pathNotFound: (input) => message`Configuration file ${input} not found.`, notAFile: message`Configuration must be a file, not a directory.`, invalidExtension: (input, extensions, actualExt) => message`Config file ${input} has wrong extension ${actualExt}. Expected: ${values(extensions)}.`, } })); // Output directory with creation support const outputDir = option("--output", path({ type: "directory", allowCreate: true, errors: { parentNotFound: (parentDir) => message`Cannot create output directory: parent ${parentDir} doesn't exist.`, notADirectory: (input) => message`Output path ${input} exists but is not a directory.`, } })); ``` #### `@optique/temporal` package ```typescript twoslash import { option } from "@optique/core/primitives"; import { instant, duration, timeZone } from "@optique/temporal"; import { message } from "@optique/core/message"; // Timestamp parser with user-friendly errors const startTime = option("--start", instant({ errors: { invalidFormat: (input) => message`Start time ${input} is invalid. Use ISO 8601 format like 2023-12-25T10:30:00Z.`, } })); // Duration parser with contextual errors const timeout = option("--timeout", duration({ errors: { invalidFormat: message`Timeout must be in ISO 8601 duration format (e.g., PT30S, PT5M, PT1H).`, } })); // Timezone parser with helpful suggestions const timezone = option("--timezone", timeZone({ errors: { invalidFormat: (input) => message`Timezone ${input} is not valid. Use IANA identifiers like America/New_York or UTC.`, } })); ``` ### Best practices for custom errors When customizing error messages, follow these patterns for consistent and helpful user experience: 1. *Be specific*: Include the problematic input value when possible 2. *Provide context*: Reference the specific option or command involved 3. *Suggest solutions*: Mention valid alternatives or corrective actions 4. *Use consistent styling*: Apply proper component types for CLI elements ```typescript twoslash import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { message, optionName, metavar, values, type Message } from "@optique/core/message"; // Good: Specific, contextual, actionable const databaseUrl = option("--database", string(), { errors: { missing: message`Database connection required. Set ${optionName("--database")} or DATABASE_URL environment variable.`, invalidValue: (error: Message) => { return message`Database URL validation failed: ${error}`; } } }); // Good: Lists valid alternatives with custom validation const logLevel = option("--log-level", string(), { errors: { invalidValue: (error: Message) => { const validLevels = ["debug", "info", "warn", "error"]; return message`Log level validation failed: ${error}. Valid levels: ${values(validLevels)}.`; } } }); ``` Custom error messages integrate seamlessly with Optique's structured message system, ensuring consistent formatting and proper terminal output regardless of whether colors are enabled or disabled. ### Suggestion message customization *Available since Optique 0.7.0.* Optique's automatic “Did you mean?” suggestions can also be customized through the `errors` option. This allows you to control how suggestion messages are formatted or disable them entirely for specific parsers. #### Option and flag parsers The `option()` and `flag()` parsers support a `noMatch` error option that receives both the invalid input and an array of similar valid options: ```typescript twoslash import { option, flag } from "@optique/core/primitives"; import { integer } from "@optique/core/valueparser"; import { message, values } from "@optique/core/message"; // Custom suggestion format const portOption = option("--port", integer(), { errors: { noMatch: (invalidOption, suggestions) => suggestions.length > 0 ? message`Unknown option ${invalidOption}. Try: ${values(suggestions)}` : message`Unknown option ${invalidOption}.` } }); // Disable suggestions by ignoring the suggestions parameter const quietOption = option("--quiet", { errors: { noMatch: (invalidOption, _suggestions) => message`Invalid option: ${invalidOption}` } }); // Use static message (no suggestions) const verboseFlag = flag("--verbose", { errors: { noMatch: message`Please use a valid flag.` } }); ``` #### Command parser The `command()` parser's `notMatched` error option now receives suggestions as an optional third parameter: ```typescript twoslash import { command } from "@optique/core/primitives"; import { object } from "@optique/core/constructs"; import { message, values } from "@optique/core/message"; const addCmd = command("add", object({}), { errors: { notMatched: (expected, actual, suggestions) => { if (actual == null) { return message`Expected ${expected} command.`; } if (suggestions && suggestions.length > 0) { return message`Unknown command ${actual}. Similar commands: ${values(suggestions)}`; } return message`Unknown command ${actual}.`; } } }); ``` #### Combinator and object parsers The `or()`, `longestMatch()`, and `object()` parsers support a `suggestions` error option that customizes how suggestions are formatted. This function receives the array of suggestions and returns a message to append to the error: ```typescript twoslash import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; import { message, values, text } from "@optique/core/message"; // Custom suggestion formatting const config = object({ host: option("--host", string()), port: option("--port", integer()) }, { errors: { suggestions: (suggestions) => suggestions.length > 0 ? message`Available options: ${values(suggestions)}` : [] } }); // Disable suggestions entirely const strictConfig = object({ host: option("--host", string()), port: option("--port", integer()) }, { errors: { suggestions: () => [] // Return empty message to disable suggestions } }); ``` The `suggestions` formatter is called with an array of similar valid option/command names found through Levenshtein distance matching. You can: * Format suggestions differently (e.g., comma-separated instead of list) * Add additional context or help text * Filter or reorder suggestions * Return an empty array to disable suggestions ```typescript twoslash import { or } from "@optique/core/constructs"; import { command } from "@optique/core/primitives"; import { object } from "@optique/core/constructs"; import { message, optionName, text, type Message } from "@optique/core/message"; const addCmd = command("add", object({})); const commitCmd = command("commit", object({})); const parser = or( addCmd, commitCmd, { errors: { suggestions: (suggestions) => { if (suggestions.length === 0) return []; if (suggestions.length === 1) { return message`Did you mean ${optionName(suggestions[0])}? Run with ${optionName("--help")} for usage.`; } // Format as comma-separated list let parts: Message = [text("Did you mean: ")]; for (let i = 0; i < suggestions.length; i++) { parts = i > 0 ? [...parts, text(", "), optionName(suggestions[i])] : [...parts, optionName(suggestions[i])]; } return [...parts, text("?")]; } } } ); ``` Note that if you provide a custom `unexpectedInput` error, suggestions will not be added automatically. You must use the `suggestions` formatter if you want suggestions with a custom `unexpectedInput` message. ## Automatic “Did you mean?” suggestions *Available since Optique 0.7.0.* Optique automatically provides helpful “Did you mean?” suggestions when users make typos in option names or command names. This feature works transparently without requiring any configuration—when a user enters an invalid option or command that's similar to a valid one, Optique suggests the correct alternative: ```typescript twoslash import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { run } from "@optique/run"; const parser = object({ verbose: option("--verbose"), version: option("--version"), verify: option("--verify"), }); run(parser, { args: ["--verbos"] }); // User typo ``` This produces the error: ``` Error: No matched option for `--verbos`. Did you mean `--verbose`? ``` ### How it works The suggestion system uses [Levenshtein distance] to find similar names among available options and commands. It automatically: * Compares the invalid input against all valid option and command names * Finds matches within an edit distance of 3 characters * Filters candidates by distance ratio (at most 50% of input length) * Suggests up to 3 closest matches * Uses case-insensitive comparison for better user experience [Levenshtein distance]: https://en.wikipedia.org/wiki/Levenshtein_distance ### Multiple suggestions When multiple similar options exist, Optique shows all relevant suggestions: ```typescript twoslash import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { run } from "@optique/run"; const parser = object({ verbose: option("--verbose"), version: option("--version"), verify: option("--verify"), }); run(parser, { args: ["--ver"] }); // Ambiguous typo ``` This produces: ``` Error: No matched option for `--ver`. Did you mean one of these? `--verify` `--version` `--verbose` ``` ### Command name suggestions The feature works equally well with subcommand names: ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { command } from "@optique/core/primitives"; const addCmd = command("add", object({})); const commitCmd = command("commit", object({})); const parser = or(addCmd, commitCmd); ``` When a user types `comit` instead of `commit`: ``` Error: Expected command commit, but got comit. Did you mean `commit`? ``` ### Suggestion thresholds Suggestions are only shown when they're likely to be helpful. Optique won't suggest options that are too different from what the user typed: ```typescript twoslash import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { run } from "@optique/run"; const parser = object({ verbose: option("--verbose"), quiet: option("--quiet"), }); run(parser, { args: ["--xyz"] }); // Too different ``` This produces an error without suggestions: ``` Error: Unexpected option or argument: `--xyz`. ``` The thresholds ensure that suggestions are relevant without overwhelming users with unrelated options. ### Integration with error messages Suggestions are automatically appended to error messages with proper formatting, including appropriate line breaks for readability. They work seamlessly with both colored and non-colored terminal output, and integrate with custom error messages you may have defined. --- --- url: /integrations/temporal.md description: >- Parse ISO 8601 dates, times, durations, and time zones into Temporal API objects with full type safety and validation. --- # Temporal integration *This API is available since Optique 0.4.0.* The *@optique/temporal* package provides value parsers for the [Temporal API], a modern JavaScript proposal for working with dates and times. These parsers offer type-safe parsing of various temporal values including instants, durations, dates, times, and time zones. > \[!IMPORTANT] > These parsers require `globalThis.Temporal` at runtime. If your runtime does > not ship the Temporal API yet, install and initialize a polyfill such as > `@js-temporal/polyfill` before parsing. ::: code-group ```bash [Deno] deno add --jsr @optique/temporal ``` ```bash [npm] npm add @optique/temporal ``` ```bash [pnpm] pnpm add @optique/temporal ``` ```bash [Yarn] yarn add @optique/temporal ``` ```bash [Bun] bun add @optique/temporal ``` ::: [Temporal API]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal ## `instant()` parser The `instant()` parser validates ISO 8601 timestamp strings and returns [`Temporal.Instant`] objects representing precise moments in time: ```typescript twoslash import { instant } from "@optique/temporal"; // Basic instant parser const timestamp = instant(); // Instant with custom metavar const createdAt = instant({ metavar: "TIMESTAMP" }); ``` The parser accepts ISO 8601 strings with timezone information: ```typescript // Valid instant formats "2023-12-25T10:30:00Z" // UTC "2023-12-25T10:30:00+09:00" // With timezone offset "2023-12-25T10:30:00.123456789Z" // With nanosecond precision "2023-12-25T10:30:00-05:00" // Negative timezone offset ``` [`Temporal.Instant`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Instant ## `duration()` parser The `duration()` parser validates ISO 8601 duration strings and returns [`Temporal.Duration`] objects for representing spans of time: ```typescript twoslash import { duration } from "@optique/temporal"; // Basic duration parser const timeout = duration(); // Duration with custom metavar const interval = duration({ metavar: "INTERVAL" }); ``` The parser accepts ISO 8601 duration format: ```typescript // Valid duration formats "PT30M" // 30 minutes "P1D" // 1 day "PT1H30M" // 1 hour 30 minutes "P1Y2M3DT4H5M6S" // 1 year, 2 months, 3 days, 4 hours, 5 minutes, 6 seconds "PT0.123S" // 123 milliseconds ``` [`Temporal.Duration`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration ## `zonedDateTime()` parser The `zonedDateTime()` parser validates ISO 8601 datetime strings with timezone information and returns [`Temporal.ZonedDateTime`] objects: ```typescript twoslash import { zonedDateTime } from "@optique/temporal"; // Basic zoned datetime parser const appointment = zonedDateTime(); // Zoned datetime with custom metavar const meetingTime = zonedDateTime({ metavar: "MEETING_TIME" }); ``` The parser accepts ISO 8601 strings with timezone identifiers or offsets: ```typescript // Valid zoned datetime formats "2023-12-25T10:30:00[Asia/Seoul]" // With IANA timezone "2023-12-25T10:30:00+09:00[Asia/Seoul]" // With offset and timezone "2023-12-25T10:30:00-05:00[America/New_York]" // Different timezone ``` [`Temporal.ZonedDateTime`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime ## `plainDate()` parser The `plainDate()` parser validates ISO 8601 date strings and returns [`Temporal.PlainDate`] objects representing calendar dates without time information: ```typescript twoslash import { plainDate } from "@optique/temporal"; // Basic date parser const birthDate = plainDate(); // Date with custom metavar const deadline = plainDate({ metavar: "DEADLINE" }); ``` The parser accepts ISO 8601 date format: ```typescript // Valid date formats "2023-12-25" // Basic date format "2023-01-01" // New Year's Day "1999-12-31" // Y2K eve ``` [`Temporal.PlainDate`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDate ## `plainTime()` parser The `plainTime()` parser validates ISO 8601 time strings and returns [`Temporal.PlainTime`] objects representing wall-clock time without date or timezone: ```typescript twoslash import { plainTime } from "@optique/temporal"; // Basic time parser const startTime = plainTime(); // Time with custom metavar const alarmTime = plainTime({ metavar: "ALARM_TIME" }); ``` The parser accepts ISO 8601 time format: ```typescript // Valid time formats "10:30:00" // Hour, minute, second "14:15" // Hour, minute (seconds default to 00) "09:30:00.123" // With milliseconds "23:59:59.999999999" // With nanosecond precision ``` [`Temporal.PlainTime`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainTime ## `plainDateTime()` parser The `plainDateTime()` parser validates ISO 8601 datetime strings without timezone information and returns [`Temporal.PlainDateTime`] objects: ```typescript twoslash import { plainDateTime } from "@optique/temporal"; // Basic datetime parser const localTime = plainDateTime(); // Datetime with custom metavar const eventTime = plainDateTime({ metavar: "EVENT_TIME" }); ``` The parser accepts ISO 8601 datetime format: ```typescript // Valid datetime formats "2023-12-25T10:30:00" // Basic datetime "2023-12-25T14:15" // Without seconds "2023-12-25T09:30:00.123" // With milliseconds "2023-12-25 10:30:00" // Space separator (also valid) ``` [`Temporal.PlainDateTime`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainDateTime ## `plainYearMonth()` parser The `plainYearMonth()` parser validates year-month strings and returns [`Temporal.PlainYearMonth`] objects for representing specific months: ```typescript twoslash import { plainYearMonth } from "@optique/temporal"; // Basic year-month parser const reportMonth = plainYearMonth(); // Year-month with custom metavar const billingPeriod = plainYearMonth({ metavar: "BILLING_PERIOD" }); ``` The parser accepts year-month format: ```typescript // Valid year-month formats "2023-12" // December 2023 "2024-01" // January 2024 "1999-06" // June 1999 ``` [`Temporal.PlainYearMonth`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainYearMonth ## `plainMonthDay()` parser The `plainMonthDay()` parser validates month-day strings and returns [`Temporal.PlainMonthDay`] objects for representing recurring dates: ```typescript twoslash import { plainMonthDay } from "@optique/temporal"; // Basic month-day parser const birthday = plainMonthDay(); // Month-day with custom metavar const holiday = plainMonthDay({ metavar: "HOLIDAY_DATE" }); ``` The parser accepts month-day format: ```typescript // Valid month-day formats "12-25" // Christmas (December 25) "01-01" // New Year's Day (January 1) "07-04" // Independence Day (July 4) "02-29" // Leap day (February 29) "--12-25" // ISO 8601 format with -- prefix is also accepted ``` [`Temporal.PlainMonthDay`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/PlainMonthDay ## `timeZone()` parser The `timeZone()` parser validates timezone identifiers and returns the `TimeZone` union type exported by *@optique/temporal*: ```typescript twoslash import { timeZone } from "@optique/temporal"; // Basic timezone parser const userTimezone = timeZone(); // Timezone with custom metavar const displayTimezone = timeZone({ metavar: "DISPLAY_TZ" }); ``` The parser accepts valid two-segment and three-segment IANA identifiers, plus a curated set of single-segment identifiers for cross-runtime compatibility: ```typescript // Valid timezone identifiers "UTC" // Coordinated Universal Time "GMT" // Greenwich Mean Time alias "EST" // POSIX-style single-segment identifier "Japan" // Deprecated alias still accepted "Cuba" // Deprecated alias still accepted "Asia/Seoul" // South Korea "America/New_York" // Eastern Time (US) "Europe/London" // Europe/London "America/Argentina/Buenos_Aires" // Three-segment identifier ``` Single-segment identifiers are matched case-insensitively and normalized to their canonical casing, so inputs like `utc`, `gmt`, and `japan` parse as `"UTC"`, `"GMT"`, and `"Japan"`. Some timezone database aliases are not accepted by every supported runtime. For example, Bun on Linux rejects aliases such as `CET`, `MET`, `WET`, `EET`, `EST5EDT`, `CST6CDT`, `MST7MDT`, and `PST8PDT`, so `timeZone()` rejects them for consistent cross-runtime behavior. Prefer canonical IANA identifiers such as `Europe/Paris` or `America/New_York` when you need regional timezone rules. ## Error messages All temporal parsers provide clear, specific error messages for different validation failures: ```bash $ myapp --timestamp "not-a-timestamp" Error: Invalid instant: not-a-timestamp. Expected ISO 8601 format like 2020-01-23T17:04:36Z. $ myapp --duration "invalid-duration" Error: Invalid duration: invalid-duration. Expected ISO 8601 format like PT1H30M. $ myapp --timezone "Invalid/Timezone" Error: Invalid timezone identifier: Invalid/Timezone. Must be a valid IANA timezone like "Asia/Seoul" or "UTC". ``` ## Integration with Temporal API The temporal parsers return native Temporal objects with rich functionality: ```typescript twoslash import { parse } from "@optique/core/parser"; import { argument } from "@optique/core/primitives"; import { instant, duration } from "@optique/temporal"; // ---cut-before--- const timestampArg = argument(instant()); const result = parse(timestampArg, ["2023-12-25T10:30:00Z"]); if (result.success) { const instant = result.value; console.log(`Epoch milliseconds: ${instant.epochMilliseconds}`); console.log(`UTC string: ${instant.toString()}`); console.log(`In timezone: ${instant.toZonedDateTimeISO("Asia/Seoul")}`); } const timeoutArg = argument(duration()); const durationResult = parse(timeoutArg, ["PT1H30M"]); if (durationResult.success) { const duration = durationResult.value; console.log(`Total seconds: ${duration.total("seconds")}`); console.log(`Hours: ${duration.hours}, Minutes: ${duration.minutes}`); } ``` The temporal parsers provide comprehensive date and time handling capabilities with full type safety and integration with the modern Temporal API. --- --- url: /integrations/valibot.md description: >- Use Valibot schemas for validating command-line arguments with a lightweight, modular validation library offering minimal bundle size. --- # Valibot integration *This API is available since Optique 0.7.0.* The *@optique/valibot* package provides seamless integration with [Valibot], enabling you to use Valibot schemas for validating command-line arguments. Valibot is a modular validation library with a significantly smaller bundle size (~10KB) compared to Zod (~52KB), making it ideal for CLI applications where bundle size matters. ::: code-group ```bash [Deno] deno add --jsr @optique/valibot @valibot/valibot ``` ```bash [npm] npm add @optique/valibot valibot ``` ```bash [pnpm] pnpm add @optique/valibot valibot ``` ```bash [Yarn] yarn add @optique/valibot valibot ``` ```bash [Bun] bun add @optique/valibot valibot ``` ::: > \[!NOTE] > When using Deno, import Valibot from `@valibot/valibot` instead of `valibot`: > > ::: code-group > > ```typescript [Deno] > import * as v from "@valibot/valibot"; > ``` > > ```typescript [Node.js] > import * as v from "valibot"; > ``` > > ```typescript [Bun] > import * as v from "valibot"; > ``` > > ::: [Valibot]: https://valibot.dev/ ## Basic usage The `valibot()` function creates a value parser from any Valibot schema: ```typescript twoslash import { valibot } from "@optique/valibot"; import * as v from "valibot"; // Email validation const email = valibot(v.pipe(v.string(), v.email()), { placeholder: "" }); // Port number with range validation const port = valibot( v.pipe( v.string(), v.transform(Number), v.number(), v.integer(), v.minValue(1024), v.maxValue(65535) ), { placeholder: 0 }, ); // Picklist choices const logLevel = valibot(v.picklist(["debug", "info", "warn", "error"]), { placeholder: "debug" }); ``` > \[!IMPORTANT] > The options object is required. In particular, `placeholder` must be a valid > stand-in value of the schema's output type. Optique uses it during deferred > prompt resolution, so it does not need to be meaningful user data, but it > must be safe for downstream transforms. ## Explicit transformations CLI arguments are always strings, so use explicit `v.transform()` for non-string types: ```typescript twoslash import { valibot } from "@optique/valibot"; import * as v from "valibot"; // ---cut-before--- // ✅ Correct: Use v.pipe with v.transform for numbers const age = valibot( v.pipe(v.string(), v.transform(Number), v.number(), v.minValue(0)), { placeholder: 0 }, ); // ❌ Won't work: v.number() expects actual numbers, not strings const num = valibot(v.number(), { placeholder: 0 }); // [!code error] ``` ## Transformations Valibot's transformation capabilities work seamlessly with Optique: ```typescript twoslash import { valibot } from "@optique/valibot"; import * as v from "valibot"; // ---cut-before--- // Parse and transform to Date const startDate = valibot( v.pipe(v.string(), v.transform((s) => new Date(s))), { placeholder: new Date(0) }, ); // Transform to uppercase const name = valibot( v.pipe(v.string(), v.transform((s) => s.toUpperCase())), { placeholder: "" }, ); ``` ## Async schemas *This API is available since Optique 1.1.0.* Use `valibotAsync()` when a schema depends on async validations. The returned value parser is async, so run the containing parser with `runAsync()` or `await run()`: ```typescript twoslash async function isKnownProject(value: string): Promise { return await Promise.resolve(value.startsWith("project-")); } // ---cut-before--- import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { runAsync } from "@optique/run"; import { valibotAsync } from "@optique/valibot"; import * as v from "valibot"; const parser = object({ project: option( "--project", valibotAsync( v.pipeAsync( v.string(), v.checkAsync(isKnownProject, "Unknown project."), ), { placeholder: "" }, ), ), }); const config = await runAsync(parser, { args: ["--project", "project-api"], }); ``` The synchronous `valibot()` helper remains synchronous and still rejects schemas that require Valibot's async parse path. `valibotAsync()` preserves the same metavar inference, choices, suggestions, formatting, and custom error handling as `valibot()`. Fallback values supplied through `bindEnv()` or `bindConfig()` are validated by the same schema before they are accepted. Async validation may run during fallback resolution and other repeated parser paths, including shell completion requests. Keep remote checks bounded and cached when possible, and prefer picklist or literal schemas for completion choices so suggestions stay metadata-driven. ## Custom error messages Customize error messages for better user experience: ```typescript twoslash import { valibot } from "@optique/valibot"; import { message } from "@optique/core/message"; import * as v from "valibot"; // ---cut-before--- const email = valibot(v.pipe(v.string(), v.email()), { placeholder: "", metavar: "EMAIL", errors: { valibotError: (issues, input) => message`Please provide a valid email address, got ${input}.` } }); ``` ## Integration with Optique Valibot parsers work seamlessly with all Optique features: ```typescript twoslash import { object } from "@optique/core/constructs"; import { option, argument } from "@optique/core/primitives"; import { valibot } from "@optique/valibot"; import * as v from "valibot"; const config = object({ email: option("--email", valibot(v.pipe(v.string(), v.email()), { placeholder: "" })), port: option( "-p", "--port", valibot( v.pipe( v.string(), v.transform(Number), v.number(), v.integer(), v.minValue(1024), v.maxValue(65535) ), { placeholder: 0 }, ) ), logLevel: option( "--log-level", valibot(v.picklist(["debug", "info", "warn", "error"]), { placeholder: "debug" }), ), startDate: argument( valibot(v.pipe(v.string(), v.transform((s) => new Date(s))), { placeholder: new Date(0) }), ), }); ``` ## Version compatibility The `@optique/valibot` package currently targets Valibot 1.x. ## Limitations * *`valibot()` remains synchronous*: Async Valibot features like `pipeAsync()` require `valibotAsync()`. The sync helper detects schemas that need async parsing when possible and throws a `TypeError` instead of silently skipping validation. The Valibot integration provides a lightweight yet powerful way to reuse validation logic across your entire application while maintaining full type safety, excellent error messages, and minimal bundle size. --- --- url: /concepts/valueparsers.md description: >- Value parsers convert string arguments into strongly-typed values with validation, supporting built-in types like integers, URLs, and UUIDs, plus custom parser creation. --- # Value parsers Value parsers are the specialized components responsible for converting raw string input from command-line arguments into strongly-typed values. While command-line arguments are always strings, your application needs them as numbers, URLs, file paths, or other typed values. Value parsers handle this critical transformation and provide validation at parse time. The philosophy behind Optique's value parsers is “fail fast, fail clearly.” Rather than letting invalid data flow through your application and cause mysterious errors later, value parsers validate input immediately when parsing occurs. When validation fails, they provide clear, actionable error messages that help users understand what went wrong and how to fix it. Every value parser implements the `ValueParser` interface, which defines how to parse strings into values of type `T` and how to format those values back into strings for help text. This consistent interface makes value parsers composable and allows you to create custom parsers that integrate seamlessly with Optique's type system. ### Parser catalog | Parser | Module | Return type | Description | | -------------------------------- | ------------------- | ------------------------------ | --------------------------------------------------- | | `string()` | *@optique/core* | `string` | Any string, with optional pattern validation | | `keyValue()` | *@optique/core* | readonly `[key, value]` | Key–value pair such as `KEY=VALUE` | | `integer()` | *@optique/core* | `number` or `bigint` | Integer with range validation | | `float()` | *@optique/core* | `number` | Floating-point number | | `fileSize()` | *@optique/core* | `number` or `bigint` | Human-readable data size (bytes) | | `color()` | *@optique/core* | `Color` | CSS color (hex, rgb, hsl, or named) | | `choice()` | *@optique/core* | string or number literal union | Enumerated values | | `firstOf()` | *@optique/core* | union of constituent types | First-match union of value parsers | | `json()` | *@optique/core* | `Json` | Any JSON value, with optional root type restriction | | `url()` | *@optique/core* | `URL` | URL with protocol filtering | | `locale()` | *@optique/core* | `Intl.Locale` | BCP 47 locale identifier | | `uuid()` | *@optique/core* | `string` | UUID with RFC 9562 validation | | `semVer()` | *@optique/core* | `SemVerString` or `SemVer` | Semantic Versioning 2.0.0 | | `port()` | *@optique/core* | `number` or `bigint` | TCP/UDP port number | | `ipv4()` | *@optique/core* | `string` | IPv4 address with restrictions | | `ipv6()` | *@optique/core* | `string` | IPv6 address | | `ip()` | *@optique/core* | `string` | IPv4 or IPv6 address | | `cidr()` | *@optique/core* | `string` | CIDR notation | | `hostname()` | *@optique/core* | `string` | DNS hostname | | `domain()` | *@optique/core* | `string` | DNS domain with TLD validation | | `email()` | *@optique/core* | `string` | Email address | | `socketAddress()` | *@optique/core* | `{ host, port }` | Host:port pair | | `portRange()` | *@optique/core* | `{ start, end }` | Port range | | `macAddress()` | *@optique/core* | `string` | MAC-48 address | | `path()` | *@optique/run* | `string` | File/directory path with existence checks | | `gitBranch()`, `gitTag()`, etc. | *@optique/git* | `string` | [Git references](../integrations/git.md) | | `instant()`, `plainDate()`, etc. | *@optique/temporal* | Temporal types | [Temporal dates/times](../integrations/temporal.md) | | `zod()` | *@optique/zod* | schema output | [Zod schema](../integrations/zod.md) | | `valibot()` | *@optique/valibot* | schema output | [Valibot schema](../integrations/valibot.md) | ## `string()` parser The `string()` parser is the most basic value parser—it accepts any string input and performs optional pattern validation. While all command-line arguments start as strings, the `string()` parser provides a way to explicitly document that you want string values and optionally validate their format. ```typescript twoslash import { string } from "@optique/core/valueparser"; // Basic string parser const name = string(); // String with custom metavar for help text const hostname = string({ metavar: "HOST" }); // String with pattern validation const identifier = string({ metavar: "ID", pattern: /^[a-zA-Z][a-zA-Z0-9_]*$/ }); ``` ### Pattern validation The optional `pattern` parameter accepts a regular expression that the input must match: ```typescript twoslash import { string } from "@optique/core/valueparser"; // ---cut-before--- // Email-like pattern const email = string({ pattern: /^[^@]+@[^@]+\.[^@]+$/, metavar: "EMAIL" }); // Semantic version pattern const version = string({ pattern: /^\d+\.\d+\.\d+$/, metavar: "VERSION" }); ``` When pattern validation fails, the parser provides a clear error message indicating what pattern was expected: ```bash $ myapp --version 1.2 Error: Expected a string matching pattern ^\d+\.\d+\.\d+$, but got 1.2. ``` The `string()` parser uses `"STRING"` as its default metavar, which appears in help text to indicate what kind of input is expected. ## `keyValue()` parser *This API is available since Optique 1.1.0.* The `keyValue()` parser accepts a single command-line value shaped like `KEY=VALUE` and returns a readonly tuple. It is useful for Docker-style environment variables, Kubernetes or Helm `--set` values, build-time defines, labels, and other repeated configuration overrides. ```typescript twoslash import { object } from "@optique/core/constructs"; import { map, multiple } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { keyValue } from "@optique/core/valueparser"; const parser = object({ env: map( multiple(option("-e", "--env", keyValue())), (pairs) => Object.fromEntries(pairs), ), set: map( multiple(option("--set", keyValue())), (pairs) => Object.fromEntries(pairs), ), }); ``` By default, `keyValue()`: * uses `=` as the separator; * rejects input without the separator; * rejects an empty key such as `=VALUE`; * allows an empty value such as `KEY=`; * splits repeated separators at the first separator, so `A=B=C` becomes `["A", "B=C"]`; * does not trim whitespace. Use `separator` for other key–value styles and `split: "last"` when the last separator should divide the key from the value: ```typescript twoslash import { keyValue } from "@optique/core/valueparser"; const label = keyValue({ separator: ":" }); const override = keyValue({ split: "last" }); const labelResult = label.parse("app:web"); labelResult; // ^? const overrideResult = override.parse("image.repository=ghcr.io=example/app"); // Parsed as ["image.repository=ghcr.io", "example/app"]. overrideResult; // ^? ``` The `key` and `value` options accept normal value parsers. Their result types are preserved in the tuple, so a constrained key or numeric value remains visible to TypeScript: ```typescript twoslash import { choice, integer, keyValue } from "@optique/core/valueparser"; const portSetting = keyValue({ key: choice(["port"] as const), value: integer({ min: 1, max: 65535 }), }); const parsed = portSetting.parse("port=5432"); if (parsed.success) { parsed.value; // ^? } ``` For string policies, use child `string()` parsers: ```typescript twoslash import { keyValue, string } from "@optique/core/valueparser"; const label = keyValue({ key: string({ metavar: "KEY", pattern: /^[a-z][a-z0-9_.-]*$/, }), value: string({ metavar: "VALUE" }), }); ``` `format([key, value])` formats both sides through their child parsers before joining them with the separator. Completion suggestions are composed when the child parsers provide suggestions: key suggestions get the separator appended, and value suggestions are prefixed with the already typed key and separator. Shell escaping and quote handling happen before Optique receives arguments. For example, a shell may pass `--set name="hello world"` to Optique as one token whose value is `name=hello world`; `keyValue()` then splits that token. It does not parse dotenv files, nested paths, or shell syntax. ## `integer()` parser The `integer()` parser converts string input to numeric values with validation. It supports both regular JavaScript numbers (safe up to `Number.MAX_SAFE_INTEGER`) and arbitrary-precision `bigint` values for very large integers. ### Number mode (default) By default, `integer()` returns JavaScript numbers and validates that input contains only digits: ```typescript twoslash import { integer } from "@optique/core/valueparser"; // Basic integer const count = integer(); // Integer with bounds checking const port = integer({ min: 1, max: 0xffff }); // Integer with custom metavar const timeout = integer({ min: 0, metavar: "SECONDS" }); ``` ### Bigint mode For very large integers that exceed JavaScript's safe integer range, specify `type: "bigint"`: ```typescript twoslash import { integer } from "@optique/core/valueparser"; // ---cut-before--- // BigInt integer with bounds const largeNumber = integer({ type: "bigint", min: 0n, max: 999999999999999999999n }); ``` ### Validation and error messages The `integer()` parser provides detailed validation: * *Format validation*: Ensures input contains only digits with an optional leading `-` * *Range validation*: Enforces minimum and maximum bounds * *Safe integer protection*: Rejects values outside `Number.MIN_SAFE_INTEGER` to `Number.MAX_SAFE_INTEGER` (use `type: "bigint"` for larger values) ```bash $ myapp --port "abc" Error: Expected a valid integer, but got abc. $ myapp --port "99999" Error: Expected a value less than or equal to 65,535, but got 99999. $ myapp --count "9007199254740993" Error: Expected a safe integer between -9,007,199,254,740,991 and 9,007,199,254,740,991, but got 9007199254740993. Use type: "bigint" for large values. ``` The parser uses `"INTEGER"` as its default metavar. ## `float()` parser The `float()` parser handles floating-point numbers with comprehensive validation options. It recognizes standard decimal notation, scientific notation, and optionally special values like `NaN` and `Infinity`. ```typescript twoslash import { float } from "@optique/core/valueparser"; // Basic float const rate = float(); // Float with bounds const percentage = float({ min: 0.0, max: 100.0 }); // Float allowing special values const scientific = float({ allowNaN: true, allowInfinity: true, metavar: "NUMBER" }); ``` #### Supported formats The `float()` parser recognizes multiple numeric formats: * *Integers*: `42`, `-17` * *Decimals*: `3.14`, `-2.5`, `.5`, `42.` * *Scientific notation*: `1.23e10`, `5.67E-4`, `1e+5` * *Special values*: `NaN`, `Infinity`, `-Infinity` (when allowed) #### Special values By default, `NaN` and `Infinity` are not allowed, which prevents accidental acceptance of these values. Enable them explicitly when they're meaningful for your use case: ```typescript twoslash import { float } from "@optique/core/valueparser"; // ---cut-before--- // Mathematics application that handles infinite limits const limit = float({ allowInfinity: true, metavar: "LIMIT" }); // Statistical application that handles missing data const value = float({ allowNaN: true, allowInfinity: true, metavar: "VALUE" }); ``` The parser uses `"NUMBER"` as its default metavar and provides clear error messages for invalid formats and out-of-range values. ## `fileSize()` parser *This API is available since Optique 1.1.0.* The `fileSize()` parser converts human-readable data size strings into a `number` representing the equivalent byte count. It is useful for CLI tools that accept storage limits, upload caps, log rotation thresholds, or similar size values. ```typescript twoslash import { fileSize } from "@optique/core/valueparser"; // Parses "10MB", "1.5GiB", "512B", etc. → number (bytes) const maxUpload = fileSize(); // Custom metavar shown in help text const cacheSize = fileSize({ metavar: "SIZE" }); ``` ### Supported units The parser accepts both SI (decimal) and IEC (binary) unit suffixes, matched case-insensitively: | Unit | Bytes (SI/default) | Unit | Bytes (IEC) | | ---- | ---------------------- | ---- | --------------------- | | B | 1 | | | | KB | 1 000 | KiB | 1 024 | | MB | 1 000 000 | MiB | 1 048 576 | | GB | 1 000 000 000 | GiB | 1 073 741 824 | | TB | 1 000 000 000 000 | TiB | 1 099 511 627 776 | | PB | 10^15 | PiB | 2^50 | | EB | 10^18 \[^filesize-safe] | EiB | 2^60 \[^filesize-safe] | Unit suffixes are matched case-insensitively: `"1kb"`, `"1KB"`, and `"1Kb"` are all treated as 1 000 bytes. Optional whitespace between the number and unit is also accepted (`"1 MB"`). \[^filesize-safe]: In `number` mode (the default), the result must be a safe integer (≤ `Number.MAX_SAFE_INTEGER` ≈ 9 × 10^15). Values in the `EB`/`EiB` range and values above roughly 9 PB or 8 PiB are therefore rejected. Use `type: "bigint"` to lift this restriction—see the [bigint mode](#bigint-mode) section. ### Default unit When a bare number is provided without a unit, `fileSize()` rejects it by default. Use the `defaultUnit` option to assume a unit in that case: ```typescript twoslash import { fileSize } from "@optique/core/valueparser"; // ---cut-before--- // "100" → 100 000 000 bytes; "100MB" → still 100 000 000 bytes const parser = fileSize({ defaultUnit: "MB" }); ``` ### Allowing negative values By default, negative values are rejected. Pass `allowNegative: true` to accept them: ```typescript twoslash import { fileSize } from "@optique/core/valueparser"; // ---cut-before--- const delta = fileSize({ allowNegative: true }); ``` ### SI-as-binary mode Some tools use `KB`, `MB`, `GB` etc. to mean powers of 1 024 rather than 1 000—a widespread but technically incorrect convention. Enable `siAsBinary: true` to match that behaviour: ```typescript twoslash import { fileSize } from "@optique/core/valueparser"; // ---cut-before--- // "1KB" → 1 024 bytes instead of 1 000 const legacySize = fileSize({ siAsBinary: true }); ``` IEC suffixes (`KiB`, `MiB`, …) are unaffected by this option and always use powers of 1 024. ### Bigint mode By default, `fileSize()` returns `number`, which cannot represent byte counts above roughly 9 PB exactly. Pass `type: "bigint"` to get a `bigint` result instead—this lifts the safe-integer restriction and makes `EB`/`EiB` values usable: ```typescript twoslash import { fileSize } from "@optique/core/valueparser"; // ---cut-before--- const diskLimit = fileSize({ type: "bigint" }); // "1EB" → 1_000_000_000_000_000_000n // "1EiB" → 1_152_921_504_606_846_976n ``` All options (`allowNegative`, `defaultUnit`, `siAsBinary`, `metavar`) work the same way in bigint mode. ### Error messages ```bash $ myapp --max-upload "abc" Error: Expected a file size like 10MB or 1.5GiB, but got abc. $ myapp --max-upload "100" Error: Expected a file size like 10MB or 1.5GiB, but got 100. $ myapp --max-upload "-1MB" Error: Expected a non-negative file size, but got -1MB. ``` The parser uses `"SIZE"` as its default metavar. ## `color()` parser *This API is available since Optique 1.1.0.* The `color()` parser converts CSS color strings into a structured `Color` object with normalized `r`, `g`, `b`, and `a` fields. It is useful for CLI tools that accept theme colors, background colors, or any other color configuration from users. ```typescript twoslash import { color } from "@optique/core/valueparser"; // Accepts any CSS color notation const background = color(); // Custom metavar shown in help text const fg = color({ metavar: "FG_COLOR" }); ``` ### Supported formats The parser accepts four CSS color notations by default: | Format | Examples | | ------- | ---------------------------------------------- | | `hex` | `#f00`, `#ff0000`, `#f00f`, `#ff0000ff` | | `rgb` | `rgb(255, 0, 0)`, `rgba(255, 0, 0, 0.5)` | | `hsl` | `hsl(0, 100%, 50%)`, `hsla(0, 100%, 50%, 0.5)` | | `named` | `red`, `rebeccapurple`, `transparent`, … | Hex digits are matched case-insensitively. The `rgb` and `hsl` function names are also case-insensitive. Named colors are matched case-insensitively as well. ### Return type The returned `Color` object has four fields: * `r`, `g`, `b` — red, green, and blue channels as integers in the range 0–255 * `a` — alpha channel as a float in the range 0–1, where 1 is fully opaque and 0 is fully transparent ```typescript twoslash import { color, type Color } from "@optique/core/valueparser"; // ---cut-before--- const result = color().parse("#ff8000"); if (result.success) { const c: Color = result.value; // c.r === 255, c.g === 128, c.b === 0, c.a === 1 } ``` ### Restricting accepted formats Use the `formats` option to limit which notations are accepted: ```typescript twoslash import { color } from "@optique/core/valueparser"; // ---cut-before--- // Only hex notation const hexOnly = color({ formats: ["hex"] }); // Hex and named colors const hexOrNamed = color({ formats: ["hex", "named"] }); ``` ### Named colors The parser recognizes all 148 CSS Level 4 named colors, including `transparent` (which has alpha 0) and `rebeccapurple`. Named color lookup is case-insensitive, so `"Red"`, `"RED"`, and `"red"` all parse to the same value. When shell completion is active, named colors are suggested based on the current prefix. ### `format()` output The `format()` method always outputs canonical lowercase hex: `#rrggbb` when alpha is 1, or `#rrggbbaa` when alpha is less than 1. This means `format()` output can always be re-parsed with `formats: ["hex"]`. ### Error messages ```bash $ myapp --bg "notacolor" Error: Expected a CSS color like #ff0000, rgb(255, 0, 0), or red, but got notacolor. ``` The parser uses `"COLOR"` as its default metavar. ## `choice()` parser The `choice()` parser creates type-safe enumerations by restricting input to one of several predefined string values. This is perfect for options like log levels, output formats, or operation modes where only specific values make sense. ```typescript twoslash import { choice } from "@optique/core/valueparser"; // Log level choice const logLevel = choice(["debug", "info", "warn", "error"]); // Output format choice const format = choice(["json", "yaml", "xml", "csv"]); // Case-insensitive choice const protocol = choice(["http", "https", "ftp"], { caseInsensitive: true }); ``` ### Type-safe string literals The `choice()` parser creates exact string literal types rather than generic `string` types, providing excellent TypeScript integration: ```typescript twoslash import { choice } from "@optique/core/valueparser"; // ---cut-before--- const level = choice(["debug", "info", "warn", "error"]); // ^? ``` ### Case sensitivity By default, matching is case-sensitive. Set `caseInsensitive: true` to accept variations: ```typescript twoslash import { choice } from "@optique/core/valueparser"; // ---cut-before--- const format = choice(["JSON", "XML"], { caseInsensitive: true, metavar: "FORMAT" }); // Accepts: "json", "JSON", "Json", "xml", "XML", "Xml" ``` ### Error messages When invalid choices are provided, the parser lists all valid options: ```bash $ myapp --format "txt" Error: Expected one of json, yaml, xml, csv, but got txt. ``` The parser uses `"TYPE"` as its default metavar. ### Number choices *This feature is available since Optique 0.9.0.* The `choice()` parser also supports number literals, which is useful for options like bit depths, port numbers, or other numeric values where only specific values are valid: ```typescript twoslash import { choice } from "@optique/core/valueparser"; // Bit depth choice const bitDepth = choice([8, 10, 12]); // Port selection const port = choice([80, 443, 8080]); ``` Number choices provide the same type-safe literal types as string choices: ```typescript twoslash import { choice } from "@optique/core/valueparser"; // ---cut-before--- const depth = choice([8, 10, 12]); // ^? ``` > \[!NOTE] > The `caseInsensitive` option is only available for string choices. > TypeScript will report an error if you try to use it with number choices. > \[!TIP] > You can display valid choices in help text by enabling the `showChoices` > option in your runner configuration. See the > [choice display](./runners.md#choice-display) section in the runners guide > for details. ## `firstOf()` combinator *This combinator is available since Optique 1.1.0.* Some options accept values of multiple incompatible types: a `--count` that takes either a number or the literal `auto`, an `--output` that is either a file path or `-` for standard output, a `--log-level` that accepts both named levels and numeric verbosity. The `firstOf()` combinator composes existing value parsers into such a union. It tries each constituent parser in declaration order and returns the result of the first one that succeeds: ```typescript twoslash import { choice, firstOf, integer } from "@optique/core/valueparser"; const count = firstOf(choice(["auto"]), integer({ min: 1 })); // ^? ``` The result type is inferred as the union of the constituent types, so the example above produces `"auto" | number` without manual type annotations or hand-written parsing logic. Each constituent keeps its own validation and error messages, which is the main advantage over parsing with `string()` and converting the result in a `map()`. ### Declaration order The first constituent that accepts the input wins. When inputs overlap (for example, `choice(["1"])` and `integer()` both accept the input `1`), the earlier constituent claims them, so put more specific parsers first: ```typescript twoslash import { choice, firstOf, string } from "@optique/core/valueparser"; // ---cut-before--- // Correct: the specific alias list comes before the catch-all string() const target = firstOf(choice(["production", "staging"]), string()); // Wrong: string() accepts everything, so choice() never gets a chance const broken = firstOf(string(), choice(["production", "staging"])); ``` ### Help text The default metavar joins the constituent metavars with `|` (for the example above, `TYPE|INTEGER`), and the `metavar` option overrides it: ```typescript twoslash import { choice, firstOf, integer } from "@optique/core/valueparser"; // ---cut-before--- const count = firstOf(choice(["auto"]), integer({ min: 1 }), { metavar: "COUNT", }); ``` Completion suggestions are merged across all constituents that provide them. When every constituent enumerates its valid values (e.g. a `firstOf()` of multiple `choice()` parsers), the merged list is also exposed for the `showChoices` display option; if any constituent is open-ended, no choice list is shown. ### Dynamic parser lists The variadic form requires at least two statically known arguments. When the constituents are built dynamically, pass them as a single array instead: ```typescript twoslash import { choice, firstOf, integer, type ValueParser, } from "@optique/core/valueparser"; // ---cut-before--- const parsers: ValueParser<"sync", "auto" | number>[] = [ choice(["auto"]), integer({ min: 1 }), ]; const count = firstOf(parsers, { metavar: "COUNT" }); ``` The array must still contain at least two parsers; shorter arrays are rejected with a `TypeError` at construction time. ### Error messages When every constituent fails, the error lists each constituent's error on its own line: ```bash $ myapp --count abc Error: `--count`: Expected one of the following: - Expected one of "auto", but got "abc". - Expected a valid integer, but got "abc". ``` The `errors.noMatch` option replaces this combined message with a static message or one computed from the input and the constituent errors: ```typescript twoslash import { message } from "@optique/core/message"; import { choice, firstOf, integer } from "@optique/core/valueparser"; // ---cut-before--- const count = firstOf(choice(["auto"]), integer({ min: 1 }), { errors: { noMatch: (input) => message`Expected ${"auto"} or a positive integer, but got ${input}.`, }, }); ``` > \[!NOTE] > `firstOf()` only supports synchronous value parsers. Passing an async > value parser (such as the Git parsers from *@optique/git*) throws a > `TypeError` at construction time. Dependency-derived value parsers > (created via `deriveFrom()` or `dependency().derive()`) are rejected the > same way: invoked through `firstOf()` they would parse with default > dependency values instead of the ones resolved during the current parse. ## `json()` parser *This API is available since Optique 1.1.0.* The `json()` parser accepts any well-formed JSON string and returns the corresponding JavaScript value. It is a lightweight way to accept structured data on the command line without pulling in a full schema library. ```typescript twoslash import { json } from "@optique/core/valueparser"; // Accept any JSON value const data = json(); const result = data.parse('{"name":"Alice","age":30}'); if (result.success) { console.log(result.value); // { name: "Alice", age: 30 } } ``` The return type is the exported `Json` union, which covers all JSON-serializable values: ```typescript type Json = | string | number | boolean | null | { readonly [property: string]: Json } | readonly Json[]; ``` ### Root type restriction Use the `rootType` option to restrict which JSON root type is accepted. When `rootType` is set, the TypeScript return type is narrowed accordingly: ```typescript twoslash import { json, type Json } from "@optique/core/valueparser"; // Only accept JSON objects — return type is { readonly [p: string]: Json } const objParser = json({ rootType: "object" }); // Only accept JSON arrays — return type is readonly Json[] const arrParser = json({ rootType: "array" }); // Only accept JSON strings — return type is string const strParser = json({ rootType: "string" }); // Only accept JSON numbers — return type is number const numParser = json({ rootType: "number" }); // Only accept JSON booleans — return type is boolean const boolParser = json({ rootType: "boolean" }); // Only accept JSON null — return type is null const nullParser = json({ rootType: "null" }); ``` ### Error messages The `json()` parser provides two customizable error messages. `invalidJson` fires when the input cannot be parsed as JSON at all: ```bash $ myapp --config '{bad json}' Error: `JSON`: Not a valid JSON: Expected property name or '}' in JSON at position 1 ``` `invalidRootType` fires when the JSON is valid but the root type does not match `rootType`: ```bash $ myapp --count '{"a":1}' Error: `JSON`: Expected JSON number, but got object. ``` Both messages can be overridden with a static `Message` or a callback: ```typescript twoslash import { json } from "@optique/core/valueparser"; import { text } from "@optique/core/message"; const parser = json({ rootType: "object", errors: { invalidJson: [text("Config must be a valid JSON string.")], invalidRootType: (_value, expected) => [text(`Expected a JSON ${expected}.`)], }, }); ``` The parser uses `"JSON"` as its default metavar. > \[!TIP] > For schema-validated JSON with complex shapes, consider the > *@optique/zod* or *@optique/valibot* integrations instead. ## `url()` parser The `url()` parser validates that input is a well-formed URL and optionally restricts the allowed protocols. The parsed result is a JavaScript [`URL`] object with all the benefits of the native URL API. ```typescript twoslash import { url } from "@optique/core/valueparser"; // Basic URL parser const endpoint = url(); // URL with protocol restrictions const apiUrl = url({ allowedProtocols: ["https:"], metavar: "HTTPS_URL" }); // URL allowing multiple protocols const downloadUrl = url({ allowedProtocols: ["http:", "https:", "ftp:"], metavar: "URL" }); ``` [`URL`]: https://developer.mozilla.org/en-US/docs/Web/API/URL ### Protocol validation The `allowedProtocols` option restricts which URL schemes are acceptable. Protocol names must include the trailing colon: ```typescript twoslash import { url } from "@optique/core/valueparser"; // ---cut-before--- // Only secure protocols const secureUrl = url({ allowedProtocols: ["https:", "wss:"] }); // Web protocols only const webUrl = url({ allowedProtocols: ["http:", "https:"] }); ``` ### URL object benefits The parser returns native [`URL`] objects, providing immediate access to URL components: ```typescript twoslash import { parse } from "@optique/core/parser"; import { argument } from "@optique/core/primitives"; import { url } from "@optique/core/valueparser"; const apiUrl = argument(url()); // ---cut-before--- const result = parse(apiUrl, ["https://api.example.com:8080/v1/users"]); if (result.success) { const url = result.value; console.log(`Host: ${url.hostname}`); // "api.example.com" console.log(`Port: ${url.port}`); // "8080" console.log(`Path: ${url.pathname}`); // "/v1/users" console.log(`Protocol: ${url.protocol}`); // "https:" } ``` ### Validation errors The parser provides specific error messages for different validation failures: ```bash $ myapp --url "not-a-url" Error: Invalid URL: not-a-url. $ myapp --url "ftp://example.com" # when only HTTPS allowed Error: URL protocol ftp: is not allowed. Allowed protocols: https:. ``` The parser uses `"URL"` as its default metavar. ## `locale()` parser The `locale()` parser validates locale identifiers according to the Unicode Locale Identifier standard ([BCP 47]). It returns [`Intl.Locale`] objects that provide rich locale information and integrate with JavaScript's internationalization APIs. ```typescript twoslash import { locale } from "@optique/core/valueparser"; // Basic locale parser const userLocale = locale(); // Locale with custom metavar const displayLocale = locale({ metavar: "LANG" }); ``` [BCP 47]: https://www.rfc-editor.org/info/bcp47 [`Intl.Locale`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale ### Locale validation The parser uses the native [`Intl.Locale`] constructor for validation, ensuring compliance with international standards: ```typescript // Valid locales const validLocales = [ "en", // Language only "en-US", // Language and region "zh-Hans-CN", // Language, script, and region "de-DE-u-co-phonebk" // With Unicode extension ]; // Invalid locales will be rejected const invalidLocales = [ "invalid-locale", "en_US", // Underscore instead of hyphen "toolong-language-tag-name" ]; ``` ### Locale object benefits The parser returns [`Intl.Locale`] objects with rich locale information: ```typescript twoslash import { parse } from "@optique/core/parser"; import { argument } from "@optique/core/primitives"; import { locale } from "@optique/core/valueparser"; const userLocale = argument(locale()); // ---cut-before--- const result = parse(userLocale, ["zh-Hans-CN"]); if (result.success) { const locale = result.value; console.log(`Language: ${locale.language}`); // "zh" console.log(`Script: ${locale.script}`); // "Hans" console.log(`Region: ${locale.region}`); // "CN" console.log(`Base name: ${locale.baseName}`); // "zh-Hans-CN" } ``` The parser uses `"LOCALE"` as its default metavar and provides clear error messages for invalid locale identifiers. ## `uuid()` parser The `uuid()` parser validates [UUID] (Universally Unique Identifier) strings according to [RFC 9562] and optionally restricts to specific UUID versions. It returns a branded `Uuid` string type for additional type safety. ```typescript twoslash import { uuid } from "@optique/core/valueparser"; // Basic UUID parser (strict RFC 9562 validation by default) const id = uuid(); // UUID with version restrictions const uuidV4 = uuid({ allowedVersions: [4], metavar: "UUID_V4" }); // UUID allowing multiple versions const trackingId = uuid({ allowedVersions: [1, 4, 5] }); ``` [UUID]: https://en.wikipedia.org/wiki/Universally_unique_identifier [RFC 9562]: https://www.rfc-editor.org/rfc/rfc9562 ### UUID format validation The parser validates the standard UUID format: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` where each `x` is a hexadecimal digit: ``` # Valid UUID formats 550e8400-e29b-41d4-a716-446655440000 # Version 4 123e4567-e89b-12d3-a456-426614174000 # Version 1 6ba7b810-9dad-11d1-80b4-00c04fd430c8 # Version 1 6ba7b811-9dad-11d1-80b4-00c04fd430c8 # Version 1 # Invalid formats 550e8400-e29b-41d4-a716-44665544000 # Too short 550e8400-e29b-41d4-a716-446655440000x # Extra character 550e8400e29b41d4a716446655440000 # Missing hyphens ``` ### Strict RFC 9562 validation By default, the parser enforces strict RFC 9562 compliance: * The *version digit* (index 14, 0-based) must be one of the currently standardized versions: 1 through 8. * The *variant nibble* (index 19) must have the RFC 9562 layout (`10xx` in binary), meaning hex digits `8`, `9`, `a`, or `b`. The *nil UUID* (`00000000-0000-0000-0000-000000000000`) and *max UUID* (`ffffffff-ffff-ffff-ffff-ffffffffffff`) are accepted as special standard values regardless of strict mode or `allowedVersions`. Set `strict: false` to disable version and variant validation, accepting any well-formed UUID string: ```typescript twoslash import { uuid } from "@optique/core/valueparser"; // ---cut-before--- // Accept any UUID-like string without RFC 9562 checks const lenient = uuid({ strict: false }); ``` ### Version validation When `allowedVersions` is specified, the parser checks the version digit (index 14) and validates it matches one of the allowed versions. This takes precedence over the strict default version set (1-8), but variant validation still applies in strict mode: ```typescript twoslash import { uuid } from "@optique/core/valueparser"; // ---cut-before--- const uuidV4Only = uuid({ allowedVersions: [4] }); // This will pass validation (version 4 UUID) const validV4 = "550e8400-e29b-41d4-a716-446655440000"; // This will fail validation (version 1 UUID) const invalidV1 = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"; ``` ### UUID type safety The parser returns a branded `Uuid` type rather than a plain string, providing additional compile-time safety: ```typescript twoslash // @errors: 2345 import { parse } from "@optique/core/parser"; import { argument } from "@optique/core/primitives"; import { uuid } from "@optique/core/valueparser"; const uuidParser = argument(uuid()) // ---cut-before--- type Uuid = `${string}-${string}-${string}-${string}-${string}`; // The branded type prevents accidental usage of regular strings as UUIDs function processUuid(id: Uuid) { // Implementation here } const result = parse(uuidParser, ["550e8400-e29b-41d4-a716-446655440000"]); if (result.success) { processUuid(result.value); // ✓ Type-safe } // This would cause a TypeScript error: processUuid("not-a-uuid"); // ✗ Type error ``` The parser uses `"UUID"` as its default metavar and provides specific error messages for format violations, version mismatches, and variant violations. ## `semVer()` parser *This API is available since Optique 1.1.0.* The `semVer()` parser validates strings according to the [Semantic Versioning 2.0.0] specification. It ensures that major, minor, and patch components are non-negative integers without leading zeros, and that pre-release and build metadata identifiers contain only permitted characters. ```typescript twoslash import { semVer } from "@optique/core/valueparser"; // String mode (default): returns a SemVerString template-literal type const version = semVer(); // Object mode: returns a structured SemVer object const structuredVersion = semVer({ type: "object" }); ``` [Semantic Versioning 2.0.0]: https://semver.org/ ### String mode By default, `semVer()` returns a `SemVerString` template-literal type that covers all four valid forms of a SemVer string: ```typescript twoslash import { semVer, type SemVerString } from "@optique/core/valueparser"; const parser = semVer(); const result = parser.parse("1.2.3-rc.1+build.42"); if (result.success) { const v: SemVerString = result.value; // "1.2.3-rc.1+build.42" } ``` The output is always the canonical form with no leading `v` prefix, even when the `v` prefix was accepted as input (see [V prefix](#v-prefix) below). ### Object mode Pass `type: "object"` to receive a structured `SemVer` value with individual components: ```typescript twoslash import { semVer, type SemVer } from "@optique/core/valueparser"; const parser = semVer({ type: "object" }); const result = parser.parse("2.0.0-alpha.1+build.42"); if (result.success) { const v: SemVer = result.value; // v.major → 2 // v.minor → 0 // v.patch → 0 // v.preRelease → "alpha.1" // v.metadata → "build.42" } ``` The `preRelease` and `metadata` fields are absent (not `undefined`) when the input contains no pre-release or build metadata. > \[!NOTE] > Object mode stores `major`, `minor`, and `patch` as JavaScript `number` > values, so it rejects inputs whose version components exceed > `Number.MAX_SAFE_INTEGER` (2⁵³ − 1 = 9,007,199,254,740,991). In the > unlikely event that you need to handle version numbers of that magnitude, > use string mode instead. ### V prefix By default the parser rejects a leading `v` character. Set `allowPrefix: true` to accept the common `v1.2.3` convention: ```typescript twoslash import { semVer } from "@optique/core/valueparser"; const parser = semVer({ allowPrefix: true }); const result = parser.parse("v1.2.3"); if (result.success) { console.log(result.value); // "1.2.3" — prefix stripped from output } ``` The `v` prefix is stripped from the output regardless of mode; the canonical form never includes it. ### Error messages When input is not a valid SemVer 2.0.0 string, the parser returns an error: ```bash $ myapp --version "1.02.0" Error: Expected a valid Semantic Versioning 2.0.0 string (e.g. 1.0.0), but got 1.02.0. ``` Custom error messages can be provided as a static message or a function: ```typescript twoslash import { semVer } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; const parser = semVer({ errors: { invalidSemVer: (input) => message`${input} is not a valid version string.`, }, }); ``` ## `port()` parser > \[!TIP] > See also: [`portRange()`](#portrange-parser), > [`socketAddress()`](#socketaddress-parser). The `port()` parser validates TCP/UDP port numbers with support for both `number` and `bigint` types, range validation, and well-known port restrictions. Port numbers are commonly used in network applications for server addresses, database connections, and service configurations. ```typescript twoslash import { port } from "@optique/core/valueparser"; // Basic port parser (1-65535) const serverPort = port(); // Non-privileged ports only (1024-65535) const userPort = port({ min: 1024, max: 65535 }); // Development ports const devPort = port({ min: 3000, max: 9000 }); // Disallow well-known ports (1-1023) const appPort = port({ disallowWellKnown: true }); ``` ### Port number validation By default, the `port()` parser validates that the input is a valid port number between 1 and 65535 (the full range of valid TCP/UDP ports). You can customize this range using the `min` and `max` options: ```typescript twoslash import { port } from "@optique/core/valueparser"; // ---cut-before--- // Web server ports (typically 80 or 443) const webPort = port({ min: 1, max: 65535 }); // Custom application ports const customPort = port({ min: 8000, max: 8999 }); ``` ### Well-known port restrictions The `disallowWellKnown` option rejects ports 1–1023, which typically require elevated privileges (root/administrator) to bind on most systems. This is useful for applications that should run without special permissions: ```typescript twoslash import { port } from "@optique/core/valueparser"; // ---cut-before--- const unprivilegedPort = port({ disallowWellKnown: true, metavar: "PORT" }); ``` When a well-known port is rejected, the error message explains why: ```bash $ myapp --port 80 Error: Port 80 is a well-known port (1-1023) and may require elevated privileges. ``` ### Number and bigint modes Like the `integer()` parser, `port()` supports both `number` (default) and `bigint` types. While all valid port numbers fit safely in JavaScript's `number` type, the `bigint` option is provided for consistency with other numeric parsers: ```typescript twoslash import { port } from "@optique/core/valueparser"; // ---cut-before--- // Default: returns number const numPort = port(); // Bigint mode: returns bigint const bigintPort = port({ type: "bigint" }); ``` ### Common use cases The `port()` parser is commonly used in network applications: ```typescript twoslash import { option } from "@optique/core/primitives"; import { port } from "@optique/core/valueparser"; // ---cut-before--- // HTTP server const httpPort = option("--port", port({ min: 1024 })); // Database connection const dbPort = option("--db-port", port()); // Redis connection (default port 6379) const redisPort = option("--redis-port", port()); ``` The parser uses `"PORT"` as its default metavar and provides specific error messages for invalid port numbers, range violations, and well-known port restrictions. ## `ipv4()` parser > \[!TIP] > See also: [`ipv6()`](#ipv6-parser), [`ip()`](#ip-parser) (accepts both), > [`cidr()`](#cidr-parser), [`hostname()`](#hostname-parser), > [`socketAddress()`](#socketaddress-parser). The `ipv4()` parser validates IPv4 addresses in dotted-decimal notation with comprehensive filtering options for different IP address types. It is commonly used for network configuration, server addresses, and IP allowlists/blocklists. ```typescript twoslash import { ipv4 } from "@optique/core/valueparser"; // Basic IPv4 parser (allows all types) const address = ipv4(); // Public IPs only (no private/loopback) const publicIp = ipv4({ allowPrivate: false, allowLoopback: false }); // Server binding (allow 0.0.0.0 and private IPs) const bindAddress = ipv4({ allowZero: true, allowPrivate: true }); ``` ### IPv4 format validation The `ipv4()` parser validates that input matches the standard IPv4 format: four decimal octets (0-255) separated by dots (e.g., “192.168.1.1”). Each octet must be in the valid range, and the parser strictly rejects: * *Leading zeros*: “192.168.001.1” is invalid (except single “0”) * *Whitespace*: Leading, trailing, or embedded spaces are rejected * *Empty octets*: “192.168..1” is invalid * *Out-of-range values*: Octets must be 0-255 ```typescript twoslash import { ipv4 } from "@optique/core/valueparser"; // ---cut-before--- const parser = ipv4(); // Valid IPv4 addresses parser.parse("192.168.1.1"); // ✓ parser.parse("10.0.0.1"); // ✓ parser.parse("255.255.255.255"); // ✓ parser.parse("0.0.0.0"); // ✓ // Invalid formats parser.parse("192.168.001.1"); // ✗ Leading zero parser.parse("256.1.1.1"); // ✗ Octet > 255 parser.parse("192.168.1"); // ✗ Only 3 octets parser.parse("192.168..1"); // ✗ Empty octet ``` ### IP address filtering The parser provides fine-grained control over which IP address types are accepted through boolean filter options. All filters default to `true` (permissive), allowing you to selectively restrict specific address types: `allowPrivate` : Controls private IP ranges (RFC 1918): 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16. Set to `false` to reject these addresses. `allowLoopback` : Controls loopback addresses (127.0.0.0/8). Set to `false` to reject addresses like 127.0.0.1. `allowLinkLocal` : Controls link-local addresses (169.254.0.0/16). Set to `false` to reject APIPA/link-local addresses. `allowMulticast` : Controls multicast addresses (224.0.0.0/4). Set to `false` to reject addresses in the 224-239 range. `allowBroadcast` : Controls the broadcast address (255.255.255.255). Set to `false` to reject the all-hosts broadcast address. `allowZero` : Controls the zero address (0.0.0.0). Set to `false` to reject the “any” or “unspecified” address. ```typescript twoslash import { option } from "@optique/core/primitives"; import { ipv4 } from "@optique/core/valueparser"; // ---cut-before--- // Public-facing API endpoint (no private/loopback IPs) const publicEndpoint = option("--api-endpoint", ipv4({ allowPrivate: false, allowLoopback: false, allowLinkLocal: false })); // Server binding address (allow 0.0.0.0 and private IPs) const bindAddr = option("--bind", ipv4({ allowZero: true, allowPrivate: true })); // Client IP address (no special addresses) const clientIp = option("--client-ip", ipv4({ allowZero: false, allowBroadcast: false, allowMulticast: false })); ``` ### Error messages The parser provides specific error messages for different validation failures: ```bash $ myapp --ip "192.168.001.1" Error: Expected a valid IPv4 address, but got 192.168.001.1. $ myapp --public-ip "192.168.1.1" # when private IPs are disallowed Error: 192.168.1.1 is a private IP address. $ myapp --endpoint "127.0.0.1" # when loopback is disallowed Error: 127.0.0.1 is a loopback address. $ myapp --bind "255.255.255.255" # when broadcast is disallowed Error: 255.255.255.255 is the broadcast address. ``` ### Common use cases The `ipv4()` parser is commonly used in network applications: ```typescript twoslash import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { ipv4, port } from "@optique/core/valueparser"; // ---cut-before--- // HTTP server configuration const serverConfig = object({ bind: option("--bind", ipv4({ allowPrivate: true })), port: option("--port", port({ min: 1024 })) }); // Firewall rule configuration const firewallRule = object({ source: option("--source", ipv4()), dest: option("--dest", ipv4()) }); // DNS server configuration const dnsConfig = option("--nameserver", ipv4({ allowLoopback: false, // Loopback doesn't make sense for DNS allowZero: false // 0.0.0.0 not valid for nameserver })); ``` The parser uses `"IPV4"` as its default metavar and returns the normalized IPv4 address as a string (e.g., “192.168.1.1”). ## `hostname()` parser > \[!TIP] > See also: [`domain()`](#domain-parser), [`email()`](#email-parser), > [`socketAddress()`](#socketaddress-parser), > [`ipv4()`](#ipv4-parser)/[`ipv6()`](#ipv6-parser). The `hostname()` parser validates DNS hostnames according to RFC 1123. It supports flexible options for wildcard hostnames, underscores, localhost filtering, and length constraints. ```typescript twoslash import { hostname } from "@optique/core/valueparser"; // Basic hostname parser const host = hostname(); // Allow wildcards for certificate validation const domain = hostname({ allowWildcard: true }); // Reject localhost const remoteHost = hostname({ allowLocalhost: false }); // Service discovery records (with underscores) const srvRecord = hostname({ allowUnderscore: true }); ``` ### Hostname validation rules The parser validates hostnames according to RFC 1123: * Labels are separated by dots (`.`) * Each label must be 1-63 characters long * Labels can contain alphanumeric characters and hyphens (`-`) * Labels cannot start or end with a hyphen * Total hostname length must not exceed 253 characters (by default) For example: * Valid: `"example.com"`, `"sub.example.com"`, `"server-01.local"` * Invalid: `"-example.com"` (starts with hyphen), `"example..com"` (empty label), `"a".repeat(64) + ".com"` (label too long) ### Wildcard hostnames By default, wildcard hostnames (starting with `*.`) are rejected. Enable them with the `allowWildcard` option: ```typescript twoslash import { hostname } from "@optique/core/valueparser"; import { option } from "@optique/core"; // ---cut-before--- // Allow wildcards for SSL certificate domains const certDomain = option("--domain", hostname({ allowWildcard: true })); ``` ```bash $ example --domain "*.example.com" # Valid $ example --domain "*.*.example.com" # Invalid: multiple wildcards not allowed ``` ### Underscores in hostnames While technically invalid per RFC 1123, underscores are commonly used in some contexts like service discovery records. Enable them with `allowUnderscore`: ```typescript twoslash import { hostname } from "@optique/core/valueparser"; import { option } from "@optique/core"; // ---cut-before--- // Allow underscores for service discovery const service = option("--srv", hostname({ allowUnderscore: true })); ``` ```bash $ example --srv "_http._tcp.example.com" # Valid with allowUnderscore $ example --srv "_http._tcp.example.com" # Invalid by default ``` ### Localhost filtering The parser accepts `"localhost"` by default. Reject it with `allowLocalhost: false`: ```typescript twoslash import { hostname } from "@optique/core/valueparser"; import { option } from "@optique/core"; // ---cut-before--- // Require remote hosts only const remoteHost = option("--remote", hostname({ allowLocalhost: false })); ``` ```bash $ example --remote "localhost" # Error: Hostname 'localhost' is not allowed. $ example --remote "example.com" # Valid ``` ### Length constraints The default maximum hostname length is 253 characters (the RFC 1123 limit). Customize it with the `maxLength` option: ```typescript twoslash import { hostname } from "@optique/core/valueparser"; import { option } from "@optique/core"; // ---cut-before--- // Limit hostname length to 50 characters const shortHost = option("--host", hostname({ maxLength: 50 })); ``` ### Custom error messages Customize error messages for various validation failures: ```typescript twoslash import { hostname } from "@optique/core/valueparser"; import { message, text } from "@optique/core/message"; import { option } from "@optique/core"; // ---cut-before--- const host = option("--host", hostname({ allowWildcard: false, allowLocalhost: false, errors: { invalidHostname: (input) => message`Not a valid hostname: ${input}`, wildcardNotAllowed: message`Wildcards are forbidden`, localhostNotAllowed: message`Remote hosts only`, tooLong: (hostname, max) => message`Hostname too long (max ${text(max.toString())} chars)`, }, })); ``` ### Common use cases **Web server host configuration:** ```typescript twoslash import { hostname } from "@optique/core/valueparser"; // ---cut-before--- // Allow localhost for local development const serverHost = hostname({ allowLocalhost: true }); ``` **Remote database connections:** ```typescript twoslash import { hostname } from "@optique/core/valueparser"; // ---cut-before--- // Require remote hosts only const dbHost = hostname({ allowLocalhost: false }); ``` **SSL certificate domain validation:** ```typescript twoslash import { hostname } from "@optique/core/valueparser"; // ---cut-before--- // Allow wildcards for certificate domains const certDomain = hostname({ allowWildcard: true }); ``` The parser uses `"HOST"` as its default metavar and returns the hostname as-is, preserving the original case. ## `email()` parser The `email()` parser validates email addresses according to simplified RFC 5322 addr-spec format. It supports display names, multiple addresses, domain filtering, and quoted local parts for practical email validation use cases. ```typescript twoslash import { email } from "@optique/core/valueparser"; // Basic email parser const userEmail = email(); // Multiple comma-separated addresses const recipients = email({ allowMultiple: true }); // Allow display names const from = email({ allowDisplayName: true }); // Restrict to specific domains const workEmail = email({ allowedDomains: ["company.com"] }); ``` ### Email validation rules The parser validates email addresses using simplified RFC 5322 rules: * Format: `local-part@domain` * Local part: alphanumeric characters, dots (`.`), hyphens (`-`), underscores (`_`), and plus signs (`+`) * Domain part: valid hostname with at least one dot * Quoted local parts (e.g., `"user name"@example.com`) allow spaces and special characters For example: * Valid: `"user@example.com"`, `"first.last@example.com"`, `"user+tag@mail.example.com"` * Invalid: `"userexample.com"` (no @ sign), `"user@example"` (no dot in domain), `"user..name@example.com"` (consecutive dots) ### Display name support By default, display names are rejected. Enable them with the `allowDisplayName` option: ```typescript twoslash import { email } from "@optique/core/valueparser"; import { option } from "@optique/core"; // ---cut-before--- // Allow "Name " format const from = option("--from", email({ allowDisplayName: true })); ``` ```bash $ example --from "John Doe " # Valid $ example --from "john@example.com" # Also valid ``` The parser extracts the email address and discards the display name. Both formats (`"Name "` and `Name `) are accepted. ### Multiple email addresses Parse comma-separated email lists with `allowMultiple`: ```typescript twoslash import { email } from "@optique/core/valueparser"; import { option } from "@optique/core"; // ---cut-before--- // Accept multiple comma-separated emails const to = option("--to", email({ allowMultiple: true })); ``` ```bash $ example --to "alice@example.com,bob@example.com" # Valid $ example --to "alice@example.com, bob@example.com" # Whitespace trimmed ``` When `allowMultiple` is `true`, the parser returns a `readonly string[]` instead of a single `string`. ### Domain filtering Restrict accepted email addresses to specific domains with `allowedDomains`: ```typescript twoslash import { email } from "@optique/core/valueparser"; import { option } from "@optique/core"; // ---cut-before--- // Only accept company email addresses const workEmail = option( "--email", email({ allowedDomains: ["company.com", "company.org"] }) ); ``` ```bash $ example --email "user@company.com" # Valid $ example --email "user@gmail.com" # Error: domain not allowed ``` Domain matching is case-insensitive. ### Lowercase conversion Normalize the domain part of email addresses to lowercase with the `lowercase` option. The local part is preserved as-is, since it is technically case-sensitive per RFC 5321. ```typescript twoslash import { email } from "@optique/core/valueparser"; import { option } from "@optique/core"; // ---cut-before--- // Normalize email domain to lowercase const normalizedEmail = option("--email", email({ lowercase: true })); ``` ```bash $ example --email "User@Example.COM" # Returns: "User@example.com" ``` ### Quoted local parts The parser supports quoted strings in local parts for special characters: ```typescript twoslash import { email } from "@optique/core/valueparser"; // ---cut-before--- const parser = email(); const result = parser.parse('"user name"@example.com'); // result.value = '"user name"@example.com' ``` Quoted strings allow spaces and special characters that are normally forbidden in local parts, such as `@` signs inside the quotes. ### Custom error messages Customize error messages for validation failures: ```typescript twoslash import { email } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; import { option } from "@optique/core"; // ---cut-before--- const workEmail = option("--email", email({ allowedDomains: ["company.com"], errors: { invalidEmail: (input) => message`Invalid email format: ${input}`, domainNotAllowed: (email, domains) => message`Email ${email} must use company domain`, }, })); ``` ### Common use cases **User registration email:** ```typescript twoslash import { email } from "@optique/core/valueparser"; // ---cut-before--- // Normalize email for consistent storage const userEmail = email({ lowercase: true }); ``` **Notification recipients:** ```typescript twoslash import { email } from "@optique/core/valueparser"; // ---cut-before--- // Multiple recipients with display names const recipients = email({ allowMultiple: true, allowDisplayName: true }); ``` **Corporate email restriction:** ```typescript twoslash import { email } from "@optique/core/valueparser"; // ---cut-before--- // Only accept company domain emails const corporateEmail = email({ allowedDomains: ["company.com", "company.org"], lowercase: true, }); ``` The parser uses `"EMAIL"` as its default metavar. ## `socketAddress()` parser The `socketAddress()` parser validates socket addresses in “host:port” format. It supports hostnames, IPv4 addresses, and IPv6 literals, configurable separators, default ports, and comprehensive host/port validation options. The parser returns a `SocketAddressValue` object containing both the host and port components. ```typescript twoslash import { socketAddress } from "@optique/core/valueparser"; // Basic socket address (requires port) const endpoint = socketAddress({ requirePort: true }); // With default port const server = socketAddress({ defaultPort: 80 }); // IP addresses only const bind = socketAddress({ defaultPort: 8080, host: { type: "ip", version: "both" }, }); // Non-privileged ports only const listen = socketAddress({ defaultPort: 8080, port: { min: 1024 }, }); ``` ### Socket address format The parser accepts addresses in the following format: * With port: `host:port` (e.g., `"localhost:3000"`, `"192.168.1.1:80"`) * With an IPv6 literal and port: `[host]:port` (e.g., `"[::1]:8080"`, `"[2001:db8::1]:443"`) * Without port: `host` (only when `defaultPort` is set, e.g., `"example.com"`) * Without port for IPv6: `host` or `[host]` (only when `defaultPort` is set, e.g., `"::1"` or `"[::1]"`) When the separator is the default `":"`, IPv6 host-and-port values use bracket notation so the parser can distinguish colons inside the address from the port separator. Bare IPv6 literals such as `"::1"` and `"2001:db8::1"` are accepted only when `defaultPort` supplies the port. Unbracketed forms such as `"::1:8080"` are treated as bare IPv6 hosts, not as host-and-port pairs; use `"[::1]:8080"` when the port is explicit. The separator between host and port can be customized using the `separator` option. Custom separators keep the ordinary split behavior; bracket notation is intended for the default `":"` separator. ### Port requirements By default, the port is optional if `defaultPort` is specified. Control this behavior with the `requirePort` option: ```typescript twoslash import { socketAddress } from "@optique/core/valueparser"; import { option } from "@optique/core"; // ---cut-before--- // Port always required const endpoint = option( "--endpoint", socketAddress({ requirePort: true }) ); ``` ```bash $ example --endpoint "localhost:3000" # Valid $ example --endpoint "localhost" # Error: port required ``` When `requirePort` is `false` (default) and `defaultPort` is set, the port may be omitted: ```typescript twoslash import { socketAddress } from "@optique/core/valueparser"; import { option } from "@optique/core"; // ---cut-before--- // Port optional, defaults to 80 const server = option( "--server", socketAddress({ defaultPort: 80 }) ); ``` ```bash $ example --server "example.com:443" # Uses port 443 $ example --server "example.com" # Uses default port 80 ``` ### Host type filtering The `host.type` option controls what types of hosts are accepted: * `"hostname"`: Accept only valid hostnames * `"ip"`: Accept only IP addresses * `"both"`: Accept both hostnames and IP addresses (default) For IP hosts, `host.version` controls which IP versions are accepted: * `4`: Accept IPv4 only * `6`: Accept IPv6 only * `"both"`: Accept IPv4 and IPv6 (default) For compatibility with earlier versions, configurations that use only the legacy `host.ip` field keep the previous IPv4-only behavior unless `host.version` or the new `host.ipv4`/`host.ipv6` fields are also set. ```typescript twoslash import { socketAddress } from "@optique/core/valueparser"; import { option } from "@optique/core"; // ---cut-before--- // Only accept IP addresses for binding const bind = option( "--bind", socketAddress({ defaultPort: 8080, host: { type: "ip", version: "both" }, }) ); ``` ```bash $ example --bind "0.0.0.0:8080" # Valid IPv4 $ example --bind "[::1]:8080" # Valid IPv6 $ example --bind "localhost:8080" # Error: hostname not allowed ``` ### Host validation options Pass options to the underlying hostname or IP parser using `host.hostname`, `host.ipv4`, or `host.ipv6`: ```typescript twoslash import { socketAddress } from "@optique/core/valueparser"; import { option } from "@optique/core"; // ---cut-before--- // Remote hosts only (no localhost) const remote = option( "--remote", socketAddress({ defaultPort: 80, host: { type: "hostname", hostname: { allowLocalhost: false }, }, }) ); ``` ```bash $ example --remote "localhost:80" # Error: localhost not allowed $ example --remote "example.com:80" # Valid ``` For IPv4 addresses, use `host.ipv4` to pass options like `allowPrivate`. The older `host.ip` field is still accepted as a compatibility alias for IPv4 options: ```typescript twoslash import { socketAddress } from "@optique/core/valueparser"; import { option } from "@optique/core"; // ---cut-before--- // Public IPs only const publicServer = option( "--server", socketAddress({ defaultPort: 443, host: { type: "ip", version: 4, ipv4: { allowPrivate: false }, }, }) ); ``` For IPv6 addresses, use `host.ipv6`: ```typescript twoslash import { socketAddress } from "@optique/core/valueparser"; import { option } from "@optique/core"; // ---cut-before--- // IPv6 addresses only, excluding loopback const publicV6 = option( "--listen-v6", socketAddress({ defaultPort: 443, host: { type: "ip", version: 6, ipv6: { allowLoopback: false }, }, }) ); ``` ### Port validation options Pass port validation options using the `port` field: ```typescript twoslash import { socketAddress } from "@optique/core/valueparser"; import { option } from "@optique/core"; // ---cut-before--- // Non-privileged ports only const listen = option( "--listen", socketAddress({ defaultPort: 8080, port: { min: 1024, max: 65535 }, }) ); ``` ```bash $ example --listen "localhost:80" # Error: port too low $ example --listen "localhost:8080" # Valid ``` Disallow well-known ports (1-1023): ```typescript twoslash import { socketAddress } from "@optique/core/valueparser"; import { option } from "@optique/core"; // ---cut-before--- const server = option( "--server", socketAddress({ defaultPort: 8080, port: { disallowWellKnown: true }, }) ); ``` ### Custom separator Change the separator between host and port: ```typescript twoslash import { socketAddress } from "@optique/core/valueparser"; import { option } from "@optique/core"; // ---cut-before--- // Use space as separator const proxy = option( "--proxy", socketAddress({ separator: " ", defaultPort: 8080 }) ); ``` ```bash $ example --proxy "localhost 3000" # Valid $ example --proxy "localhost:3000" # Invalid: wrong separator ``` The parser tries splitting at the separator to find a valid host+port pair before falling back to the host-only interpretation. This ensures that `parse(format(value))` always recovers the original value. If no split produces both a valid host and a valid port because the suffix is non-numeric (e.g., the separator appears inside the hostname), the entire input is treated as a hostname with the default port. If the suffix is numeric but not a valid port (e.g., out of range), parsing reports an invalid format error instead of falling back to host-only. For example, `"toronto"` with `separator: "to"` has no valid split and is correctly treated as a hostname. ### Return value The parser returns a `SocketAddressValue` object: ```typescript twoslash import { socketAddress } from "@optique/core/valueparser"; // ---cut-before--- const parser = socketAddress({ defaultPort: 80 }); const result = parser.parse("example.com:443"); if (result.success) { console.log(result.value.host); // "example.com" console.log(result.value.port); // 443 } ``` Parsed IPv6 hosts are normalized with the same canonicalization behavior as `ipv6()`. When formatting, the default `":"` separator emits bracket notation for IPv6 hosts: ```typescript twoslash import { socketAddress } from "@optique/core/valueparser"; // ---cut-before--- const parser = socketAddress(); parser.format({ host: "::1", port: 8080 }); // "[::1]:8080" parser.format({ host: "2001:0db8:0:0:0:0:0:1", port: 443, }); // "[2001:db8::1]:443" ``` ### Custom error messages Customize error messages for validation failures: ```typescript twoslash import { socketAddress } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; import { option } from "@optique/core"; // ---cut-before--- const endpoint = option( "--endpoint", socketAddress({ requirePort: true, errors: { invalidFormat: (input) => message`Invalid endpoint: ${input}`, missingPort: message`You must specify a port number`, }, }) ); ``` ### Common use cases **Web server configuration:** ```typescript twoslash import { socketAddress } from "@optique/core/valueparser"; // ---cut-before--- // Allow any host, default to port 8080 const listen = socketAddress({ defaultPort: 8080 }); ``` **Database connections:** ```typescript twoslash import { socketAddress } from "@optique/core/valueparser"; // ---cut-before--- // Require explicit host and port const dbServer = socketAddress({ requirePort: true }); ``` **Proxy configuration:** ```typescript twoslash import { socketAddress } from "@optique/core/valueparser"; // ---cut-before--- // Accept hostnames and IPs, default port 3128 const proxy = socketAddress({ defaultPort: 3128, host: { type: "both" }, }); ``` **Service binding (IP addresses only):** ```typescript twoslash import { socketAddress } from "@optique/core/valueparser"; // ---cut-before--- // Bind to IP addresses only, non-privileged ports const bind = socketAddress({ defaultPort: 8080, host: { type: "ip", version: "both" }, port: { min: 1024 }, }); ``` The parser uses `"HOST:PORT"` as its default metavar. ## `portRange()` parser The `portRange()` parser validates port ranges in the format `start-end` (e.g., `8000-8080`). It supports both number and bigint types, custom separators, and comprehensive validation options including min/max constraints and well-known port filtering. ```typescript twoslash import { portRange } from "@optique/core/valueparser"; // Basic port range (number type) const range = portRange(); // Custom separator const colonRange = portRange({ separator: ":" }); // Allow single ports (returns {start: 8080, end: 8080}) const flexibleRange = portRange({ allowSingle: true }); // Restrict to non-privileged ports const unprivilegedRange = portRange({ min: 1024 }); // BigInt type for consistency with other APIs const bigintRange = portRange({ type: "bigint" }); ``` ### Port range format The parser accepts port ranges in `start-end` format where both start and end must be valid port numbers (1-65535): ```typescript twoslash import { portRange } from "@optique/core/valueparser"; const parser = portRange(); // ---cut-before--- parser.parse("8000-8080"); // { start: 8000, end: 8080 } parser.parse("80-443"); // { start: 80, end: 443 } parser.parse("1-65535"); // { start: 1, end: 65535 } ``` The start port must be less than or equal to the end port: ```typescript twoslash import { portRange } from "@optique/core/valueparser"; const parser = portRange(); // ---cut-before--- parser.parse("8080-8000"); // Error: start port must not be greater than end port ``` ### Single port mode When `allowSingle` is enabled, the parser accepts single port numbers and returns a range where start equals end: ```typescript twoslash import { portRange } from "@optique/core/valueparser"; const parser = portRange({ allowSingle: true }); parser.parse("8080"); // { start: 8080, end: 8080 } parser.parse("80-443"); // { start: 80, end: 443 } ``` Without `allowSingle`, single ports are rejected: ```typescript twoslash import { portRange } from "@optique/core/valueparser"; const parser = portRange(); parser.parse("8080"); // Error: must be in the format PORT-PORT ``` ### Custom separator The default separator is `"-"`, but you can specify any string: ```typescript twoslash import { portRange } from "@optique/core/valueparser"; // Colon separator const colonRange = portRange({ separator: ":" }); colonRange.parse("8000:8080"); // { start: 8000, end: 8080 } // Multi-character separator const toRange = portRange({ separator: " to " }); toRange.parse("8000 to 8080"); // { start: 8000, end: 8080 } ``` The separator is also used in the metavar and error messages: ```typescript twoslash import { portRange } from "@optique/core/valueparser"; portRange({ separator: ":" }).metavar; // "PORT:PORT" portRange({ separator: " to " }).metavar; // "PORT to PORT" ``` ### Min/max constraints Restrict the allowed port range using `min` and `max` options. These constraints apply to both the start and end ports: ```typescript twoslash import { portRange } from "@optique/core/valueparser"; // Only non-privileged ports const unprivileged = portRange({ min: 1024 }); unprivileged.parse("1024-8080"); // { start: 1024, end: 8080 } unprivileged.parse("80-443"); // Error: must be at least 1024 // Limit to ephemeral port range const ephemeral = portRange({ min: 49152, max: 65535 }); ephemeral.parse("50000-51000"); // { start: 50000, end: 51000 } ephemeral.parse("8000-8080"); // Error: must be at least 49152 ``` ### Well-known port filtering The `disallowWellKnown` option rejects well-known ports (1-1023) in both the start and end positions: ```typescript twoslash import { portRange } from "@optique/core/valueparser"; const parser = portRange({ disallowWellKnown: true }); parser.parse("1024-8080"); // { start: 1024, end: 8080 } parser.parse("80-443"); // Error: must not be a well-known port (1-1023) parser.parse("1024-1023"); // Error: must not be a well-known port (1-1023) ``` ### Number vs bigint types The parser supports both `number` (default) and `bigint` types: ```typescript twoslash import { portRange } from "@optique/core/valueparser"; // Number type (default) const numRange = portRange(); numRange.parse("8000-8080"); // { start: 8000, end: 8080 } // BigInt type const bigintRange = portRange({ type: "bigint" }); bigintRange.parse("8000-8080"); // { start: 8000n, end: 8080n } ``` The bigint type is useful for consistency when working with APIs that use bigint for port numbers. ### Return value The parser returns a `PortRangeValue` object with `start` and `end` properties: ```typescript twoslash import { portRange } from "@optique/core/valueparser"; interface PortRangeValueNumber { readonly start: number; readonly end: number; } interface PortRangeValueBigInt { readonly start: bigint; readonly end: bigint; } ``` The type is inferred based on the `type` option: ```typescript twoslash import { portRange } from "@optique/core/valueparser"; const numRange = portRange(); const result1 = numRange.parse("8000-8080"); if (result1.success) { result1.value.start; // type: number } const bigintRange = portRange({ type: "bigint" }); const result2 = bigintRange.parse("8000-8080"); if (result2.success) { result2.value.start; // type: bigint } ``` ### Custom error messages All error messages can be customized via the `errors` option: ```typescript twoslash import { portRange } from "@optique/core/valueparser"; import { message, text } from "@optique/core/message"; import { option } from "@optique/core"; // ---cut-before--- const portOpt = option("--ports", portRange({ errors: { invalidFormat: message`Port range must be START-END`, invalidRange: message`START must be ≤ END`, invalidPort: message`Ports must be 1-65535`, belowMinimum: (port, min) => message`Port ${text(port.toString())} is below minimum ${text(min.toString())}`, aboveMaximum: (port, max) => message`Port ${text(port.toString())} is above maximum ${text(max.toString())}`, wellKnownNotAllowed: message`System ports (1-1023) are not allowed`, }, })); ``` The `invalidFormat` and `invalidRange` errors are specific to port ranges, while other errors (`invalidPort`, `belowMinimum`, `aboveMaximum`, `wellKnownNotAllowed`) are inherited from the underlying `port()` parser. ### Common use cases **Dynamic server port pools:** ```typescript twoslash import { portRange } from "@optique/core/valueparser"; // ---cut-before--- // Accept ranges or single ports for service binding const ports = portRange({ allowSingle: true, min: 1024, metavar: "PORTS", }); ``` **Load balancer configuration:** ```typescript twoslash import { portRange } from "@optique/core/valueparser"; // ---cut-before--- // Backend server port range const backendPorts = portRange({ min: 8000, max: 9000, }); ``` **Firewall rules:** ```typescript twoslash import { portRange } from "@optique/core/valueparser"; // ---cut-before--- // Allow specifying port ranges for firewall configuration const allowedPorts = portRange({ allowSingle: true, disallowWellKnown: true, }); ``` **Container port mapping:** ```typescript twoslash import { portRange } from "@optique/core/valueparser"; // ---cut-before--- // Map container port ranges (colon separator for Docker-style syntax) const portMapping = portRange({ separator: ":", min: 1024, }); ``` The parser uses `"PORT-PORT"` as its default metavar (or `"PORT{separator}PORT"` when a custom separator is specified). ## `macAddress()` parser The `macAddress()` parser validates MAC (Media Access Control) addresses, commonly used to identify network interface hardware. It accepts MAC-48 addresses (6 octets, 12 hexadecimal digits) in multiple common formats with support for case conversion and output normalization. ### Supported formats The parser accepts MAC addresses in four standard formats: ```typescript twoslash import { macAddress } from "@optique/core/valueparser"; const parser = macAddress(); const result1 = parser.parse("00:1A:2B:3C:4D:5E"); // Colon-separated const result2 = parser.parse("00-1A-2B-3C-4D-5E"); // Hyphen-separated const result3 = parser.parse("001A.2B3C.4D5E"); // Cisco format (dot-separated) const result4 = parser.parse("001A2B3C4D5E"); // No separator ``` Colon-separated and hyphen-separated formats also accept single-digit octets (1 hex digit), which are automatically zero-padded to canonical two-digit form: ```typescript twoslash import { macAddress } from "@optique/core/valueparser"; const parser = macAddress(); const result = parser.parse("0:1:2:3:4:5"); if (result.success) { result.value; // "00:01:02:03:04:05" } ``` > \[!NOTE] > Dot-separated (Cisco) and no-separator formats require exactly the original > fixed widths (4 hex digits per group and 12 hex digits total, respectively) > and do not accept short-form input. By default, the parser accepts any of these formats. You can restrict it to a specific format using the `separator` option: ```typescript twoslash import { macAddress } from "@optique/core/valueparser"; // ---cut-before--- // Accept only colon-separated format const colonOnly = macAddress({ separator: ":" }); const result = colonOnly.parse("00:1A:2B:3C:4D:5E"); if (result.success) { result.value; // "00:1A:2B:3C:4D:5E" } // Rejects other formats const invalid = colonOnly.parse("00-1A-2B-3C-4D-5E"); if (!invalid.success) { invalid.error; // "Invalid MAC address." } ``` The `separator` option accepts: * `":"` - Colon-separated format (e.g., `00:1A:2B:3C:4D:5E`) * `"-"` - Hyphen-separated format (e.g., `00-1A-2B-3C-4D-5E`) * `"."` - Cisco dot notation (e.g., `001A.2B3C.4D5E`) * `"none"` - No separator (e.g., `001A2B3C4D5E`) * `"any"` - Accept any format (default) ### Case conversion The parser provides case conversion options for hexadecimal digits: ```typescript twoslash import { macAddress } from "@optique/core/valueparser"; // ---cut-before--- // Preserve original case (default) const preserveCase = macAddress({ case: "preserve" }); const result1 = preserveCase.parse("00:1a:2B:3c:4D:5e"); if (result1.success) { result1.value; // "00:1a:2B:3c:4D:5e" } // Convert to uppercase const upperCase = macAddress({ case: "upper" }); const result2 = upperCase.parse("00:1a:2b:3c:4d:5e"); if (result2.success) { result2.value; // "00:1A:2B:3C:4D:5E" } // Convert to lowercase const lowerCase = macAddress({ case: "lower" }); const result3 = lowerCase.parse("00:1A:2B:3C:4D:5E"); if (result3.success) { result3.value; // "00:1a:2b:3c:4d:5e" } ``` ### Output normalization The `outputSeparator` option normalizes the output format regardless of the input format: ```typescript twoslash import { macAddress } from "@optique/core/valueparser"; // ---cut-before--- // Normalize all inputs to colon-separated const normalize = macAddress({ outputSeparator: ":", case: "upper", }); const result1 = normalize.parse("00-1a-2b-3c-4d-5e"); if (result1.success) { result1.value; // "00:1A:2B:3C:4D:5E" } const result2 = normalize.parse("001a.2b3c.4d5e"); if (result2.success) { result2.value; // "00:1A:2B:3C:4D:5E" } ``` Single-digit octets are also zero-padded during output normalization, ensuring canonical MAC-48 strings and correct round-tripping: ```typescript twoslash import { macAddress } from "@optique/core/valueparser"; // ---cut-before--- const dotNormalize = macAddress({ outputSeparator: ".", case: "upper" }); const result3 = dotNormalize.parse("0:1:2:3:4:5"); if (result3.success) { result3.value; // "0001.0203.0405" } ``` The `outputSeparator` option accepts the same values as `separator` except `"any"`. When not specified, the output preserves the input format. ### Return value The parser returns a formatted string according to the `case` and `outputSeparator` options: ```typescript twoslash import { macAddress } from "@optique/core/valueparser"; // ---cut-before--- const parser = macAddress({ outputSeparator: ":", case: "upper", }); const result = parser.parse("00-1a-2b-3c-4d-5e"); if (result.success) { const mac: string = result.value; // "00:1A:2B:3C:4D:5E" } ``` ### Custom error messages You can customize error messages using the `errors` option: ```typescript twoslash import { macAddress } from "@optique/core/valueparser"; import { message, text } from "@optique/core/message"; // ---cut-before--- const parser = macAddress({ errors: { invalidMacAddress: (input) => message`Invalid MAC address: ${text(input)}. Expected format: XX:XX:XX:XX:XX:XX`, }, }); const result = parser.parse("not-a-mac"); if (!result.success) { result.error; // "Invalid MAC address: not-a-mac. Expected format: XX:XX:XX:XX:XX:XX" } ``` ### Common use cases **Network device configuration:** ```typescript twoslash import { macAddress } from "@optique/core/valueparser"; // ---cut-before--- // Standardize MAC addresses to uppercase colon format const deviceMac = macAddress({ outputSeparator: ":", case: "upper", }); ``` **Cisco router configuration:** ```typescript twoslash import { macAddress } from "@optique/core/valueparser"; // ---cut-before--- // Accept only Cisco dot notation format const ciscoMac = macAddress({ separator: ".", case: "lower", }); ``` **Access control lists:** ```typescript twoslash import { macAddress } from "@optique/core/valueparser"; // ---cut-before--- // Accept any format but normalize for storage const aclMac = macAddress({ outputSeparator: "none", case: "upper", }); ``` **Network monitoring tools:** ```typescript twoslash import { macAddress } from "@optique/core/valueparser"; // ---cut-before--- // Accept any format for user convenience const monitorMac = macAddress(); ``` The parser uses `"MAC"` as its default metavar. ## `domain()` parser The `domain()` parser validates domain names according to RFC 1035 with configurable options for subdomain filtering, TLD restrictions, minimum label requirements, and case normalization. ### Basic validation The parser validates domain names with the following rules: * Each label (part separated by dots) must be 1-63 characters * Labels can contain alphanumeric characters and hyphens * Labels cannot start or end with a hyphen * By default, requires at least 2 labels (e.g., `example.com`) ```typescript twoslash import { domain } from "@optique/core/valueparser"; const parser = domain(); const result1 = parser.parse("example.com"); if (result1.success) { result1.value; // "example.com" } const result2 = parser.parse("www.example.com"); if (result2.success) { result2.value; // "www.example.com" } const result3 = parser.parse("api.staging.example.com"); if (result3.success) { result3.value; // "api.staging.example.com" } ``` ### Subdomain filtering Use the `allowSubdomains` option to restrict to root domains only: ```typescript twoslash import { domain } from "@optique/core/valueparser"; // ---cut-before--- // Accept only root domains (2 labels) const rootOnly = domain({ allowSubdomains: false }); const result1 = rootOnly.parse("example.com"); if (result1.success) { result1.value; // "example.com" } // Rejects subdomains const result2 = rootOnly.parse("www.example.com"); if (!result2.success) { result2.error; // "Subdomains are not allowed, but got www.example.com." } ``` ### TLD restrictions Use the `allowedTlds` option to restrict accepted top-level domains: ```typescript twoslash import { domain } from "@optique/core/valueparser"; // ---cut-before--- // Accept only specific TLDs const restrictedTLD = domain({ allowedTlds: ["com", "org", "net"] }); const result1 = restrictedTLD.parse("example.com"); if (result1.success) { result1.value; // "example.com" } // Case-insensitive TLD matching const result2 = restrictedTLD.parse("example.COM"); if (result2.success) { result2.value; // "example.COM" } // Rejects disallowed TLDs const result3 = restrictedTLD.parse("example.io"); if (!result3.success) { result3.error; // "Top-level domain io is not allowed. Allowed TLDs: com, org, net." } ``` ### Minimum labels Use the `minLabels` option to require a specific number of labels: ```typescript twoslash import { domain } from "@optique/core/valueparser"; // ---cut-before--- // Require at least 3 labels const threeLabels = domain({ minLabels: 3 }); const result1 = threeLabels.parse("www.example.com"); if (result1.success) { result1.value; // "www.example.com" } const result2 = threeLabels.parse("example.com"); if (!result2.success) { result2.error; // "Domain example.com must have at least 3 labels." } // Allow single-label domains (e.g., "localhost") const singleLabel = domain({ minLabels: 1 }); const result3 = singleLabel.parse("localhost"); if (result3.success) { result3.value; // "localhost" } ``` ### Case normalization Use the `lowercase` option to normalize domain names to lowercase: ```typescript twoslash import { domain } from "@optique/core/valueparser"; // ---cut-before--- // Preserve original case (default) const preserveCase = domain(); const result1 = preserveCase.parse("Example.COM"); if (result1.success) { result1.value; // "Example.COM" } // Convert to lowercase const lowerCase = domain({ lowercase: true }); const result2 = lowerCase.parse("Example.COM"); if (result2.success) { result2.value; // "example.com" } ``` ### Return value The parser returns a string representing the domain name, optionally normalized to lowercase: ```typescript twoslash import { domain } from "@optique/core/valueparser"; // ---cut-before--- const parser = domain({ lowercase: true }); const result = parser.parse("WWW.Example.COM"); if (result.success) { const domainName: string = result.value; // "www.example.com" } ``` ### Custom error messages You can customize error messages using the `errors` option: ```typescript twoslash import { domain } from "@optique/core/valueparser"; import { message, text } from "@optique/core/message"; // ---cut-before--- const parser = domain({ allowSubdomains: false, allowedTlds: ["com", "org"], errors: { invalidDomain: (input) => message`Invalid domain: ${text(input)}`, subdomainsNotAllowed: (domain) => message`Only root domains allowed, got: ${text(domain)}`, tldNotAllowed: (tld, allowed) => message`TLD ${text(tld)} not in: ${text(allowed.join(", "))}`, tooFewLabels: (domain, min) => message`${text(domain)} needs ${text(min.toString())} labels`, }, }); const result = parser.parse("example..com"); if (!result.success) { result.error; // Custom error message } ``` ### Common use cases **Website configuration:** ```typescript twoslash import { domain } from "@optique/core/valueparser"; // ---cut-before--- // Accept any valid domain, normalize to lowercase const websiteDomain = domain({ lowercase: true }); ``` **Email domain validation:** ```typescript twoslash import { domain } from "@optique/core/valueparser"; // ---cut-before--- // Restrict to specific TLDs for corporate email const emailDomain = domain({ allowedTlds: ["com", "org", "edu"], lowercase: true, }); ``` **DNS configuration:** ```typescript twoslash import { domain } from "@optique/core/valueparser"; // ---cut-before--- // Root domains only for DNS zone setup const zoneDomain = domain({ allowSubdomains: false, lowercase: true, }); ``` **API endpoint configuration:** ```typescript twoslash import { domain } from "@optique/core/valueparser"; // ---cut-before--- // Any valid domain with minimum 2 labels const apiDomain = domain({ minLabels: 2, lowercase: true, }); ``` The parser uses `"DOMAIN"` as its default metavar. ## `ipv6()` parser The `ipv6()` parser validates and normalizes IPv6 addresses to canonical form (lowercase, compressed using `::` notation where appropriate). It supports full addresses, compressed notation, and IPv4-mapped IPv6 addresses, with configurable address type restrictions. ### Basic validation The parser validates IPv6 addresses in various formats: ```typescript twoslash import { ipv6 } from "@optique/core/valueparser"; const parser = ipv6(); // Full format const result1 = parser.parse("2001:0db8:85a3:0000:0000:8a2e:0370:7334"); if (result1.success) { result1.value; // "2001:db8:85a3::8a2e:370:7334" (compressed) } // Compressed format const result2 = parser.parse("2001:db8::1"); if (result2.success) { result2.value; // "2001:db8::1" } // IPv4-mapped IPv6 const result3 = parser.parse("::ffff:192.0.2.1"); if (result3.success) { result3.value; // "::ffff:c000:201" } // Loopback const result4 = parser.parse("::1"); if (result4.success) { result4.value; // "::1" } ``` ### Address type filtering The parser provides options to restrict specific address types: ```typescript twoslash import { ipv6 } from "@optique/core/valueparser"; // ---cut-before--- // No loopback addresses const noLoopback = ipv6({ allowLoopback: false }); const result1 = noLoopback.parse("::1"); if (!result1.success) { result1.error; // "::1 is a loopback address." } // Global unicast only (no link-local, no unique local) const publicOnly = ipv6({ allowLinkLocal: false, allowUniqueLocal: false, }); const result2 = publicOnly.parse("fe80::1"); if (!result2.success) { result2.error; // "fe80::1 is a link-local address." } const result3 = publicOnly.parse("fc00::1"); if (!result3.success) { result3.error; // "fc00::1 is a unique local address." } ``` ### Address type categories The parser recognizes these IPv6 address types: * **Loopback** (`::1`): Local loopback address * **Link-local** (`fe80::/10`): Addresses for single network segment * **Unique local** (`fc00::/7`): Private addresses for local communication * **Multicast** (`ff00::/8`): Addresses for multicast groups * **Zero** (`::`): All-zeros address ### Normalization The parser automatically normalizes IPv6 addresses to canonical form: * Converts to lowercase * Removes leading zeros from each group * Compresses the longest sequence of consecutive zero groups with `::` ```typescript twoslash import { ipv6 } from "@optique/core/valueparser"; // ---cut-before--- const parser = ipv6(); // Uppercase converted to lowercase const result1 = parser.parse("2001:DB8:85A3::8A2E:370:7334"); if (result1.success) { result1.value; // "2001:db8:85a3::8a2e:370:7334" } // Leading zeros removed and compressed const result2 = parser.parse("2001:0db8:0000:0000:0000:0000:0000:0001"); if (result2.success) { result2.value; // "2001:db8::1" } ``` ### Return value The parser returns a string representing the normalized IPv6 address in canonical form (lowercase, compressed). The parser uses `"IPV6"` as its default metavar. ## `ip()` parser The `ip()` parser accepts both IPv4 and IPv6 addresses, delegating to the `ipv4()` and `ipv6()` parsers based on the detected format. It can be configured to accept only IPv4, only IPv6, or both (default). ### Basic validation By default, the parser accepts both IP versions: ```typescript twoslash import { ip } from "@optique/core/valueparser"; const parser = ip(); // IPv4 address const result1 = parser.parse("192.0.2.1"); if (result1.success) { result1.value; // "192.0.2.1" } // IPv6 address const result2 = parser.parse("2001:db8::1"); if (result2.success) { result2.value; // "2001:db8::1" } ``` ### Version filtering Use the `version` option to restrict to a specific IP version: ```typescript twoslash import { ip } from "@optique/core/valueparser"; // ---cut-before--- // IPv4 only const ipv4Only = ip({ version: 4 }); const result1 = ipv4Only.parse("192.0.2.1"); if (result1.success) { result1.value; // "192.0.2.1" } const result2 = ipv4Only.parse("2001:db8::1"); if (!result2.success) { result2.error; // "Expected a valid IP address, but got 2001:db8::1." } // IPv6 only const ipv6Only = ip({ version: 6 }); const result3 = ipv6Only.parse("2001:db8::1"); if (result3.success) { result3.value; // "2001:db8::1" } ``` ### Passing options to IPv4/IPv6 parsers The parser accepts separate options for IPv4 and IPv6 validation: ```typescript twoslash import { ip } from "@optique/core/valueparser"; // ---cut-before--- // Public IPs only (both versions) const publicOnly = ip({ ipv4: { allowPrivate: false, allowLoopback: false }, ipv6: { allowLinkLocal: false, allowUniqueLocal: false }, }); const result1 = publicOnly.parse("192.168.1.1"); if (!result1.success) { result1.error; // "192.168.1.1 is a private IP address." } const result2 = publicOnly.parse("fe80::1"); if (!result2.success) { result2.error; // "fe80::1 is a link-local address." } ``` ### Shared error messages The parser supports shared error messages that apply to both IP versions: ```typescript twoslash import { ip } from "@optique/core/valueparser"; import type { Message } from "@optique/core/message"; // ---cut-before--- const parser = ip({ errors: { loopbackNotAllowed: [ { type: "text", text: "Loopback addresses are not allowed" }, ] satisfies Message, }, ipv4: { allowLoopback: false }, ipv6: { allowLoopback: false }, }); const result1 = parser.parse("127.0.0.1"); if (!result1.success) { result1.error; // "Loopback addresses are not allowed" } const result2 = parser.parse("::1"); if (!result2.success) { result2.error; // "Loopback addresses are not allowed" } ``` ### Return value The parser returns a normalized IP address string. IPv4 addresses are returned as-is, while IPv6 addresses are normalized to canonical form (lowercase, compressed). When `version` is `"both"`, the parser tries IPv4 first, then IPv6 if IPv4 fails. This means IPv4-mapped IPv6 addresses like `::ffff:192.0.2.1` are parsed as IPv6. However, IPv4 restrictions (e.g., `allowPrivate: false`, `allowLoopback: false`) are still applied to the embedded IPv4 address in IPv4-mapped IPv6 addresses, so they cannot be used to bypass IPv4 policy. The parser uses `"IP"` as its default metavar. ## `cidr()` parser The `cidr()` parser validates CIDR notation (IP address with prefix length) and returns a structured object containing the normalized IP address, prefix length, and IP version. ### Basic validation The parser validates CIDR notation for both IPv4 and IPv6: ```typescript twoslash import { cidr } from "@optique/core/valueparser"; const parser = cidr(); // IPv4 CIDR const result1 = parser.parse("192.0.2.0/24"); if (result1.success) { result1.value.address; // "192.0.2.0" result1.value.prefix; // 24 result1.value.version; // 4 } // IPv6 CIDR const result2 = parser.parse("2001:db8::/32"); if (result2.success) { result2.value.address; // "2001:db8::" result2.value.prefix; // 32 result2.value.version; // 6 } ``` ### Prefix validation The parser validates prefix lengths based on IP version: * IPv4: 0-32 * IPv6: 0-128 ```typescript twoslash import { cidr } from "@optique/core/valueparser"; // ---cut-before--- const parser = cidr(); // Valid IPv4 prefix const result1 = parser.parse("192.0.2.0/32"); if (result1.success) { result1.value.prefix; // 32 } // Invalid IPv4 prefix (>32) const result2 = parser.parse("192.0.2.0/33"); if (!result2.success) { result2.error; // "Expected a prefix length between 0 and 32 for IPv4, but got 33." } // Valid IPv6 prefix const result3 = parser.parse("2001:db8::/128"); if (result3.success) { result3.value.prefix; // 128 } ``` ### Prefix constraints Use `minPrefix` and `maxPrefix` options to constrain the prefix length: ```typescript twoslash import { cidr } from "@optique/core/valueparser"; // ---cut-before--- // Subnet sizes between /16 and /24 const subnet = cidr({ version: 4, minPrefix: 16, maxPrefix: 24, }); const result1 = subnet.parse("192.0.2.0/20"); if (result1.success) { result1.value.prefix; // 20 } const result2 = subnet.parse("192.0.2.0/8"); if (!result2.success) { result2.error; // "Expected a prefix length greater than or equal to 16, but got 8." } const result3 = subnet.parse("192.0.2.0/28"); if (!result3.success) { result3.error; // "Expected a prefix length less than or equal to 24, but got 28." } ``` ### Version filtering Use the `version` option to restrict to IPv4 or IPv6 CIDR: ```typescript twoslash import { cidr } from "@optique/core/valueparser"; // ---cut-before--- // IPv4 CIDR only const ipv4Cidr = cidr({ version: 4 }); const result1 = ipv4Cidr.parse("192.0.2.0/24"); if (result1.success) { result1.value.version; // 4 } const result2 = ipv4Cidr.parse("2001:db8::/32"); if (!result2.success) { result2.error; // "Expected a valid CIDR notation, but got 2001:db8::/32." } ``` ### IP address validation The parser delegates IP address validation to `ipv4()` and `ipv6()` parsers, so you can pass IPv4 and IPv6 options: ```typescript twoslash import { cidr } from "@optique/core/valueparser"; // ---cut-before--- // Public IP ranges only const publicCidr = cidr({ ipv4: { allowPrivate: false }, ipv6: { allowLinkLocal: false, allowUniqueLocal: false }, }); const result1 = publicCidr.parse("192.168.0.0/24"); if (!result1.success) { result1.error; // "192.168.0.0 is a private IP address." } ``` ### Return value The parser returns a `CidrValue` object with three fields: * `address`: The normalized IP address (string) * `prefix`: The prefix length (number) * `version`: The IP version (4 or 6) ```typescript twoslash import { cidr } from "@optique/core/valueparser"; // ---cut-before--- const parser = cidr(); const result = parser.parse("192.0.2.0/24"); if (result.success) { const { address, prefix, version } = result.value; console.log(`${address}/${prefix} (IPv${version})`); // "192.0.2.0/24 (IPv4)" } ``` IPv6 addresses are normalized to canonical form (lowercase, compressed): ```typescript twoslash import { cidr } from "@optique/core/valueparser"; // ---cut-before--- const parser = cidr(); const result = parser.parse("2001:0DB8:0000:0000:0000:0000:0000:0000/32"); if (result.success) { result.value.address; // "2001:db8::" result.value.prefix; // 32 } ``` The parser uses `"CIDR"` as its default metavar. ## `path()` parser The `path()` parser validates file system paths with comprehensive options for existence checking, type validation, and file extension filtering. Unlike other built-in value parsers, `path()` is provided by the *@optique/run* package since it uses Node.js file system APIs. ```typescript twoslash import { path } from "@optique/run/valueparser"; // Basic path parser (any path, no validation) const configPath = path({ metavar: "CONFIG" }); // File must exist const inputFile = path({ metavar: "FILE", mustExist: true, type: "file" }); // Directory must exist const outputDir = path({ metavar: "DIR", mustExist: true, type: "directory" }); // Config files with specific extensions const configFile = path({ metavar: "CONFIG", mustExist: true, type: "file", extensions: [".json", ".yaml", ".yml"] }); ``` ### Path validation options The `path()` parser accepts comprehensive configuration options: ```typescript twoslash interface PathOptions { metavar?: string; // Custom metavar (default: "PATH") mustExist?: boolean; // Path must exist on filesystem (default: false) mustNotExist?: boolean; // Path must not exist (default: false) type?: "file" | "directory" | "either"; // Expected path type (default: "either") allowCreate?: boolean; // Allow creating new files (default: false) extensions?: string[]; // Allowed file extensions (e.g., [".json", ".txt"]) } ``` > \[!NOTE] > The `mustExist` and `mustNotExist` options are mutually exclusive. > You cannot set both to `true` at the same time—TypeScript will catch this > as a compile-time error. ### Existence validation When `mustExist: true`, the parser verifies that the path exists on the file system: ```typescript twoslash import { path } from "@optique/run/valueparser"; // Must exist (file or directory) const existingPath = path({ mustExist: true }); // Must be an existing file const existingFile = path({ mustExist: true, type: "file", metavar: "INPUT_FILE" }); // Must be an existing directory const existingDir = path({ mustExist: true, type: "directory", metavar: "OUTPUT_DIR" }); ``` ### Non-existence validation When `mustNotExist: true`, the parser rejects paths that already exist on the file system. This is useful for output files where you want to prevent accidental overwrites: ```typescript twoslash import { path } from "@optique/run/valueparser"; // Output file must not exist (prevent accidental overwrites) const outputFile = path({ mustNotExist: true, type: "file", metavar: "OUTPUT_FILE" }); // Output file with extension validation const reportFile = path({ mustNotExist: true, extensions: [".json", ".csv"], metavar: "REPORT" }); // Combine with allowCreate to also check parent directory const logFile = path({ mustNotExist: true, allowCreate: true, metavar: "LOG_FILE" }); ``` ### File creation validation With `allowCreate: true`, the parser validates that the parent directory exists for new files: ```typescript twoslash import { path } from "@optique/run/valueparser"; // Allow creating new files (parent directory must exist) const newFile = path({ allowCreate: true, type: "file", metavar: "LOG_FILE" }); // Combination: file can be new or existing const flexibleFile = path({ type: "file", allowCreate: true, extensions: [".log", ".txt"] }); ``` ### Extension filtering Restrict file paths to specific extensions using the `extensions` option: ```typescript twoslash import { path } from "@optique/run/valueparser"; // Configuration files only const configFile = path({ mustExist: true, type: "file", extensions: [".json", ".yaml", ".yml", ".toml"], metavar: "CONFIG_FILE" }); // Image files only const imageFile = path({ mustExist: true, type: "file", extensions: [".jpg", ".jpeg", ".png", ".gif", ".webp"], metavar: "IMAGE_FILE" }); // Script files (existing or new) const scriptFile = path({ allowCreate: true, type: "file", extensions: [".js", ".ts", ".py", ".sh"], metavar: "SCRIPT" }); ``` ### Error messages The `path()` parser provides specific error messages for different validation failures: ```bash $ myapp --config "nonexistent.json" Error: Path nonexistent.json does not exist. $ myapp --input "directory_not_file" Error: Expected a file, but directory_not_file is not a file. $ myapp --config "config.txt" Error: Expected file with extension .json, .yaml, .yml, got .txt. $ myapp --output "new_file/in/nonexistent/dir.txt" Error: Parent directory new_file/in/nonexistent does not exist. $ myapp --output "existing_file.txt" Error: Path existing_file.txt already exists. ``` ## Git integration See the [Git integration](../integrations/git.md) page for documentation on using Git reference parsers with Optique. ## Temporal integration See the [Temporal integration](../integrations/temporal.md) page for documentation on using Temporal API parsers with Optique. ## Zod integration See the [Zod integration](../integrations/zod.md) page for documentation on using Zod schemas with Optique. ## Valibot integration See the [Valibot integration](../integrations/valibot.md) page for documentation on using Valibot schemas with Optique. ## Creating custom value parsers When the built-in value parsers don't meet your needs, you can create custom value parsers by implementing the `ValueParser` interface. Custom parsers integrate seamlessly with Optique's type system and provide the same error handling and help text generation as built-in parsers. ### ValueParser interface The `ValueParser` interface defines four required properties plus a mode marker, and two optional methods: ```typescript twoslash import type { Mode, ModeValue, NonEmptyString, ValueParserResult } from "@optique/core/valueparser"; // ---cut-before--- interface ValueParser { readonly mode: M; readonly metavar: NonEmptyString; readonly placeholder: T; parse(input: string): ModeValue>; format(value: T): string; normalize?(value: T): T; validate?(value: T): ValueParserResult; } ``` `mode` : The parser's runtime execution mode. Use `"sync"` for value parsers that return values directly and `"async"` for value parsers that return `Promise`s. Unlike parser type markers such as `$valueType` and `$stateType`, this field is consumed at runtime. `metavar` : The placeholder text shown in help messages (usually uppercase). Must be a non-empty string—TypeScript will reject empty string literals at compile time, and factory functions will throw `TypeError` at runtime if given an empty string. `placeholder` : A type-appropriate stand-in value of type `T`. During phase-one parsing (e.g., while a prompt is deferred), this value keeps downstream combinators and `map()` transforms working with the expected shape. Before phase-two context collection, deferred values may still be replaced with `undefined`, so contexts must not rely on seeing the placeholder itself. It does not need to be meaningful—only a valid inhabitant of the result type that will not crash downstream transforms. `parse()` : Converts string input to typed value or returns error `format()` : Converts typed value back to string for display `normalize()` : Optional. Canonicalizes a value of type `T` according to the parser's configuration (e.g., case conversion, separator normalization). Built-in implementations delegate to `parse()` internally and return invalid values unchanged when parsing fails. When present, `withDefault()` calls this on default values so that runtime defaults match the representation that `parse()` would produce. ``` > [!NOTE] > For dependency-derived value parsers (`deriveFrom()`, > `dependency().derive()`), `normalize()` uses the default dependency > value to build the inner parser, not the dependency value resolved > during the current parse—the same trade-off that `format()` makes. > Exclusive combinators (`or()`, `longestMatch()`) and multi-source > combinators (`merge()`) intentionally do not forward normalization > because the active branch or key ownership is unknown at default time. ``` `validate()` : Optional. *Available since Optique 1.1.0.* Validates a value of type `T` as if it had been parsed from CLI input, returning a success result with the possibly canonicalized value or a failure with an error message. When present, `option()` and `argument()` use this method to validate fallback values (e.g. from `bindEnv()`/`bindConfig()`) instead of the generic `format()`+`parse()` round-trip. Most parsers do not need it: implement it only when the round-trip cannot faithfully express validation for some values, as with `firstOf()` whose constituents may produce overlapping string representations. Like `normalize()`, this method is synchronous regardless of the parser's mode. ### Basic custom parser Here's a simple custom parser for IPv4 addresses: ```typescript twoslash import { message } from "@optique/core/message"; import type { ValueParser, ValueParserResult } from "@optique/core/valueparser"; interface IPv4Address { octets: [number, number, number, number]; toString(): string; } function ipv4(): ValueParser<"sync", IPv4Address> { return { mode: "sync", metavar: "IP_ADDRESS", placeholder: { octets: [0, 0, 0, 0], toString() { return "0.0.0.0"; }, }, parse(input: string): ValueParserResult { const parts = input.split('.'); if (parts.length !== 4) { return { success: false, error: message`Expected IPv4 address in format a.b.c.d, but got ${input}.` }; } const octets: number[] = []; for (const part of parts) { const num = parseInt(part, 10); if (isNaN(num) || num < 0 || num > 255) { return { success: false, error: message`Invalid IPv4 octet: ${part}. Must be 0-255.` }; } octets.push(num); } return { success: true, value: { octets: octets as [number, number, number, number], toString() { return octets.join('.'); } } }; }, format(value: IPv4Address): string { return value.toString(); } }; } ``` ### Parser with options More sophisticated parsers can accept configuration options: ```typescript twoslash import { message } from "@optique/core/message"; import type { NonEmptyString, ValueParser, ValueParserResult, } from "@optique/core/valueparser"; interface DateParserOptions { metavar?: NonEmptyString; format?: 'iso' | 'us' | 'eu'; allowFuture?: boolean; } function date(options: DateParserOptions = {}): ValueParser<"sync", Date> { const { metavar = "DATE", format = 'iso', allowFuture = true } = options; return { mode: "sync", metavar, placeholder: new Date(0), parse(input: string): ValueParserResult { let date: Date; // Parse according to format switch (format) { case 'iso': date = new Date(input); break; case 'us': // MM/DD/YYYY format const usMatch = input.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); if (!usMatch) { return { success: false, error: message`Expected US date format MM/DD/YYYY, but got ${input}.` }; } date = new Date(parseInt(usMatch[3]), parseInt(usMatch[1]) - 1, parseInt(usMatch[2])); break; case 'eu': // DD/MM/YYYY format const euMatch = input.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); if (!euMatch) { return { success: false, error: message`Expected EU date format DD/MM/YYYY, but got ${input}.` }; } date = new Date(parseInt(euMatch[3]), parseInt(euMatch[2]) - 1, parseInt(euMatch[1])); break; } // Validate parsed date if (isNaN(date.getTime())) { return { success: false, error: message`Invalid date: ${input}.` }; } // Check future constraint if (!allowFuture && date > new Date()) { return { success: false, error: message`Future dates not allowed, but got ${input}.` }; } return { success: true, value: date }; }, format(value: Date): string { switch (format) { case 'iso': return value.toISOString().split('T')[0]; case 'us': return `${value.getMonth() + 1}/${value.getDate()}/${value.getFullYear()}`; case 'eu': return `${value.getDate()}/${value.getMonth() + 1}/${value.getFullYear()}`; } } }; } // Usage const birthDate = date({ format: 'us', allowFuture: false, metavar: "BIRTH_DATE" }); ``` ### Integration with parsers Custom value parsers work seamlessly with Optique's parser combinators: ```typescript twoslash import { message } from "@optique/core/message"; import type { NonEmptyString, ValueParser, ValueParserResult, } from "@optique/core/valueparser"; interface IPv4Address { octets: [number, number, number, number]; toString(): string; } function ipv4(): ValueParser<"sync", IPv4Address> { return { mode: "sync", metavar: "IP_ADDRESS", placeholder: { octets: [0, 0, 0, 0], toString() { return "0.0.0.0"; }, }, parse(input: string): ValueParserResult { return { success: false, error: message`` }; }, format(value: IPv4Address): string { return value.toString(); } }; } interface DateParserOptions { metavar?: NonEmptyString; format?: 'iso' | 'us' | 'eu'; allowFuture?: boolean; } function date(options: DateParserOptions = {}): ValueParser<"sync", Date> { const { metavar = "DATE", format = 'iso', allowFuture = true } = options; return { mode: "sync", metavar, placeholder: new Date(0), parse(input: string): ValueParserResult { return { success: false, error: message`` }; }, format(value: Date): string { return ""; } }; } // ---cut-before--- import { object } from "@optique/core/constructs"; import { parse } from "@optique/core/parser"; import { argument, option } from "@optique/core/primitives"; import { integer } from "@optique/core/valueparser"; const serverConfig = object({ address: option("--bind", ipv4()), startDate: argument(date({ format: 'iso' })), port: option("-p", "--port", integer({ min: 1, max: 0xffff })) }); // Full type safety with custom types const config = parse(serverConfig, [ "--bind", "192.168.1.100", "--port", "8080", "2023-12-25" ]); if (config.success) { console.log(`Binding to ${config.value.address.toString()}:${config.value.port}.`); console.log(`Start date: ${config.value.startDate.toDateString()}.`); } ``` ### Validation best practices When creating custom value parsers, follow these best practices: #### Clear error messages Provide specific, actionable error messages that help users understand what went wrong: ```typescript twoslash import { message } from "@optique/core/message"; function _(input: string) { // ---cut-before--- // Good: Specific and helpful return { success: false, error: message`Expected IPv4 address in format a.b.c.d, but got ${input}.` }; // Bad: Vague and unhelpful return { success: false, error: message`Invalid input.` }; // ---cut-after--- } ``` #### Comprehensive validation Validate all aspects of your input format: ```typescript twoslash // @noErrors: 2391 2389 import { message } from "@optique/core/message"; import type { ValueParser, ValueParserResult } from "@optique/core/valueparser"; function parseValue(i: string): T; const formatError = message``; const boundsError = message``; const semanticError = message``; function correctFormat(input: string): boolean; function withinBounds(input: string): boolean; function semanticallyValid(input: string): boolean; function parser(placeholder: T): ValueParser<"sync", T> { return { mode: "sync", metavar: "VALUE", // Generic T has no intrinsic default, so accept a stand-in from the caller. placeholder, format() { return ""; }, // ---cut-before--- parse(input: string): ValueParserResult { // 1. Format validation if (!correctFormat(input)) { return { success: false, error: formatError }; } // 2. Range/constraint validation if (!withinBounds(input)) { return { success: false, error: boundsError }; } // 3. Semantic validation if (!semanticallyValid(input)) { return { success: false, error: semanticError }; } return { success: true, value: parseValue(input) }; } // ---cut-after--- } } ``` #### Consistent metavar naming Use descriptive, uppercase metavar names that clearly indicate the expected input: ```typescript // Good metavar examples metavar: "EMAIL" metavar: "FILE" metavar: "PORT" metavar: "UUID" // Poor metavar examples metavar: "input" metavar: "value" metavar: "thing" ``` Custom value parsers extend Optique's type safety and validation capabilities to handle domain-specific data types while maintaining consistency with the built-in parsers and providing excellent integration with TypeScript's type system. ## Async value parsers *This API is available since Optique 0.9.0.* Value parsers can operate in either synchronous (`"sync"`) or asynchronous (`"async"`) mode. The mode is declared via the `mode` property, which affects the return types of the `parse()` and `suggest()` methods. All built-in value parsers are synchronous, but you can create async parsers for scenarios like: * Validating values against a remote API * Reading configuration from external sources * Performing I/O-based validation (e.g., checking DNS records) ### Creating async value parsers An async value parser declares `mode: "async"` and returns a `Promise` from its `parse()` method: ```typescript twoslash import { type ValueParser, type ValueParserResult } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; // An async value parser that validates a URL by checking if it's reachable function reachableUrl(): ValueParser<"async", URL> { return { mode: "async", metavar: "URL", placeholder: new URL("http://0.invalid"), async parse(input: string): Promise> { // First validate URL format let url: URL; try { url = new URL(input); } catch { return { success: false, error: message`Invalid URL format: ${input}.`, }; } // Then check if the URL is reachable try { const response = await fetch(url, { method: "HEAD" }); if (!response.ok) { return { success: false, error: message`URL ${input} returned status ${response.status.toString()}.`, }; } } catch (e) { return { success: false, error: message`Could not reach URL ${input}.`, }; } return { success: true, value: url }; }, format(value: URL): string { return value.toString(); }, }; } ``` ### Mode propagation When you use an async value parser with primitives and combinators, the mode automatically propagates through the parser tree. If any value parser in a composite parser is async, the entire parser becomes async: ```typescript twoslash import type { ValueParser, ValueParserResult } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; declare function reachableUrl(): ValueParser<"async", URL>; // ---cut-before--- // This parser is async because reachableUrl() is async const parser = object({ endpoint: option("--endpoint", reachableUrl()), // async name: option("-n", "--name", string()), // sync }); // parser.mode is "async" ``` The mode is tracked at compile time through TypeScript's type system, ensuring type safety when working with async parsers. ### Parsing with async parsers For async parsers, use `parseAsync()` instead of `parse()`: ```typescript twoslash import type { Parser, ValueParser, ValueParserResult } from "@optique/core"; import { message } from "@optique/core/message"; import { object } from "@optique/core/constructs"; import { parseAsync } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; declare function reachableUrl(): ValueParser<"async", URL>; const parser = object({ endpoint: option("--endpoint", reachableUrl()), name: option("-n", "--name", string()), }); // ---cut-before--- // parseAsync() returns a Promise const result = await parseAsync(parser, ["--endpoint", "https://api.example.com"]); if (result.success) { console.log(`Connecting to ${result.value.endpoint.toString()}.`); } ``` The `parseAsync()` function works with both sync and async parsers, always returning a `Promise`. For sync-only parsers, use `parseSync()` which returns the result directly. ### Async suggestions Async value parsers can also provide async completion suggestions by returning an `AsyncIterable` from the `suggest()` method: ```typescript twoslash import type { Suggestion, ValueParser, ValueParserResult } from "@optique/core"; import { message } from "@optique/core/message"; // A value parser that suggests valid user IDs from a remote service function userId(): ValueParser<"async", string> { return { mode: "async", metavar: "USER_ID", placeholder: "", async parse(input: string): Promise> { // Validate against remote service... return { success: true, value: input }; }, format(value: string): string { return value; }, async *suggest(prefix: string): AsyncIterable { // Fetch matching user IDs from remote service const response = await fetch( `https://api.example.com/users?prefix=${encodeURIComponent(prefix)}` ); const users = await response.json() as { id: string; name: string }[]; for (const user of users) { yield { kind: "literal", text: user.id, description: message`${user.name}`, }; } }, }; } ``` Similarly, use `suggestAsync()` to get suggestions from async parsers: ```typescript twoslash import type { Parser, ValueParser, ValueParserResult } from "@optique/core"; import { message } from "@optique/core/message"; import { suggestAsync } from "@optique/core/parser"; import { argument } from "@optique/core/primitives"; declare function userId(): ValueParser<"async", string>; const parser = argument(userId()); // ---cut-before--- // suggestAsync() returns a Promise with an array of suggestions const suggestions = await suggestAsync(parser, ["us"]); for (const suggestion of suggestions) { if (suggestion.kind === "literal") { console.log(suggestion.text); } } ``` ## Completion suggestions *This API is available since Optique 0.6.0.* Value parsers can implement an optional `suggest()` method to provide intelligent completion suggestions for shell completion. This method enables users to discover valid values by pressing Tab, improving usability and reducing input errors. ### Built-in parser suggestions Many built-in value parsers automatically provide completion suggestions: ```typescript twoslash import { choice, locale, url } from "@optique/core/valueparser"; import { timeZone } from "@optique/temporal"; // Choice parser suggests all available options const format = choice(["json", "yaml", "xml"]); // Completing "j" suggests: ["json"] // URL parser suggests protocol completions when allowedProtocols is set const apiUrl = url({ allowedProtocols: ["https:", "http:"] }); // Completing "ht" suggests: ["http://", "https://"] // Locale parser suggests common locale identifiers const userLocale = locale(); // Completing "en" suggests: ["en", "en-US", "en-GB", "en-CA", ...] // Timezone parser uses Intl.supportedValuesOf for dynamic suggestions const timezone = timeZone(); // Completing "America/" suggests: ["America/New_York", "America/Chicago", ...] ``` ### Custom suggestion implementation Implement the `suggest()` method in custom value parsers to provide domain-specific completions: ```typescript twoslash import { type ValueParser, type ValueParserResult } from "@optique/core/valueparser"; import { type Suggestion } from "@optique/core/parser"; import { message } from "@optique/core/message"; function httpMethod(): ValueParser<"sync", string> { const methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]; return { mode: "sync", metavar: "METHOD", placeholder: "", parse(input: string): ValueParserResult { const method = input.toUpperCase(); if (methods.includes(method)) { return { success: true, value: method }; } return { success: false, // Note: For proper formatting of choice lists, see the "Formatting choice lists" // section in the Concepts guide on Messages error: message`Invalid HTTP method: ${input}. Valid methods: ${methods.join(", ")}.`, }; }, format(value: string): string { return value; }, *suggest(prefix: string): Iterable { for (const method of methods) { if (method.toLowerCase().startsWith(prefix.toLowerCase())) { yield { kind: "literal", text: method, description: message`HTTP ${method} request method` }; } } }, }; } ``` ### File completion delegation For file and directory inputs, delegate completion to the shell's native file system integration using file-type suggestions: ```typescript twoslash import { type ValueParser, type ValueParserResult } from "@optique/core/valueparser"; import { type Suggestion } from "@optique/core/parser"; import { message } from "@optique/core/message"; function configFile(): ValueParser<"sync", string> { return { mode: "sync", metavar: "CONFIG", placeholder: "", parse(input: string): ValueParserResult { // Validation logic here return { success: true, value: input }; }, format(value: string): string { return value; }, *suggest(prefix: string): Iterable { yield { kind: "file", type: "file", extensions: [".json", ".yaml", ".yml"], includeHidden: false, description: message`Configuration file` }; }, }; } ``` The `suggest()` method receives the current input prefix and should yield `Suggestion` objects. The shell completion system handles filtering and display, while native file completion provides better performance and platform-specific behavior for file system operations. Completion suggestions improve user experience by making CLI applications more discoverable and reducing typing errors, while maintaining the same type safety and validation guarantees as the parsing logic. --- --- url: /why.md description: >- Discover how Optique brings functional composition to TypeScript CLI development, enabling truly reusable parser components that other libraries can't match through configuration-based approaches. --- # Why Optique? The TypeScript CLI ecosystem has come a long way from the early days of commander.js. Today's libraries like Gunshi, Brocli, Cleye, and Deno Cliffy all provide excellent type safety and developer-friendly APIs. Each has its strengths: Gunshi offers declarative configurations with good type inference, Brocli provides fluent option builders, and Cleye delivers strongly typed parameters with automatic help generation. Yet despite these advances, building complex CLI applications still feels like assembling configurations rather than composing logic. You define options, set up handlers, and coordinate between different command structures through manual abstraction layers. Sharing common patterns requires copying configuration objects or building helper functions that lose type information. Optique takes a fundamentally different approach. Instead of configuring CLI parsers, you compose them from small, reusable functions that naturally combine while preserving full type information. This isn't just a different API—it's a different way of thinking about CLI development that unlocks possibilities other libraries simply can't achieve. ## Complex option constraints made simple To understand what makes Optique truly unique, consider a challenge that stumps most CLI libraries: expressing complex relationships between option groups. Imagine a deployment tool where certain options must be used together, but different groups are mutually exclusive. With traditional CLI libraries, you'd define all options individually and then add runtime validation to check the relationships: ```typescript // Traditional approach - validation scattered in business logic const cli = require('some-cli-lib'); cli .option('--auth-token ') .option('--auth-key ') .option('--auth-secret ') .option('--config-file ') .option('--config-host ') .option('--config-port ') .action((options) => { // Manual validation of complex relationships const hasAuthGroup = options.authToken && options.authKey && options.authSecret; const hasConfigGroup = options.configFile && options.configHost && options.configPort; if (!hasAuthGroup && !hasConfigGroup) { throw new Error('Must provide either auth options (token, key, secret) or config options (file, host, port)'); } if (hasAuthGroup && hasConfigGroup) { throw new Error('Cannot use both auth and config options together'); } // More validation... }); ``` This approach scatters constraint logic throughout your code, makes testing difficult, and provides no compile-time guarantees about option relationships. Optique's [`or()`](./concepts/constructs.md#or-parser) combinator lets you express these constraints directly in the parser structure: ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { constant, option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; const authOptions = object({ mode: constant("auth"), token: option("--auth-token", string()), key: option("--auth-key", string()), secret: option("--auth-secret", string()) }); const configOptions = object({ mode: constant("config"), file: option("--config-file", string()), host: option("--config-host", string()), port: option("--config-port", integer()) }); // Express complex constraints naturally const deployParser = or(authOptions, configOptions); // ^? // TypeScript automatically understands the relationship ``` The constraints are embedded in the parser structure itself. You can't accidentally use both auth and config options together—the parser simply won't accept such input. The type system understands these relationships and provides perfect autocompletion and error checking. This scales to arbitrarily complex constraint patterns. Need three mutually exclusive groups where each group requires multiple options? Just nest more [`object()`](./concepts/constructs.md#object-parser) and `or()` combinators: ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { constant, option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; const localDeploy = object({ type: constant("local"), path: option("--path", string()), port: option("--port", integer()) }); const remoteDeploy = object({ type: constant("remote"), host: option("--host", string()), user: option("--user", string()), key: option("--ssh-key", string()) }); const cloudDeploy = object({ type: constant("cloud"), provider: option("--provider", string()), region: option("--region", string()), credentials: option("--credentials", string()) }); const deploymentStrategy = or(localDeploy, remoteDeploy, cloudDeploy); // ^? // Each option group is self-contained and mutually exclusive ``` Try expressing this with configuration-based libraries and you'll quickly find yourself writing complex validation functions. With Optique, the constraints are the parser—clear, type-safe, and impossible to get wrong. ## Natural parser composition Beyond complex constraints, Optique excels at the more common challenge of sharing option sets across multiple commands. While other libraries handle this through object spreading or helper abstractions, Optique treats parsers as first-class values that naturally compose. Consider sharing common options across different commands: ```typescript twoslash import { object, merge } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; const CommonOptions = object({ verbose: option("--verbose"), config: option("--config", string()) }); const DeployOptions = object({ environment: option("--env", string()) }); const deployParser = merge(CommonOptions, DeployOptions); // ^? // Natural composition with preserved type information ``` The difference becomes more pronounced as your CLI grows. With configuration-based libraries, shared logic requires increasingly complex abstractions. With Optique, you just compose functions—the same way you'd compose any other logic in your application. ## Parser combinators in practice The power of Optique's approach becomes clear when building real CLI applications. Consider a deployment tool that needs different option sets for different environments, with some options shared and others specific to each context. ```typescript twoslash import { merge, object, or } from "@optique/core/constructs"; import { command, constant, option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; // Base components that capture common patterns const databaseConfig = object({ host: option("--db-host", string()), port: option("--db-port", integer({ min: 1000 })), database: option("--database", string()) }); const loggingConfig = object({ verbose: option("--verbose"), logLevel: option("--log-level", string()) }); // Environment-specific variations const productionConfig = object({ ssl: option("--ssl"), backup: option("--backup") }); const developmentConfig = object({ watch: option("--watch"), hotReload: option("--hot-reload") }); // Commands that compose these pieces differently const prodDeploy = command( "production", merge( object({ type: constant("production") }), databaseConfig, loggingConfig, productionConfig ) ); const devDeploy = command( "development", merge( object({ type: constant("development") }), databaseConfig, loggingConfig, developmentConfig ) ); const deployCommand = command("deploy", or(prodDeploy, devDeploy)); ``` Each component remains independently testable and reusable. You can share `databaseConfig` across multiple projects, modify `loggingConfig` for different applications, or create new combinations without touching existing code. The type system tracks everything automatically, ensuring your CLI evolves safely. ## The functional advantage Traditional CLI libraries work within object-oriented or configuration patterns that limit how you can transform and extend parsers. You might extract common options into shared objects, but you can't easily create variations or apply transformations while preserving type safety. Optique's functional approach means parsers are just values you can transform with standard functional programming techniques: ```typescript twoslash import { object } from "@optique/core/constructs"; import { withDefault } from "@optique/core/modifiers"; import { option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; // Start with a base parser const serverOptions = object({ port: option("--port", integer()), host: option("--host", string()) }); // Transform it for different contexts const withDefaults = object({ port: withDefault(option("--port", integer()), 3000), host: withDefault(option("--host", string()), "localhost") }); const forProduction = object({ port: withDefault(option("--port", integer()), 80), host: withDefault(option("--host", string()), "0.0.0.0") }); const config = forProduction; // ^? // Each variation preserves full type information ``` You can create parser libraries that export not just specific configurations, but transformation functions that let consumers adapt parsers to their needs. This enables a level of reusability that's impossible with configuration-based approaches. ## Context-aware options and generated documentation Most CLI libraries treat options as independent values—each flag is parsed in isolation with no knowledge of the others. But real tools often have options that are logically related: the valid log levels may differ between development and production modes, or the available branches depend on which repository you've selected. Optique's [`dependency()`](./concepts/dependencies.md) system lets you express these relationships directly in the parser: ```typescript twoslash import { object } from "@optique/core/constructs"; import { dependency } from "@optique/core/dependency"; import { option } from "@optique/core/primitives"; import { choice } from "@optique/core/valueparser"; // The mode option is a dependency source const modeParser = dependency(choice(["dev", "prod"] as const)); // Log levels depend on the selected mode const logLevel = modeParser.derive({ metavar: "LEVEL", mode: "sync", factory: (mode) => { if (mode === "dev") return choice(["debug", "info", "warn", "error"]); return choice(["warn", "error"]); }, defaultValue: () => "dev" as const, }); const parser = object({ mode: option("--mode", modeParser), logLevel: option("--log-level", logLevel), }); ``` This isn't just validation—shell completion also uses the dependency relationship to suggest only valid values. When the user types `--mode prod --log-level ` and presses Tab, only `warn` and `error` are offered as completions. The same parser definitions that drive parsing and completion can also generate Unix man pages through *@optique/man*: ```typescript twoslash import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; import { generateManPage } from "@optique/man"; const parser = object({ host: option("--host", string(), { description: message`Hostname to bind to.`, }), port: option("--port", integer(), { description: message`Port number to listen on.`, }), }); const manPage = generateManPage(parser, { name: "myapp", section: 1, version: "1.0.0", author: message`Jane Doe `, }); ``` Help text, completion scripts, and man pages all derive from the same source. When you add an option or change a description, everything stays in sync without separate maintenance. ## Type inference that scales While modern CLI libraries provide good type safety, they typically require manual type annotations for complex scenarios. You define your configuration, then separately define types that hopefully match what the configuration produces. Optique's type inference scales naturally from simple to complex cases. The same combinators that handle basic options automatically infer sophisticated discriminated unions for complex command structures: ```typescript twoslash import { object, or } from "@optique/core/constructs"; import { optional, withDefault } from "@optique/core/modifiers"; import type { InferValue } from "@optique/core/parser"; import { argument, command, constant, option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; const userCommands = or( command("create", object({ action: constant("create"), name: argument(string()), email: option("--email", string()), role: option("--role", string()) })), command("list", object({ action: constant("list"), filter: optional(option("--filter", string())), limit: withDefault(option("--limit", integer()), 10), })), command("delete", object({ action: constant("delete"), id: argument(integer()), force: option("--force") })) ); type UserAction = InferValue; // ^? // TypeScript automatically infers the perfect discriminated union ``` This type is derived entirely from the parser structure. No manual annotations, no separate type definitions, no risk of the types and implementation drifting apart. The parser is the type, and the type is the parser. ## Integration packages The core library handles argument parsing and type inference. Optional integration packages extend it for common use cases: * *[@optique/env](./integrations/env.md)*: Binds parser values to environment variables with configurable priority (CLI > environment > default). * *[@optique/config](./integrations/config.md)*: Loads default values from configuration files, with schema validation via any Standard Schema- compatible library (Zod, Valibot, ArkType). * *[@optique/inquirer](./integrations/inquirer.md)*: Falls back to an interactive Inquirer.js prompt when a CLI argument is not provided. * *[@optique/zod](./integrations/zod.md)* and *[@optique/valibot](./integrations/valibot.md)*: Use Zod or Valibot schemas directly as value parsers. * *[@optique/git](./integrations/git.md)*: Async value parsers for Git references (branches, tags, commits, remotes). * *[@optique/temporal](./integrations/temporal.md)*: Value parsers for Temporal date and time types. * *[@optique/man](./concepts/man.md)*: Generates Unix man pages from parser definitions and program metadata. * *[@optique/logtape](./integrations/logtape.md)*: Parsers for log level options that configure LogTape sinks. Because every integration returns a regular parser, they compose with each other. Wrapping order determines fallback priority: ```typescript twoslash import { z } from "zod"; import { bindConfig, createConfigContext } from "@optique/config"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { bindEnv, createEnvContext } from "@optique/env"; import { prompt } from "@optique/inquirer"; const envContext = createEnvContext({ prefix: "MYAPP_" }); const configContext = createConfigContext({ schema: z.object({ host: z.string().optional() }), }); // CLI > env > config > interactive prompt const host = prompt( bindEnv( bindConfig(option("--host", string()), { context: configContext, key: "host", }), { context: envContext, key: "HOST", parser: string() }, ), { type: "input", message: "Host:", default: "localhost" }, ); ``` The parser produced by this chain works the same way everywhere: inside `object()`, `merge()`, `or()`, or any other combinator. ## When Optique makes sense Optique shines in scenarios where CLI complexity grows over time and where consistency across related tools matters. Beyond argument parsing, Optique handles the full value resolution lifecycle: CLI arguments, environment variables, configuration files, and interactive prompts, all through the same composition model. If you're building a single simple script, the configuration approach of other libraries might serve you better. The functional programming concepts in Optique add value when you need the flexibility and reusability they enable. Choose Optique when you're building CLI applications that will evolve, when you need to share patterns across multiple tools, or when you're working in teams where consistent CLI patterns matter. The upfront investment in learning parser combinators pays dividends as your CLI ecosystem grows. Consider alternatives when you're building straightforward, one-off tools, when your team strongly prefers configuration over composition, or when you need to integrate with existing CLI frameworks that follow different patterns. ## Starting with Optique Understanding Optique means understanding that CLI parsers can be more than configurations: they are composable functions that naturally combine to create sophisticated interfaces. This shift in perspective, from configuring to composing, unlocks new levels of reusability and maintainability in CLI development. The patterns you learn with Optique apply beyond CLI parsing. They are the same functional composition techniques that make libraries like Zod so powerful for validation and that enable the kind of type-safe, composable APIs that make TypeScript development productive and reliable. --- --- url: /integrations/zod.md description: >- Use Zod schemas for validating command-line arguments with seamless integration, type inference, and transformation support. --- # Zod integration *This API is available since Optique 0.7.0.* The *@optique/zod* package provides seamless integration with [Zod], enabling you to use Zod schemas for validating command-line arguments. This allows you to leverage Zod's powerful validation capabilities and reuse existing schemas across your CLI and application code. ::: code-group ```bash [Deno] deno add jsr:@optique/zod zod ``` ```bash [npm] npm add @optique/zod zod ``` ```bash [pnpm] pnpm add @optique/zod zod ``` ```bash [Yarn] yarn add @optique/zod zod ``` ```bash [Bun] bun add @optique/zod zod ``` ::: [Zod]: https://zod.dev/ ## Basic usage The `zod()` function creates a value parser from any Zod schema: ```typescript twoslash import { zod } from "@optique/zod"; import { z } from "zod"; // Email validation const email = zod(z.string().email(), { placeholder: "" }); // Port number with range validation const port = zod(z.coerce.number().int().min(1024).max(65535), { placeholder: 1024 }); // Enum choices const logLevel = zod(z.enum(["debug", "info", "warn", "error"]), { placeholder: "debug" }); ``` > \[!IMPORTANT] > The options object is required. In particular, `placeholder` must be a valid > stand-in value of the schema's output type. Optique uses it during deferred > prompt resolution, so it does not need to be meaningful user data, but it > must be safe for downstream transforms. ## String coercion CLI arguments are always strings, so use `z.coerce` for non-string types: ```typescript twoslash import { zod } from "@optique/zod"; import { z } from "zod"; // ---cut-before--- // ✅ Correct: Use z.coerce for numbers const age = zod(z.coerce.number().int().min(0), { placeholder: 0 }); // ❌ Won't work: z.number() expects actual numbers, not strings const num = zod(z.number(), { placeholder: 0 }); // [!code error] ``` > \[!NOTE] > Both `z.boolean()` and `z.coerce.boolean()` are handled specially: > instead of rejecting CLI strings or applying JavaScript truthiness > semantics, Optique accepts CLI-friendly literals (`true`/`false`, > `1`/`0`, `yes`/`no`, `on`/`off`, case-insensitive). ## Transformations Zod's transformation capabilities work seamlessly with Optique: ```typescript twoslash import { zod } from "@optique/zod"; import { z } from "zod"; // ---cut-before--- // Parse and transform to Date const startDate = zod(z.string().transform((s) => new Date(s)), { placeholder: new Date(0) }); // Transform to uppercase const name = zod(z.string().transform((s) => s.toUpperCase()), { placeholder: "" }); ``` ## Async schemas *This API is available since Optique 1.1.0.* Use `zodAsync()` when a schema depends on async refinements or transforms. The returned value parser is async, so run the containing parser with `runAsync()` or `await run()`: ```typescript twoslash async function isKnownApiKey(value: string): Promise { return await Promise.resolve(value.startsWith("live_")); } // ---cut-before--- import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { runAsync } from "@optique/run"; import { zodAsync } from "@optique/zod"; import { z } from "zod"; const parser = object({ apiKey: option( "--api-key", zodAsync( z.string().refine(isKnownApiKey, "Unknown API key."), { placeholder: "" }, ), ), }); const config = await runAsync(parser, { args: ["--api-key", "live_secret"], }); ``` The synchronous `zod()` helper remains synchronous and still rejects schemas that require Zod's async parse path. `zodAsync()` preserves the same metavar inference, choices, suggestions, Boolean literal conversion, formatting, and custom error handling as `zod()`. Fallback values supplied through `bindEnv()` or `bindConfig()` are validated by the same schema before they are accepted. Async validation may run during fallback resolution and other repeated parser paths, including shell completion requests. Keep remote checks bounded and cached when possible, and prefer enum or literal schemas for completion choices so suggestions stay metadata-driven. ## Custom error messages Customize error messages for better user experience: ```typescript twoslash import { zod } from "@optique/zod"; import { message } from "@optique/core/message"; import { z } from "zod"; // ---cut-before--- const email = zod(z.string().email(), { placeholder: "", metavar: "EMAIL", errors: { zodError: (error, input) => message`Please provide a valid email address, got ${input}.` } }); ``` ## Integration with Optique Zod parsers work seamlessly with all Optique features: ```typescript twoslash import { object } from "@optique/core/constructs"; import { option, argument } from "@optique/core/primitives"; import { zod } from "@optique/zod"; import { z } from "zod"; const config = object({ email: option("--email", zod(z.string().email(), { placeholder: "" })), port: option("-p", "--port", zod(z.coerce.number().int().min(1024).max(65535), { placeholder: 1024 })), logLevel: option("--log-level", zod(z.enum(["debug", "info", "warn", "error"]), { placeholder: "debug" })), startDate: argument(zod(z.string().transform((s) => new Date(s)), { placeholder: new Date(0) })), }); ``` ## Version compatibility The `@optique/zod` package supports both Zod v3 (3.25.0+) and Zod v4 (4.0.0+): * **Zod v3**: Uses standard error messages from `error.issues[0].message` * **Zod v4**: Automatically uses `prettifyError()` when available for better error formatting ## Limitations * *`zod()` remains synchronous*: Async Zod features like `refine(async ...)` and async transforms require `zodAsync()`. The sync helper detects schemas that need async parsing when possible and throws a `TypeError` instead of silently skipping validation. * *Boolean parsing in unions*: The CLI-friendly boolean parsing (accepting `true`/`false`, `1`/`0`, `yes`/`no`, `on`/`off`) applies only when the entire schema is recognized as a boolean type. For unions that are not recognized as wholly boolean, arm precedence is preserved and parsing follows Zod's native union/coercion behavior. The Zod integration provides a powerful way to reuse validation logic across your entire application while maintaining full type safety and excellent error messages.