cli-nano 1.2.1 → 1.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 +58 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +151 -11
- package/package.json +1 -1
- package/src/index.ts +152 -11
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
|
|
@@ -69,7 +86,7 @@ const config: Config = {
|
|
|
69
86
|
dryRun: {
|
|
70
87
|
alias: 'd',
|
|
71
88
|
type: 'boolean',
|
|
72
|
-
describe: 'Show what would be
|
|
89
|
+
describe: 'Show what would be executed but without starting the server',
|
|
73
90
|
default: false, // optional default value
|
|
74
91
|
},
|
|
75
92
|
display: {
|
|
@@ -117,14 +134,15 @@ const config: Config = {
|
|
|
117
134
|
helpFlagCasing: 'camel', // show help flag option in which casing (camel/kebab) (defaults to 'kebab')
|
|
118
135
|
helpDescMinLength: 40, // min description length shown in help (defaults to 50)
|
|
119
136
|
helpDescMaxLength: 120, // max description length shown in help (defaults to 100), will show ellipsis (...) when greater
|
|
137
|
+
helpUsageSeparator: ':', // defaults to "→"
|
|
120
138
|
};
|
|
121
139
|
|
|
122
140
|
const args = parseArgs(config);
|
|
123
141
|
console.log(args);
|
|
124
142
|
|
|
125
143
|
// do something with parsed arguments, for example
|
|
126
|
-
|
|
127
|
-
|
|
144
|
+
const { input, port, open } = args;
|
|
145
|
+
startServer({ input, port, open });
|
|
128
146
|
```
|
|
129
147
|
|
|
130
148
|
### Usage with Type Inference
|
|
@@ -140,8 +158,8 @@ const args = parseArgs<typeof config>(config);
|
|
|
140
158
|
|
|
141
159
|
// TypeScript will infer the correct types:
|
|
142
160
|
args.input; // [string, ...string[]] (required, variadic)
|
|
143
|
-
args.port; // number
|
|
144
|
-
args.verbose; // boolean
|
|
161
|
+
args.port; // number (optional, has default)
|
|
162
|
+
args.verbose; // boolean (optional)
|
|
145
163
|
args.display; // boolean (required)
|
|
146
164
|
```
|
|
147
165
|
|
|
@@ -150,12 +168,12 @@ args.display; // boolean (required)
|
|
|
150
168
|
> If you use `const config: Config = { ... }`, you get type checking but not full intelliSense for parsed arguments.
|
|
151
169
|
|
|
152
170
|
> [!NOTE]
|
|
153
|
-
> 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`.
|
|
154
172
|
|
|
155
173
|
#### Example CLI Calls
|
|
156
174
|
|
|
157
175
|
```sh
|
|
158
|
-
# Show help guide (
|
|
176
|
+
# Show help guide (creates it by reading CLI config)
|
|
159
177
|
serve --help
|
|
160
178
|
|
|
161
179
|
# Show version (when defined)
|
|
@@ -164,7 +182,7 @@ serve --version
|
|
|
164
182
|
# Uses default port 5000
|
|
165
183
|
serve dist/index.html
|
|
166
184
|
|
|
167
|
-
# With required and optional
|
|
185
|
+
# With required and optional positional args
|
|
168
186
|
serve index1.html index2.html 8080 -D value
|
|
169
187
|
|
|
170
188
|
# With boolean and array options entered as camelCase (kebab-case works too)
|
|
@@ -178,6 +196,19 @@ serve index.html 7000 -d -e pattern1 -e pattern2 -D value
|
|
|
178
196
|
|
|
179
197
|
# With number option
|
|
180
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
|
|
181
212
|
```
|
|
182
213
|
|
|
183
214
|
#### Notes
|
|
@@ -192,20 +223,19 @@ serve index.html 7000 --up 2 -D value
|
|
|
192
223
|
- **Aliases**: Use `alias` for short flags (e.g., `-d` for `--dryRun`).
|
|
193
224
|
- **Groups**: Use `group` for grouping some commands in help (e.g., `{ group: 'Extra Commands' }`).
|
|
194
225
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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.
|
|
198
231
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
- [native-copyfiles](https://github.com/ghiscoding/native-copyfiles)
|
|
202
|
-
- [remove-glob](https://github.com/ghiscoding/remove-glob)
|
|
232
|
+
See [examples/](examples/) for more usage patterns.
|
|
203
233
|
|
|
204
234
|
## Help Example
|
|
205
235
|
|
|
206
236
|
You can see below an example of a CLI help (which is the result of calling `--help` with the [config](#usage) shown above).
|
|
207
237
|
|
|
208
|
-
Please note:
|
|
238
|
+
Please note the following expectations:
|
|
209
239
|
|
|
210
240
|
- `<option>` → required
|
|
211
241
|
- `[option]` → optional
|
|
@@ -223,7 +253,7 @@ Arguments:
|
|
|
223
253
|
port port to bind on [number]
|
|
224
254
|
|
|
225
255
|
Options:
|
|
226
|
-
-d, --dry-run Show what would be
|
|
256
|
+
-d, --dry-run Show what would be executed but without starting the server [boolean]
|
|
227
257
|
-e, --exclude pattern or glob to exclude (may be passed multiple times) [array]
|
|
228
258
|
-V, --verbose print more information to console [boolean]
|
|
229
259
|
-o, --open open browser when starting server [boolean]
|
|
@@ -236,3 +266,10 @@ Advanced Options:
|
|
|
236
266
|
-D, --display a required display option <boolean>
|
|
237
267
|
-r, --rainbow Enable rainbow mode [boolean]
|
|
238
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,CA6UpE"}
|
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
|
}
|
|
@@ -186,6 +298,34 @@ export function parseArgs(config) {
|
|
|
186
298
|
throw new Error(`Missing required option: ${aliasStr}--${key}`);
|
|
187
299
|
}
|
|
188
300
|
});
|
|
301
|
+
// Expose raw non-option positionals as `._` for convenience
|
|
302
|
+
if (result._ === undefined) {
|
|
303
|
+
result._ = nonOptionArgs.slice();
|
|
304
|
+
}
|
|
305
|
+
// Duplicate parsed keys into both kebab-case and camelCase for parity with yargs.
|
|
306
|
+
// Use a snapshot of keys to avoid iterating over newly-created duplicates.
|
|
307
|
+
const parsedKeys = Object.keys(result);
|
|
308
|
+
for (const key of parsedKeys) {
|
|
309
|
+
if (key === '--' || key === '__rawArgs') {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const value = result[key];
|
|
313
|
+
const kebab = camelToKebab(key);
|
|
314
|
+
const camel = kebabToCamel(key);
|
|
315
|
+
if (kebab !== key && result[kebab] === undefined) {
|
|
316
|
+
result[kebab] = value;
|
|
317
|
+
}
|
|
318
|
+
if (camel !== key && result[camel] === undefined) {
|
|
319
|
+
result[camel] = value;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Expose passthrough tokens and raw args for downstream consumers
|
|
323
|
+
if (!result['--']) {
|
|
324
|
+
result['--'] = dashArgs.slice();
|
|
325
|
+
}
|
|
326
|
+
if (result.__rawArgs === undefined) {
|
|
327
|
+
result.__rawArgs = __rawArgs.slice();
|
|
328
|
+
}
|
|
189
329
|
return result;
|
|
190
330
|
}
|
|
191
331
|
/** 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.0",
|
|
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 {
|
|
@@ -198,6 +308,37 @@ export function parseArgs<C extends Config>(config: C): ArgsResult<C> {
|
|
|
198
308
|
}
|
|
199
309
|
});
|
|
200
310
|
|
|
311
|
+
// Expose raw non-option positionals as `._` for convenience
|
|
312
|
+
if ((result as any)._ === undefined) {
|
|
313
|
+
(result as any)._ = nonOptionArgs.slice();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Duplicate parsed keys into both kebab-case and camelCase for parity with yargs.
|
|
317
|
+
// Use a snapshot of keys to avoid iterating over newly-created duplicates.
|
|
318
|
+
const parsedKeys = Object.keys(result);
|
|
319
|
+
for (const key of parsedKeys) {
|
|
320
|
+
if (key === '--' || key === '__rawArgs') {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const value = result[key];
|
|
324
|
+
const kebab = camelToKebab(key);
|
|
325
|
+
const camel = kebabToCamel(key);
|
|
326
|
+
if (kebab !== key && result[kebab] === undefined) {
|
|
327
|
+
result[kebab] = value;
|
|
328
|
+
}
|
|
329
|
+
if (camel !== key && result[camel] === undefined) {
|
|
330
|
+
result[camel] = value;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Expose passthrough tokens and raw args for downstream consumers
|
|
335
|
+
if (!result['--']) {
|
|
336
|
+
result['--'] = dashArgs.slice();
|
|
337
|
+
}
|
|
338
|
+
if ((result as any).__rawArgs === undefined) {
|
|
339
|
+
(result as any).__rawArgs = __rawArgs.slice();
|
|
340
|
+
}
|
|
341
|
+
|
|
201
342
|
return result as ArgsResult<C>;
|
|
202
343
|
}
|
|
203
344
|
|