Skip to content

Repository files navigation

bargs: a barg parser

⁓ bargs ⁓

"Ex argumentis, veritas"
by @boneskull

Install

npm install @boneskull/bargs

Why bargs?

Most argument parsers make you choose: either a simple API with weak types, or a complex and overengineered DSL. bargs provides a combinator-style API for building type-safe CLIs—composable schema definitions with full type inference.

Also: this is the only argument parser I know of that comes with frickin' themes. Themes, dogg.

Quick Start

A CLI with an optional command and a couple options:

import{bargs,opt,pos}from'@boneskull/bargs';awaitbargs('greet',{version: '1.0.0'}).globals(opt.options({name: opt.string({default: 'world'}),loud: opt.boolean({aliases: ['l']}),}),).command('say',pos.positionals(pos.string({name: 'message',required: true})),({ positionals, values })=>{const[message]=positionals;constgreeting=`${message}, ${values.name}!`;console.log(values.loud ? greeting.toUpperCase() : greeting);},'Say a greeting',).defaultCommand('say').parseAsync();
$ greet Hello --name Alice --loud
HELLO, ALICE!

Usage

Type-Safe by Construction

Each helper returns a fully-typed definition:

import{opt,pos}from'@boneskull/bargs';constverbose=opt.boolean({aliases: ['v']});// Type: BooleanOption & { aliases: ['v'] }constlevel=opt.enum(['low','medium','high'],{default: 'medium'});// Type: EnumOption<'low' | 'medium' | 'high'> & { default: 'medium' }constfile=pos.string({name: 'file',required: true});// Type: StringPositional & { name: 'file', required: true }

When you build a CLI with these, the result types flow through automatically—options with defaults or required: true are non-nullable.

Composable

Options and positionals can be merged using callable parsers:

import{opt,pos}from'@boneskull/bargs';// Create separate parsersconstoptions=opt.options({verbose: opt.boolean({aliases: ['v']}),output: opt.string({aliases: ['o'],default: 'stdout'}),});constpositionals=pos.positionals(pos.string({name: 'input',required: true}),);// Merge them: positionals(options) combines bothconstparser=positionals(options);// Type: Parser<{ verbose: boolean | undefined, output: string }, [string]>

Simple CLI

For a CLI without subcommands, use .globals() with merged options and positionals, then handle the result yourself:

import{bargs,opt,pos}from'@boneskull/bargs';// Merge options and positionals into one parser// when a positional is variadic, it becomes an array within the resultconstparser=pos.positionals(pos.variadic('string',{name: 'text'}))(opt.options({uppercase: opt.boolean({aliases: ['u'],default: false}),}),);const{ values, positionals }=awaitbargs('echo',{description: 'Echo text to stdout',version: '1.0.0',}).globals(parser).parseAsync();const[words]=positionals;consttext=words.join(' ');console.log(values.uppercase ? text.toUpperCase() : text);

Command-Based CLI

For a CLI with multiple subcommands:

import{bargs,merge,opt,pos}from'@boneskull/bargs';awaitbargs('tasks',{description: 'A task manager',version: '1.0.0',}).globals(opt.options({verbose: opt.boolean({aliases: ['v'],default: false}),}),).command('add',// Use merge() to combine positionals with command-specific optionsmerge(opt.options({priority: opt.enum(['low','medium','high'],{default: 'medium'}),}),pos.positionals(pos.string({name: 'text',required: true})),),({ positionals, values })=>{const[text]=positionals;console.log(`Adding ${values.priority} priority task: ${text}`);if(values.verbose)console.log('Verbose mode enabled');},'Add a task',).command('list',opt.options({all: opt.boolean({default: false}),}),({ values })=>{console.log(values.all ? 'All tasks' : 'Pending tasks');},'List tasks',).defaultCommand('list').parseAsync();
$ tasks add "Buy groceries" --priority high --verbose
Adding high priority task: Buy groceries
Verbose mode enabled
$ tasks list --all
All tasks

Nested Commands (Subcommands)

Commands can be nested to arbitrary depth. The only limit is your contempt for your users. Use the factory pattern:

import{bargs,opt,pos}from'@boneskull/bargs';awaitbargs('git').globals(opt.options({verbose: opt.boolean({aliases: ['v']})}))// Factory pattern: receives a builder with parent globals already typed.command('remote',(remote)=>remote.command('add',pos.positionals(pos.string({name: 'name',required: true}),pos.string({name: 'url',required: true}),),({ positionals, values })=>{const[name,url]=positionals;// values.verbose is fully typed! (from parent globals)if(values.verbose)console.log(`Adding ${name}: ${url}`);},'Add a remote',).command('remove'/* ... */).defaultCommand('add'),'Manage remotes',).command('commit',commitParser,commitHandler)// Regular command.parseAsync();
$ git --verbose remote add origin https://github.com/...
Adding origin: https://github.com/...
$ git remote remove origin

The factory function receives a CliBuilder that already has parent globals typed, so all nested command handlers get full type inference for merged global + command options. See examples/nested-commands.ts for a full example.

API

bargs(name, options?)

Create a CLI builder.

OptionTypeDescription
descriptionstringDescription shown in help
versionstringEnables --version flag
epilogstring or falseFooter text in help (see Epilog)
themeThemeHelp color theme (see Theming)

.globals(parser)

Set global options and transforms that apply to all commands.

bargs('my-cli').globals(opt.options({verbose: opt.boolean()}));// ...

.command(name, parser, handler, description?)

Register a command. The handler receives merged global + command types.

.command('build',opt.options({watch: opt.boolean()}),({ values })=>{// values has both global options AND { watch: boolean }console.log(values.verbose,values.watch);},'Build the project',)

.command(name, factory, description?)

Register a nested command group using a factory function. The factory receives a builder that already has parent globals typed, giving full type inference in nested handlers.

bargs('main').globals(opt.options({verbose: opt.boolean()})).command('nested',(nested)=>nested.command('foo',fooParser,({ values })=>{// values.verbose is typed correctly!}).command('bar',barParser,barHandler),'Nested commands',).parseAsync();

.defaultCommand(name)

Or .defaultCommand(parser, handler)

Set the command that runs when no command is specified.

// Reference an existing command by name.defaultCommand('list')// Or define an inline default.defaultCommand(pos.positionals(pos.string({name: 'file'})),({ positionals })=>console.log(positionals[0]),)

.parse(args?) / .parseAsync(args?)

Parse arguments and execute handlers.

  • .parse() - Synchronous. Throws if any transform or handler returns a Promise.
  • .parseAsync() - Asynchronous. Supports async transforms and handlers.
// Async (supports async transforms/handlers)constresult=awaitbargs('my-cli').globals(...).parseAsync();console.log(result.values,result.positionals,result.command);// Sync (no async transforms/handlers)constresult=bargs('my-cli').globals(...).parse();

Option Helpers

import{opt}from'@boneskull/bargs';opt.string({default: 'value'});// --name valueopt.number({default: 42});// --count 42opt.boolean({aliases: ['v']});// --verbose, -vopt.boolean({aliases: ['v','verb']});// --verbose, --verb, -vopt.enum(['a','b','c']);// --level aopt.array('string');// --file x --file yopt.array(['low','medium','high']);// --priority low --priority highopt.count();// -vvv → 3

Option Properties

PropertyTypeDescription
aliasesstring[]Short (['v'] for -v) or long aliases (['verb'] for --verb)
defaultvariesDefault value (makes the option non-nullable)
descriptionstringHelp text description
groupstringGroups options under a custom section header
hiddenbooleanHide from --help output
requiredbooleanMark as required (makes the option non-nullable)

Aliases

Options can have both short (single-character) and long (multi-character) aliases:

opt.options({verbose: opt.boolean({aliases: ['v','verb']}),output: opt.string({aliases: ['o','out']}),});

All of these are equivalent:

$ my-cli -v # verbose: true
$ my-cli --verb # verbose: true
$ my-cli --verbose # verbose: true
$ my-cli -o file.txt # output: "file.txt"
$ my-cli --out file.txt # output: "file.txt"
$ my-cli --output file.txt # output: "file.txt"

For non-array options, using both an alias and the canonical name throws an error:

$ my-cli --verb --verbose
Error: Conflicting options: --verb and --verbose cannot both be specified

For array options, values from all aliases are merged. Single-character aliases and the canonical name are processed first (in command-line order), then multi-character aliases are appended:

opt.options({files: opt.array('string',{aliases: ['f','file']}),});
$ my-cli --file a.txt -f b.txt --files c.txt
# files: ["b.txt", "c.txt", "a.txt"]# (-f and --files first, then --file appended)

Boolean Negation (--no-)

All boolean options automatically support a negated form --no-<flag> to explicitly set the option to false:

$ my-cli --verbose # verbose: true
$ my-cli --no-verbose # verbose: false
$ my-cli # verbose: undefined (or default)

If both --flag and --no-flag are specified, bargs throws an error:

$ my-cli --verbose --no-verbose
Error: Conflicting options: --verbose and --no-verbose cannot both be specified

In help output, booleans with default: true display as --no-<flag> (since that's how users would turn them off):

opt.options({colors: opt.boolean({default: true,description: 'Use colors'}),});// Help output shows: --no-colors Use colors [boolean] default: true

opt.options(schema)

Create a parser from an options schema:

constparser=opt.options({verbose: opt.boolean({aliases: ['v']}),output: opt.string({default: 'out.txt'}),});// Type: Parser<{ verbose: boolean | undefined, output: string }, []>

Positional Helpers

import{pos}from'@boneskull/bargs';pos.string({required: true});// <file>pos.number({default: 8080});// [port]pos.enum(['dev','prod']);// [env]pos.variadic('string');// [files...]

Positional Properties

PropertyTypeDescription
defaultvariesDefault value
descriptionstringHelp text description
namestringDisplay name in help (defaults to arg0, arg1, ...)
requiredbooleanMark as required (shown as <name> vs [name])

pos.positionals(...defs)

Create a parser from positional definitions:

constparser=pos.positionals(pos.string({name: 'source',required: true}),pos.string({name: 'dest',required: true}),);// Type: Parser<{}, [string, string]>

Use variadic for rest arguments (must be last):

constparser=pos.positionals(pos.variadic('string',{name: 'files'}));// Type: Parser<{}, [string[]]>

Merging Parsers

Use merge() to combine multiple parsers into one:

import{merge,opt,pos}from'@boneskull/bargs';constcombined=merge(opt.options({priority: opt.enum(['low','medium','high'],{default: 'medium'}),}),pos.positionals(pos.string({name: 'task',required: true})),);// Type: Parser<{ priority: 'low' | 'medium' | 'high' }, [string]>

You can merge as many parsers as needed—options are merged (later overrides earlier), and positionals are concatenated.

Alternatively, parsers can be merged by calling one with the other:

constoptions=opt.options({priority: opt.enum(['low','medium','high'])});constpositionals=pos.positionals(pos.string({name: 'task',required: true}),);// These are equivalent:constcombined1=positionals(options);constcombined2=options(positionals);

Use whichever style you find more readable.

Transforms

Use map() to transform parsed values before they reach your handler:

import{bargs,map,opt}from'@boneskull/bargs';constglobals=map(opt.options({config: opt.string(),verbose: opt.boolean({default: false}),}),({ values, positionals })=>({
positionals,values: {
...values,// Add computed propertiestimestamp: newDate().toISOString(),configLoaded: !!values.config,},}),);awaitbargs('my-cli').globals(globals).command('info',opt.options({}),({ values })=>{// values.timestamp and values.configLoaded are availableconsole.log(values.timestamp);},'Show info',).parseAsync();

Transforms are fully type-safe—the return type becomes the type available in handlers.

Async Transforms

Transforms can be async:

constglobals=map(opt.options({url: opt.string({required: true})}),async({ values, positionals })=>{constresponse=awaitfetch(values.url);return{
positionals,values: {
...values,data: awaitresponse.json(),},};},);

CamelCase Option Keys

If you prefer camelCase property names instead of kebab-case, use the camelCaseValues transform:

import{bargs,map,opt,camelCaseValues}from'@boneskull/bargs';const{ values }=awaitbargs('my-cli').globals(map(opt.options({'output-dir': opt.string({default: '/tmp'}),'dry-run': opt.boolean(),}),camelCaseValues,),).parseAsync(['--output-dir','./dist','--dry-run']);console.log(values.outputDir);// './dist'console.log(values.dryRun);// true

The camelCaseValues transform:

  • Converts all kebab-case keys to camelCase (output-diroutputDir)
  • Preserves keys that are already camelCase or have no hyphens
  • Is fully type-safe—TypeScript knows the transformed key names

Epilog

By default, bargs displays your package's homepage and repository URLs (from package.json) at the end of help output. URLs become clickable hyperlinks in supported terminals.

// Custom epilogbargs('my-cli',{epilog: 'For more info, visit https://example.com',});// Disable epilog entirelybargs('my-cli',{epilog: false});

Theming

Customize help output colors with built-in themes or your own:

// Use a built-in theme: 'default', 'mono', 'ocean', 'warm'bargs('my-cli',{theme: 'ocean'});// Disable colors entirelybargs('my-cli',{theme: 'mono'});

The ansi export provides common ANSI escape codes for styled terminal output:

import{ansi}from'@boneskull/bargs';bargs('my-cli',{theme: {colors: {command: ansi.bold,flag: ansi.brightCyan,positional: ansi.magenta,// ...},},});

Available theme color slots:

SlotWhat it styles
commandCommand names (e.g., init, build)
defaultTextThe default: label
defaultValueDefault value (e.g., false, "hello")
descriptionDescription text for options and commands
epilogFooter text (homepage, repository)
exampleExample code/commands
flagFlag names (e.g., --verbose, -v)
positionalPositional argument names (e.g., <file>)
scriptNameCLI name shown in header
sectionHeaderSection headers (e.g., USAGE, OPTIONS)
typeType annotations (e.g., [string], [number])
urlURLs (for clickable hyperlinks)
usageThe usage line text

Tip

You don't need to specify all color slots. Missing colors fall back to the default theme.

Shell Completion

bargs can generate shell completion scripts for bash, zsh, and fish. Enable it with the completion option:

bargs('my-cli',{completion: true,version: '1.0.0',});

Then generate and install the completion script for your shell:

Bash

# Add to ~/.bashrc (or ~/.bash_profile on macOS)
my-cli --completion-script bash >>~/.bashrc
source~/.bashrc

Zsh

# Add to ~/.zshrc
my-cli --completion-script zsh >>~/.zshrc
source~/.zshrc
# Or save to a file in your $fpath
my-cli --completion-script zsh >~/.zsh/completions/_my-cli

Fish

# Save to completions directory
my-cli --completion-script fish >~/.config/fish/completions/my-cli.fish

What Gets Completed

Once installed, pressing Tab will complete:

  • Commands and subcommands (including nested commands and aliases)
  • Options (--verbose, -v, --no-verbose for booleans)
  • Enum values for options and positionals with defined choices
  • Global options at any command level
$ my-cli <TAB>
build test lint
$ my-cli build --target <TAB>
dev staging prod
$ my-cli --<TAB>
--verbose --config --help --version

See examples/completion.ts for a complete example.

Advanced Usage

Process Termination

bargs automatically terminates the process (via process.exit()) in certain scenarios. This is standard CLI behavior—users expect these flags to print output and exit immediately:

ScenarioExit CodeOutput
--help / -h0Help text to stdout
--version0Version string to stdout
--completion-script0Shell completion script to stdout
Unknown command1Error + help text to stderr
Missing required command1Error + help text to stderr
--get-bargs-completions0Completion candidates to stdout

For testing, you can mock process.exit to capture the exit code:

// Sentinel error to distinguish process.exit from other errorsclassProcessExitErrorextendsError{constructor(publiccode: number){super(`process.exit(${code})`);}}constoriginalExit=process.exit;letexitCode: number|undefined;process.exit=((code?: number)=>{exitCode=code??0;thrownewProcessExitError(exitCode);})astypeofprocess.exit;try{cli.parse(['--help']);}catch(err){if(!(errinstanceofProcessExitError)){throwerr;// Re-throw unexpected errors}}finally{process.exit=originalExit;// Always restore}console.log(exitCode);// 0

Error Handling

bargs exports some Error subclasses for errors that don't cause automatic process termination:

import{bargs,BargsError,ValidationError}from'@boneskull/bargs';try{awaitbargs('my-cli').parseAsync();}catch(error){if(errorinstanceofValidationError){// Config validation failed (e.g., invalid schema)// i.e., "you screwed up"console.error(`Config error at "${error.path}": ${error.message}`);}elseif(errorinstanceofBargsError){// General bargs errorconsole.error(error.message);}}

Programmatic Help

Generate help text programmatically:

import{generateHelp,generateCommandHelp}from'@boneskull/bargs';// These require the internal config structure—see source for detailsconsthelpText=generateHelp(config);constcommandHelp=generateCommandHelp(config,'migrate');

Hyperlink Utilities

Create clickable terminal hyperlinks (OSC 8):

import{link,linkifyUrls,supportsHyperlinks}from'@boneskull/bargs';// Check if terminal supports hyperlinksif(supportsHyperlinks()){// Create a hyperlinkconsole.log(link('Click me','https://example.com'));// Auto-linkify URLs in textconsole.log(linkifyUrls('Visit https://example.com for more info'));}

Tip

bargs already automatically links URLs in --help output if the terminal supports hyperlinks.

Additional Theme Utilities

import{ansi,// ANSI escape codescreateStyler,// Create a styler from a themedefaultTheme,// The default theme objectstripAnsi,// Remove ANSI codes from stringthemes,// All built-in themes}from'@boneskull/bargs';// Create a custom stylerconststyler=createStyler({colors: {flag: ansi.green}});console.log(styler.flag('--verbose'));// Strip ANSI codes for plain text outputconstplain=stripAnsi('\x1b[32m--verbose\x1b[0m');// '--verbose'

Low-Level Utilities

The handle(parser, fn) function is exported for advanced use cases where you need to create a Command object outside the fluent builder. It's mostly superseded by .command(name, parser, handler).

Dependencies

bargs has zero (0) dependencies. Only Node.js v20+.

Motivation

I've always reached for yargs in my CLI projects. However, I find myself repeatedly doing the same things; I have a sort of boilerplate in my head, ready to go (requiresArg: true and nargs: 1, amirite?). I don't want boilerplate in my head. I wanted to distill my chosen subset of yargs' behavior into a composable API. And so bargs was begat.

License

Copyright © 2025 Christopher "boneskull" Hiller. Licensed under the Blue Oak Model License 1.0.0.