argblock
v0.0.15
Published
`argblock` is a lightweight and flexible JavaScript/TypeScript library for parsing command-line arguments. It supports long (`--flag`), short (`-f`), and negated (`--no-flag`) parameter formats, positional arguments, and nested command structures with cus
Readme
Argblock
argblock is a lightweight and flexible JavaScript/TypeScript library for parsing command-line arguments. It supports long (--flag), short (-f), and negated (--no-flag) parameter formats, positional arguments, and nested command structures with custom argument matching.
Installation
Install the library via npm:
npm i argblockUsage
There are two ways to use argblock: the declarative Cli builder (recommended for most CLIs), or the lower-level Block/Param/parse API it's built on.
The Cli builder
import { Cli } from "argblock";
const cli = new Cli()
.command("run <file>", "Run a file")
.param("--verbose -v boolean 0", "Verbose output")
.param("--output -o string", "Output directory")
.command("build [...files]", "Build the project")
.param("--watch -w boolean 0", "Watch for changes");
const result = cli.parse(process.argv.slice(2));
console.log(result);node cli.js run app.ts --verbose -o distOutput:
[
{ arg: "__globalArg", params: {}, positionals: {} },
{
arg: "run",
params: { verbose: true, output: "dist" },
positionals: { file: "app.ts" },
},
];Flags and positional arguments can be interleaved freely — run app.ts --verbose, run --verbose app.ts, and run --verbose app.ts --output dist all fill file the same way. Only the relative order among the positionals themselves matters (the first non-flag, non-subcommand token fills the first positional, the second fills the second, and so on).
.command(pattern, description?)declares a command and makes it the current context for subsequent.param()calls. The pattern is the command name followed by positional arguments:<name>for required,[name]for optional,[...name]for optional variadic, and<...name>for required variadic, which needs at least one token. A variadic positional must be last. Always attaches as a sibling at the current nesting level (top-level, or inside the.block()it was declared in)..block(pattern, description, build)declares a command group: a command that has its own subcommands instead of being runnable itself. It creates the block (same pattern syntax as.command()) and callsbuild(nested)with a freshCliscoped to it — use.command()/.block()/.param()onnestedto populate the group. Chaining continues on the outerCliafterwards, so a.command()right after a.block()is a sibling of the group, not nested inside it:const cli = new Cli() .command("status", "Show status") .block("remote", "Manage remotes", (remote) => { remote .command("add <name> <url>", "Add a remote") .command("remove <name>", "Remove a remote"); }); cli.parse(["remote", "add", "origin", "https://example.com"]);.param(pattern, description?)declares a parameter on the current command (or on the global block if called before any.command()/.block()). The pattern is--name [-s] <type> [default], wheretypeis one ofstring/str,number/num/int,boolean/bool. When a default is given and the flag is absent, the default lands inparams, converted to the parameter's type —--concurrency -c number 4yields{ concurrency: 4 }. A default that does not match the type throws at.param().new Cli({ commandLink })links the global command to one of its commands, somycli app.tsbehaves likemycli run app.ts. Parsing enters the linked command when a token is neither a command nor a free global positional, when a flag is unknown to the global block but known to the linked command, or when the arguments end without any command. Global flags are checked first, somycli --verbose buildstill runsbuildeven ifrunalso declares--verbose. After entering the link, the same token is read again from the linked command — first its subcommands, then its positionals — so withcommandLink: "remote"and a.block("remote", ...)group,add originparses asremote add origin. Inside the linked command global flags are still accepted and still checked first: with--debugon the global block and--tagonrun, both--debug --tag beta app.tsand--tag beta --debug app.tsputdebugon the global entry. The same holds after typingrunexplicitly (mycli run --debug). Once flags forrunhave started,runitself is no longer a command switch — it becomesrun's positional, orUnknown argif there is none. A link to a missing command throws on parse;.action()on the global block together withcommandLinkthrows at declaration.new Cli({ commandLink: "run" }) .command("run <file>", "Run a file") .param("--tag -t string all") .action(({ params, positionals }) => {}) .command("build", "Build the project") .action(() => {}) .run(process.argv.slice(2)); // `app.ts --tag beta` → run.action(handler)attaches a handler to the current command —handler({ arg, params, positionals, globalParams })— called by.run()when that command is the one actually invoked. Called before any.command()/.block(), it attaches to the global command, which runs when no command is given. There is no way back to the global block after a.command(), so a later.action()replaces that command's handler instead. The handler receives the deepest parsed entry, plusglobalParams— the global block's params, including defaults of global flags (for a global action,globalParamsare its ownparams)..parse(args)parsesargsand returns the same shape as the low-levelparse()function, always including a leading entry for the global block. Does not call any.action()handlers..run(args)parsesargsand dispatches to the.action()handler of whichever command was actually matched (the deepest entry in the parsed result). Throws if that command has no.action()attached. Does nothing if--helpwas passed (parsing already printed help and stopped).
const cli = new Cli()
.command("run <file>", "Run a file")
.param("--verbose -v boolean 0", "Verbose output")
.action(({ params, positionals, globalParams }) => {
console.log(`running ${positionals.file}, verbose=${params.verbose}`);
console.log("global params:", globalParams);
});
cli.run(process.argv.slice(2));The low-level API
Importing
import { Param, Block, parse, globalArg } from "argblock";Defining Parameters and Blocks
Create Parameters using the
Paramclass:const verboseParam = new Param({ name: "verbose", type: "boolean", short: "v", defaultValue: false, });Create Blocks using the
Blockclass:const mainBlock = new Block({ arg: "run", params: [verboseParam], description: "Run the application", children: [], });Parse Arguments using the
parsefunction:const args = ["run", "--verbose", "1"]; const result = parse(args, [mainBlock]); console.log(result);Example output:
[ { arg: "__globalArg", params: {}, positionals: {} }, { arg: "run", params: { verbose: true }, positionals: {}, }, ];
Key Features
- Long Parameters: Supports
--name valueand--name=valueformats. - Short Parameters: Supports
-ffor single flags and-abcfor multiple boolean flags. - Negated Parameters: Supports
--no-namefor boolean flags. --Separator: every token after a bare--is a positional, even if it looks like a flag or matches a command name —mycli run -- --weird, or with a linkmycli -- build. The--itself is not stored.- Positional Arguments: Required, optional, and variadic positionals per block via
positionals, freely interleaved with flags. - Custom Matchers: Allows custom matching logic for blocks via the
matcherproperty. - Nested Commands: Supports hierarchical command structures through
childreninBlock. - Error Handling: Throws descriptive errors for unknown or duplicated parameters.
- Help Output: Passing
--helpanywhere in the arguments prints usage for the current block (positionals, options with their defaults, and subcommands) to the console and stops parsing (returns[]).
Code Structure
The library consists of several internal modules:
block.ts: Defines theBlockclass and a default matcher for argument matching.Block: Represents a command with an argument name, parameters, positionals, description, matcher, child blocks, and an optionallink— the name of a child block that parsing falls into when no command is given (this is whatCli'scommandLinksets).- Methods:
findParam(name)andfindShortParam(name)to locate parameters by name or short form.
param.ts: Defines theParamclass for parameter configuration.- Properties:
name,type,short,defaultValue,description. defaultValueis used byparsefor every param the arguments did not set; it is converted to the param's type, so a string like"0"on a boolean param becomesfalse.
- Properties:
parse/parse.ts: Contains the mainparsefunction and global block logic.- Handles argument parsing and block traversal.
- Supports a default global block for top-level parameters. The result always starts with the global block's entry — either the block passed as
[globalBlock], or a synthetic one wrapping the given top-level blocks. - Walks the argument list token by token: a token starting with
-is parsed as a flag of the current block; any other token first tries the current block's children (commands win over positionals), then fills the next unfilled positional (or is appended to a trailing variadic positional). If the current block has alinkand nothing took the token — or a flag is unknown to the current block but known to the linked one, or the arguments end without a command — parsing enters the linked block and reads the same token again there. A bare--ends flag and command parsing: every remaining token goes to positionals, entering the linked block when the current one has no free slot. Required positionals are checked once the block is done being read (on switching to a new command, or at the end of the arguments), so flags and positionals can be interleaved in any order. - On
--help, printsformatHelp(currentBlock)(seeparse/help.ts) and stops parsing.
parse/help.ts:formatHelp(block)renders a usage string (positionals, options, subcommands, description) for a singleBlock, used for--helpoutput. An option with a default ends with(default: 4), converted to the option's type.parse/global-arg.ts: TheglobalArgsentinel string used to mark/detect the synthetic root block.positional/positional.ts: Owns positional-argument declaration validation.validatePositionals(positionals): enforces that required positionals can't follow optional ones and that a variadic positional is always last. Runs both whenBlockis constructed and when aClicommand pattern is parsed, so both APIs reject invalid positional declarations up front.
cli/: Defines theClibuilder (cli.ts) and the string-pattern parsers it's built on (parse-command.tsfor command/positional patterns,parse-param.tsfor parameter patterns).
Example
import { Param, Block, parse } from "argblock";
const verboseParam = new Param({
name: "verbose",
type: "boolean",
short: "v",
defaultValue: false,
});
const outputParam = new Param({
name: "output",
type: "string",
short: "o",
defaultValue: "./output",
});
const runBlock = new Block({
arg: "run",
params: [verboseParam, outputParam],
description: "Run the application",
children: [],
});
const args = ["run", "--verbose", "-o", "dist"];
const result = parse(args, [runBlock]);
console.log(result);Output:
[
{ arg: "__globalArg", params: {}, positionals: {} },
{
arg: "run",
params: {
verbose: true,
output: "dist",
},
positionals: {},
},
];Error Handling
The parser throws errors in the following cases:
- Unknown parameters (e.g.,
--unknown). - Unknown positional tokens that no command or positional accepts (
Unknown arg: ...). - Duplicated parameters in the same block.
- Invalid argument formats.
- Missing required positional arguments, including a required variadic with no tokens.
- Empty block list provided to
parse. - A
link/commandLinknaming a child block that doesn't exist. - A pattern default that doesn't match the parameter type — at
.param(), not at parse time. .action()on the global block together withcommandLink— at declaration.
Custom Matchers
You can define custom matchers for blocks to handle complex argument patterns. A matcher receives the remaining argument list and returns whether it matched, along with the remaining elements to continue parsing from. The tokens it consumed, joined by spaces, become the entry's arg. A matcher may also match without consuming anything: parsing then enters that block with an empty arg and reads the same token there. Every such step goes one level deeper, so parsing always finishes — as long as no block is its own descendant:
import { Block } from "argblock";
const customMatcher = (elems) => {
if (elems[0]?.startsWith("custom:")) {
return { elems: elems.slice(1), match: true };
}
return { elems, match: false };
};
const customBlock = new Block({
arg: "custom",
params: [],
description: "Custom command",
matcher: customMatcher,
children: [],
});Limitations
- Boolean parameters expect values like
0,1,true, orfalse. - Short parameters (
-abc) assume boolean type and are set to1unless specified. - The parser does not support advanced features like parameter validation beyond type checking.
- After a command, global flags are accepted only for the linked command:
mycli build --debugthrows unlessbuilddeclares--debugitself. - A positional whose value equals a command name is taken as the command unless it comes after
--. - A
.block()group can't set a link throughCli; setlinkon the low-levelBlockinstead.
