cli-nano 1.2.2 → 1.3.1
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 +55 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +167 -17
- package/package.json +1 -1
- package/src/index.ts +168 -17
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://codecov.io/gh/ghiscoding/cli-nano)
|
|
5
5
|
[](https://www.npmjs.com/package/cli-nano)
|
|
6
6
|
[](https://www.npmjs.com/package/cli-nano)
|
|
7
|
-
[](https://bundlejs.com/?q=cli-nano)
|
|
8
8
|
<a href="https://nodejs.org/en/about/previous-releases"><img src="https://img.shields.io/node/v/cli-nano.svg" alt="Node" /></a>
|
|
9
9
|
|
|
10
10
|
## cli-nano
|
|
@@ -13,8 +13,8 @@ Small library to create command-line tool (aka CLI) which is quite similar to [`
|
|
|
13
13
|
|
|
14
14
|
### Features
|
|
15
15
|
- Parses arguments
|
|
16
|
-
- Supports defining Positional (input)
|
|
17
|
-
- Supports Variadic
|
|
16
|
+
- Supports defining Positional arguments (input args)
|
|
17
|
+
- Supports Variadic arguments (1 or more positional args)
|
|
18
18
|
- Automatically converts flags to camelCase to match config options
|
|
19
19
|
- accepts both `--camelCase` and `--kebab-case`
|
|
20
20
|
- Negates flags when using the `--no-` prefix
|
|
@@ -23,7 +23,24 @@ Small library to create command-line tool (aka CLI) which is quite similar to [`
|
|
|
23
23
|
- Supports defining `required` options
|
|
24
24
|
- Supports `default` values
|
|
25
25
|
- Supports `group` for grouping command options in help
|
|
26
|
-
- No dependencies!
|
|
26
|
+
- No dependencies and very lightweight!
|
|
27
|
+
|
|
28
|
+
### Recent additions
|
|
29
|
+
|
|
30
|
+
- Grouped short flags: support clustered short aliases (e.g. `-ad`) where the last short may consume a value when appropriate.
|
|
31
|
+
- Kebab/camel duplication: parsed options are available under both `camelCase` and `kebab-case` keys (e.g. both `dryRun` and `dry-run`).
|
|
32
|
+
- `--` passthrough: tokens after `--` are exposed on `result['--']` and are not parsed as options.
|
|
33
|
+
- Raw argv exposure: the original argv slice is available on `result.__rawArgs` for diagnostics or passthrough adapters.
|
|
34
|
+
- Bare long-form behavior: a bare long option is treated as an empty value (e.g. `--opt` → `''`) and a bare long array option becomes `['']`.
|
|
35
|
+
- Boolean negation parity: negated booleans expose both `noFoo` and `no-foo` keys (e.g. `--no-dry-run` sets `dryRun=false` and also exposes `noDryRun=true` and `no-dry-run=true`).
|
|
36
|
+
- Negation guard: single-dash forms like `-no-foo` / `-noFoo` are treated as negated long options rather than short clusters.
|
|
37
|
+
- Error message consistency: option/argument errors use the `Unknown argument: X` wording to match existing positional error messages.
|
|
38
|
+
|
|
39
|
+
Short examples:
|
|
40
|
+
|
|
41
|
+
- Clustered short flags: `-ad` behaves like `-a -d` and `-ab value` lets `b` consume `value` when `b` expects a value.
|
|
42
|
+
- Bare long option: `--bar` (with no following token) results in `bar: ''` for string options.
|
|
43
|
+
|
|
27
44
|
|
|
28
45
|
### Install
|
|
29
46
|
```sh
|
|
@@ -124,8 +141,8 @@ const args = parseArgs(config);
|
|
|
124
141
|
console.log(args);
|
|
125
142
|
|
|
126
143
|
// do something with parsed arguments, for example
|
|
127
|
-
|
|
128
|
-
|
|
144
|
+
const { input, port, open } = args;
|
|
145
|
+
startServer({ input, port, open });
|
|
129
146
|
```
|
|
130
147
|
|
|
131
148
|
### Usage with Type Inference
|
|
@@ -141,8 +158,8 @@ const args = parseArgs<typeof config>(config);
|
|
|
141
158
|
|
|
142
159
|
// TypeScript will infer the correct types:
|
|
143
160
|
args.input; // [string, ...string[]] (required, variadic)
|
|
144
|
-
args.port; // number
|
|
145
|
-
args.verbose; // boolean
|
|
161
|
+
args.port; // number (optional, has default)
|
|
162
|
+
args.verbose; // boolean (optional)
|
|
146
163
|
args.display; // boolean (required)
|
|
147
164
|
```
|
|
148
165
|
|
|
@@ -151,12 +168,12 @@ args.display; // boolean (required)
|
|
|
151
168
|
> If you use `const config: Config = { ... }`, you get type checking but not full intelliSense for parsed arguments.
|
|
152
169
|
|
|
153
170
|
> [!NOTE]
|
|
154
|
-
> For required+variadic positionals, the type is `[string, ...string[]]` (at least one value required). For optional variadic, it's `string[]`.
|
|
171
|
+
> For required+variadic positionals, the type is `[string, ...string[]]` (at least one value required). For optional variadic, it's `string[]`. And Finally for non-variadic, it's `string`.
|
|
155
172
|
|
|
156
173
|
#### Example CLI Calls
|
|
157
174
|
|
|
158
175
|
```sh
|
|
159
|
-
# Show help guide (
|
|
176
|
+
# Show help guide (creates it by reading CLI config)
|
|
160
177
|
serve --help
|
|
161
178
|
|
|
162
179
|
# Show version (when defined)
|
|
@@ -165,7 +182,7 @@ serve --version
|
|
|
165
182
|
# Uses default port 5000
|
|
166
183
|
serve dist/index.html
|
|
167
184
|
|
|
168
|
-
# With required and optional
|
|
185
|
+
# With required and optional positional args
|
|
169
186
|
serve index1.html index2.html 8080 -D value
|
|
170
187
|
|
|
171
188
|
# With boolean and array options entered as camelCase (kebab-case works too)
|
|
@@ -179,6 +196,19 @@ serve index.html 7000 -d -e pattern1 -e pattern2 -D value
|
|
|
179
196
|
|
|
180
197
|
# With number option
|
|
181
198
|
serve index.html 7000 --up 2 -D value
|
|
199
|
+
|
|
200
|
+
# Clustered short flags / last-short consumption
|
|
201
|
+
serve index.html 7000 -ad
|
|
202
|
+
serve index.html 7000 -ab value
|
|
203
|
+
|
|
204
|
+
# Equals form and bare-long
|
|
205
|
+
serve index.html 7000 --opt=value
|
|
206
|
+
serve index.html 7000 --opt=
|
|
207
|
+
serve index.html 7000 --bar
|
|
208
|
+
|
|
209
|
+
# Negation parity and passthrough
|
|
210
|
+
serve index.html 7000 --no-dry-run
|
|
211
|
+
serve index.html 7000 -- file.txt --not-a-flag
|
|
182
212
|
```
|
|
183
213
|
|
|
184
214
|
#### Notes
|
|
@@ -193,20 +223,19 @@ serve index.html 7000 --up 2 -D value
|
|
|
193
223
|
- **Aliases**: Use `alias` for short flags (e.g., `-d` for `--dryRun`).
|
|
194
224
|
- **Groups**: Use `group` for grouping some commands in help (e.g., `{ group: 'Extra Commands' }`).
|
|
195
225
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
226
|
+
- **Equals form**: `--opt=value` and `--opt=` are supported; an empty value after `=` yields an empty string.
|
|
227
|
+
- **Passthrough**: Tokens after `--` are not parsed and are available on `result['--']`.
|
|
228
|
+
- **Raw argv**: The original argv slice is available on `result.__rawArgs` for diagnostics or passthrough adapters.
|
|
229
|
+
- **Negation duplicates**: Using `--no-foo` sets `foo=false` and also exposes `noFoo` and `no-foo` keys.
|
|
230
|
+
- **Alias**: `alias` must be a single string (multi-char aliases are supported). Array-style aliases are not supported and will be rejected.
|
|
199
231
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
- [native-copyfiles](https://github.com/ghiscoding/native-copyfiles)
|
|
203
|
-
- [remove-glob](https://github.com/ghiscoding/remove-glob)
|
|
232
|
+
See [examples/](examples/) for more usage patterns.
|
|
204
233
|
|
|
205
234
|
## Help Example
|
|
206
235
|
|
|
207
236
|
You can see below an example of a CLI help (which is the result of calling `--help` with the [config](#usage) shown above).
|
|
208
237
|
|
|
209
|
-
Please note
|
|
238
|
+
Please note the following expectations:
|
|
210
239
|
|
|
211
240
|
- `<option>` → required
|
|
212
241
|
- `[option]` → optional
|
|
@@ -237,3 +266,10 @@ Advanced Options:
|
|
|
237
266
|
-D, --display a required display option <boolean>
|
|
238
267
|
-r, --rainbow Enable rainbow mode [boolean]
|
|
239
268
|
```
|
|
269
|
+
|
|
270
|
+
## Used by
|
|
271
|
+
|
|
272
|
+
`cli-nano` is currently being used by the following projects, which is actually why I created this CLI tool, that I currently maintain as well (feel free to edit this list):
|
|
273
|
+
|
|
274
|
+
- [native-copyfiles](https://github.com/ghiscoding/native-copyfiles)
|
|
275
|
+
- [remove-glob](https://github.com/ghiscoding/remove-glob)
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAc,MAAM,iBAAiB,CAAC;AAEtE,mBAAmB,iBAAiB,CAAC;AAOrC,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAc,MAAM,iBAAiB,CAAC;AAEtE,mBAAmB,iBAAiB,CAAC;AAOrC,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAuVpE"}
|
package/dist/index.js
CHANGED
|
@@ -4,14 +4,22 @@ const defaultOptions = {
|
|
|
4
4
|
};
|
|
5
5
|
export function parseArgs(config) {
|
|
6
6
|
const { command, options, version } = config;
|
|
7
|
+
// Capture raw argv for consumers/diagnostics
|
|
8
|
+
const __rawArgs = process.argv.slice(2);
|
|
7
9
|
// Normalize args to support --option=value and -o=value
|
|
8
|
-
|
|
10
|
+
let args = __rawArgs.flatMap(arg => {
|
|
9
11
|
if (/^--?\w[\w-]*=/.test(arg)) {
|
|
10
12
|
const [flag, ...rest] = arg.split('=');
|
|
11
13
|
return [flag, rest.join('=')];
|
|
12
14
|
}
|
|
13
15
|
return arg;
|
|
14
16
|
});
|
|
17
|
+
// Capture `--` passthrough (everything after `--`) and remove from args to avoid parsing
|
|
18
|
+
const dashIndex = args.indexOf('--');
|
|
19
|
+
const dashArgs = dashIndex >= 0 ? args.slice(dashIndex + 1) : [];
|
|
20
|
+
if (dashIndex >= 0) {
|
|
21
|
+
args = args.slice(0, dashIndex);
|
|
22
|
+
}
|
|
15
23
|
const result = {};
|
|
16
24
|
// Check for duplicate aliases
|
|
17
25
|
const aliasMap = new Map();
|
|
@@ -116,8 +124,71 @@ export function parseArgs(config) {
|
|
|
116
124
|
[option, configKey] = findOption(options, arg);
|
|
117
125
|
}
|
|
118
126
|
else if (argOrg.startsWith('-')) {
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
// Support grouped short flags, e.g. `-ab` where `a` and `b` are aliases.
|
|
128
|
+
// The last short in the cluster may consume the next token as its value
|
|
129
|
+
// when it's not a boolean option.
|
|
130
|
+
const body = argOrg.slice(1);
|
|
131
|
+
arg = body;
|
|
132
|
+
// If this exact token matches an option (rare), prefer that (e.g. -xy where xy is alias)
|
|
133
|
+
[option, configKey] = findOption(options, body);
|
|
134
|
+
// Avoid treating `-no-foo` or `-noFoo` as a short-char cluster — those are
|
|
135
|
+
// negated long forms and should be handled by the negation branch below.
|
|
136
|
+
if (!option && body.length > 1 && !/^no-/.test(body) && !/^no[A-Z]/.test(body)) {
|
|
137
|
+
// Treat as a cluster of single-char aliases
|
|
138
|
+
const chars = body.split('');
|
|
139
|
+
for (let ci = 0; ci < chars.length; ci++) {
|
|
140
|
+
const ch = chars[ci];
|
|
141
|
+
const isLast = ci === chars.length - 1;
|
|
142
|
+
const [cOpt, cKey] = findOption(options, ch);
|
|
143
|
+
if (!cOpt || !cKey) {
|
|
144
|
+
throw new Error(`Unknown argument: ${ch}`);
|
|
145
|
+
}
|
|
146
|
+
if (!isLast) {
|
|
147
|
+
// Middle flags must be boolean
|
|
148
|
+
if (cOpt.type !== 'boolean') {
|
|
149
|
+
throw new Error(`Missing value for option: ${cKey}`);
|
|
150
|
+
}
|
|
151
|
+
if (result[cKey] !== undefined) {
|
|
152
|
+
throw new Error('Providing same negated and truthy argument are not allowed');
|
|
153
|
+
}
|
|
154
|
+
result[cKey] = true;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
// Last flag: if boolean, set true; otherwise consume next token as its value
|
|
158
|
+
if (cOpt.type === 'boolean') {
|
|
159
|
+
if (result[cKey] !== undefined) {
|
|
160
|
+
throw new Error('Providing same negated and truthy argument are not allowed');
|
|
161
|
+
}
|
|
162
|
+
result[cKey] = true;
|
|
163
|
+
}
|
|
164
|
+
else if (cOpt.type === 'number') {
|
|
165
|
+
if (args[argIndex + 1] === undefined || args[argIndex + 1].startsWith('-')) {
|
|
166
|
+
throw new Error(`Missing value for option: ${cKey}`);
|
|
167
|
+
}
|
|
168
|
+
result[cKey] = Number(args[++argIndex]);
|
|
169
|
+
}
|
|
170
|
+
else if (cOpt.type === 'array') {
|
|
171
|
+
if (!result[cKey]) {
|
|
172
|
+
result[cKey] = [];
|
|
173
|
+
}
|
|
174
|
+
const arrayValue = args[++argIndex];
|
|
175
|
+
if (arrayValue === undefined || arrayValue.startsWith('-')) {
|
|
176
|
+
throw new Error(`Missing value for array option: ${cKey}`);
|
|
177
|
+
}
|
|
178
|
+
result[cKey].push(arrayValue);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
if (args[argIndex + 1] === undefined || args[argIndex + 1].startsWith('-')) {
|
|
182
|
+
throw new Error(`Missing value for option: ${cKey}`);
|
|
183
|
+
}
|
|
184
|
+
result[cKey] = args[++argIndex];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// We've consumed any value for the last short (if present), continue to next arg
|
|
189
|
+
argIndex++;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
121
192
|
}
|
|
122
193
|
// Handle negated boolean in both forms
|
|
123
194
|
if (!option) {
|
|
@@ -131,12 +202,24 @@ export function parseArgs(config) {
|
|
|
131
202
|
throw new Error('Providing same negated and truthy argument are not allowed');
|
|
132
203
|
}
|
|
133
204
|
result[configKey] = !isNegated;
|
|
205
|
+
// When explicitly negated (e.g. --no-foo) also expose `noFoo` and `no-foo` keys
|
|
206
|
+
if (isNegated) {
|
|
207
|
+
const kebabKey = camelToKebab(configKey);
|
|
208
|
+
const noCamel = `no${configKey[0].toUpperCase()}${configKey.slice(1)}`;
|
|
209
|
+
const noKebab = `no-${kebabKey}`;
|
|
210
|
+
if (result[noCamel] === undefined) {
|
|
211
|
+
result[noCamel] = true;
|
|
212
|
+
}
|
|
213
|
+
if (result[noKebab] === undefined) {
|
|
214
|
+
result[noKebab] = true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
134
217
|
argIndex++;
|
|
135
218
|
continue;
|
|
136
219
|
}
|
|
137
220
|
}
|
|
138
221
|
if (!option || !configKey) {
|
|
139
|
-
throw new Error(`Unknown
|
|
222
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
140
223
|
}
|
|
141
224
|
switch (option.type) {
|
|
142
225
|
case 'boolean':
|
|
@@ -144,6 +227,18 @@ export function parseArgs(config) {
|
|
|
144
227
|
throw new Error('Providing same negated and truthy argument are not allowed');
|
|
145
228
|
}
|
|
146
229
|
result[configKey] = !argOrg.startsWith('--no-') && !argOrg.startsWith('-no-');
|
|
230
|
+
// When a boolean was explicitly negated, also expose `noFoo` and `no-foo` keys
|
|
231
|
+
if (result[configKey] === false) {
|
|
232
|
+
const kebabKey = camelToKebab(configKey);
|
|
233
|
+
const noCamel = `no${configKey[0].toUpperCase()}${configKey.slice(1)}`;
|
|
234
|
+
const noKebab = `no-${kebabKey}`;
|
|
235
|
+
if (result[noCamel] === undefined) {
|
|
236
|
+
result[noCamel] = true;
|
|
237
|
+
}
|
|
238
|
+
if (result[noKebab] === undefined) {
|
|
239
|
+
result[noKebab] = true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
147
242
|
break;
|
|
148
243
|
case 'number':
|
|
149
244
|
if (args[argIndex + 1] === undefined || args[argIndex + 1].startsWith('-')) {
|
|
@@ -152,21 +247,38 @@ export function parseArgs(config) {
|
|
|
152
247
|
result[configKey] = Number(args[++argIndex]);
|
|
153
248
|
break;
|
|
154
249
|
case 'array': {
|
|
155
|
-
if (!result[configKey])
|
|
250
|
+
if (!result[configKey]) {
|
|
156
251
|
result[configKey] = [];
|
|
157
|
-
const arrayValue = args[++argIndex];
|
|
158
|
-
if (arrayValue === undefined || arrayValue.startsWith('-')) {
|
|
159
|
-
throw new Error(`Missing value for array option: ${configKey}`);
|
|
160
252
|
}
|
|
161
|
-
|
|
253
|
+
const next = args[argIndex + 1];
|
|
254
|
+
if (next === undefined || next.startsWith('-')) {
|
|
255
|
+
// For bare long-form options we allow empty value; short/clustered forms still throw
|
|
256
|
+
if (argOrg.startsWith('--')) {
|
|
257
|
+
result[configKey].push('');
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
throw new Error(`Missing value for array option: ${configKey}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
result[configKey].push(args[++argIndex]);
|
|
265
|
+
}
|
|
162
266
|
break;
|
|
163
267
|
}
|
|
164
268
|
case 'string':
|
|
165
269
|
default:
|
|
166
270
|
if (args[argIndex + 1] === undefined || args[argIndex + 1].startsWith('-')) {
|
|
167
|
-
|
|
271
|
+
if (argOrg.startsWith('--')) {
|
|
272
|
+
// treat bare long option as empty string
|
|
273
|
+
result[configKey] = '';
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
throw new Error(`Missing value for option: ${configKey}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
result[configKey] = args[++argIndex];
|
|
168
281
|
}
|
|
169
|
-
result[configKey] = args[++argIndex];
|
|
170
282
|
break;
|
|
171
283
|
}
|
|
172
284
|
}
|
|
@@ -175,17 +287,55 @@ export function parseArgs(config) {
|
|
|
175
287
|
}
|
|
176
288
|
argIndex++;
|
|
177
289
|
}
|
|
178
|
-
// After all parsing,
|
|
179
|
-
//
|
|
290
|
+
// After all parsing, first ensure any `required` CLI options are present
|
|
291
|
+
// (required should error even for boolean flags). Then assign any
|
|
292
|
+
// explicit `default` values and finally apply yargs-parity boolean
|
|
293
|
+
// defaults for non-required boolean flags.
|
|
180
294
|
Object.entries(options).forEach(([key, opt]) => {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
if (opt.required && result[key] === undefined) {
|
|
295
|
+
// If the option wasn't provided and is required, throw immediately.
|
|
296
|
+
if (result[key] === undefined && opt.required) {
|
|
185
297
|
const aliasStr = opt.alias ? `-${opt.alias}, ` : '';
|
|
186
298
|
throw new Error(`Missing required option: ${aliasStr}--${key}`);
|
|
187
299
|
}
|
|
300
|
+
// If the option wasn't provided, apply any explicit default.
|
|
301
|
+
if (result[key] === undefined) {
|
|
302
|
+
if (opt.default !== undefined) {
|
|
303
|
+
result[key] = opt.default;
|
|
304
|
+
}
|
|
305
|
+
else if (opt.type === 'boolean') {
|
|
306
|
+
// Parity with yargs: boolean flags default to false when not provided
|
|
307
|
+
result[key] = false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
188
310
|
});
|
|
311
|
+
// Expose raw non-option positionals as `._` for convenience
|
|
312
|
+
if (result._ === undefined) {
|
|
313
|
+
result._ = nonOptionArgs.slice();
|
|
314
|
+
}
|
|
315
|
+
// Duplicate parsed keys into both kebab-case and camelCase for parity with yargs.
|
|
316
|
+
// Use a snapshot of keys to avoid iterating over newly-created duplicates.
|
|
317
|
+
const parsedKeys = Object.keys(result);
|
|
318
|
+
for (const key of parsedKeys) {
|
|
319
|
+
if (key === '--' || key === '__rawArgs') {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const value = result[key];
|
|
323
|
+
const kebab = camelToKebab(key);
|
|
324
|
+
const camel = kebabToCamel(key);
|
|
325
|
+
if (kebab !== key && result[kebab] === undefined) {
|
|
326
|
+
result[kebab] = value;
|
|
327
|
+
}
|
|
328
|
+
if (camel !== key && result[camel] === undefined) {
|
|
329
|
+
result[camel] = value;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Expose passthrough tokens and raw args for downstream consumers
|
|
333
|
+
if (!result['--']) {
|
|
334
|
+
result['--'] = dashArgs.slice();
|
|
335
|
+
}
|
|
336
|
+
if (result.__rawArgs === undefined) {
|
|
337
|
+
result.__rawArgs = __rawArgs.slice();
|
|
338
|
+
}
|
|
189
339
|
return result;
|
|
190
340
|
}
|
|
191
341
|
/** Format a text to a fixed length, truncating and padding as needed. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cli-nano",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Small command-line tool similar to `yargs` or `parseArgs` from Node.js to create a CLI accepting positional arguments, flags and options.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
package/src/index.ts
CHANGED
|
@@ -10,14 +10,24 @@ const defaultOptions: Record<string, FlagOption> = {
|
|
|
10
10
|
export function parseArgs<C extends Config>(config: C): ArgsResult<C> {
|
|
11
11
|
const { command, options, version } = config;
|
|
12
12
|
|
|
13
|
+
// Capture raw argv for consumers/diagnostics
|
|
14
|
+
const __rawArgs = process.argv.slice(2);
|
|
15
|
+
|
|
13
16
|
// Normalize args to support --option=value and -o=value
|
|
14
|
-
|
|
17
|
+
let args = __rawArgs.flatMap(arg => {
|
|
15
18
|
if (/^--?\w[\w-]*=/.test(arg)) {
|
|
16
19
|
const [flag, ...rest] = arg.split('=');
|
|
17
20
|
return [flag, rest.join('=')];
|
|
18
21
|
}
|
|
19
22
|
return arg;
|
|
20
23
|
});
|
|
24
|
+
|
|
25
|
+
// Capture `--` passthrough (everything after `--`) and remove from args to avoid parsing
|
|
26
|
+
const dashIndex = args.indexOf('--');
|
|
27
|
+
const dashArgs: string[] = dashIndex >= 0 ? args.slice(dashIndex + 1) : [];
|
|
28
|
+
if (dashIndex >= 0) {
|
|
29
|
+
args = args.slice(0, dashIndex);
|
|
30
|
+
}
|
|
21
31
|
const result: Record<string, any> = {};
|
|
22
32
|
|
|
23
33
|
// Check for duplicate aliases
|
|
@@ -125,8 +135,70 @@ export function parseArgs<C extends Config>(config: C): ArgsResult<C> {
|
|
|
125
135
|
arg = argOrg.slice(2);
|
|
126
136
|
[option, configKey] = findOption(options, arg);
|
|
127
137
|
} else if (argOrg.startsWith('-')) {
|
|
128
|
-
|
|
129
|
-
|
|
138
|
+
// Support grouped short flags, e.g. `-ab` where `a` and `b` are aliases.
|
|
139
|
+
// The last short in the cluster may consume the next token as its value
|
|
140
|
+
// when it's not a boolean option.
|
|
141
|
+
const body = argOrg.slice(1);
|
|
142
|
+
arg = body;
|
|
143
|
+
|
|
144
|
+
// If this exact token matches an option (rare), prefer that (e.g. -xy where xy is alias)
|
|
145
|
+
[option, configKey] = findOption(options, body);
|
|
146
|
+
// Avoid treating `-no-foo` or `-noFoo` as a short-char cluster — those are
|
|
147
|
+
// negated long forms and should be handled by the negation branch below.
|
|
148
|
+
if (!option && body.length > 1 && !/^no-/.test(body) && !/^no[A-Z]/.test(body)) {
|
|
149
|
+
// Treat as a cluster of single-char aliases
|
|
150
|
+
const chars = body.split('');
|
|
151
|
+
for (let ci = 0; ci < chars.length; ci++) {
|
|
152
|
+
const ch = chars[ci];
|
|
153
|
+
const isLast = ci === chars.length - 1;
|
|
154
|
+
const [cOpt, cKey] = findOption(options, ch);
|
|
155
|
+
if (!cOpt || !cKey) {
|
|
156
|
+
throw new Error(`Unknown argument: ${ch}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!isLast) {
|
|
160
|
+
// Middle flags must be boolean
|
|
161
|
+
if (cOpt.type !== 'boolean') {
|
|
162
|
+
throw new Error(`Missing value for option: ${cKey}`);
|
|
163
|
+
}
|
|
164
|
+
if (result[cKey] !== undefined) {
|
|
165
|
+
throw new Error('Providing same negated and truthy argument are not allowed');
|
|
166
|
+
}
|
|
167
|
+
result[cKey] = true;
|
|
168
|
+
} else {
|
|
169
|
+
// Last flag: if boolean, set true; otherwise consume next token as its value
|
|
170
|
+
if (cOpt.type === 'boolean') {
|
|
171
|
+
if (result[cKey] !== undefined) {
|
|
172
|
+
throw new Error('Providing same negated and truthy argument are not allowed');
|
|
173
|
+
}
|
|
174
|
+
result[cKey] = true;
|
|
175
|
+
} else if (cOpt.type === 'number') {
|
|
176
|
+
if (args[argIndex + 1] === undefined || args[argIndex + 1].startsWith('-')) {
|
|
177
|
+
throw new Error(`Missing value for option: ${cKey}`);
|
|
178
|
+
}
|
|
179
|
+
result[cKey] = Number(args[++argIndex]);
|
|
180
|
+
} else if (cOpt.type === 'array') {
|
|
181
|
+
if (!result[cKey]) {
|
|
182
|
+
result[cKey] = [];
|
|
183
|
+
}
|
|
184
|
+
const arrayValue = args[++argIndex];
|
|
185
|
+
if (arrayValue === undefined || arrayValue.startsWith('-')) {
|
|
186
|
+
throw new Error(`Missing value for array option: ${cKey}`);
|
|
187
|
+
}
|
|
188
|
+
result[cKey].push(arrayValue);
|
|
189
|
+
} else {
|
|
190
|
+
if (args[argIndex + 1] === undefined || args[argIndex + 1].startsWith('-')) {
|
|
191
|
+
throw new Error(`Missing value for option: ${cKey}`);
|
|
192
|
+
}
|
|
193
|
+
result[cKey] = args[++argIndex];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// We've consumed any value for the last short (if present), continue to next arg
|
|
199
|
+
argIndex++;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
130
202
|
}
|
|
131
203
|
|
|
132
204
|
// Handle negated boolean in both forms
|
|
@@ -141,13 +213,25 @@ export function parseArgs<C extends Config>(config: C): ArgsResult<C> {
|
|
|
141
213
|
throw new Error('Providing same negated and truthy argument are not allowed');
|
|
142
214
|
}
|
|
143
215
|
result[configKey] = !isNegated;
|
|
216
|
+
// When explicitly negated (e.g. --no-foo) also expose `noFoo` and `no-foo` keys
|
|
217
|
+
if (isNegated) {
|
|
218
|
+
const kebabKey = camelToKebab(configKey);
|
|
219
|
+
const noCamel = `no${configKey[0].toUpperCase()}${configKey.slice(1)}`;
|
|
220
|
+
const noKebab = `no-${kebabKey}`;
|
|
221
|
+
if (result[noCamel] === undefined) {
|
|
222
|
+
result[noCamel] = true;
|
|
223
|
+
}
|
|
224
|
+
if (result[noKebab] === undefined) {
|
|
225
|
+
result[noKebab] = true;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
144
228
|
argIndex++;
|
|
145
229
|
continue;
|
|
146
230
|
}
|
|
147
231
|
}
|
|
148
232
|
|
|
149
233
|
if (!option || !configKey) {
|
|
150
|
-
throw new Error(`Unknown
|
|
234
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
151
235
|
}
|
|
152
236
|
|
|
153
237
|
switch (option.type) {
|
|
@@ -156,6 +240,18 @@ export function parseArgs<C extends Config>(config: C): ArgsResult<C> {
|
|
|
156
240
|
throw new Error('Providing same negated and truthy argument are not allowed');
|
|
157
241
|
}
|
|
158
242
|
result[configKey] = !argOrg.startsWith('--no-') && !argOrg.startsWith('-no-');
|
|
243
|
+
// When a boolean was explicitly negated, also expose `noFoo` and `no-foo` keys
|
|
244
|
+
if (result[configKey] === false) {
|
|
245
|
+
const kebabKey = camelToKebab(configKey);
|
|
246
|
+
const noCamel = `no${configKey[0].toUpperCase()}${configKey.slice(1)}`;
|
|
247
|
+
const noKebab = `no-${kebabKey}`;
|
|
248
|
+
if (result[noCamel] === undefined) {
|
|
249
|
+
result[noCamel] = true;
|
|
250
|
+
}
|
|
251
|
+
if (result[noKebab] === undefined) {
|
|
252
|
+
result[noKebab] = true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
159
255
|
break;
|
|
160
256
|
case 'number':
|
|
161
257
|
if (args[argIndex + 1] === undefined || args[argIndex + 1].startsWith('-')) {
|
|
@@ -164,20 +260,34 @@ export function parseArgs<C extends Config>(config: C): ArgsResult<C> {
|
|
|
164
260
|
result[configKey] = Number(args[++argIndex]);
|
|
165
261
|
break;
|
|
166
262
|
case 'array': {
|
|
167
|
-
if (!result[configKey])
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
263
|
+
if (!result[configKey]) {
|
|
264
|
+
result[configKey] = [];
|
|
265
|
+
}
|
|
266
|
+
const next = args[argIndex + 1];
|
|
267
|
+
if (next === undefined || next.startsWith('-')) {
|
|
268
|
+
// For bare long-form options we allow empty value; short/clustered forms still throw
|
|
269
|
+
if (argOrg.startsWith('--')) {
|
|
270
|
+
result[configKey].push('');
|
|
271
|
+
} else {
|
|
272
|
+
throw new Error(`Missing value for array option: ${configKey}`);
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
result[configKey].push(args[++argIndex]);
|
|
171
276
|
}
|
|
172
|
-
result[configKey].push(arrayValue);
|
|
173
277
|
break;
|
|
174
278
|
}
|
|
175
279
|
case 'string':
|
|
176
280
|
default:
|
|
177
281
|
if (args[argIndex + 1] === undefined || args[argIndex + 1].startsWith('-')) {
|
|
178
|
-
|
|
282
|
+
if (argOrg.startsWith('--')) {
|
|
283
|
+
// treat bare long option as empty string
|
|
284
|
+
result[configKey] = '';
|
|
285
|
+
} else {
|
|
286
|
+
throw new Error(`Missing value for option: ${configKey}`);
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
result[configKey] = args[++argIndex];
|
|
179
290
|
}
|
|
180
|
-
result[configKey] = args[++argIndex];
|
|
181
291
|
break;
|
|
182
292
|
}
|
|
183
293
|
} else {
|
|
@@ -186,18 +296,59 @@ export function parseArgs<C extends Config>(config: C): ArgsResult<C> {
|
|
|
186
296
|
argIndex++;
|
|
187
297
|
}
|
|
188
298
|
|
|
189
|
-
// After all parsing,
|
|
190
|
-
//
|
|
299
|
+
// After all parsing, first ensure any `required` CLI options are present
|
|
300
|
+
// (required should error even for boolean flags). Then assign any
|
|
301
|
+
// explicit `default` values and finally apply yargs-parity boolean
|
|
302
|
+
// defaults for non-required boolean flags.
|
|
191
303
|
Object.entries(options).forEach(([key, opt]) => {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
if (opt.required && result[key] === undefined) {
|
|
304
|
+
// If the option wasn't provided and is required, throw immediately.
|
|
305
|
+
if (result[key] === undefined && opt.required) {
|
|
196
306
|
const aliasStr = opt.alias ? `-${opt.alias}, ` : '';
|
|
197
307
|
throw new Error(`Missing required option: ${aliasStr}--${key}`);
|
|
198
308
|
}
|
|
309
|
+
|
|
310
|
+
// If the option wasn't provided, apply any explicit default.
|
|
311
|
+
if (result[key] === undefined) {
|
|
312
|
+
if (opt.default !== undefined) {
|
|
313
|
+
result[key] = opt.default;
|
|
314
|
+
} else if (opt.type === 'boolean') {
|
|
315
|
+
// Parity with yargs: boolean flags default to false when not provided
|
|
316
|
+
result[key] = false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
199
319
|
});
|
|
200
320
|
|
|
321
|
+
// Expose raw non-option positionals as `._` for convenience
|
|
322
|
+
if ((result as any)._ === undefined) {
|
|
323
|
+
(result as any)._ = nonOptionArgs.slice();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Duplicate parsed keys into both kebab-case and camelCase for parity with yargs.
|
|
327
|
+
// Use a snapshot of keys to avoid iterating over newly-created duplicates.
|
|
328
|
+
const parsedKeys = Object.keys(result);
|
|
329
|
+
for (const key of parsedKeys) {
|
|
330
|
+
if (key === '--' || key === '__rawArgs') {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const value = result[key];
|
|
334
|
+
const kebab = camelToKebab(key);
|
|
335
|
+
const camel = kebabToCamel(key);
|
|
336
|
+
if (kebab !== key && result[kebab] === undefined) {
|
|
337
|
+
result[kebab] = value;
|
|
338
|
+
}
|
|
339
|
+
if (camel !== key && result[camel] === undefined) {
|
|
340
|
+
result[camel] = value;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Expose passthrough tokens and raw args for downstream consumers
|
|
345
|
+
if (!result['--']) {
|
|
346
|
+
result['--'] = dashArgs.slice();
|
|
347
|
+
}
|
|
348
|
+
if ((result as any).__rawArgs === undefined) {
|
|
349
|
+
(result as any).__rawArgs = __rawArgs.slice();
|
|
350
|
+
}
|
|
351
|
+
|
|
201
352
|
return result as ArgsResult<C>;
|
|
202
353
|
}
|
|
203
354
|
|