clap-ts 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +711 -150
- package/dist/argfile.d.ts +55 -0
- package/dist/argfile.js +155 -0
- package/dist/completions.d.ts +3 -0
- package/dist/completions.js +199 -16
- package/dist/config.d.ts +75 -0
- package/dist/config.js +134 -0
- package/dist/help.d.ts +4 -4
- package/dist/help.js +276 -81
- package/dist/index.d.ts +2 -3
- package/dist/index.js +3 -3
- package/dist/install.d.ts +55 -0
- package/dist/install.js +185 -0
- package/dist/log.d.ts +78 -0
- package/dist/log.js +164 -0
- package/dist/man.d.ts +28 -0
- package/dist/man.js +234 -0
- package/dist/markdown.d.ts +17 -0
- package/dist/markdown.js +165 -0
- package/dist/output.d.ts +111 -0
- package/dist/output.js +356 -0
- package/dist/parser.d.ts +40 -19
- package/dist/parser.js +789 -461
- package/dist/plugins.d.ts +58 -0
- package/dist/plugins.js +145 -0
- package/dist/progress.d.ts +89 -0
- package/dist/progress.js +205 -0
- package/dist/prompt.d.ts +99 -0
- package/dist/prompt.js +299 -0
- package/dist/runner.d.ts +5 -2
- package/dist/runner.js +341 -135
- package/dist/spec.d.ts +83 -0
- package/dist/spec.js +124 -0
- package/dist/testing.d.ts +59 -0
- package/dist/testing.js +113 -0
- package/dist/types.d.ts +279 -11
- package/dist/validation.js +191 -69
- package/package.json +63 -6
package/README.md
CHANGED
|
@@ -1,30 +1,14 @@
|
|
|
1
1
|
# clap-ts
|
|
2
2
|
|
|
3
|
-
A type-safe CLI argument parser for TypeScript,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **Validation** -- required, exclusive, conflictsWith, requires, requiredUnlessPresent, requiredIfEq, valueParser, numArgs, argument groups
|
|
13
|
-
- **Custom value parsers** -- function-based parsers for custom validation and type conversion
|
|
14
|
-
- **Clap-style help** -- colored, terminal-width-aware help with custom headings, templates, and styles
|
|
15
|
-
- **Clap-style errors** -- "did you mean?" typo suggestions via Levenshtein distance
|
|
16
|
-
- **Environment variable fallback** -- `env` field on args, with CLI > env > default precedence
|
|
17
|
-
- **Lifecycle hooks** -- setup/run/cleanup pattern for resource management
|
|
18
|
-
- **Actions** -- `set` (default), `append` (collect into array), `count` (e.g. `-vvv` = 3)
|
|
19
|
-
- **Value delimiters** -- `--tags=a,b,c` splits into array with `valueDelimiter`
|
|
20
|
-
- **Boolean negation** -- `--no-verbose` automatically supported for boolean flags
|
|
21
|
-
- **Global args** -- `global: true` args inherited by all subcommands
|
|
22
|
-
- **Reusable arg groups** -- `defineArgs()` + spread for sharing args across commands
|
|
23
|
-
- **Trailing var args** -- last positional consumes all remaining args
|
|
24
|
-
- **Negative numbers** -- `--offset -10` with `allowNegativeNumbers`
|
|
25
|
-
- **Hyphen values** -- `--grep -pattern` with `allowHyphenValues`
|
|
26
|
-
- **Zero dependencies** -- only uses `node:util` (parseArgs + styleText)
|
|
27
|
-
- **Bun and Node.js** -- works on both runtimes
|
|
3
|
+
A type-safe CLI argument parser for TypeScript, modelled on Rust's
|
|
4
|
+
[clap](https://docs.rs/clap/latest/clap/).
|
|
5
|
+
|
|
6
|
+
- **Full type inference.** `defineCommand` infers exact argument types; no casts, no schema to repeat.
|
|
7
|
+
- **Complete clap parity.** Every `Arg` and `Command` builder option, from `numArgs` to `overridesWith` to `multicall`.
|
|
8
|
+
- **Fast.** A single pass over argv against a spec compiled once per command: 40ns for a bare parse, 1.5us for a full pipeline.
|
|
9
|
+
- **Zero dependencies.** Only `node:util`, for colour detection.
|
|
10
|
+
- **Batteries behind subpaths.** Config files, prompts, completions for six shells, man pages, markdown docs, plugins, tables, logging and progress, none of which load unless imported.
|
|
11
|
+
- **Node and Bun.** Requires Node 20 or Bun 1.0.
|
|
28
12
|
|
|
29
13
|
## Install
|
|
30
14
|
|
|
@@ -97,7 +81,31 @@ Options:
|
|
|
97
81
|
-V, --version Print version
|
|
98
82
|
```
|
|
99
83
|
|
|
100
|
-
##
|
|
84
|
+
## Documentation
|
|
85
|
+
|
|
86
|
+
**Getting started:** [Install](#install) · [Quick Start](#quick-start)
|
|
87
|
+
|
|
88
|
+
**Defining commands:** [defineCommand](#definecommand) · [Arguments](#arguments) ·
|
|
89
|
+
[Constraints](#constraints) · [Positionals](#positionals) ·
|
|
90
|
+
[Subcommands](#subcommands) · [Argument Groups](#argument-groups) ·
|
|
91
|
+
[Deprecation](#deprecating-arguments-and-commands) · [Lazy Subcommands](#lazy-subcommands)
|
|
92
|
+
|
|
93
|
+
**Running:** [runMain](#runmain) · [Lifecycle Hooks](#lifecycle-hooks) ·
|
|
94
|
+
[Value Precedence](#value-precedence-and-sources) · [Exit Codes](#exit-codes) ·
|
|
95
|
+
[Error Messages](#error-messages) · [Low-Level API](#low-level-api)
|
|
96
|
+
|
|
97
|
+
**Help:** [Help Output](#help-output) · [Styles](#styles) · [Templates](#help-template)
|
|
98
|
+
|
|
99
|
+
**Optional modules:** [config](#configuration-files) · [prompt](#interactive-prompts) ·
|
|
100
|
+
[testing](#testing) · [completions](#shell-completions) · [man](#man-pages) ·
|
|
101
|
+
[markdown](#markdown-documentation) · [install](#installing-completions-and-man-pages) ·
|
|
102
|
+
[spec](#machine-readable-spec) · [plugins](#plugins) · [argfile](#response-files-and-stdin) ·
|
|
103
|
+
[output](#terminal-output) · [log and progress](#logging-and-progress)
|
|
104
|
+
|
|
105
|
+
**Reference:** [Performance](#performance) · [Comparison with clap](#comparison-with-rust-clap) ·
|
|
106
|
+
[Upgrading](#upgrading-from-02)
|
|
107
|
+
|
|
108
|
+
## Defining Commands
|
|
101
109
|
|
|
102
110
|
### defineCommand
|
|
103
111
|
|
|
@@ -116,7 +124,18 @@ const cmd = defineCommand({
|
|
|
116
124
|
beforeHelp: 'NOTE: Requires auth.', // text before help output
|
|
117
125
|
afterHelp: 'Examples:\n serve -p 8080', // text after help output
|
|
118
126
|
hidden: false, // hide from parent help
|
|
119
|
-
aliases: ['s', 'start'], // subcommand aliases
|
|
127
|
+
aliases: ['s', 'start'], // subcommand aliases, shown in help
|
|
128
|
+
hiddenAliases: ['srv'], // aliases that work but stay hidden
|
|
129
|
+
author: 'Salama Ashoush', // available to templates as {author}
|
|
130
|
+
longVersion: '1.0.0 (build abc123)', // --version; -V still shows `version`
|
|
131
|
+
propagateVersion: true, // subcommands inherit this version
|
|
132
|
+
displayOrder: 1, // position among sibling subcommands
|
|
133
|
+
shortFlag: 's', // invoke as `tool -s`
|
|
134
|
+
longFlag: 'serve', // invoke as `tool --serve`
|
|
135
|
+
shortFlagAliases: ['S'], // extra forms, hidden from help
|
|
136
|
+
longFlagAliases: ['start'],
|
|
137
|
+
visibleShortFlagAliases: ['r'], // extra forms shown in help
|
|
138
|
+
visibleLongFlagAliases: ['run'],
|
|
120
139
|
|
|
121
140
|
// Subcommand behavior
|
|
122
141
|
subcommandRequired: true, // error if no subcommand
|
|
@@ -126,10 +145,48 @@ const cmd = defineCommand({
|
|
|
126
145
|
subcommandNegatesReqs: true, // subcommand waives parent required args
|
|
127
146
|
argsConflictsWithSubcommands: true, // args and subcommands mutually exclusive
|
|
128
147
|
argRequiredElseHelp: true, // show help if no args provided
|
|
148
|
+
subcommandPrecedenceOverArg: true, // a subcommand name ends value collection
|
|
149
|
+
multicall: true, // dispatch on the invoked binary name
|
|
150
|
+
noBinaryName: true, // argv carries no binary name to strip
|
|
151
|
+
|
|
152
|
+
// Naming
|
|
153
|
+
binName: 'git stash', // shown in the usage line
|
|
154
|
+
displayName: 'git-stash', // shown in the help header
|
|
155
|
+
|
|
156
|
+
// Parsing behavior
|
|
157
|
+
allowHyphenValues: true, // every arg may take -values
|
|
158
|
+
allowNegativeNumbers: true, // every arg may take negative numbers
|
|
159
|
+
allowMissingPositional: true, // `cp DEST` fills the trailing positional
|
|
160
|
+
argsOverrideSelf: true, // repeating an arg replaces, not errors
|
|
129
161
|
|
|
130
162
|
// Help customization
|
|
131
163
|
helpTemplate: '{name} v{version}\n{usage}\n{options}',
|
|
164
|
+
beforeLongHelp: 'Shown only with --help',
|
|
165
|
+
afterLongHelp: 'Shown only with --help',
|
|
166
|
+
subcommandHelpHeading: 'Operations', // default "Commands"
|
|
167
|
+
subcommandValueName: 'OP', // usage placeholder, default "COMMAND"
|
|
168
|
+
overrideUsage: 'serve <FILE> [--port N]', // replace the usage line
|
|
169
|
+
overrideHelp: '...', // replace the whole help output
|
|
170
|
+
termWidth: 100, // fixed help width
|
|
171
|
+
maxTermWidth: 120, // cap on the detected terminal width
|
|
172
|
+
disableHelpFlag: true, // drop the built-in -h/--help
|
|
173
|
+
disableVersionFlag: true, // drop the built-in -V/--version
|
|
174
|
+
disableHelpSubcommand: true, // drop the built-in `help` subcommand
|
|
175
|
+
disableColoredHelp: true, // render help without colour
|
|
176
|
+
color: 'always', // 'auto' | 'always' | 'never'
|
|
177
|
+
flattenHelp: true, // summarise subcommand args in place
|
|
178
|
+
nextHelpHeading: 'Global', // default heading for args that set none
|
|
179
|
+
nextDisplayOrder: 100, // starting order for args that set none
|
|
180
|
+
helpExpected: true, // reject a visible arg with no description
|
|
181
|
+
|
|
182
|
+
// Error tolerance
|
|
183
|
+
ignoreErrors: true, // collect on ParseResult.errors, keep going
|
|
184
|
+
dontDelimitTrailingValues: true, // leave values after -- unsplit
|
|
132
185
|
},
|
|
186
|
+
// Built on first use instead of at definition time
|
|
187
|
+
lazySubCommands: () => ({ deploy: heavyDeployCommand }),
|
|
188
|
+
// Applied to each argument of an external subcommand
|
|
189
|
+
externalSubcommandValueParser: (v) => v.trim(),
|
|
133
190
|
args: { /* ... */ },
|
|
134
191
|
subCommands: { /* ... */ },
|
|
135
192
|
groups: [ /* ... */ ],
|
|
@@ -139,7 +196,7 @@ const cmd = defineCommand({
|
|
|
139
196
|
});
|
|
140
197
|
```
|
|
141
198
|
|
|
142
|
-
###
|
|
199
|
+
### Arguments
|
|
143
200
|
|
|
144
201
|
Each argument is defined with an `ArgDef`:
|
|
145
202
|
|
|
@@ -286,11 +343,97 @@ const cmd = defineCommand({
|
|
|
286
343
|
hidePossibleValues: true,
|
|
287
344
|
description: 'Output format',
|
|
288
345
|
},
|
|
346
|
+
|
|
347
|
+
// Multi-value option: --point 1 2 3
|
|
348
|
+
point: {
|
|
349
|
+
type: 'string',
|
|
350
|
+
numArgs: { min: 3, max: 3 },
|
|
351
|
+
valueNames: ['X', 'Y', 'Z'], // help shows --point <X> <Y> <Z>
|
|
352
|
+
description: 'Coordinates',
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
// Value terminator: --cmds ls -la ; file.txt
|
|
356
|
+
cmds: {
|
|
357
|
+
type: 'string',
|
|
358
|
+
action: 'append',
|
|
359
|
+
numArgs: { min: 1, max: 99 },
|
|
360
|
+
allowHyphenValues: true,
|
|
361
|
+
valueTerminator: ';',
|
|
362
|
+
description: 'Command to run',
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
// Require the equals form: --key=value, never --key value
|
|
366
|
+
key: {
|
|
367
|
+
type: 'string',
|
|
368
|
+
requireEquals: true,
|
|
369
|
+
description: 'API key',
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
// Possible values carrying help text, aliases and hidden entries
|
|
373
|
+
mode: {
|
|
374
|
+
type: 'string',
|
|
375
|
+
ignoreCase: true, // --mode FAST matches 'fast'
|
|
376
|
+
valueParser: [
|
|
377
|
+
{ name: 'fast', help: 'Skip the slow checks' },
|
|
378
|
+
{ name: 'thorough', aliases: ['full'] },
|
|
379
|
+
{ name: 'legacy', hidden: true },
|
|
380
|
+
],
|
|
381
|
+
description: 'Build mode',
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
// The later of the two wins
|
|
385
|
+
dev: { type: 'boolean', overridesWith: ['release'] },
|
|
386
|
+
release: { type: 'boolean', overridesWith: ['dev'] },
|
|
387
|
+
|
|
388
|
+
// Help presentation
|
|
389
|
+
bind: {
|
|
390
|
+
type: 'string',
|
|
391
|
+
longDescription: 'The longer explanation, shown with --help only',
|
|
392
|
+
displayOrder: 1, // position within its help section
|
|
393
|
+
nextLineHelp: true, // description on its own line
|
|
394
|
+
hideDefaultValue: true, // drop the [default: ...] note
|
|
395
|
+
description: 'Address to bind',
|
|
396
|
+
},
|
|
397
|
+
token: {
|
|
398
|
+
type: 'string',
|
|
399
|
+
env: 'TOOL_TOKEN',
|
|
400
|
+
hideEnvValues: true, // show [env: TOOL_TOKEN], not its value
|
|
401
|
+
description: 'API token',
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
// Explicit positional position, and group membership
|
|
405
|
+
dest: { type: 'positional', index: 2 },
|
|
406
|
+
src: { type: 'positional', index: 1 },
|
|
407
|
+
yaml: { type: 'boolean', group: 'format' }, // or groups: ['format', 'output']
|
|
408
|
+
|
|
409
|
+
// Actions beyond set, append and count
|
|
410
|
+
colour: { type: 'boolean', action: 'setTrue' },
|
|
411
|
+
noColour: { type: 'boolean', long: 'no-colour', action: 'setFalse' },
|
|
412
|
+
usage: { type: 'boolean', short: '?', action: 'help' }, // also helpShort, helpLong
|
|
413
|
+
revision: { type: 'boolean', action: 'version' },
|
|
414
|
+
|
|
415
|
+
// Values for a bare multi-value flag
|
|
416
|
+
origin: {
|
|
417
|
+
type: 'string',
|
|
418
|
+
numArgs: { min: 0, max: 3 },
|
|
419
|
+
defaultMissingValues: ['0', '0', '0'],
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
// Requires another arg only at a particular value
|
|
423
|
+
source: {
|
|
424
|
+
type: 'string',
|
|
425
|
+
requiresIf: ['remote', 'url'], // or requiresIfs: [[v, arg], ...]
|
|
426
|
+
},
|
|
427
|
+
url: { type: 'string' },
|
|
289
428
|
},
|
|
290
429
|
});
|
|
291
430
|
```
|
|
292
431
|
|
|
293
|
-
|
|
432
|
+
By default a value shown for `env` in help includes the variable's current value,
|
|
433
|
+
matching clap. Use `hideEnvValues` to show only the name, or `hideEnv` to drop the
|
|
434
|
+
note entirely, when the variable holds a secret.
|
|
435
|
+
|
|
436
|
+
### Constraints
|
|
294
437
|
|
|
295
438
|
```ts
|
|
296
439
|
const cmd = defineCommand({
|
|
@@ -359,7 +502,7 @@ const cmd = defineCommand({
|
|
|
359
502
|
});
|
|
360
503
|
```
|
|
361
504
|
|
|
362
|
-
###
|
|
505
|
+
### Positionals
|
|
363
506
|
|
|
364
507
|
```ts
|
|
365
508
|
// trailingVarArg: last positional consumes all remaining args
|
|
@@ -445,7 +588,7 @@ const git = defineCommand({
|
|
|
445
588
|
});
|
|
446
589
|
```
|
|
447
590
|
|
|
448
|
-
###
|
|
591
|
+
### Argument Groups
|
|
449
592
|
|
|
450
593
|
```ts
|
|
451
594
|
import { defineArgs, defineCommand } from 'clap-ts';
|
|
@@ -474,6 +617,63 @@ const cmd = defineCommand({
|
|
|
474
617
|
});
|
|
475
618
|
```
|
|
476
619
|
|
|
620
|
+
### Deprecating Arguments and Commands
|
|
621
|
+
|
|
622
|
+
```ts
|
|
623
|
+
const main = defineCommand({
|
|
624
|
+
meta: { name: 'my-tool' },
|
|
625
|
+
args: {
|
|
626
|
+
output: { type: 'string', description: 'Where to write' },
|
|
627
|
+
out: { type: 'string', deprecated: 'renamed', replacedBy: 'output' },
|
|
628
|
+
},
|
|
629
|
+
run({ args }) {
|
|
630
|
+
console.log(args.output);
|
|
631
|
+
},
|
|
632
|
+
});
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
Using `--out` warns once on stderr, labels itself `[deprecated: renamed]` in
|
|
636
|
+
help, and forwards its value to `--output`, so a rename keeps working without
|
|
637
|
+
the handler knowing. `deprecated` works the same on `meta` for a whole command.
|
|
638
|
+
|
|
639
|
+
### Lazy Subcommands
|
|
640
|
+
|
|
641
|
+
`lazySubCommands` takes a thunk, so a command tree whose branches each pull in
|
|
642
|
+
heavy modules only builds the branch being run. It is merged with `subCommands`,
|
|
643
|
+
which wins on a name collision.
|
|
644
|
+
|
|
645
|
+
```ts
|
|
646
|
+
const main = defineCommand({
|
|
647
|
+
meta: { name: 'tool' },
|
|
648
|
+
lazySubCommands: () => ({
|
|
649
|
+
build: require('./commands/build').default,
|
|
650
|
+
deploy: require('./commands/deploy').default,
|
|
651
|
+
}),
|
|
652
|
+
});
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
The thunk runs on the first token that could be a subcommand, so `tool --version`
|
|
656
|
+
never calls it.
|
|
657
|
+
|
|
658
|
+
## Running
|
|
659
|
+
|
|
660
|
+
### runMain
|
|
661
|
+
|
|
662
|
+
Entry point for CLI applications. Handles argv parsing, subcommand resolution, validation, help/version, and error display.
|
|
663
|
+
|
|
664
|
+
```ts
|
|
665
|
+
import { runMain } from 'clap-ts';
|
|
666
|
+
|
|
667
|
+
runMain(rootCommand);
|
|
668
|
+
|
|
669
|
+
runMain(rootCommand, {
|
|
670
|
+
argv: ['serve', '--port', '8080'], // override argv (for testing)
|
|
671
|
+
exit: false, // don't call process.exit (for testing)
|
|
672
|
+
showHelpOnEmpty: true, // show help when no args (default: true)
|
|
673
|
+
styles: { /* custom styles */ }, // override terminal colors
|
|
674
|
+
});
|
|
675
|
+
```
|
|
676
|
+
|
|
477
677
|
### Lifecycle Hooks
|
|
478
678
|
|
|
479
679
|
Commands support setup/run/cleanup lifecycle hooks. `cleanup` always runs, even if `run` throws.
|
|
@@ -499,7 +699,137 @@ const cmd = defineCommand({
|
|
|
499
699
|
});
|
|
500
700
|
```
|
|
501
701
|
|
|
502
|
-
###
|
|
702
|
+
### Value Precedence and Sources
|
|
703
|
+
|
|
704
|
+
Arguments are resolved in this order (highest wins):
|
|
705
|
+
|
|
706
|
+
1. **CLI flags** -- `--port 8080`
|
|
707
|
+
2. **Environment variables** -- `PORT=8080` (when `env: 'PORT'` is set)
|
|
708
|
+
3. **Conditional defaults** -- `defaultValueIf: ['env', 'prod', 443]`
|
|
709
|
+
4. **Static defaults** -- `default: 3000`
|
|
710
|
+
|
|
711
|
+
Every handler gets a `valueSources` map saying where each argument's value came
|
|
712
|
+
from, which is the only way to tell `--port 3000` from a default of the same
|
|
713
|
+
number.
|
|
714
|
+
|
|
715
|
+
```ts
|
|
716
|
+
defineCommand({
|
|
717
|
+
meta: { name: 'serve' },
|
|
718
|
+
args: {
|
|
719
|
+
port: { type: 'number', default: 3000, env: 'PORT' },
|
|
720
|
+
},
|
|
721
|
+
run({ args, valueSources }) {
|
|
722
|
+
// 'cli' | 'env' | 'default', or undefined when the arg has no value
|
|
723
|
+
if (valueSources.get('port') === 'default') {
|
|
724
|
+
console.log(`Using the default port ${args.port}`);
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
});
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
### Exit Codes
|
|
731
|
+
|
|
732
|
+
| Code | Meaning |
|
|
733
|
+
|------|---------|
|
|
734
|
+
| 0 | Success |
|
|
735
|
+
| 1 | Runtime error (unhandled exception in run/setup/cleanup) |
|
|
736
|
+
| 2 | Usage error (parse failure, validation failure, unknown flags) |
|
|
737
|
+
|
|
738
|
+
### Error Messages
|
|
739
|
+
|
|
740
|
+
Errors match clap's format with typo suggestions:
|
|
741
|
+
|
|
742
|
+
```
|
|
743
|
+
error: unexpected argument '--verbos' found
|
|
744
|
+
|
|
745
|
+
tip: a similar argument exists: '--verbose'
|
|
746
|
+
|
|
747
|
+
Usage: my-tool [OPTIONS]
|
|
748
|
+
|
|
749
|
+
For more information, try '--help'.
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
```
|
|
753
|
+
error: the argument '--json' cannot be used with '--yaml'
|
|
754
|
+
|
|
755
|
+
Usage: my-tool [OPTIONS]
|
|
756
|
+
|
|
757
|
+
For more information, try '--help'.
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
```
|
|
761
|
+
error: the following required arguments were not provided:
|
|
762
|
+
--name
|
|
763
|
+
--port
|
|
764
|
+
|
|
765
|
+
Usage: my-tool [OPTIONS]
|
|
766
|
+
|
|
767
|
+
For more information, try '--help'.
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
### Low-Level API
|
|
771
|
+
|
|
772
|
+
For advanced use cases, you can use the parser and validator directly:
|
|
773
|
+
|
|
774
|
+
```ts
|
|
775
|
+
import {
|
|
776
|
+
parseArgs, validate, renderHelp, CliParseError,
|
|
777
|
+
subCommandsOf, hasSubCommands, possibleValues,
|
|
778
|
+
} from 'clap-ts';
|
|
779
|
+
|
|
780
|
+
const command = defineCommand({
|
|
781
|
+
meta: { name: 'tool' },
|
|
782
|
+
args: { port: { type: 'number', default: 3000 } },
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
// Parse without validation
|
|
786
|
+
const result = parseArgs(['--port', '8080'], command);
|
|
787
|
+
// result.args, result.positionals, result.rest, result.unknown
|
|
788
|
+
// result.explicitlySet -- Set of arg keys that were explicitly provided
|
|
789
|
+
|
|
790
|
+
// Validate separately
|
|
791
|
+
validate(result, command); // throws CliParseError on failure
|
|
792
|
+
|
|
793
|
+
// Render help text
|
|
794
|
+
const helpText = renderHelp(command);
|
|
795
|
+
|
|
796
|
+
// Render short help (-h style, hides hideShortHelp args)
|
|
797
|
+
const shortHelp = renderHelp(command, undefined, true);
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
## Help Output
|
|
801
|
+
|
|
802
|
+
Help is automatically generated in clap's format, respecting `NO_COLOR`, `TERM=dumb`, and terminal width:
|
|
803
|
+
|
|
804
|
+
```
|
|
805
|
+
A great CLI tool (my-tool v1.0.0)
|
|
806
|
+
|
|
807
|
+
Usage: my-tool [OPTIONS] <FILE> [COMMAND]
|
|
808
|
+
|
|
809
|
+
Arguments:
|
|
810
|
+
<FILE> Input file path [required]
|
|
811
|
+
|
|
812
|
+
Commands:
|
|
813
|
+
serve (s) Start the development server
|
|
814
|
+
build Build the project
|
|
815
|
+
|
|
816
|
+
Options:
|
|
817
|
+
-n, --name <NAME> User name [required]
|
|
818
|
+
-e, --env <ENV> Environment [possible values: dev, staging, prod]
|
|
819
|
+
-p, --port <PORT> Port number [default: 3000] [env: PORT]
|
|
820
|
+
--verbose Enable verbose output
|
|
821
|
+
-h, --help Print help
|
|
822
|
+
-V, --version Print version
|
|
823
|
+
|
|
824
|
+
Network:
|
|
825
|
+
--host <STRING> Server host
|
|
826
|
+
--proxy <STRING> Proxy URL
|
|
827
|
+
|
|
828
|
+
Examples:
|
|
829
|
+
my-tool -n World serve -p 8080
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
### Styles
|
|
503
833
|
|
|
504
834
|
Override the default terminal colors for help and error output:
|
|
505
835
|
|
|
@@ -515,6 +845,155 @@ runMain(rootCommand, {
|
|
|
515
845
|
});
|
|
516
846
|
```
|
|
517
847
|
|
|
848
|
+
### Help Template
|
|
849
|
+
|
|
850
|
+
Use `helpTemplate` for full control over help layout:
|
|
851
|
+
|
|
852
|
+
```ts
|
|
853
|
+
const cmd = defineCommand({
|
|
854
|
+
meta: {
|
|
855
|
+
name: 'tool',
|
|
856
|
+
version: '1.0.0',
|
|
857
|
+
helpTemplate: `{before-help}{name} v{version}
|
|
858
|
+
|
|
859
|
+
{usage}
|
|
860
|
+
|
|
861
|
+
{all-args}
|
|
862
|
+
{commands}
|
|
863
|
+
{after-help}`,
|
|
864
|
+
},
|
|
865
|
+
// ...
|
|
866
|
+
});
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
Placeholders: `{name}`, `{version}`, `{about}`, `{usage}`, `{all-args}`, `{arguments}`, `{options}`, `{commands}`, `{before-help}`, `{after-help}`
|
|
870
|
+
|
|
871
|
+
## Optional Modules
|
|
872
|
+
|
|
873
|
+
Everything past the core parser lives behind its own entry point, so a running
|
|
874
|
+
CLI never loads a generator it does not call. `import 'clap-ts'` is 3.0ms on
|
|
875
|
+
Node; each module below is another 0.5ms to 1ms, and only when imported.
|
|
876
|
+
|
|
877
|
+
| Import | Provides |
|
|
878
|
+
|--------|----------|
|
|
879
|
+
| `clap-ts` | `defineCommand`, `runMain`, parsing, validation, help |
|
|
880
|
+
| `clap-ts/config` | Config file discovery, layered under argv and env |
|
|
881
|
+
| `clap-ts/prompt` | Ask for required arguments argv left out |
|
|
882
|
+
| `clap-ts/testing` | `runCli`, `captureArgs`, no spawning or global patching |
|
|
883
|
+
| `clap-ts/completions` | bash, zsh, fish, powershell, elvish, nushell |
|
|
884
|
+
| `clap-ts/man` | roff man pages |
|
|
885
|
+
| `clap-ts/markdown` | Markdown documentation |
|
|
886
|
+
| `clap-ts/install` | Write completions and man pages where the system finds them |
|
|
887
|
+
| `clap-ts/spec` | The command tree as plain JSON |
|
|
888
|
+
| `clap-ts/plugins` | Subcommands discovered from installed packages |
|
|
889
|
+
| `clap-ts/argfile` | `@file` response files and stdin |
|
|
890
|
+
| `clap-ts/output` | Tables, key-value blocks, trees |
|
|
891
|
+
| `clap-ts/log` | Levelled logger wired to `-v` and `--quiet` |
|
|
892
|
+
| `clap-ts/progress` | Spinners and progress bars |
|
|
893
|
+
|
|
894
|
+
### Configuration Files
|
|
895
|
+
|
|
896
|
+
```ts
|
|
897
|
+
import { runMain } from 'clap-ts';
|
|
898
|
+
import { configOptions } from 'clap-ts/config';
|
|
899
|
+
|
|
900
|
+
await runMain(main, { ...configOptions('mytool') });
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
Precedence becomes command line, then environment, then config file, then the
|
|
904
|
+
argument's default, and `ctx.valueSources` reports which layer won. By default
|
|
905
|
+
the search looks for `.mytoolrc`, `.mytoolrc.json`, `mytool.config.json`,
|
|
906
|
+
`.config/mytool.json` and a `mytool` key in `package.json`, walking up from the
|
|
907
|
+
working directory.
|
|
908
|
+
|
|
909
|
+
A nested object named for a subcommand scopes its contents to that command,
|
|
910
|
+
while scalar keys stay in scope all the way down:
|
|
911
|
+
|
|
912
|
+
```json
|
|
913
|
+
{
|
|
914
|
+
"verbose": true,
|
|
915
|
+
"serve": { "port": 8080 }
|
|
916
|
+
}
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
Only JSON is read out of the box, which is what keeps the package
|
|
920
|
+
dependency-free. Point `parse` at a TOML or YAML reader for those:
|
|
921
|
+
|
|
922
|
+
```ts
|
|
923
|
+
import { parse as parseToml } from 'smol-toml';
|
|
924
|
+
|
|
925
|
+
configOptions('mytool', {
|
|
926
|
+
files: ['.mytoolrc.toml', 'mytool.toml'],
|
|
927
|
+
parse: (text) => parseToml(text),
|
|
928
|
+
stopAtProjectRoot: true, // stop at the directory holding package.json or .git
|
|
929
|
+
});
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
`configOptions` returns a thunk rather than the values, so the filesystem is
|
|
933
|
+
only searched when some argument is still on its default. A fully specified
|
|
934
|
+
command line costs nothing: 1.0us against 23us when the search actually runs.
|
|
935
|
+
|
|
936
|
+
### Interactive Prompts
|
|
937
|
+
|
|
938
|
+
```ts
|
|
939
|
+
import { promptMissing } from 'clap-ts/prompt';
|
|
940
|
+
|
|
941
|
+
await runMain(main, { fillMissing: promptMissing() });
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
A required argument nobody supplied is asked for rather than rejected, and the
|
|
945
|
+
prompt comes from the definition that already exists: a boolean becomes a
|
|
946
|
+
confirm, possible values become a numbered list, an argument marked `secret` is
|
|
947
|
+
read without echoing, and the answer goes through the argument's own
|
|
948
|
+
`valueParser` before being accepted. `ctx.valueSources` reports these as
|
|
949
|
+
`'prompt'`.
|
|
950
|
+
|
|
951
|
+
Nothing is asked when stdin is not a terminal, so a script or a CI job still
|
|
952
|
+
fails fast with the usual error rather than waiting on input that never comes.
|
|
953
|
+
Pass `force: true` to ask anyway.
|
|
954
|
+
|
|
955
|
+
The individual prompts are available on their own, and each takes an `io` so a
|
|
956
|
+
test can script the answers without a terminal:
|
|
957
|
+
|
|
958
|
+
```ts
|
|
959
|
+
import { input, confirm, select, password, scriptedIO } from 'clap-ts/prompt';
|
|
960
|
+
|
|
961
|
+
const name = await input('Release name');
|
|
962
|
+
const mode = await select('Mode', ['fast', 'safe']);
|
|
963
|
+
const sure = await confirm('Deploy to production', { defaultValue: false });
|
|
964
|
+
const token = await password('API token');
|
|
965
|
+
|
|
966
|
+
// In a test
|
|
967
|
+
await select('Mode', ['fast', 'safe'], { io: scriptedIO(['2']) }); // 'safe'
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
### Testing
|
|
971
|
+
|
|
972
|
+
```ts
|
|
973
|
+
import { runCli, captureArgs } from 'clap-ts/testing';
|
|
974
|
+
|
|
975
|
+
const result = await runCli(main, ['serve', '--port', '8080']);
|
|
976
|
+
expect(result.exitCode).toBe(0);
|
|
977
|
+
expect(result.plainStdout).toContain('listening');
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
`runCli` returns `stdout`, `stderr`, `plainStdout`, `plainStderr`, `exitCode` and
|
|
981
|
+
`error`. No process is spawned and no global is patched: output goes to
|
|
982
|
+
collectors through `RunOptions.stdout`/`stderr` and the exit code arrives via
|
|
983
|
+
`onExit`. An error thrown by a handler comes back on `error` rather than being
|
|
984
|
+
rethrown, so one assertion style covers success and failure.
|
|
985
|
+
|
|
986
|
+
`captureArgs` reports the parsed arguments without running the handler body,
|
|
987
|
+
which works for a subcommand as well as the root:
|
|
988
|
+
|
|
989
|
+
```ts
|
|
990
|
+
const { args } = await captureArgs(main, ['serve', '--port', '9']);
|
|
991
|
+
expect(args.port).toBe(9);
|
|
992
|
+
```
|
|
993
|
+
|
|
994
|
+
Handlers that write through `ctx.stdout` rather than `process.stdout` are
|
|
995
|
+
captured too.
|
|
996
|
+
|
|
518
997
|
### Shell Completions
|
|
519
998
|
|
|
520
999
|
Add Tab completion for bash, zsh, fish, and powershell with one line:
|
|
@@ -571,172 +1050,171 @@ const bashScript = generateCompletions(root, 'bash');
|
|
|
571
1050
|
const zshScript = generateCompletions(root, 'zsh', 'custom-binary-name');
|
|
572
1051
|
```
|
|
573
1052
|
|
|
574
|
-
###
|
|
575
|
-
|
|
576
|
-
Entry point for CLI applications. Handles argv parsing, subcommand resolution, validation, help/version, and error display.
|
|
1053
|
+
### Man Pages
|
|
577
1054
|
|
|
578
1055
|
```ts
|
|
579
|
-
import {
|
|
1056
|
+
import { renderManPage, generateManPages } from 'clap-ts';
|
|
1057
|
+
import { writeFileSync } from 'node:fs';
|
|
580
1058
|
|
|
581
|
-
|
|
1059
|
+
// One page
|
|
1060
|
+
writeFileSync('my-tool.1', renderManPage(main));
|
|
582
1061
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
styles: { /* custom styles */ }, // override terminal colors
|
|
588
|
-
});
|
|
1062
|
+
// One page per command, named my-tool.1, my-tool-serve.1, and so on
|
|
1063
|
+
for (const [file, roff] of generateManPages(main, { section: '1' })) {
|
|
1064
|
+
writeFileSync(`man/${file}`, roff);
|
|
1065
|
+
}
|
|
589
1066
|
```
|
|
590
1067
|
|
|
591
|
-
|
|
1068
|
+
The output carries NAME, SYNOPSIS, DESCRIPTION, OPTIONS, SUBCOMMANDS, EXTRA,
|
|
1069
|
+
VERSION and AUTHORS, with possible values as a bullet list and notes for
|
|
1070
|
+
defaults and environment variables. Check it with `man -l my-tool.1`.
|
|
592
1071
|
|
|
593
|
-
|
|
1072
|
+
### Markdown Documentation
|
|
594
1073
|
|
|
595
1074
|
```ts
|
|
596
|
-
import {
|
|
597
|
-
|
|
598
|
-
const command = defineCommand({
|
|
599
|
-
meta: { name: 'tool' },
|
|
600
|
-
args: { port: { type: 'number', default: 3000 } },
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
// Parse without validation
|
|
604
|
-
const result = parseArgs(['--port', '8080'], command);
|
|
605
|
-
// result.args, result.positionals, result.rest, result.unknown
|
|
606
|
-
// result.explicitlySet -- Set of arg keys that were explicitly provided
|
|
1075
|
+
import { renderMarkdownHelp } from 'clap-ts';
|
|
607
1076
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
const helpText = renderHelp(command);
|
|
613
|
-
|
|
614
|
-
// Render short help (-h style, hides hideShortHelp args)
|
|
615
|
-
const shortHelp = renderHelp(command, undefined, true);
|
|
1077
|
+
writeFileSync('docs/cli.md', renderMarkdownHelp(main, {
|
|
1078
|
+
title: 'CLI Reference', // optional, demotes command headings by one level
|
|
1079
|
+
footer: 'Generated from the command definitions.',
|
|
1080
|
+
}));
|
|
616
1081
|
```
|
|
617
1082
|
|
|
618
|
-
|
|
1083
|
+
Produces one heading per command with its usage line and Commands, Arguments and
|
|
1084
|
+
Options lists, nesting subcommands as deeper headings.
|
|
619
1085
|
|
|
620
|
-
|
|
1086
|
+
### Installing Completions and Man Pages
|
|
621
1087
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
3. **Conditional defaults** -- `defaultValueIf: ['env', 'prod', 443]`
|
|
625
|
-
4. **Static defaults** -- `default: 3000`
|
|
626
|
-
|
|
627
|
-
## Error Messages
|
|
628
|
-
|
|
629
|
-
Errors match clap's format with typo suggestions:
|
|
1088
|
+
```ts
|
|
1089
|
+
import { withInstallers } from 'clap-ts/install';
|
|
630
1090
|
|
|
1091
|
+
runMain(withInstallers(main));
|
|
1092
|
+
// my-tool completions install zsh
|
|
1093
|
+
// my-tool man install --dryRun
|
|
631
1094
|
```
|
|
632
|
-
error: unexpected argument '--verbos' found
|
|
633
1095
|
|
|
634
|
-
|
|
1096
|
+
Writes to the XDG per-user locations, names the file the way each shell expects,
|
|
1097
|
+
and reports the profile line to add where sourcing is not automatic.
|
|
635
1098
|
|
|
636
|
-
|
|
1099
|
+
### Machine-Readable Spec
|
|
637
1100
|
|
|
638
|
-
|
|
639
|
-
|
|
1101
|
+
```ts
|
|
1102
|
+
import { toSpecJson } from 'clap-ts/spec';
|
|
640
1103
|
|
|
1104
|
+
writeFileSync('cli.json', toSpecJson(main));
|
|
641
1105
|
```
|
|
642
|
-
error: the argument '--json' cannot be used with '--yaml'
|
|
643
|
-
|
|
644
|
-
Usage: my-tool [OPTIONS]
|
|
645
1106
|
|
|
646
|
-
|
|
647
|
-
|
|
1107
|
+
The whole tree as plain JSON, for a docs site, editor integration, or generating
|
|
1108
|
+
tool definitions from a CLI.
|
|
648
1109
|
|
|
649
|
-
|
|
650
|
-
error: the following required arguments were not provided:
|
|
651
|
-
--name
|
|
652
|
-
--port
|
|
1110
|
+
### Plugins
|
|
653
1111
|
|
|
654
|
-
|
|
1112
|
+
```ts
|
|
1113
|
+
import { pluginSubCommands } from 'clap-ts/plugins';
|
|
655
1114
|
|
|
656
|
-
|
|
1115
|
+
const main = defineCommand({
|
|
1116
|
+
meta: { name: 'my-tool' },
|
|
1117
|
+
lazySubCommands: pluginSubCommands('my-tool'),
|
|
1118
|
+
});
|
|
657
1119
|
```
|
|
658
1120
|
|
|
659
|
-
|
|
1121
|
+
Installing `my-tool-plugin-deploy` makes `my-tool deploy` work. Because it is a
|
|
1122
|
+
`lazySubCommands` thunk, neither the directory scan nor the module import
|
|
1123
|
+
happens until a token could be a subcommand.
|
|
660
1124
|
|
|
661
|
-
|
|
1125
|
+
### Response Files and stdin
|
|
662
1126
|
|
|
663
|
-
```
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
Usage: my-tool [OPTIONS] <FILE> [COMMAND]
|
|
1127
|
+
```ts
|
|
1128
|
+
import { expandArgFiles, readPathOrStdin } from 'clap-ts/argfile';
|
|
667
1129
|
|
|
668
|
-
|
|
669
|
-
|
|
1130
|
+
await runMain(main, { argv: expandArgFiles() });
|
|
1131
|
+
```
|
|
670
1132
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
1133
|
+
`@args.txt` is replaced by that file's contents, following the convention git,
|
|
1134
|
+
gcc and java use: one argument per line, `#` comments and blank lines skipped,
|
|
1135
|
+
quoted runs kept whole, `@@` for a literal at-sign, and nothing after `--`
|
|
1136
|
+
touched. `readPathOrStdin` covers the other half, where `-` means stdin.
|
|
674
1137
|
|
|
675
|
-
|
|
676
|
-
-n, --name <NAME> User name [required]
|
|
677
|
-
-e, --env <ENV> Environment [possible values: dev, staging, prod]
|
|
678
|
-
-p, --port <PORT> Port number [default: 3000] [env: PORT]
|
|
679
|
-
--verbose Enable verbose output
|
|
680
|
-
-h, --help Print help
|
|
681
|
-
-V, --version Print version
|
|
1138
|
+
### Terminal Output
|
|
682
1139
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
--proxy <STRING> Proxy URL
|
|
1140
|
+
```ts
|
|
1141
|
+
import { table, keyValue, tree } from 'clap-ts/output';
|
|
686
1142
|
|
|
687
|
-
|
|
688
|
-
|
|
1143
|
+
ctx.stdout.write(table(rows, {
|
|
1144
|
+
columns: [{ key: 'name' }, { key: 'size', align: 'right' }],
|
|
1145
|
+
rule: true,
|
|
1146
|
+
}));
|
|
689
1147
|
```
|
|
690
1148
|
|
|
691
|
-
|
|
1149
|
+
Columns size to their widest cell, then the widest shrinks until the table fits
|
|
1150
|
+
the terminal. Padding measures visible width, so styled cells line up with plain
|
|
1151
|
+
ones.
|
|
692
1152
|
|
|
693
|
-
|
|
1153
|
+
### Logging and Progress
|
|
694
1154
|
|
|
695
1155
|
```ts
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
name: 'tool',
|
|
699
|
-
version: '1.0.0',
|
|
700
|
-
helpTemplate: `{before-help}{name} v{version}
|
|
1156
|
+
import { loggerFrom } from 'clap-ts/log';
|
|
1157
|
+
import { spinner } from 'clap-ts/progress';
|
|
701
1158
|
|
|
702
|
-
{
|
|
1159
|
+
const main = defineCommand({
|
|
1160
|
+
meta: { name: 'my-tool' },
|
|
1161
|
+
args: {
|
|
1162
|
+
verbose: { type: 'boolean', short: 'v', action: 'count', description: 'More output' },
|
|
1163
|
+
quiet: { type: 'boolean', short: 'q', description: 'Errors only' },
|
|
1164
|
+
},
|
|
1165
|
+
async run(ctx) {
|
|
1166
|
+
const log = loggerFrom(ctx); // -v climbs a level, --quiet drops to errors
|
|
1167
|
+
log.debug('shown with -v');
|
|
703
1168
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
1169
|
+
const spin = spinner('Fetching').start();
|
|
1170
|
+
await fetchThings();
|
|
1171
|
+
spin.succeed('Fetched 12 items');
|
|
707
1172
|
},
|
|
708
|
-
// ...
|
|
709
1173
|
});
|
|
710
1174
|
```
|
|
711
1175
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
| Code | Meaning |
|
|
717
|
-
|------|---------|
|
|
718
|
-
| 0 | Success |
|
|
719
|
-
| 1 | Runtime error (unhandled exception in run/setup/cleanup) |
|
|
720
|
-
| 2 | Usage error (parse failure, validation failure, unknown flags) |
|
|
1176
|
+
Both write to stderr, leaving stdout for real output. Spinners and bars draw
|
|
1177
|
+
only when stderr is a terminal and `CI` is unset, so a piped run never fills a
|
|
1178
|
+
log with redraw escapes, and still reports its final message once.
|
|
721
1179
|
|
|
722
1180
|
## Performance
|
|
723
1181
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
1182
|
+
Parsing is a single pass over argv against a spec compiled once per command and
|
|
1183
|
+
cached, so flag lookup, value counts and camelCase keys are all resolved ahead of
|
|
1184
|
+
time. Feature fields short-circuit on `undefined`, so an unused one costs nothing.
|
|
1185
|
+
Subcommand maps are built only when a token could actually be a subcommand, which
|
|
1186
|
+
is also what keeps `lazySubCommands` from running its thunk on a flags-only
|
|
1187
|
+
invocation.
|
|
1188
|
+
|
|
1189
|
+
Config discovery follows the same idea. The search costs one `existsSync` per
|
|
1190
|
+
candidate per directory, so it is O(directories x candidates) and independent of
|
|
1191
|
+
how large those directories are. Listing each directory once with `readdirSync`
|
|
1192
|
+
would be a single syscall per level, but it is O(entries): against a 2000-entry
|
|
1193
|
+
directory that took 117us where four `existsSync` calls took 2.4us, and walking
|
|
1194
|
+
up through a large directory is exactly the case that has to stay cheap.
|
|
1195
|
+
|
|
1196
|
+
Earlier versions delegated to `node:util parseArgs`, which re-validates its whole
|
|
1197
|
+
`options` object on every call, roughly 170ns per declared option regardless of how
|
|
1198
|
+
long argv is. A 33-option command spent 5.8us there before reading a single token.
|
|
1199
|
+
|
|
1200
|
+
Benchmarks on AMD Ryzen 9 9950X3D (Bun 1.4), against that `node:util` baseline:
|
|
1201
|
+
|
|
1202
|
+
| Scenario | Before | After | Change |
|
|
1203
|
+
|----------|--------|-------|--------|
|
|
1204
|
+
| Minimal (no args) | 1.47us | 40ns | 36x |
|
|
1205
|
+
| Simple (5 flags) | 9.01us | 494ns | 18x |
|
|
1206
|
+
| Complex (22 flags) | 14.15us | 1.80us | 8x |
|
|
1207
|
+
| Subcommand detection | 9.96us | 418ns | 24x |
|
|
1208
|
+
| Full pipeline (parse + validate) | 23.01us | 1.45us | 16x |
|
|
1209
|
+
|
|
1210
|
+
Figures are the minimum of nine pinned runs. Microbenchmarks on this machine are
|
|
1211
|
+
bimodal by roughly 1.5x depending on core placement, so a single run tells you
|
|
1212
|
+
very little.
|
|
735
1213
|
|
|
736
1214
|
To run benchmarks yourself:
|
|
737
1215
|
|
|
738
1216
|
```bash
|
|
739
|
-
bun run bench
|
|
1217
|
+
bun run bench
|
|
740
1218
|
```
|
|
741
1219
|
|
|
742
1220
|
## Comparison with Rust clap
|
|
@@ -788,19 +1266,102 @@ bun run bench/parse.bench.ts
|
|
|
788
1266
|
| hideShortHelp / hideLongHelp | Yes | Yes |
|
|
789
1267
|
| hidePossibleValues | Yes | Yes |
|
|
790
1268
|
| Hidden args/commands | Yes | Yes |
|
|
1269
|
+
| Multi-token numArgs (`--point 1 2 3`) | Yes | Yes |
|
|
1270
|
+
| requireEquals | Yes | Yes |
|
|
1271
|
+
| valueTerminator | Yes | Yes |
|
|
1272
|
+
| overridesWith | Yes | Yes |
|
|
1273
|
+
| ignoreCase | Yes | Yes |
|
|
1274
|
+
| Possible values with help/aliases | Yes | Yes |
|
|
1275
|
+
| valueNames, index, displayOrder, nextLineHelp | Yes | Yes |
|
|
1276
|
+
| hideDefaultValue / hideEnv / hideEnvValues | Yes | Yes |
|
|
1277
|
+
| defaultValueIfs, requiredIfEqAny/All | Yes | Yes |
|
|
1278
|
+
| Arg-level group membership | Yes | Yes |
|
|
1279
|
+
| Group conflictsWith / requires | Yes | Yes |
|
|
1280
|
+
| Built-in `help` subcommand | Yes | Yes |
|
|
1281
|
+
| disableHelpFlag / disableVersionFlag | Yes | Yes |
|
|
1282
|
+
| termWidth / maxTermWidth | Yes | Yes |
|
|
1283
|
+
| overrideUsage / overrideHelp | Yes | Yes |
|
|
1284
|
+
| propagateVersion / longVersion | Yes | Yes |
|
|
1285
|
+
| Flag subcommands (`pacman -S`) | Yes | Yes |
|
|
1286
|
+
| allowMissingPositional | Yes | Yes |
|
|
1287
|
+
| argsOverrideSelf | Yes | Yes |
|
|
1288
|
+
| subcommandPrecedenceOverArg | Yes | Yes |
|
|
1289
|
+
| multicall / noBinaryName | Yes | Yes |
|
|
791
1290
|
| Type-safe parsed args | derive macro | generics |
|
|
792
1291
|
| Shell completions (bash/zsh/fish/powershell) | Yes | Yes |
|
|
793
|
-
|
|
|
1292
|
+
| Shell completions (elvish, nushell) | Yes | Yes |
|
|
1293
|
+
| Man page generation | Yes | Yes |
|
|
1294
|
+
| Value source (CLI vs env vs default) | Yes | Yes |
|
|
1295
|
+
| setTrue / setFalse / help / version actions | Yes | Yes |
|
|
1296
|
+
| defaultMissingValues, requiresIf, singular group | Yes | Yes |
|
|
1297
|
+
| binName, displayName, color, helpExpected | Yes | Yes |
|
|
1298
|
+
| flattenHelp, ignoreErrors, nextHelpHeading | Yes | Yes |
|
|
1299
|
+
| nextDisplayOrder, dontDelimitTrailingValues | Yes | Yes |
|
|
1300
|
+
| Subcommand flag aliases (short and long) | Yes | Yes |
|
|
1301
|
+
| Lazy subcommand building (`defer`) | Yes | Yes |
|
|
1302
|
+
| externalSubcommandValueParser | Yes | Yes |
|
|
1303
|
+
| Markdown documentation | clap-markdown | Yes |
|
|
1304
|
+
| Config file layering | no | Yes (`clap-ts/config`) |
|
|
1305
|
+
| Interactive prompts for missing args | no | Yes (`clap-ts/prompt`) |
|
|
1306
|
+
| Test harness with no spawning | no | Yes (`clap-ts/testing`) |
|
|
1307
|
+
| Response files (`@file`) | no | Yes (`clap-ts/argfile`) |
|
|
1308
|
+
| Plugin subcommands from packages | no | Yes (`clap-ts/plugins`) |
|
|
1309
|
+
| Tables, logging, progress | no | Yes (`clap-ts/output`, `/log`, `/progress`) |
|
|
1310
|
+
| Derive macro | Yes | n/a in TypeScript |
|
|
1311
|
+
| dontCollapseArgsInUsage | deprecated no-op | Not implemented |
|
|
1312
|
+
|
|
1313
|
+
Two clap settings are deliberately absent. `dont_collapse_args_in_usage` is a
|
|
1314
|
+
deprecated no-op upstream, and this usage line never collapsed positionals in the
|
|
1315
|
+
first place. The derive macro has no analogue: `defineCommand` already infers the
|
|
1316
|
+
parsed argument types from the definition object.
|
|
1317
|
+
|
|
1318
|
+
## Upgrading from 0.2
|
|
1319
|
+
|
|
1320
|
+
The parser was rewritten to tokenize argv directly, which brought several defaults
|
|
1321
|
+
in line with clap. Each of these was previously wrong or silently permissive:
|
|
1322
|
+
|
|
1323
|
+
- `--version` and `-V` exist only where `meta.version` is set. A subcommand that
|
|
1324
|
+
needs the root's version should set `propagateVersion: true` on the root.
|
|
1325
|
+
- Repeating a single-value arg is an error. `argsOverrideSelf: true` restores the
|
|
1326
|
+
old behaviour of keeping the last one.
|
|
1327
|
+
- A flag missing its value is an error. `--name` with nothing after it used to
|
|
1328
|
+
yield the boolean `true` in a string-typed arg, and `--name --verbose` used to
|
|
1329
|
+
swallow the next flag as the value.
|
|
1330
|
+
- `--flag=true` is now `true`. Every `--flag=<value>` form used to yield `false`.
|
|
1331
|
+
- `-p=80` yields `80`. The leading `=` used to end up in the value.
|
|
1332
|
+
- Flags before a subcommand no longer break dispatch. `app --verbose serve` used
|
|
1333
|
+
to run nothing at all, and a subcommand's flags are now parsed against the
|
|
1334
|
+
subcommand rather than its parent.
|
|
1335
|
+
- `global: true` args declared on an intermediate command now reach its
|
|
1336
|
+
grandchildren, not just the root's.
|
|
1337
|
+
- Errors print the usage line of the command that failed rather than the root's.
|
|
1338
|
+
- Help shows `[env: VAR=value]`; see `hideEnvValues` and `hideEnv`.
|
|
1339
|
+
- The `completions` subcommand validates its shell through `valueParser`, so an
|
|
1340
|
+
unknown one now reports the accepted values and honours a caller's
|
|
1341
|
+
`exit: false` instead of calling `process.exit` from inside its handler.
|
|
1342
|
+
- `ParseResult` gained `valueSources`, `errors`, `subCommandArgs`,
|
|
1343
|
+
`subCommandIsExternal` and `versionIsShort`. Only the low-level API sees these;
|
|
1344
|
+
`defineCommand` and `runMain` are unaffected.
|
|
794
1345
|
|
|
795
1346
|
## Roadmap
|
|
796
1347
|
|
|
797
|
-
|
|
798
|
-
|
|
1348
|
+
Parity with clap's builder API is complete, and the optional modules cover the
|
|
1349
|
+
ergonomics around it. What is left is polish:
|
|
1350
|
+
|
|
1351
|
+
- A `docs` subcommand helper, the way `withInstallers` wraps the generators
|
|
1352
|
+
- Arrow-key selection in `clap-ts/prompt` where the terminal supports it, keeping
|
|
1353
|
+
the numbered list as the fallback
|
|
1354
|
+
- Shell-side dynamic completion, so a value list can come from the running CLI
|
|
1355
|
+
rather than only from the definition
|
|
799
1356
|
|
|
800
1357
|
## Requirements
|
|
801
1358
|
|
|
802
1359
|
- Node.js >= 20.0.0 or Bun >= 1.0
|
|
803
|
-
- TypeScript >= 5.0
|
|
1360
|
+
- TypeScript >= 5.0, for `const` type parameter inference
|
|
1361
|
+
- ESM only. A CommonJS file has to reach it through a dynamic `import()`; the
|
|
1362
|
+
package sets `"type": "module"` and ships no CJS build.
|
|
1363
|
+
|
|
1364
|
+
Type resolution is verified against `nodenext`, `node16` and `bundler`.
|
|
804
1365
|
|
|
805
1366
|
## License
|
|
806
1367
|
|