@naturalcycles/nodejs-lib 15.115.0 → 15.115.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/dist/cli/parseArgs.js +55 -0
- package/package.json +3 -3
- package/src/cli/parseArgs.ts +62 -0
package/dist/cli/parseArgs.js
CHANGED
|
@@ -43,6 +43,7 @@ export function _parseArgs(options, opt = {}) {
|
|
|
43
43
|
options: nodeOptions,
|
|
44
44
|
allowPositionals: true,
|
|
45
45
|
allowNegative: true, // native `--no-flag` support for booleans
|
|
46
|
+
tokens: true, // needed to detect the ambiguous `--boolFlag value` space form
|
|
46
47
|
// In non-strict mode, unknown options are collected into `values` but
|
|
47
48
|
// ignored below, since we only read declared options. See `strict` docs.
|
|
48
49
|
strict,
|
|
@@ -57,6 +58,7 @@ export function _parseArgs(options, opt = {}) {
|
|
|
57
58
|
process.stdout.write(buildHelp(options, usage));
|
|
58
59
|
process.exit(0);
|
|
59
60
|
}
|
|
61
|
+
assertNoSpaceValuedBoolean(parsed.tokens, options);
|
|
60
62
|
const result = { _: parsed.positionals };
|
|
61
63
|
for (const [name, def] of Object.entries(options)) {
|
|
62
64
|
let v = values[name];
|
|
@@ -78,10 +80,32 @@ export function _parseArgs(options, opt = {}) {
|
|
|
78
80
|
if (def.array) {
|
|
79
81
|
v = Array.isArray(v) ? v : [v];
|
|
80
82
|
}
|
|
83
|
+
// A non-boolean option passed as a bare flag (`--out` with no value) comes back
|
|
84
|
+
// from node's parseArgs (in non-strict mode) as boolean `true`. Reject it: the
|
|
85
|
+
// user almost certainly forgot the value, and silently coercing `true` (to `1`
|
|
86
|
+
// for numbers, `"true"` for strings, or crashing a `transform`) would hide the
|
|
87
|
+
// mistake. Only arg-sourced values are checked; a boolean `default` is left
|
|
88
|
+
// alone. `def.type === 'boolean'` legitimately produces booleans, so skip it.
|
|
89
|
+
if (fromArgs && def.type !== 'boolean') {
|
|
90
|
+
const bareFlag = Array.isArray(v)
|
|
91
|
+
? v.some(x => typeof x === 'boolean')
|
|
92
|
+
: typeof v === 'boolean';
|
|
93
|
+
if (bareFlag) {
|
|
94
|
+
throw new ParseArgsError(`Missing value for --${name}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
81
97
|
// `transform` owns conversion, so built-in number coercion is skipped for it
|
|
82
98
|
if (def.type === 'number' && !def.transform) {
|
|
83
99
|
v = Array.isArray(v) ? v.map(x => toNumber(x, name)) : toNumber(v, name);
|
|
84
100
|
}
|
|
101
|
+
// node's parseArgs (in non-strict mode) captures the inline value of
|
|
102
|
+
// `--flag=value` on a boolean option as a string ("false"/"true"), rather than
|
|
103
|
+
// rejecting it as strict mode does. Coerce known tokens so `--flag=false` means
|
|
104
|
+
// boolean false, not a truthy "false" string. Real booleans produced by
|
|
105
|
+
// `--flag` / `--no-flag` (and boolean defaults) pass through untouched.
|
|
106
|
+
if (def.type === 'boolean') {
|
|
107
|
+
v = Array.isArray(v) ? v.map(x => toBoolean(x, name)) : toBoolean(v, name);
|
|
108
|
+
}
|
|
85
109
|
if (def.choices) {
|
|
86
110
|
const list = Array.isArray(v) ? v : [v];
|
|
87
111
|
for (const x of list) {
|
|
@@ -101,6 +125,28 @@ export function _parseArgs(options, opt = {}) {
|
|
|
101
125
|
}
|
|
102
126
|
return result;
|
|
103
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Reject the ambiguous `--boolFlag value` space form. node never consumes the
|
|
130
|
+
* next token as a boolean's value (getopt convention), so `--arg false` would
|
|
131
|
+
* silently yield `arg: true` and leak "false" into positionals. Unlike the
|
|
132
|
+
* `=value` form (handled by toBoolean) we can't recover the intended value here,
|
|
133
|
+
* so fail loudly. Only `true`/`false` tokens are treated as ambiguous; any other
|
|
134
|
+
* positional (e.g. a filename) is left as a genuine positional.
|
|
135
|
+
*/
|
|
136
|
+
function assertNoSpaceValuedBoolean(tokens, options) {
|
|
137
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
138
|
+
const tok = tokens[i];
|
|
139
|
+
// `tok.value === undefined` => bare flag (no inline `=value`); applies to
|
|
140
|
+
// declared boolean options only (unknown options are ignored, see `strict`).
|
|
141
|
+
if (tok.kind !== 'option' || tok.value !== undefined || options[tok.name]?.type !== 'boolean') {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const next = tokens[i + 1];
|
|
145
|
+
if (next.kind === 'positional' && (next.value === 'true' || next.value === 'false')) {
|
|
146
|
+
throw new ParseArgsError(`Boolean option --${tok.name} does not take a space-separated value ("${next.value}"); use --${tok.name}=${next.value} or --${next.value === 'false' ? `no-${tok.name}` : tok.name}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
104
150
|
function toNumber(raw, name) {
|
|
105
151
|
const n = Number(raw);
|
|
106
152
|
if (Number.isNaN(n)) {
|
|
@@ -108,6 +154,15 @@ function toNumber(raw, name) {
|
|
|
108
154
|
}
|
|
109
155
|
return n;
|
|
110
156
|
}
|
|
157
|
+
function toBoolean(raw, name) {
|
|
158
|
+
if (typeof raw === 'boolean')
|
|
159
|
+
return raw; // real boolean from --flag / --no-flag / default
|
|
160
|
+
if (raw === 'true')
|
|
161
|
+
return true;
|
|
162
|
+
if (raw === 'false')
|
|
163
|
+
return false;
|
|
164
|
+
throw new ParseArgsError(`Invalid boolean for --${name}: "${raw}"`);
|
|
165
|
+
}
|
|
111
166
|
function buildHelp(options, usage) {
|
|
112
167
|
const lines = [];
|
|
113
168
|
if (usage)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/nodejs-lib",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "15.115.
|
|
4
|
+
"version": "15.115.1",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@naturalcycles/js-lib": "^15",
|
|
7
7
|
"@standard-schema/spec": "^1",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"yaml": "^2"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"typescript": "
|
|
19
|
-
"@naturalcycles/dev-lib": "
|
|
18
|
+
"typescript": "^7",
|
|
19
|
+
"@naturalcycles/dev-lib": "0.0.0"
|
|
20
20
|
},
|
|
21
21
|
"exports": {
|
|
22
22
|
".": "./dist/index.js",
|
package/src/cli/parseArgs.ts
CHANGED
|
@@ -186,6 +186,7 @@ export function _parseArgs<const O extends CliOptions>(
|
|
|
186
186
|
options: nodeOptions,
|
|
187
187
|
allowPositionals: true,
|
|
188
188
|
allowNegative: true, // native `--no-flag` support for booleans
|
|
189
|
+
tokens: true, // needed to detect the ambiguous `--boolFlag value` space form
|
|
189
190
|
// In non-strict mode, unknown options are collected into `values` but
|
|
190
191
|
// ignored below, since we only read declared options. See `strict` docs.
|
|
191
192
|
strict,
|
|
@@ -202,6 +203,8 @@ export function _parseArgs<const O extends CliOptions>(
|
|
|
202
203
|
process.exit(0)
|
|
203
204
|
}
|
|
204
205
|
|
|
206
|
+
assertNoSpaceValuedBoolean(parsed.tokens, options)
|
|
207
|
+
|
|
205
208
|
const result: Record<string, unknown> = { _: parsed.positionals }
|
|
206
209
|
|
|
207
210
|
for (const [name, def] of Object.entries(options)) {
|
|
@@ -225,11 +228,35 @@ export function _parseArgs<const O extends CliOptions>(
|
|
|
225
228
|
v = Array.isArray(v) ? v : [v]
|
|
226
229
|
}
|
|
227
230
|
|
|
231
|
+
// A non-boolean option passed as a bare flag (`--out` with no value) comes back
|
|
232
|
+
// from node's parseArgs (in non-strict mode) as boolean `true`. Reject it: the
|
|
233
|
+
// user almost certainly forgot the value, and silently coercing `true` (to `1`
|
|
234
|
+
// for numbers, `"true"` for strings, or crashing a `transform`) would hide the
|
|
235
|
+
// mistake. Only arg-sourced values are checked; a boolean `default` is left
|
|
236
|
+
// alone. `def.type === 'boolean'` legitimately produces booleans, so skip it.
|
|
237
|
+
if (fromArgs && def.type !== 'boolean') {
|
|
238
|
+
const bareFlag = Array.isArray(v)
|
|
239
|
+
? v.some(x => typeof x === 'boolean')
|
|
240
|
+
: typeof v === 'boolean'
|
|
241
|
+
if (bareFlag) {
|
|
242
|
+
throw new ParseArgsError(`Missing value for --${name}`)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
228
246
|
// `transform` owns conversion, so built-in number coercion is skipped for it
|
|
229
247
|
if (def.type === 'number' && !def.transform) {
|
|
230
248
|
v = Array.isArray(v) ? v.map(x => toNumber(x, name)) : toNumber(v, name)
|
|
231
249
|
}
|
|
232
250
|
|
|
251
|
+
// node's parseArgs (in non-strict mode) captures the inline value of
|
|
252
|
+
// `--flag=value` on a boolean option as a string ("false"/"true"), rather than
|
|
253
|
+
// rejecting it as strict mode does. Coerce known tokens so `--flag=false` means
|
|
254
|
+
// boolean false, not a truthy "false" string. Real booleans produced by
|
|
255
|
+
// `--flag` / `--no-flag` (and boolean defaults) pass through untouched.
|
|
256
|
+
if (def.type === 'boolean') {
|
|
257
|
+
v = Array.isArray(v) ? v.map(x => toBoolean(x, name)) : toBoolean(v, name)
|
|
258
|
+
}
|
|
259
|
+
|
|
233
260
|
if (def.choices) {
|
|
234
261
|
const list = Array.isArray(v) ? v : [v]
|
|
235
262
|
for (const x of list) {
|
|
@@ -258,6 +285,34 @@ export function _parseArgs<const O extends CliOptions>(
|
|
|
258
285
|
return result as InferCliArgs<O>
|
|
259
286
|
}
|
|
260
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Reject the ambiguous `--boolFlag value` space form. node never consumes the
|
|
290
|
+
* next token as a boolean's value (getopt convention), so `--arg false` would
|
|
291
|
+
* silently yield `arg: true` and leak "false" into positionals. Unlike the
|
|
292
|
+
* `=value` form (handled by toBoolean) we can't recover the intended value here,
|
|
293
|
+
* so fail loudly. Only `true`/`false` tokens are treated as ambiguous; any other
|
|
294
|
+
* positional (e.g. a filename) is left as a genuine positional.
|
|
295
|
+
*/
|
|
296
|
+
function assertNoSpaceValuedBoolean(
|
|
297
|
+
tokens: NonNullable<ReturnType<typeof parseArgs>['tokens']>,
|
|
298
|
+
options: CliOptions,
|
|
299
|
+
): void {
|
|
300
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
301
|
+
const tok = tokens[i]!
|
|
302
|
+
// `tok.value === undefined` => bare flag (no inline `=value`); applies to
|
|
303
|
+
// declared boolean options only (unknown options are ignored, see `strict`).
|
|
304
|
+
if (tok.kind !== 'option' || tok.value !== undefined || options[tok.name]?.type !== 'boolean') {
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
const next = tokens[i + 1]!
|
|
308
|
+
if (next.kind === 'positional' && (next.value === 'true' || next.value === 'false')) {
|
|
309
|
+
throw new ParseArgsError(
|
|
310
|
+
`Boolean option --${tok.name} does not take a space-separated value ("${next.value}"); use --${tok.name}=${next.value} or --${next.value === 'false' ? `no-${tok.name}` : tok.name}`,
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
261
316
|
function toNumber(raw: unknown, name: string): number {
|
|
262
317
|
const n = Number(raw)
|
|
263
318
|
if (Number.isNaN(n)) {
|
|
@@ -266,6 +321,13 @@ function toNumber(raw: unknown, name: string): number {
|
|
|
266
321
|
return n
|
|
267
322
|
}
|
|
268
323
|
|
|
324
|
+
function toBoolean(raw: unknown, name: string): boolean {
|
|
325
|
+
if (typeof raw === 'boolean') return raw // real boolean from --flag / --no-flag / default
|
|
326
|
+
if (raw === 'true') return true
|
|
327
|
+
if (raw === 'false') return false
|
|
328
|
+
throw new ParseArgsError(`Invalid boolean for --${name}: "${raw}"`)
|
|
329
|
+
}
|
|
330
|
+
|
|
269
331
|
function buildHelp(options: CliOptions, usage?: string): string {
|
|
270
332
|
const lines: string[] = []
|
|
271
333
|
if (usage) lines.push(usage, '')
|