Files
skills/docs/devtools/tooling/cli/src/parser/parser.md
2026-05-29 08:20:56 +01:00

4.6 KiB

parser

(no submodules)

Purpose

The parser module turns a raw argv token array into a structured ParsedArgs object with typed options, named positional arguments, and a rest passthrough channel. It is the low-level argument-parsing engine that higher-level command definitions build on, designed to model the behaviour CLI users expect from mature tools like git, npm, and cargo.

The module is deliberately imperative and framework-free: no reflection, no decorators, no hidden state. Every behaviour — type coercion, --no-* negation, short-flag resolution, choice validation, required-option checking — is enforced in a single linear pass over argv, and every failure mode raises a CliError with a stable code drawn from CLI_CODES.PARSE.* so callers can branch on machine-readable codes instead of message strings.

Features

  • Boolean flags (--verbose) and negation (--no-verbose)
  • String/number/array options via --key value, --key=value, or short-flag (-v) forms
  • Short-flag mapping via short: "v" on the option config
  • Passthrough rest channel after --
  • Variadic positionals (<files...>) collected as arrays
  • Required positionals and required options with dedicated error codes
  • Choice validation with INVALID_VALUE
  • allowUnknown: true to silently ignore unrecognised flags
  • Defaults for options and positionals
  • Helper parsers for flag specs (parseOptionFlags) and positional specs (parseArgumentSpec)

Usage: Basic Example

import { parseArgs } from "@suaveplan/cli/parser";

const result = parseArgs(["--verbose", "src/file.ts"], {
    options: { verbose: { type: "boolean" } },
    arguments: [{ name: "file", required: true, variadic: false }],
});

console.log(result.options.verbose); // true
console.log(result.positionals.file); // "src/file.ts"

Usage: Advanced Example

import { parseArgs, parseOptionFlags, parseArgumentSpec } from "@suaveplan/cli/parser";

const flag = parseOptionFlags("-o, --output <file>", "Output file");
const posArg = parseArgumentSpec("<sources...>", "Source files");

const result = parseArgs(
    ["-o", "out.txt", "--format=json", "a.ts", "b.ts", "--", "--not-a-flag"],
    {
        options: {
            [flag.name]: flag.config,
            format: { type: "string", choices: ["json", "text"], default: "text" },
            retries: { type: "number", default: 3 },
        },
        arguments: [posArg],
    }
);

// result.options.output === "out.txt"
// result.options.format === "json"
// result.positionals.sources === ["a.ts", "b.ts"]
// result.rest === ["--not-a-flag"]

API Reference

parseArgs(argv, config?)

  • argv: string[] — raw CLI tokens (without the node/bun prefix)
  • config?: ParserConfig — optional options/arguments definitions, allowUnknown, stopEarly
  • Returns ParsedArgs with options, positionals, and rest
  • Throws CliError with codes UNKNOWN_OPTION, MISSING_VALUE, INVALID_VALUE, MISSING_ARGUMENT, MISSING_REQUIRED_OPTION

parseOptionFlags(flags, description?, defaultValue?)

  • flags: string — commander-style spec, e.g. "-v, --verbose" or "--output <file>"
  • Returns { name, config } where name is the camelCased long-flag name
  • Throws CliError(UNKNOWN_OPTION) if no long flag is provided

parseArgumentSpec(spec, description?)

  • spec: string — "<name>", "[name]", "<name...>", or "[name...]"
  • Returns an ArgumentConfig with required, variadic, name, and optional description

Implementation Notes

  • Single-pass state machine: tokens are consumed left-to-right with a small restMode flag that flips when -- is encountered. There are no lookaheads beyond peeking at argv[i + 1] to decide whether a flag's value is the next token.
  • --no-* handling strips the literal --no- prefix (not the camelCased form) and then camelCases the remainder, so --no-dry-run correctly maps to the dryRun option.
  • Values are only consumed as the next token when that token does not start with -, preventing --foo --bar from stealing --bar as the value of --foo.
  • number type uses Number(raw) and rejects NaN so strings like "abc" raise INVALID_VALUE rather than silently becoming NaN.
  • All errors are CliError instances carrying context metadata (option name, offending value, allowed choices) so UIs and logs can render actionable diagnostics without re-parsing message strings.
  • Schemas mirror the TypeScript types exactly and are exported separately so callers can validate untrusted ParserConfig inputs at the boundary without pulling Zod into hot paths.