UNPKG

@jondotsoy/flags

Version:

A Javascript arguments processor

953 lines (712 loc) 23.9 kB
# <span style="color: gray">></span> flags{args} A powerful and type-safe JavaScript/TypeScript command-line arguments parser with a fluent builder API. [Code Wiki](https://codewiki.google/github.com/JonDotsoy/flags) ## Features - 🔥 **Type-safe**: Full TypeScript support with type inference - 🎯 **Fluent API**: Chainable builder pattern for intuitive flag definitions - 📋 **Multiple formats**: Support for `--flag=value`, `--flag value`, and `-f` syntax - 🔗 **Combined flags**: Automatic expansion of combined short flags like `-abc` → `-a -b -c` - 🔧 **Built-in types**: Boolean, string, strings array, number, and key-value handlers - 📚 **Commands**: Support for subcommands and rest arguments - 📍 **Positional arguments**: Ordered argument parsing - 🆘 **Help generation**: Automatic help message generation - ⚡ **Lightweight**: Zero dependencies, minimal overhead ## Installation ```bash npm install @jondotsoy/flags # or yarn add @jondotsoy/flags # or bun add @jondotsoy/flags ``` ## AI Assistant Skill A skill is available for AI coding assistants to better understand this library. Install it with: ```bash npx skills add jondotsoy/flags ``` See [SKILL.md](./skills/flags-builder/SKILL.md) for details. ## Quick Start ```ts import { flags, flag, command, argument } from "@jondotsoy/flags"; // Define your schema with fluent builder API const parser = flags({ verbose: flag("--verbose", "-v").boolean().describe("Enable verbose output"), name: flag("--name", "-n").string().describe("Set application name"), port: flag("--port", "-p").number().default(3000).describe("Server port"), help: flag("--help", "-h").boolean().describe("Show help message"), serve: command("serve").restArgs().describe("Start the server"), input: argument().string().required().describe("Input file"), }) .program("mycli") .describe("My awesome CLI tool"); // Parse command line arguments const args = ["input.txt", "--name=myapp", "-v", "--port", "8080"]; try { const options = parser.parse(args); // Handle help flag if (options.help) { console.log(parser.helpMessage()); process.exit(0); } console.log("Parsed options:", options); // Output: { input: "input.txt", name: "myapp", verbose: true, port: 8080, help: false, serve: null } } catch (error) { console.error(`Error: ${error.message}`); console.log(parser.helpMessage()); process.exit(1); } ``` ## Core Concepts The library uses a fluent builder API with three main building blocks: - **Flags**: Named options like `--verbose` or `-v` - **Commands**: Subcommands like `build` or `serve` - **Arguments**: Positional arguments like file paths ## API Reference ### `flags(schema)` Creates a parser with the given schema. Returns a `FlagsParser` instance. ```ts const parser = flags({ verbose: flag("--verbose", "-v").boolean(), port: flag("--port").number().default(3000), }); ``` ### Parser Methods #### `.program(name: string)` Sets the program name for help messages. ```ts parser.program("mycli"); ``` #### `.describe(description: string)` Sets the program description for help messages. ```ts parser.describe("A powerful CLI tool"); ``` #### `.combineShortFlags()` Enables automatic expansion of combined short boolean flags (e.g., `-abc` → `-a -b -c`). ```ts const parser = flags({ all: flag("-a").boolean(), long: flag("-l").boolean(), human: flag("-h").boolean(), }).combineShortFlags(); parser.parse(["-alh"]); // { all: true, long: true, human: true } ``` #### `.parse(args: string[])` Parses the arguments and returns the result object. ```ts const options = parser.parse(process.argv.slice(2)); ``` **Throws:** - `UnexpectedArgumentError` - When an unknown argument is encountered - `RequiredFlagMissingError` - When a required flag is missing - `RequiredArgumentMissingError` - When a required argument is missing #### `.safeParse(args: string[])` Like `.parse()`, but never throws. Returns a labeled tuple `[ok, error, output]`. ```ts const [ok, error, output] = parser.safeParse(process.argv.slice(2)); if (ok) { console.log(output); // typed result, same as .parse() } else { console.error(error); // UnexpectedArgumentError, RequiredFlagMissingError, etc. console.log(parser.helpMessage()); process.exit(1); } ``` | Element | Type | Description | | -------- | -------------------------- | ---------------------------------------------- | | `ok` | `boolean` | `true` if parsing succeeded, `false` otherwise | | `error` | `unknown \| undefined` | The thrown error when `ok` is `false` | | `output` | `ParseResult \| undefined` | The parsed result when `ok` is `true` | #### `.helpMessage()` Generates and returns a formatted help message. ```ts console.log(parser.helpMessage()); ``` ## Builder Functions ### `flag(...names: string[])` Creates a flag builder for named options. Supports multiple aliases. ```ts // Single name flag("--port"); // Matches --port only // Multiple names (aliases) flag("--verbose", "-v"); // Matches --verbose or -v flag("-t", "--tty"); // Matches -t or --tty // Multiple long and short names flag("--help", "-h", "-?"); // Matches --help, -h, or -? ``` **Examples:** ```ts const parser = flags({ // Long form only version: flag("--version").boolean(), // Short and long form verbose: flag("-v", "--verbose").boolean(), // Multiple aliases help: flag("-h", "--help", "-?").boolean(), // Docker-style: short first, then long tty: flag("-t", "--tty").boolean(), interactive: flag("-i", "--interactive").boolean(), }); // All of these work: parser.parse(["--verbose"]); // { verbose: true, ... } parser.parse(["-v"]); // { verbose: true, ... } parser.parse(["-ti"]); // { tty: true, interactive: true, ... } (combined!) parser.parse(["--tty", "--interactive"]); // { tty: true, interactive: true, ... } ``` #### Flag Type Methods **`.boolean()`** - Boolean flag (presence = true) ```ts flag("--verbose").boolean(); // --verbose → true // Single-letter boolean flags support combined syntax flag("-t").boolean(); flag("-i").boolean(); // -ti → { t: true, i: true } (automatically expanded) ``` **`.string()`** - String value flag ```ts flag("--name").string(); // --name John → "John" // --name=John → "John" // --name=John=Doe → "John=Doe" (multiple = preserved) // --name =John → "=John" (leading = preserved) // Single dash also supported flag("-name").string(); // -name John → "John" // -name=John → "John" ``` **`.strings()`** - Array of strings (accumulates multiple values) ```ts flag("--include").strings(); // --include src --include lib → ["src", "lib"] // -l blue -l red → ["blue", "red"] // Supports = syntax and flag-like values flag("-l").strings(); // -l=-l -l=red → ["-l", "red"] // -l -l -l red → ["-l", "red"] (flag-like values preserved) ``` **`.number()`** - Numeric value flag ```ts flag("--port").number(); // --port 3000 → 3000 ``` **`.keyValue()`** - Key-value pairs (accumulates into object) ```ts flag("--config").keyValue(); // --config name=value → { name: "value" } // --config name value → { name: "value" } // --config name=value --config foo=bar → { name: "value", foo: "bar" } // Multiple syntaxes supported flag("--set").keyValue(); // --set foo taz → { foo: "taz" } // --set foo=taz → { foo: "taz" } // --set=foo=taz → { foo: "taz" } // Flag-like keys are preserved // --set --foo taz → { "--foo": "taz" } // --set=--foo=taz → { "--foo": "taz" } ``` #### Flag Modifiers **`.required()`** - Makes the flag required ```ts flag("--output").string().required(); // Throws RequiredFlagMissingError if not provided ``` **`.default(value)`** - Sets a default value ```ts flag("--port").number().default(3000); // Returns 3000 if --port is not provided // Without default, optional flags return null when not provided flag("--name").string(); // Returns null if --name is not provided flag("--verbose").boolean(); // Returns false if --verbose is not provided (boolean default) ``` **`.describe(description)`** - Adds description for help ```ts flag("--verbose").boolean().describe("Enable verbose output"); ``` ### `command(name: string)` Creates a command builder for subcommands. ```ts command("build"); // Matches exactly "build" ``` #### Command Type Methods **`.boolean()`** - Boolean command (presence = true) ```ts command("build").boolean(); // build → true ``` **`.restArgs()`** - Captures all remaining arguments ```ts command("serve").restArgs(); // serve --watch --port 3000 → ["--watch", "--port", "3000"] ``` #### Command Modifiers **`.describe(description)`** - Adds description for help ```ts command("build").boolean().describe("Build the project"); ``` ### `argument()` Creates a positional argument builder. Arguments are matched in order. ```ts argument(); // Matches the next positional argument ``` #### Argument Type Methods **`.string()`** - String argument (default) ```ts argument().string(); // Returns null if not provided ``` **`.strings()`** - Array of strings (captures all remaining positional arguments) ```ts argument().strings(); // foo tar biz → ["foo", "tar", "biz"] // Works with interspersed flags flags({ verbose: flag("-V", "--verbose").boolean(), names: argument().strings(), }); // foo --verbose tar biz → { verbose: true, names: ["foo", "tar", "biz"] } ``` **Note:** When `argument().strings()` is defined first in the schema, it takes priority and captures all arguments, including flag-like values: ```ts flags({ arg: argument().strings(), labels: flag("-l").strings(), }); // -l=-l -l=red foo → { labels: [], arg: ['-l=-l', '-l=red', "foo"] } ``` #### Argument Modifiers **`.required()`** - Makes the argument required ```ts argument().string().required(); // Throws RequiredArgumentMissingError if not provided ``` **`.describe(description)`** - Adds description for help ```ts argument().string().required().describe("Input file path"); ``` **`.delimiter(separator)`** - Parse flag with custom delimiter ```ts flag("pr").string().delimiter(":"); // pr:foo → "foo" // pr:bar → "bar" // Returns null if not provided ``` **`.match(regex)`** - Match argument with regex and extract named groups ```ts argument().match(/^(?<part1>\w+):(?<part2>\w+)$/); // tar:foo → { part1: "tar", part2: "foo" } // Returns null if pattern doesn't match ``` **`.refine(fn)`** - Custom refinement function for advanced parsing ```ts argument().refine((arg, index, args, context) => { if (!arg.startsWith("tar:")) return null; return { index: index + 1, args: [arg], value: arg.split(":")[1], }; }); // tar:foo → "foo" ``` **`.transform(fn)`** - Transform the parsed value ```ts argument() .string() .transform((value) => value.toUpperCase()); // tar → "TAR" ``` ## Type Inference The library provides full type inference based on your schema: ```ts const parser = flags({ verbose: flag("--verbose").boolean(), name: flag("--name").string(), port: flag("--port").number().default(3000), tags: flag("--tag").strings(), config: flag("--config").keyValue(), build: command("build").boolean(), serve: command("serve").restArgs(), input: argument().string().required(), }); const result = parser.parse(args); // TypeScript knows the types: // result.verbose: boolean // result.name: string | null // result.port: number (never null due to default) // result.tags: string[] // result.config: Record<string, string> // result.build: boolean // result.serve: string[] | null // result.input: string (never null due to required) ``` ## Help Messages The parser automatically generates help messages based on your schema: ```ts const parser = flags({ verbose: flag("--verbose", "-v").boolean().describe("Enable verbose output"), port: flag("--port", "-p").number().default(3000).describe("Server port"), ip: flag("--ip").string().describe("IPv4 address (e.g., 172.30.100.104)"), build: command("build").boolean().describe("Build the project"), }) .program("mycli") .describe("My awesome CLI tool"); console.log(parser.helpMessage()); ``` Output: ``` Usage: mycli My awesome CLI tool Options: -v, --verbose. Enable verbose output -p, --port <number> Server port --ip IPv4 address (e.g., 172.30.100.104) Commands: build Build the project ``` ## Error Handling The library provides specific error types for better error handling: ### `FlagsParseError` Base error class for all parsing errors. ### `UnexpectedArgumentError` Thrown when an unknown argument is encountered. ```ts import { flags, UnexpectedArgumentError } from "@jondotsoy/flags"; try { const options = parser.parse(process.argv.slice(2)); } catch (error) { if (error instanceof UnexpectedArgumentError) { console.error(`Error: ${error.message}`); console.error(`Unexpected argument: ${error.argument}`); console.log(parser.helpMessage()); process.exit(1); } throw error; } ``` ### `RequiredFlagMissingError` Thrown when a required flag is not provided. ```ts import { RequiredFlagMissingError } from "@jondotsoy/flags"; try { const options = parser.parse(args); } catch (error) { if (error instanceof RequiredFlagMissingError) { console.error(`Required flag missing: ${error.flagName}`); process.exit(1); } } ``` ### `RequiredArgumentMissingError` Thrown when a required positional argument is not provided. ```ts import { RequiredArgumentMissingError } from "@jondotsoy/flags"; try { const options = parser.parse(args); } catch (error) { if (error instanceof RequiredArgumentMissingError) { console.error("Required argument missing"); process.exit(1); } } ``` ## Combined Short Flags Single-letter boolean flags can be combined for convenience, similar to common Unix tools. You must call `.combineShortFlags()` on the parser to enable this feature: ```ts import { flags, flag } from "@jondotsoy/flags"; const parser = flags({ all: flag("-a", "--all").boolean().describe("Show all files"), long: flag("-l", "--long").boolean().describe("Use long listing format"), human: flag("-h", "--human-readable") .boolean() .describe("Human readable sizes"), }).combineShortFlags(); // All of these are equivalent: parser.parse(["-a", "-l", "-h"]); // Separate flags parser.parse(["-alh"]); // Combined flags parser.parse(["-lah"]); // Order doesn't matter parser.parse(["-al", "-h"]); // Partially combined // Result: { all: true, long: true, human: true } ``` ### Docker-style Example ```ts const dockerParser = flags({ tty: flag("-t", "--tty").boolean().describe("Allocate a pseudo-TTY"), interactive: flag("-i", "--interactive") .boolean() .describe("Keep STDIN open"), detach: flag("-d", "--detach").boolean().describe("Run in background"), }).combineShortFlags(); // Docker-style combined flags dockerParser.parse(["-ti"]); // { tty: true, interactive: true, detach: false } dockerParser.parse(["-tid"]); // { tty: true, interactive: true, detach: true } ``` ### Rules for Combined Flags 1. **Must enable feature**: Call `.combineShortFlags()` on the parser 2. **Only single-letter flags**: `-abc` works, but `-test` does not expand 3. **Only boolean flags**: All flags in the combination must be boolean type 4. **No equals syntax**: `-a=value` is not expanded (treated as single flag) 5. **All must exist**: If any letter is not a defined flag, the combination is not expanded ```ts const parser = flags({ all: flag("-a").boolean(), brief: flag("-b").boolean(), count: flag("-c").number(), // Not boolean! }).combineShortFlags(); parser.parse(["-ab"]); // ✅ Works: { all: true, brief: true, count: null } parser.parse(["-abc"]); // ❌ Throws: -c requires a value, cannot be combined parser.parse(["-abx"]); // ❌ Throws: Unknown flag -x ``` ## Complete Examples ### Basic CLI Tool ```ts import { flags, flag, UnexpectedArgumentError } from "@jondotsoy/flags"; const parser = flags({ verbose: flag("--verbose", "-v").boolean().describe("Enable verbose logging"), output: flag("--output", "-o") .string() .default("dist") .describe("Output file path"), port: flag("--port", "-p") .number() .default(3000) .describe("Server port number"), help: flag("--help", "-h").boolean().describe("Show help message"), }) .program("mycli") .describe("A simple CLI tool"); try { const options = parser.parse(process.argv.slice(2)); if (options.help) { console.log(parser.helpMessage()); process.exit(0); } console.log("Starting with options:", options); } catch (error) { if (error instanceof UnexpectedArgumentError) { console.error(`Error: ${error.message}`); console.log(parser.helpMessage()); process.exit(1); } throw error; } ``` ### Multi-Command CLI (like npm/git) ```ts import { flags, flag, command } from "@jondotsoy/flags"; const parser = flags({ global: flag("--global", "-g").boolean().describe("Global mode"), verbose: flag("--verbose", "-v").boolean().describe("Verbose output"), build: command("build").restArgs().describe("Build the project"), test: command("test").restArgs().describe("Run tests"), serve: command("serve").restArgs().describe("Start dev server"), }) .program("mycli") .describe("Multi-command CLI tool"); const options = parser.parse(process.argv.slice(2)); if (options.build) { console.log("Building with args:", options.build); // Build logic here } else if (options.test) { console.log("Testing with args:", options.test); // Test logic here } else if (options.serve) { console.log("Serving with args:", options.serve); // Serve logic here } // Usage examples: // mycli build --verbose // mycli test src/**/*.test.ts // mycli serve --port 8080 ``` ### File Processing Tool with Multiple Inputs ```ts import { flags, flag, argument } from "@jondotsoy/flags"; const parser = flags({ input: flag("--input", "-i").strings().describe("Input files"), output: flag("--output", "-o").string().describe("Output directory"), format: flag("--format", "-f") .string() .describe("Output format (json, yaml, xml)"), exclude: flag("--exclude").strings().describe("Exclude patterns"), recursive: flag("--recursive", "-r") .boolean() .describe("Process recursively"), dryRun: flag("--dry-run").boolean().describe("Dry run mode"), file: argument().string().describe("Input file (positional)"), }) .program("fileprocessor") .describe("Process files with various options"); const options = parser.parse(process.argv.slice(2)); // Usage: fileprocessor input.txt --input src/ --exclude node_modules --format json --output dist/ ``` ### Configuration with Key-Value Pairs ```ts import { flags, flag } from "@jondotsoy/flags"; const parser = flags({ env: flag("--env", "-e") .string() .default("development") .describe("Environment"), debug: flag("--debug", "-d").boolean().describe("Enable debug mode"), config: flag("--config", "-c") .keyValue() .describe("Configuration key-value pairs"), feature: flag("--feature").strings().describe("Enable features"), }) .program("myapp") .describe("Application with configuration"); const options = parser.parse(process.argv.slice(2)); console.log("Environment:", options.env); console.log("Debug:", options.debug); console.log("Config:", options.config); console.log("Features:", options.feature); // Usage: myapp --env production --config db=postgres --config port=5432 --feature auth --feature payments // Result: { // env: "production", // debug: false, // config: { db: "postgres", port: "5432" }, // feature: ["auth", "payments"] // } ``` ## Best Practices ### 1. Use the Fluent API Chain methods for clear and concise flag definitions: ```ts flag("--port", "-p") .number() .default(3000) .describe("Port number for the server"); ``` ### 2. Provide Defaults Use `.default()` for sensible default values: ```ts const parser = flags({ port: flag("--port").number().default(3000), output: flag("--output").string().default("dist"), verbose: flag("--verbose").boolean(), // defaults to false }); ``` ### 3. Add Descriptions Use `.describe()` for automatic help generation: ```ts const parser = flags({ verbose: flag("--verbose", "-v").boolean().describe("Enable verbose output"), }) .program("mycli") .describe("My CLI tool description"); ``` ### 4. Handle Errors Gracefully Use `.safeParse()` to avoid try-catch boilerplate: ```ts const [ok, error, options] = parser.safeParse(args); if (!ok) { console.error(`Error: ${(error as Error).message}`); console.log(parser.helpMessage()); process.exit(1); } // Use options ``` Or use `.parse()` with a try-catch block: ```ts try { const options = parser.parse(args); // Use options } catch (error) { if (error instanceof UnexpectedArgumentError) { console.error(`Error: ${error.message}`); console.log(parser.helpMessage()); process.exit(1); } throw error; } ``` ### 5. Use Required for Mandatory Options Mark required flags and arguments explicitly: ```ts const parser = flags({ output: flag("--output").string().required(), input: argument().string().required(), }); ``` ## Migration Guide ### From other CLI libraries #### From `yargs`: ```ts // Before (yargs) const argv = yargs .option("verbose", { type: "boolean", alias: "v" }) .option("port", { type: "number", default: 3000 }).argv; // After (@jondotsoy/flags) const parser = flags({ verbose: flag("--verbose", "-v").boolean(), port: flag("--port").number().default(3000), }); const options = parser.parse(process.argv.slice(2)); ``` #### From `commander`: ```ts // Before (commander) program .option("-v, --verbose", "verbose output") .option("-p, --port <port>", "port number", "3000") .parse(); // After (@jondotsoy/flags) const parser = flags({ verbose: flag("--verbose", "-v").boolean().describe("verbose output"), port: flag("--port", "-p").number().default(3000).describe("port number"), }); const options = parser.parse(process.argv.slice(2)); ``` ## FAQ **Q: How do I handle subcommands like `git commit` or `npm install`?** A: Use the `command()` builder with `.restArgs()`: ```ts const parser = flags({ commit: command("commit").restArgs(), install: command("install").restArgs(), }); ``` **Q: Can I have flags that don't require values?** A: Yes! Use `.boolean()` for boolean flags: ```ts flag("--verbose").boolean(); flag("--debug", "-d").boolean(); ``` **Q: How do I validate flag values?** A: Parse the result and validate after: ```ts const options = parser.parse(args); if (options.port && (options.port < 1 || options.port > 65535)) { throw new Error("Port must be between 1 and 65535"); } ``` **Q: Can I have the same flag appear multiple times?** A: Yes! Use `.strings()` for string arrays: ```ts flag("--include").strings(); // --include src --include lib → ["src", "lib"] ``` **Q: Can I combine short flags like `-abc`?** A: Yes! Call `.combineShortFlags()` on the parser to enable automatic expansion of single-letter boolean flags: ```ts const parser = flags({ all: flag("-a").boolean(), brief: flag("-b").boolean(), color: flag("-c").boolean(), }).combineShortFlags(); parser.parse(["-abc"]); // { all: true, brief: true, color: true } ``` Note: This only works for single-letter boolean flags. Flags with values or multi-letter flags are not expanded. **Q: How do I handle key-value pairs?** A: Use `.keyValue()`: ```ts flag("--config").keyValue(); // --config db=postgres --config port=5432 → { db: "postgres", port: "5432" } ``` ## License MIT - see [LICENSE](./LICENSE) file for details. ## Contributing Contributions are welcome! Please feel free to submit a Pull Request.