Simple, lightweight, flexible, and comprehensive Un*x Command Line Argument Parsing for Node.js.
cli, default, user)-# flag to consume ambiguous argumentsnpm install @fynjs/cli-args
import { NixClap } from "@fynjs/cli-args";
new NixClap()
.version("1.0.0")
.init2({
subCommands: {
build: {
desc: "Build the project",
exec: cmd => console.log("Building...", cmd.opts, cmd.args)
},
test: {
desc: "Run tests",
exec: cmd => console.log("Testing...", cmd.opts, cmd.args)
}
}
})
.parse();
$ my-cli build # "Building..."
$ my-cli test # "Testing..."
$ my-cli --help # Shows help
$ my-cli --version # Shows version
import { NixClap } from "@fynjs/cli-args";
const parsed = new NixClap()
.init2({
options: {
name: { alias: "n", desc: "Your name", args: "<string>" }
}
})
.parse();
// Access parsed options
const name = parsed.command.opts.name;
if (name) {
console.log(`Hello ${name}!`);
}
$ my-cli --name Alice # "Hello Alice!"
$ my-cli -n Bob # "Hello Bob!"
import { NixClap } from "@fynjs/cli-args";
new NixClap()
.init2({
subCommands: {
copy: {
desc: "Copy files",
args: "<source string> <dest string>",
exec: cmd => {
const { source, dest } = cmd.args;
console.log(`Copying ${source} to ${dest}`);
}
}
}
})
.parse();
$ my-cli copy file.txt backup.txt
# "Copying file.txt to backup.txt"
import { NixClap } from "@fynjs/cli-args";
new NixClap({ name: "process" })
.init2({
args: "<files string..>",
exec: cmd => {
const files = cmd.args.files;
console.log(`Processing: ${files.join(", ")}`);
}
})
.parse();
$ process file.txt # "Processing: file.txt"
$ process file1.txt file2.txt # "Processing: file1.txt, file2.txt"
Note: Root command only executes when:
- Arguments are provided on the command line, AND
- No sub-command matches those arguments
This means sub-commands always take precedence. For CLIs that should run without any arguments, use a default command instead.
import { NixClap } from "@fynjs/cli-args";
const nc = new NixClap()
.version("1.0.0")
.usage("$0 [options] <command> [options]")
.init2({
options: {
verbose: {
desc: "enable verbose mode",
alias: "v",
args: "<flag boolean>",
argDefault: "false"
}
},
subCommands: {
compile: {
desc: "run compile on the files",
args: "<files...>",
exec: cmd => {
const meta = cmd.jsonMeta;
console.log("compile", meta.args.files, "verbose", meta.opts.verbose);
}
}
}
});
nc.parse();
Important:
version(),help(), andusage()must be called beforeinit2().
Usage:
$ my-prog compile --verbose file1.jsx file2.jsx file3.jsx
const nc = new NixClap().init2({
subCommands: {
deploy: {
desc: "Deploy to production",
exec: async cmd => {
await someAsyncOperation();
console.log("Deployed!");
}
}
}
});
await nc.parseAsync(); // Use parseAsync for async exec handlers
@fynjs/cli-args provides multiple ways to access parsed data:
const parsed = nc.parse();
// Access via jsonMeta (recommended)
const opts = parsed.command.jsonMeta.opts; // { verbose: true, output: "file.txt" }
const args = parsed.command.jsonMeta.args; // { files: ["a.js", "b.js"] }
const source = parsed.command.jsonMeta.source; // { verbose: "cli" }
// Access sub-commands
const subCmd = parsed.command.jsonMeta.subCommands.compile;
// Check for errors
if (parsed.errorNodes && parsed.errorNodes.length > 0) {
console.error(
"Parse errors:",
parsed.errorNodes.map(n => n.error.message)
);
}
Understanding opts vs optsFull
@fynjs/cli-args provides two ways to access option values:
opts - Simplified Direct Values (Recommended)
The first/only argument value is stored directly on the option name:
const nc = new NixClap().init2({
options: {
output: { args: "<path string>", desc: "Output file" },
count: { args: "<num number>", desc: "Item count" },
format: { args: "<fmt>", desc: "Output format" }
},
exec: cmd => {
const meta = cmd.jsonMeta;
// ✅ Correct - access values directly
const output = meta.opts.output; // "file.txt"
const count = meta.opts.count; // 42
const format = meta.opts.format; // "json"
}
});
optsFull - Complete Objects with Argument Names
All arguments are stored with both positional index and their named arguments:
const nc = new NixClap().init2({
options: {
output: { args: "<path string>" },
count: { args: "<num number>" }
},
exec: cmd => {
const meta = cmd.jsonMeta;
// Access by argument name
const path = meta.optsFull.output?.path; // "file.txt"
const num = meta.optsFull.count?.num; // 42
// Or by positional index
const first = meta.optsFull.output?.[0]; // "file.txt"
}
});
Structure comparison:
// Given: --output result.txt --count 42
// meta.opts (simplified)
{
output: "result.txt", // Direct value
count: 42 // Direct value
}
// meta.optsFull (complete)
{
output: {
0: "result.txt", // Positional
path: "result.txt" // Named from args: "<path string>"
},
count: {
0: 42, // Positional
num: 42 // Named from args: "<num number>"
}
}
In TypeScript, coerced values are unknown because custom converters can return any value.
Narrow them before using type-specific methods, for example:
const count = meta.optsFull.count.num;
if (typeof count === "number") {
console.log(count.toFixed(2));
}
Common mistake:
// ❌ WRONG - trying to access argument name on simplified value
const output = meta.opts.output?.path; // undefined! (output is a string)
// ✅ CORRECT - use optsFull for argument names
const output = meta.optsFull.output?.path; // "file.txt"
// ✅ BETTER - use opts for simplicity
const output = meta.opts.output; // "file.txt"
@fynjs/cli-args provides two initialization methods. init2() is the recommended primary API, while init() is a convenience wrapper for simple cases.
init2() (Recommended)init2() is the modern, flexible API that allows you to define everything in one place:
new NixClap().init2({
args: "[files string..]", // Root command arguments
exec: cmd => {
/* ... */
}, // Root command handler
options: {
/* ... */
}, // Global options
subCommands: {
/* ... */
} // Sub-commands
});
Benefits:
When to use:
mycli file.txt)init() (Legacy/Convenience)init() is a simpler wrapper that's useful for backwards compatibility or when you only need options and sub-commands:
new NixClap().init(
{
/* options */
},
{
/* commands */
}
);
This is equivalent to:
new NixClap().init2({
options: {
/* options */
},
subCommands: {
/* commands */
}
});
When to use:
init()Tip: When in doubt, use
init2()- it's more flexible and is the recommended approach going forward.
Complete Example:
import { NixClap } from "@fynjs/cli-args";
const nc = new NixClap({ name: "file-processor" })
.version("1.0.0")
.usage("$0 [options] <input-file> [additional-files...]")
.init2({
// Root command arguments
args: "[inputFile string] [additionalFiles string..]",
// Root command execution handler
exec: cmd => {
const meta = cmd.jsonMeta;
console.log("Processing:", meta.args.inputFile);
console.log("Additional files:", meta.args.additionalFiles);
console.log("Output:", meta.opts.output);
},
// Root-level options (available to root command and sub-commands)
options: {
output: { alias: "o", desc: "Output file path", args: "<path string>" },
verbose: { alias: "v", desc: "Enable verbose output" }
},
// Sub-commands (optional)
subCommands: {
convert: {
desc: "Convert file format",
args: "<input string> <output string>",
exec: cmd => console.log("Converting...")
}
}
});
nc.parse();
Usage examples:
# Root command (direct file processing)
$ file-processor input.txt --output result.txt
$ file-processor file1.txt file2.txt file3.txt --verbose
# Sub-commands still work
$ file-processor convert input.txt output.txt
$ file-processor --help
Important notes:
[arg]) rather than required (<arg>) to avoid parsing ambiguity with sub-command namesinit2() are available to both the root command and sub-commandsWhen you configure a root command with exec and args, @fynjs/cli-args determines whether to execute it based on this logic:
Preprocess CLI arguments
↓
Are there only option args (no non-option args)?
├─ YES → Insert default command upfront (if configured)
└─ NO → Continue parsing normally
↓
Parse CLI arguments
↓
Does a sub-command match?
├─ YES → Execute matched sub-command (root command skipped)
└─ NO → Continue to next check
↓
Are non-option arguments provided?
├─ NO → Execute default command (if inserted during parsing)
│ └─ No default? → Emit 'no-action' event
└─ YES → Execute root command
Key Points:
Example Scenarios:
# CLI defined with: init2({ args: "[file]", exec: rootHandler, subCommands: { build: {...} } })
$ mycli file.txt # ✅ Executes root command (no sub-command match, args provided)
$ mycli build # ✅ Executes 'build' sub-command (sub-command match)
$ mycli # ❌ No execution (no args, no default command)
$ mycli --help # ⚠️ Shows help (--help is handled before execution logic)
Multiple Arguments with Sub-Commands:
When multiple non-option arguments are provided and none match sub-command names, all arguments are passed to the root command:
# CLI defined with: init2({
# args: "[arg1 string] [arg2 string] [arg3 string]",
# exec: rootHandler,
# subCommands: { build: {...}, test: {...} }
# })
$ mycli arg1 arg2 arg3 # ✅ Executes root command with all 3 args
# (none match 'build' or 'test', so all go to root)
$ mycli build # ✅ Executes 'build' sub-command (first arg matches)
$ mycli arg1 build # ✅ Executes 'build' sub-command (second arg matches)
# (arg1 is ignored, only 'build' executes)
Try the examples above: simple-cli.ts, cli-with-options.ts, cli-command-args.ts, simple-root-command.ts
See examples folder for more working samples:
Getting Started:
Root Commands:
Options and Arguments:
Advanced Features:
TypeScript:
@fynjs/cli-args implements comprehensive Un*x-style CLI parsing with several unique design principles:
Core Philosophy:
-., --., --) to resolve ambiguity rather than guessingUnique Aspects:
Command-Centric Parsing & Execution: Unlike option-centric parsers, @fynjs/cli-args treats commands as the organizing principle
jsonMeta containing its args, options, and stateMultiple Commands in One Invocation: Execute multiple commands in a single CLI call
prog add 1 2 -. mult 3 4 executes both add and mult commandsFlexible Command Hierarchies: Commands can have sub-commands, sub-sub-commands, and each level can have its own options
Smart Argument Termination: Variadic arguments are automatically terminated by:
-x or --foo)-. or --.)Greedy Mode Control: When you need to pass arbitrary strings that look like commands/options as arguments
-#, -, or --- to enter greedy modeExecution Model: @fynjs/cli-args separates parsing from execution
skipExec)Example: prog -xazvf=hello --foo-option hello bar -. --enable-blah
- single char options or -- long form options.= or space.
--foo-option=bar or --foo-option bar-f=bar or -f bar--foo-option hello bar or -f hello bar- options can be compounded, like -xazvf.
-xazvf=hello or -xazvf hello.boolean options automatically.-x or --xyz, or explicitly with -. or --.
cmd1 arg1 arg2 --some-array abc def ghi -. cmd2 arg1 arg2.= only.
When allowUnknownOption is enabled, unknown options follow a specific resolution pattern:
Option Resolution: Unknown options bubble up to parent commands
Unknown options are first attempted to be resolved on the current command. If not found, they bubble up to parent commands until reaching the root command. This allows sub-commands to inherit options from their parents while still allowing command-specific overrides.
Root Command Storage: The root command stores unknown options
If an unknown option cannot be resolved on any command in the hierarchy, it gets stored on the root command (when allowUnknownOption: true). This makes the root command the central repository for all unrecognized options.
Access Pattern: Use cmd.rootCmd.opts to access parent/root command options
To access options that may be stored on parent commands or the root:
const nc = new NixClap({ allowUnknownOption: true }).init2({
subCommands: {
build: {
exec: cmd => {
// Access options from current command
const localOpts = cmd.opts;
// Access options from root command (includes bubbled up unknown options)
const rootOpts = cmd.rootCmd.opts;
// Combine or prioritize as needed
const verbose = localOpts.verbose || rootOpts.verbose;
console.log("Verbose:", verbose);
}
}
}
});
Example:
# Unknown option --custom-flag bubbles up to root command
$ my-cli build --custom-flag=value
# In the build command exec handler:
exec: cmd => {
console.log(cmd.opts.customFlag); // undefined (not on build command)
console.log(cmd.rootCmd.opts.customFlag); // "value" (stored on root)
}
This behavior enables flexible option inheritance while maintaining command-specific option isolation.
--no- prefix.Example: prog add 1 2 3 4
string, but can have an optional type-x or --xyz, or explicitly with -. or --.
prog order pizza soda -. pickup (specifies two commands: order and pickup)Example: prog add 1 2 3 4 -. mult 4 5 6 7 8
First Command:
addwith four numbers input Second Command:multwith five numbers input
-. in the middle terminates the add command.Example: prog calc add 1 2 3 4 -. mult 4 5 6 7
Main command:
calcSub Command:addthat can take variadic number of numbers Sub Command:multthat can take variadic number of numbers
-. in the middle terminates the add sub command.By default, sub-commands cannot define options with the same name as their parent command. To enable this feature, set allowDuplicateOption: true in the NixClap configuration. When enabled, the sub-command's option will shadow (override) the parent's option when parsing at that command level.
Example:
const nc = new NixClap({
allowDuplicateOption: true // Enable duplicate option names
}).init(
{
// Root-level --verbose is a boolean flag
verbose: { alias: "v", desc: "Enable verbose output" }
},
{
build: {
desc: "Build the project",
options: {
// Sub-command --verbose takes a level argument
verbose: { alias: "v", desc: "Verbosity level", args: "<level>" }
},
exec: ({ opts, rootCmd }) => {
console.log("Root verbose:", rootCmd.opts.verbose); // boolean or undefined
console.log("Build verbose:", opts.verbose); // level string or undefined
}
}
}
);
Usage:
$ prog --verbose build --verbose=debug
# Root verbose: true
# Build verbose: debug
$ prog build --verbose=2
# Root verbose: undefined
# Build verbose: 2
$ prog -v build -v
# Root verbose: true
# Build verbose: true (boolean, since no argument provided)
Key points:
allowDuplicateOption: true in config (default is false)cmd.rootCmd.opts or by traversing the command chainCommands can enter "greedy mode" using the -#, -, or --- flags, which allows them to consume all remaining arguments blindly, even if they look like commands or options.
Example:
# Without greedy mode - "command" is parsed as a new command
$ prog compile file1.js file2.js command other arguments
# With greedy mode - everything after -# is an argument to "compile"
$ prog compile file1.js file2.js -# command other arguments
Greedy mode continues until a terminator token (-. or --.) is encountered:
$ prog compile file1.js -# some-command-name more-args -. actual-command
# ↑ ↑ ↑
# Start greedy Stop greedy New command
Use cases:
-- terminates parsing, with remaining args returned in parsed._.-. or --. can terminate variadic params for commands and options.npm install @fynjs/cli-args
Requirements:
This module exposes a class with a few methods.
See APIs for more details.
options specconst options = {
"some-option": {
alias: ["s", "so"],
desc: "description",
args: "[number cans] [beverage] [boolean diet] [string..]",
argDefault: ["6", "coke", "true", "foo"],
// customTypes defines custom type validators/coercers
customTypes: {
beverage: /^(coke|pepsi)$/ // beverage must match this RegExp
},
allowCmd: ["cmd1", "cmd2"]
},
"another-option": {}
};
Where:
| field | description |
|---|---|
alias |
Specify aliases for the option, as a single string or an array of strings. |
args |
Arguments for the option. <type name> means it's required and [type name] optional. |
desc |
Description for the option - a string or a function that returns string. |
argDefault |
Default values to use when all args are optional. |
required |
true/false whether this option must be specified. |
allowCmd |
list of command names this option is allow to follow only. |
customTypes |
Specify value coercion for custom types. Keys are type names, values are converters. |
counting |
Set to a number (e.g. Infinity) to count option occurrences. The value is not enforced as a maximum. |
Note: Options with kebab-case names (like
some-option) are automatically accessible in camelCase (someOption) in the parsed results.
commands specCommand spec share some properties that are the same as option spec.
Command doesn't support the following:
allowCmd property.count type.Command supports a few more properties: usage, exec, and options.
const commands = {
// Regular sub-commands
cmd1: {
alias: ["c"],
desc: "description",
args: "[number cans] [enum] [boolean diet] [string..]",
argDefault: ["6", "coke", "true", "foo"],
customTypes: {
enum: /^(coke|pepsi)$/
},
usage: "$0 $1",
exec: cmd => {
// cmd is the CommandNode instance
const meta = cmd.jsonMeta;
console.log(meta.args, meta.opts);
},
options: {},
subCommands: {}
},
cmd2: {}
};
Where:
| field | description |
|---|---|
alias |
Specify aliases for the command, as a single string or an array of strings. |
args |
Specify arguments for the command. <> means it's required and [] optional. See rules for more info. |
usage |
Usage message when help for the command is invoked - a string or a function that returns a string. |
$0 will be replaced with program name and $1 with command name. |
|
desc |
Description for the command - can be a string or a function that returns a string. |
exec |
The callback handler for the command. Receives CommandNode as first arg. Can be async. |
options |
Options private to this command only. Follows the same spec as top level options |
subCommands |
Nested sub-commands under this command. Follows the same spec as commands. |
allowUnknownOption |
If true, allows unknown options for this command. |
Note: Set a default command via the
defaultCommandconfig option in theNixClapconstructor, not in the command spec.
argsThe args string uses a specific format to define command and option arguments:
Basic Format:
<arg> - Required argument[arg] - Optional argument"<first> <second> [third]"Naming and Types:
<name type> or [name type]<name> or [name]<type> or [type]<> or []string, number, float, boolean, count (options only), or custom types via coercionBuilt-in Type Behaviors:
| Type | Coercion Behavior | Example Input | Parsed Value | Notes |
|---|---|---|---|---|
string |
No coercion | "hello" |
"hello" |
Default type if not specified |
number |
parseInt(val, 10) |
"42" |
42 |
Parses as integer |
float |
parseFloat(val) |
"3.14" |
3.14 |
Parses as floating point |
boolean |
Checks for truthy values | "true", "1", "yes" |
true |
"false", "0", "no" → false |
count |
Increments on each use | -vvv |
3 |
Only for options, counts occurrences |
Type Coercion Failures:
When a value cannot be coerced to the specified type:
NaNregex-unmatch eventargDefault is specified, the default value is usedExamples:
args: "<filename string>"; // Required named string arg
args: "<count number>"; // Required named number arg (parsed as integer)
args: "<price float>"; // Required named float arg (parsed as decimal)
args: "[enabled boolean]"; // Optional named boolean arg
args: "[output]"; // Optional named string arg (default type)
args: "<input> [output]"; // Required + optional args
args: "<value boolean> <name>"; // Multiple typed args
Array Arguments:
Use .. suffix to specify array arguments (must be the last argument):
Unlimited variadic: <args..> or [args..]
"<files..>" accepts any number of filesFixed size: <args..N>
"<coords..3>" requires exactly 3 coordinatesRange with minimum: <args..N,> or <args..N,Inf>
"<files..1,>" requires 1+ files"<names..2,Inf>" requires 2+ namesRange with min and max: <args..N,M>
"<items..1,3>" accepts 1-3 items"<values..2,5>" accepts 2-5 valuesArray Examples:
args: "<files string..>"; // 0+ files (variadic)
args: "<coords number..3>"; // Exactly 3 numbers
args: "<names..1,>"; // 1+ arguments (at least one required)
args: "<items string..1,5>"; // 1-5 string items
args: "[files..]"; // 0+ optional files
args: "<input> [extras..2,]"; // Required input + 2+ optional extras
Special Cases:
<..>, <..3>, <..1,>, <..1,5><string..>, <number..3>, <boolean..1,3>Rules:
jsonMeta.argsIf none of the predefined types work for you, you can specify your own as a function or a RegExp, or any value.
You use any valid identifier for the value type, and then you define a field with the same name in your spec that can be:
function - will be called with the value to convertRegExp - will be used to match the value. undefined is returned if it didn't match.For example:
const options = {
customFn: {
args: "<val fnval>",
customTypes: {
fnval: value => value.substring(0, 1)
}
},
customRegex: {
args: "<val rx>",
customTypes: {
rx: /^test$/i
}
},
customValue: {
args: "<val foo>",
customTypes: {
foo: "bar" // Always returns "bar"
}
}
};
const commands = {
foo: {
args: "<value1 type1> <value2 type2>",
customTypes: {
type1: value => `test-${value}`,
type2: /^test$/i
}
}
};
See examples/value-coercion.ts for a working example.
Use the method parse to parse command line arguments. It will return a parse result object.
{
command: {
jsonMeta: {
name: "~root-command~",
alias: "~root-command~",
argList: [],
args: {},
opts: {},
optsFull: {},
optsCount: {},
source: {},
verbatim: {},
subCommands: {}
}
},
execCmd: undefined, // Set after runExec/runExecAsync - the command that was executed
index: 5,
error,
_: [],
argv: []
}
Where:
command - The parsed command object with a jsonMeta property containing detailed informationexecCmd - The command that was executed (set after runExec/runExecAsync). This is the primary command that ran, useful for accessing the executed command directly without traversing the command tree.index - the index in argv parse stoppederror - If parse failed and your parse-fail event handler throws, then this will contain the parse error. See skip default event behaviors for more details.argv - original array of argv_ - remaining args in the argv array in case parsing was terminated by --.If any command with exec handlers were specified, then parse will invoke them before returning the parse result object.
@fynjs/cli-args provides comprehensive error tracking during parsing. Errors are collected in parsed.errorNodes:
Checking for Errors:
const parsed = nc.parse();
if (parsed.errorNodes && parsed.errorNodes.length > 0) {
console.error("Parsing errors occurred:");
parsed.errorNodes.forEach(node => {
console.error(` - ${node.error.message}`);
});
process.exit(1);
}
Common Error Types:
| Error Type | When It Occurs | Example |
|---|---|---|
InvalidArgSpecifierError |
Invalid args spec format | args: "<required> [optional]" (required after optional) |
UnknownOptionError |
Unknown option encountered | --unknown when allowUnknownOption: false |
UnknownCliArgError |
Unknown argument provided | Extra args when strict mode enabled |
| Missing required argument | Required arg not provided | <file> not provided |
| Type coercion failure | Value doesn't match custom type | --port abc when expecting number |
| RegExp validation failure | Value doesn't match RegExp | --env prod when only /(dev|test)/ allowed |
Error Handling Patterns:
// Pattern 1: Let default handlers work (shows help + exits)
const nc = new NixClap().init(options, commands);
nc.parse(); // Errors trigger help automatically
// Pattern 2: Custom error handling (disable default handlers)
const nc = new NixClap({ noDefaultHandlers: true }).init(options, commands);
const parsed = nc.parse();
if (parsed.errorNodes?.length) {
// Handle errors your way
logErrors(parsed.errorNodes);
showCustomHelp();
process.exit(1);
}
// Pattern 3: Selective error handling
const nc = new NixClap().removeDefaultHandlers("parse-fail").init(options, commands);
const parsed = nc.parse();
if (parsed.errorNodes?.length) {
// Custom handling for parse errors only
}
Accessing Error Details:
parsed.errorNodes.forEach(node => {
console.log("Error message:", node.error.message);
console.log("Error type:", node.error.constructor.name);
console.log("Node type:", node.type); // "option", "command", etc.
if (node.type === "option") {
console.log("Option name:", node.name);
}
});
The command.jsonMeta object contains the following information:
{
name: "~root-command~",
alias: "~root-command~",
argList: [],
args: {},
opts: {}, // contains the parsed values for each option (coerced to proper types)
optsFull: {}, // contains each option's complete argument map after coercion
optsCount: {}, // contains the count for counting options
source: {}, // contains info about where the option value came from
verbatim: {}, // contains original unprocessed values as provided on command line
subCommands: {} // contains parsed sub-commands
}
The source field can have the following values:
cli - option specified by user in the command linecli-default - User specified a value that didn't match RegExp and fallback to default.cli-unmatch - User specified a value that didn't match RegExp and there's no default to fallback to.default - default value in your options specuser - values from the config hook, or from calling the applyConfig methodexec handlerIf the command has an exec handler, it receives the following arguments:
exec(cmd, parsed);
cmd - The CommandNode instance for this commandparsed - The parsed result containing remaining args after -- (optional)You can access command-specific arguments and options through the jsonMeta property:
exec(cmd) {
const meta = cmd.jsonMeta;
console.log("Args:", meta.args); // { filename: "test.js", count: 5 }
console.log("Options:", meta.opts); // { verbose: true }
console.log("Source:", meta.source); // { verbose: "cli" }
console.log("Sub-commands:", meta.subCommands);
// Access command chain (sequence from root to this command)
const cmdChain = cmd.cmdChain; // Array of CommandNodes from root to this command
}
Accessing remaining args after --:
exec(cmd, parsed) {
// Regular command arguments
console.log("Args:", cmd.jsonMeta.argList); // ["a", "b", "c"]
// Remaining args after --
if (parsed && parsed._.length > 0) {
console.log("Remaining:", parsed._); // ["d", "e", "f", "--blah"]
}
}
Async handlers:
exec: async (cmd, parsed) => {
await someAsyncTask();
// Use parseAsync() to ensure this completes before parse returns
};
You can turn off automatic exec invocation with the
skipExecconfig flag passed toNixClapconstructor
NixClap extends EventEmitter and emits various events during the parsing and execution lifecycle.
| Event | When Emitted | Parameters | Common Use Case |
|---|---|---|---|
pre-help |
Before help output is displayed | { self: NixClap } |
Modify help display, add headers |
help |
When --help is requested |
ParseResult |
Custom help formatting |
post-help |
After help output is displayed | { self: NixClap } |
Add footer, cleanup |
parsed |
After parsing, before exec handlers | { nixClap: NixClap, parsed: ParseResult } |
Validation, logging, transformations |
parse-fail |
When parsing encounters errors | ParseResult (with errorNodes) |
Custom error reporting |
unknown-option |
Unknown option encountered | string (option name) |
Dynamic option handling |
unknown-command |
Unknown command encountered | { name: string, ... } |
Dynamic command routing |
no-action |
No command with exec was invoked | none | Show help or default behavior |
regex-unmatch |
Value doesn't match RegExp validator | { value: string, name: string, ... } |
Custom validation messages |
exit |
Program should terminate | number (exit code) |
Cleanup, logging before exit |
User runs CLI
↓
parse() called
↓
Argument parsing
↓
emit('parsed') ← After parsing, before execution
↓
Parse errors? ──YES→ emit('parse-fail') → emit('exit', 1)
↓ NO
Execute commands
↓
--help flag? ──YES→ emit('pre-help') → emit('help') → emit('post-help') → emit('exit', 0)
↓ NO
No exec invoked? ──YES→ emit('no-action') → emit('exit', 1)
↓ NO
Return parse result
Basic Event Listening:
const nc = new NixClap().init(options, commands);
nc.on("parsed", ({ parsed }) => {
console.log("Parsed successfully:", parsed.command.name);
});
nc.on("parse-fail", parsed => {
console.error("Parse failed with", parsed.errorNodes.length, "errors");
});
nc.parse();
Custom Help Handling:
nc.on("pre-help", ({ self }) => {
console.log("╔═══════════════════╗");
console.log("║ My Awesome CLI ║");
console.log("╚═══════════════════╝\n");
});
nc.on("post-help", () => {
console.log("\nFor more info: https://example.com/docs");
});
Dynamic Command Handling:
nc.on("unknown-command", ctx => {
console.log(`Did you mean: ${suggestCommand(ctx.name)}?`);
process.exit(1);
});
@fynjs/cli-args has default handlers for these events:
help - Output help and emit exitversion - If version has been set, then output version and emit exit.parse-fail - Output help and error message, and emit exit.unknown-option - Throws Error Unknown option ${name}unknown-command - Throws Error Unkown command ${ctx.name}no-action - Output help with error No command given and emit exitregex-unmatch - Output a message to let user know that value didn't match and default will be used.exit - calls process.exit(code)You can remove the default event handlers with one of these approaches:
removeDefaultHandlers method.handlers object in the config for the constructor.For example, using removeDefaultHandlers:
const nc = new NixClap().init(options, commands);
const parsed = nc.removeDefaultHandlers("parse-fail").parse();
if (parsed.error) {
// handle the parse error here
}
Using constructor config.
const parsed = new NixClap({ handlers: { "parse-fail": false } }).parse();
if (parsed.error) {
// handle the parse error here
}
These are methods NixClap class supports.
constructor(config)config is an object with:
| Property | Type | Description |
|---|---|---|
name |
string |
Program name. Auto-detected from process.argv if not specified. |
version |
string | number |
Program version. Can also set with version() method. |
help |
object | false |
Custom help option setting. Can also set with help() method. |
usage |
string |
Usage message. Can also set with usage() method. |
cmdUsage |
string |
Generic usage message for commands. Can also set with cmdUsage() method. |
defaultCommand |
string |
Name of the default command to invoke when no command is given. |
unknownCommandFallback |
string |
When unknown command encountered at root, treat it as arguments to this command (e.g., "run"). |
allowUnknownCommand |
boolean |
Allow unknown commands to be parsed without error. |
allowUnknownOption |
boolean |
Allow unknown options to be parsed without error. |
allowDuplicateOption |
boolean |
Allow sub-commands to define options with the same name as parent commands. |
skipExec |
boolean |
If true, will not call command exec handlers after parse. |
skipExecDefault |
boolean |
If true, default command will not be inserted during parsing (prevents execution). |
output |
function |
Callback for printing to console. Defaults to process.stdout.write. |
exit |
function |
Custom exit function. Defaults to emitting the exit event. |
handlers |
object |
Custom event handlers (see below). |
noDefaultHandlers |
boolean |
If true, skip installing all default handlers. You must handle errors yourself. |
config |
function |
(command) => object. Loads user config during parse. See applyConfig. |
Handlers Example:
The handlers object can specify a function for each event or set it to false to turn off the default handler.
const nc = new NixClap({
handlers: {
"parse-fail": parsed => {
console.error("Parse failed:", parsed.errorNodes[0].error.message);
process.exit(1);
},
"unknown-option": false // Disable default handler
}
});
version(v)Set program version with a string. ie: 1.0.0
Return: The NixClap instance itself.
help(setting)Set a custom option setting for invoking help. Default is:
Return: The NixClap instance itself.
{
alias: "h",
desc: "Show help"
}
Option name is always help. Call help(false) to turn off the default --help option.
usage(msg), cmdUsage(msg)Set usage message for the program or command, which can be override by individual command's own usage.
msg format is any string. $0 will be replaced with program name and $1 with command name.
Return: The NixClap instance itself.
init(options, commands) - Legacy/Convenience WrapperNote: init() is a convenience wrapper around init2(). Consider using init2() directly for a more flexible and consistent API.
Initialize your options and commands using separate parameters.
Parameters:
options - Top-level options available globally (or to specific commands via allowCmd)commands - Sub-commands under the root commandReturn: The NixClap instance itself.
What this does internally:
// init(options, commands) is equivalent to:
init2({
options: options,
subCommands: commands
});
Example:
import { NixClap } from "@fynjs/cli-args";
// Using init()
const nc = new NixClap().init(
{ verbose: { alias: "v" } },
{ build: { desc: "Build project", exec: () => {} } }
);
// Equivalent using init2() (recommended)
const nc = new NixClap().init2({
options: { verbose: { alias: "v" } },
subCommands: { build: { desc: "Build project", exec: () => {} } }
});
Limitations:
init2()Recommendation: Use
init2()for new code - it's more powerful and provides a cleaner API.
init2(rootCommandSpec) - Primary API (Recommended)Initialize your CLI by defining the root command directly. This is the recommended primary API for setting up NixClap.
Parameters:
rootCommandSpec - A command spec object that defines the root command, including:
args - Arguments for the root command (optional)exec - Execution handler for the root command (optional)options - Top-level options available to root and sub-commands (optional)subCommands - Sub-commands under the root command (optional)Return: The NixClap instance itself.
Basic Example:
import { NixClap } from "@fynjs/cli-args";
// Root command with arguments
const nc = new NixClap({ name: "process" }).init2({
args: "<files string..>",
exec: cmd => {
const files = cmd.jsonMeta.args.files;
console.log("Processing:", files);
}
});
Complete Example with Options and Sub-commands:
const nc = new NixClap({ name: "file-processor" }).init2({
// Root command arguments
args: "[input string] [files string..]",
// Root command exec
exec: cmd => {
const meta = cmd.jsonMeta;
console.log("Root command:", meta.args);
console.log("Options:", meta.opts);
},
// Top-level options
options: {
verbose: { alias: "v", desc: "Verbose output" },
output: { alias: "o", desc: "Output file", args: "<path string>" }
},
// Sub-commands
subCommands: {
build: {
desc: "Build the project",
exec: cmd => console.log("Building...")
},
convert: {
desc: "Convert files",
args: "<input string> <output string>",
exec: cmd => console.log("Converting...")
}
}
});
Advantages over init():
See the Understanding init() vs init2() section for detailed comparison.
defaultCommand configThe defaultCommand option of the NixClap constructor names the command that is invoked when no non-option arguments are provided in the command line. There is no defaultCommand() method.
How it works:
exec handlerRequirements:
exec handlerExample with init2():
// Make 'build' the default command
const nc = new NixClap({ defaultCommand: "build" }).version("1.0.0").init2({
subCommands: {
build: {
desc: "Build the project",
exec: cmd => console.log("Building...")
},
test: {
desc: "Run tests",
exec: cmd => console.log("Testing...")
}
}
});
// These are equivalent:
// $ my-prog # Runs 'build' (default)
// $ my-prog build # Explicitly runs 'build'
How it interacts with root command:
When using init2() with both a root command and a default command:
const nc = new NixClap({ defaultCommand: "serve" }).init2({
args: "[files string..]", // Root command args
exec: cmd => console.log("Root:", cmd.jsonMeta.args.files),
subCommands: {
serve: { desc: "Start server", exec: () => console.log("Serving...") },
build: { desc: "Build project", exec: () => console.log("Building...") }
}
});
Execution priority:
$ my-prog build # 1. Executes 'build' sub-command
$ my-prog file.txt # 2. Executes root command with args
$ my-prog # 3. Executes 'serve' (default command, inserted during parsing)
$ my-prog --verbose # 3. Executes 'serve' with --verbose option (default command inserted during parsing)
Note: The default command is inserted during preprocessing when only option arguments are detected. This ensures options are correctly routed to the default command, while root-level options (if any) remain at the root level.
Note: When unknownCommandFallback is also configured, unknown commands take precedence over default command. See unknownCommandFallback for details.
Tip: Use default command when you have a primary action for your CLI (e.g.,
servefor a server,buildfor a build tool)
unknownCommandFallbackWhen an unknown command is encountered at the root level, treat it as arguments to a specified fallback command. For example, with unknownCommandFallback: "run", prog unknown becomes prog run unknown.
Requirements:
allowUnknownCommand is enabledExample:
const nc = new NixClap({
defaultCommand: "install",
unknownCommandFallback: "run"
}).init2({
subCommands: {
install: {
desc: "Install packages",
exec: cmd => console.log("Installing...")
},
run: {
desc: "Run a script",
args: "[script string..]",
exec: cmd => {
const script = cmd.jsonMeta.argList[0];
console.log(`Running script: ${script}`);
}
}
}
});
Behavior:
$ my-prog # Runs 'install' (defaultCommand)
$ my-prog --verbose # Runs 'install' with --verbose option (defaultCommand)
$ my-prog install # Runs 'install' explicitly
$ my-prog run build # Runs 'run' with 'build' as argument
$ my-prog build # Unknown command → becomes 'run build'
$ my-prog test x y z # Unknown command → becomes 'run test x y z'
Interaction with defaultCommand:
The unknownCommandFallback and defaultCommand work together seamlessly:
No arguments or only options → defaultCommand is inserted during parsing and executed
prog → runs default command (inserted during parsing)prog --verbose → runs default command with options (inserted during parsing)First non-option argument is unknown → unknownCommandFallback is triggered
prog unknown → becomes prog <fallback> unknownFirst non-option argument is a known command → That command runs normally
prog install → runs install commandprog run script → runs run command with script as argumentKey points:
install, run, etc.)defaultCommand insteadparse(argv, start)Parse command line. Call without any params to parse process.argv.
Return: The parse result object.
argv - array of CLI args. Defaults to process.argv.start - index for argv from where to start parsingparseAsync(argv, start)async version of parse.
exec handlers serially.Return: A promise the resolve with the parse result object.
showHelp(err, cmdName)Show help message and then emit exit.
err - if valid, then err.message will be printed after help message and exit with code 1.cmdName - if valid, then will print help for the specific command.removeDefaultHandlers()Remove NixClap's default handlers for the list of event names.
If you've replaced the handler through specifying handlers in config for the constructor, then this will not remove your handler.
Return: The NixClap instance itself.
"*" to remove all default handlers.ie:
nc.removeDefaultHandlers("parse-fail", "unknown-option", "unknown-command");
applyConfig(config, src)Apply configuration from external sources (e.g., config files) to a parsed command.
Note: This method is called on a CommandNode instance, not on the NixClap instance.
Parameters:
config - Object containing option values to applysrc - Source name for tracking. Should be one of: "cli", "cli-default", "cli-unmatch", "default", or "user" (default: "user")This method only overrides options whose source does not start with "cli", ensuring command-line arguments always take precedence.
Each key goes to the command and any parsed sub command that declares the option. Keys that match no declared option are ignored.
Recommended: pass a config hook to the constructor. It runs during parse, after argv is read and before defaults, the required check and exec. So config can satisfy a required option and reaches exec handlers. The command passed to the hook only holds CLI values, so the hook can read options like --cwd or --config to find the config file.
import { readFileSync } from "fs";
const nc = new NixClap({
config: cmd => {
const file = cmd.jsonMeta.opts.config || "./package.json";
return JSON.parse(readFileSync(file, "utf-8")).myCliConfig;
}
}).init2({ options, subCommands });
nc.parse();
Calling applyConfig after parse() is too late for required and exec, because parse() checks failures and runs exec before it returns. If you call it after parsing, set skipExec and run exec yourself. This path still can not satisfy required:
const nc = new NixClap({ skipExec: true }).init2({ options, subCommands });
const parsed = nc.parse();
parsed.command.applyConfig(pkg.myCliConfig, "user");
nc.runExec(parsed);
runExec(parsed)Go through the commands in parsed and call their exec handler.
The
parsemethod will call this at the end unlessskipExecflag is set.
Return: The number of commands with exec was invoked.
parsed - The parse result object.Note: Default command execution is controlled by the skipExecDefault config option. If skipExecDefault is true, the default command will not be inserted during parsing, and therefore won't be executed.
Note: This is meant to be called once per parsed result - parse already does it for you unless
skipExec is set. There is no "already executed" tracking, so calling it a second time on the same
result simply runs every exec handler again.
runExecAsync(parsed)Async version of runExec
Return: A promise that resolves with the number of commands with exec invoked.
parsed - The parse result object.Warning: If your exec handlers are async, use parseAsync rather than
reaching for this after a plain parse. Since parse already calls runExec unless skipExec is set,
following it with runExecAsync(parsed) to await the handlers executes everything a second time,
sub-commands included.
runExec detects this for you: if any exec handler returns a promise it cannot await, it writes a
warning naming the offending commands.
Warning: async exec handler for command 'build' invoked synchronously - myprog will not wait for it.
Use parseAsync instead of parse, or skipExec with runExecAsync.
// wrong - every exec handler runs twice
const parsed = nc.parse(argv);
await nc.runExecAsync(parsed);
// right
const parsed = await nc.parseAsync(argv);
// also right - opt out of the implicit run, then drive it yourself
const nc = new NixClap({ skipExec: true }).init(...);
const parsed = nc.parse(argv);
await nc.runExecAsync(parsed);
@fynjs/cli-args is written in TypeScript and provides full type definitions out of the box.
import { NixClap, CommandSpec, OptionSpec, ParseResult } from "@fynjs/cli-args";
const options: Record<string, OptionSpec> = {
verbose: {
alias: "v",
desc: "Enable verbose output",
args: "<boolean>"
}
};
const commands: Record<string, CommandSpec> = {
build: {
desc: "Build the project",
exec: cmd => {
// cmd is typed as CommandNode
const meta = cmd.jsonMeta;
console.log(meta.opts.verbose); // Type-safe access
}
}
};
const nc = new NixClap().init(options, commands);
const parsed: ParseResult = nc.parse();
import { NixClap, CommandSpec } from "@fynjs/cli-args";
// Define root command spec with full type safety
const rootSpec: CommandSpec = {
args: "[input string] [files string..]",
desc: "Process input files",
exec: cmd => {
const { input, files } = cmd.jsonMeta.args;
console.log("Processing:", input, files);
},
options: {
verbose: { alias: "v", desc: "Verbose output" }
},
subCommands: {
build: {
desc: "Build the project",
exec: () => console.log("Building...")
}
}
};
const nc = new NixClap().init2(rootSpec);
import { NixClap, CommandSpec, OptionSpec } from "@fynjs/cli-args";
// Define options with full types
const options: Record<string, OptionSpec> = {
output: {
alias: "o",
desc: "Output file path",
args: "<path string>"
},
verbose: {
alias: "v",
desc: "Enable verbose output"
}
};
// Define commands with full types
const commands: Record<string, CommandSpec> = {
process: {
args: "<files string..>",
desc: "Process files",
exec: cmd => {
const { files } = cmd.jsonMeta.args;
const { output, verbose } = cmd.jsonMeta.opts;
console.log("Files:", files);
console.log("Output:", output);
console.log("Verbose:", verbose);
}
}
};
// Use with init2() for root command
const nc = new NixClap().init2({
args: "[input string]",
exec: cmd => {
const { input } = cmd.jsonMeta.args;
console.log("Input:", input);
},
options,
subCommands: commands
});
Type Safety Benefits:
init2() over init()// ✅ Recommended - modern, flexible API
const nc = new NixClap().version("1.0.0").init2({
args: "[files string..]",
exec: cmd => console.log(cmd.jsonMeta.args.files),
options: { verbose: { alias: "v" } },
subCommands: { build: { desc: "Build", exec: () => {} } }
});
// ⚠️ Older approach - works but less flexible
const nc = new NixClap().init(
{ verbose: { alias: "v" } },
{ build: { desc: "Build", exec: () => {} } }
);
init2()// ✅ Correct order
const nc = new NixClap().version("1.0.0").usage("$0 [cmd]").init2({ options, subCommands });
// ❌ Wrong - version/usage/help must come before init2()
const nc = new NixClap().init2({ options, subCommands }).version("1.0.0"); // Too late!
jsonMeta for accessing parsed data// ✅ Recommended - clean and typed
const opts = parsed.command.jsonMeta.opts;
const args = parsed.command.jsonMeta.args;
// ⚠️ Avoid - raw node access is more complex
const opts = parsed.command.optNodes;
const nc = new NixClap({ noDefaultHandlers: true }).init2({ options, subCommands });
const parsed = nc.parse();
if (parsed.errorNodes?.length > 0) {
for (const node of parsed.errorNodes) {
console.error(`Error: ${node.error.message}`);
}
process.exit(1);
}
parseAsync()const nc = new NixClap().init2({
subCommands: {
deploy: {
exec: async cmd => {
await deployToServer();
}
}
}
});
await nc.parseAsync(); // Waits for all async exec handlers
Other popular CLI parsers you might consider:
Why @fynjs/cli-args?