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
restchannel after-- - Variadic positionals (
<files...>) collected as arrays - Required positionals and required options with dedicated error codes
- Choice validation with
INVALID_VALUE allowUnknown: trueto 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
ParsedArgswithoptions,positionals, andrest - Throws
CliErrorwith codesUNKNOWN_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 }wherenameis 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
ArgumentConfigwithrequired,variadic,name, and optionaldescription
Implementation Notes
- Single-pass state machine: tokens are consumed left-to-right with a small
restModeflag that flips when--is encountered. There are no lookaheads beyond peeking atargv[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-runcorrectly maps to thedryRunoption.- Values are only consumed as the next token when that token does not start with
-, preventing--foo --barfrom stealing--baras the value of--foo. numbertype usesNumber(raw)and rejectsNaNso strings like"abc"raiseINVALID_VALUErather than silently becomingNaN.- All errors are
CliErrorinstances carryingcontextmetadata (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
ParserConfiginputs at the boundary without pulling Zod into hot paths.