npm.io
3.2.1 • Published 4 months ago

@jondotsoy/flags

Licence
MIT
Version
3.2.1
Deps
0
Size
314 kB
Vulns
0
Weekly
0

> flags{args}

A powerful and type-safe JavaScript/TypeScript command-line arguments parser with a fluent builder API.

Code Wiki

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

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:

npx skills add jondotsoy/flags

See SKILL.md for details.

Quick Start

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.

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.

parser.program("mycli");
.describe(description: string)

Sets the program description for help messages.

parser.describe("A powerful CLI tool");
.combineShortFlags()

Enables automatic expansion of combined short boolean flags (e.g., -abc-a -b -c).

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.

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].

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.

console.log(parser.helpMessage());

Builder Functions

flag(...names: string[])

Creates a flag builder for named options. Supports multiple aliases.

// 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:

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)

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

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)

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

flag("--port").number();
// --port 3000 → 3000

.keyValue() - Key-value pairs (accumulates into object)

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

flag("--output").string().required();
// Throws RequiredFlagMissingError if not provided

.default(value) - Sets a default value

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

flag("--verbose").boolean().describe("Enable verbose output");
command(name: string)

Creates a command builder for subcommands.

command("build"); // Matches exactly "build"
Command Type Methods

.boolean() - Boolean command (presence = true)

command("build").boolean();
// build → true

.restArgs() - Captures all remaining arguments

command("serve").restArgs();
// serve --watch --port 3000 → ["--watch", "--port", "3000"]
Command Modifiers

.describe(description) - Adds description for help

command("build").boolean().describe("Build the project");
argument()

Creates a positional argument builder. Arguments are matched in order.

argument(); // Matches the next positional argument
Argument Type Methods

.string() - String argument (default)

argument().string();
// Returns null if not provided

.strings() - Array of strings (captures all remaining positional arguments)

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:

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

argument().string().required();
// Throws RequiredArgumentMissingError if not provided

.describe(description) - Adds description for help

argument().string().required().describe("Input file path");

.delimiter(separator) - Parse flag with custom delimiter

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

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

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

argument()
  .string()
  .transform((value) => value.toUpperCase());
// tar → "TAR"

Type Inference

The library provides full type inference based on your schema:

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:

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.

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.

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.

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:

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
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
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
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)
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
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
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:

flag("--port", "-p")
  .number()
  .default(3000)
  .describe("Port number for the server");
2. Provide Defaults

Use .default() for sensible default values:

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:

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:

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:

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:

const parser = flags({
  output: flag("--output").string().required(),
  input: argument().string().required(),
});

Migration Guide

From other CLI libraries
From yargs:
// 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:
// 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():

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:

flag("--verbose").boolean();
flag("--debug", "-d").boolean();

Q: How do I validate flag values?

A: Parse the result and validate after:

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:

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:

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():

flag("--config").keyValue();
// --config db=postgres --config port=5432 → { db: "postgres", port: "5432" }

License

MIT - see LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.