argsbarg 7.0.4 → 7.0.5
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/CHANGELOG.md +8 -1
- package/package.json +1 -1
- package/src/core/parse.test.ts +178 -0
- package/src/core/parse.ts +54 -54
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [7.0.5] - 2026-09-15
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Interleaved options between positionals** — options (presence flags and value options) can now be placed anywhere between bounded or optional positional arguments (e.g. `cmd file1 --force file2`), matching varargs tail behavior rather than requiring all options to precede or follow all bounded positionals.
|
|
15
|
+
|
|
10
16
|
## [7.0.4] - 2026-08-17
|
|
11
17
|
|
|
12
18
|
### Changed
|
|
@@ -959,7 +965,8 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
|
|
|
959
965
|
- Migrate schemas: rename every `children` property to **`commands`**; move positional definitions to **`CliPositional`** objects on `positionals` and strip `positional` / `argMin` / `argMax` from flag definitions under `options` (flags only carry `name`, `description`, `kind`, and optional `shortName`).
|
|
960
966
|
- Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
|
|
961
967
|
|
|
962
|
-
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v7.0.
|
|
968
|
+
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v7.0.5...HEAD
|
|
969
|
+
[7.0.5]: https://github.com/bdombro/bun-argsbarg/releases/tag/v7.0.5
|
|
963
970
|
[7.0.4]: https://github.com/bdombro/bun-argsbarg/releases/tag/v7.0.4
|
|
964
971
|
[7.0.3]: https://github.com/bdombro/bun-argsbarg/releases/tag/v7.0.3
|
|
965
972
|
[7.0.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v7.0.2
|
package/package.json
CHANGED
package/src/core/parse.test.ts
CHANGED
|
@@ -370,6 +370,184 @@ test("trailing options after bounded positionals", () => {
|
|
|
370
370
|
expect(pr.opts.verbose).toBe("1");
|
|
371
371
|
});
|
|
372
372
|
|
|
373
|
+
/** Tests that options can be interleaved between bounded positionals. */
|
|
374
|
+
test("options interleaved between bounded positionals", () => {
|
|
375
|
+
const root = testProgram({
|
|
376
|
+
key: "app",
|
|
377
|
+
description: "",
|
|
378
|
+
commands: [
|
|
379
|
+
{
|
|
380
|
+
key: "copy",
|
|
381
|
+
description: "copy",
|
|
382
|
+
options: [
|
|
383
|
+
{
|
|
384
|
+
name: "force",
|
|
385
|
+
description: "",
|
|
386
|
+
kind: CliOptionKind.Presence,
|
|
387
|
+
shortName: "f",
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
name: "mode",
|
|
391
|
+
description: "",
|
|
392
|
+
kind: CliOptionKind.String,
|
|
393
|
+
},
|
|
394
|
+
],
|
|
395
|
+
positionals: [
|
|
396
|
+
{
|
|
397
|
+
name: "src",
|
|
398
|
+
description: "",
|
|
399
|
+
kind: CliOptionKind.String,
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
name: "dest",
|
|
403
|
+
description: "",
|
|
404
|
+
kind: CliOptionKind.String,
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
handler: () => {},
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
});
|
|
411
|
+
cliValidateProgram(root);
|
|
412
|
+
|
|
413
|
+
// Presence flag interleaved between positionals
|
|
414
|
+
const prPresence = postParseValidate(root, parse(root, ["copy", "file1", "--force", "file2"]));
|
|
415
|
+
expect(prPresence.kind).toBe(ParseKind.Ok);
|
|
416
|
+
expect(prPresence.args).toEqual(["file1", "file2"]);
|
|
417
|
+
expect(prPresence.opts.force).toBe("1");
|
|
418
|
+
|
|
419
|
+
// String option with value interleaved between positionals
|
|
420
|
+
const prString = postParseValidate(root, parse(root, ["copy", "file1", "--mode", "fast", "file2"]));
|
|
421
|
+
expect(prString.kind).toBe(ParseKind.Ok);
|
|
422
|
+
expect(prString.args).toEqual(["file1", "file2"]);
|
|
423
|
+
expect(prString.opts.mode).toBe("fast");
|
|
424
|
+
|
|
425
|
+
// Multiple flags interleaved between positionals
|
|
426
|
+
const prMulti = postParseValidate(root, parse(root, ["copy", "file1", "--mode", "fast", "-f", "file2"]));
|
|
427
|
+
expect(prMulti.kind).toBe(ParseKind.Ok);
|
|
428
|
+
expect(prMulti.args).toEqual(["file1", "file2"]);
|
|
429
|
+
expect(prMulti.opts.mode).toBe("fast");
|
|
430
|
+
expect(prMulti.opts.force).toBe("1");
|
|
431
|
+
|
|
432
|
+
// Unknown option interleaved between positionals returns error
|
|
433
|
+
const prUnknown = postParseValidate(root, parse(root, ["copy", "file1", "--unknown", "file2"]));
|
|
434
|
+
expect(prUnknown.kind).toBe(ParseKind.Error);
|
|
435
|
+
expect(prUnknown.errorMsg).toContain("Unknown option: --unknown");
|
|
436
|
+
|
|
437
|
+
// Interleaved help request triggers contextual help
|
|
438
|
+
const prHelp = parse(root, ["copy", "file1", "-h"]);
|
|
439
|
+
expect(prHelp.kind).toBe(ParseKind.Help);
|
|
440
|
+
expect(prHelp.helpPath).toEqual(["copy"]);
|
|
441
|
+
|
|
442
|
+
// Double dash between positionals disables option consumption
|
|
443
|
+
const prDoubleDash = postParseValidate(root, parse(root, ["copy", "file1", "--", "--force"]));
|
|
444
|
+
expect(prDoubleDash.kind).toBe(ParseKind.Ok);
|
|
445
|
+
expect(prDoubleDash.args).toEqual(["file1", "--force"]);
|
|
446
|
+
expect(prDoubleDash.opts.force).toBeUndefined();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
/** Tests that options can be interleaved with optional positionals. */
|
|
450
|
+
test("options interleaved with optional positionals", () => {
|
|
451
|
+
const root = testProgram({
|
|
452
|
+
key: "app",
|
|
453
|
+
description: "",
|
|
454
|
+
commands: [
|
|
455
|
+
{
|
|
456
|
+
key: "deploy",
|
|
457
|
+
description: "deploy",
|
|
458
|
+
options: [
|
|
459
|
+
{
|
|
460
|
+
name: "force",
|
|
461
|
+
description: "",
|
|
462
|
+
kind: CliOptionKind.Presence,
|
|
463
|
+
},
|
|
464
|
+
],
|
|
465
|
+
positionals: [
|
|
466
|
+
{
|
|
467
|
+
name: "env",
|
|
468
|
+
description: "",
|
|
469
|
+
kind: CliOptionKind.String,
|
|
470
|
+
argMin: 0,
|
|
471
|
+
argMax: 1,
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
name: "target",
|
|
475
|
+
description: "",
|
|
476
|
+
kind: CliOptionKind.String,
|
|
477
|
+
argMin: 0,
|
|
478
|
+
argMax: 1,
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
handler: () => {},
|
|
482
|
+
},
|
|
483
|
+
],
|
|
484
|
+
});
|
|
485
|
+
cliValidateProgram(root);
|
|
486
|
+
|
|
487
|
+
// Interleaved between two optional positionals
|
|
488
|
+
const prBoth = postParseValidate(root, parse(root, ["deploy", "prod", "--force", "us-east"]));
|
|
489
|
+
expect(prBoth.kind).toBe(ParseKind.Ok);
|
|
490
|
+
expect(prBoth.args).toEqual(["prod", "us-east"]);
|
|
491
|
+
expect(prBoth.opts.force).toBe("1");
|
|
492
|
+
|
|
493
|
+
// Option after first optional positional when second is omitted
|
|
494
|
+
const prOne = postParseValidate(root, parse(root, ["deploy", "prod", "--force"]));
|
|
495
|
+
expect(prOne.kind).toBe(ParseKind.Ok);
|
|
496
|
+
expect(prOne.args).toEqual(["prod"]);
|
|
497
|
+
expect(prOne.opts.force).toBe("1");
|
|
498
|
+
|
|
499
|
+
// Option before optional positionals when all are omitted
|
|
500
|
+
const prNone = postParseValidate(root, parse(root, ["deploy", "--force"]));
|
|
501
|
+
expect(prNone.kind).toBe(ParseKind.Ok);
|
|
502
|
+
expect(prNone.args).toEqual([]);
|
|
503
|
+
expect(prNone.opts.force).toBe("1");
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
/** Tests that options can be interleaved between bounded positional and varargs tail. */
|
|
507
|
+
test("options interleaved between bounded positional and varargs tail", () => {
|
|
508
|
+
const root = testProgram({
|
|
509
|
+
key: "app",
|
|
510
|
+
description: "",
|
|
511
|
+
commands: [
|
|
512
|
+
{
|
|
513
|
+
key: "upload",
|
|
514
|
+
description: "upload",
|
|
515
|
+
options: [
|
|
516
|
+
{
|
|
517
|
+
name: "json",
|
|
518
|
+
description: "",
|
|
519
|
+
kind: CliOptionKind.Presence,
|
|
520
|
+
},
|
|
521
|
+
],
|
|
522
|
+
positionals: [
|
|
523
|
+
{
|
|
524
|
+
name: "target",
|
|
525
|
+
description: "",
|
|
526
|
+
kind: CliOptionKind.String,
|
|
527
|
+
argMin: 1,
|
|
528
|
+
argMax: 1,
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
name: "files",
|
|
532
|
+
description: "",
|
|
533
|
+
kind: CliOptionKind.String,
|
|
534
|
+
argMin: 1,
|
|
535
|
+
argMax: 0,
|
|
536
|
+
},
|
|
537
|
+
],
|
|
538
|
+
handler: () => {},
|
|
539
|
+
},
|
|
540
|
+
],
|
|
541
|
+
});
|
|
542
|
+
cliValidateProgram(root);
|
|
543
|
+
|
|
544
|
+
// Flag between target and files does not get captured as first file
|
|
545
|
+
const pr = postParseValidate(root, parse(root, ["upload", "s3", "--json", "a.txt", "b.txt"]));
|
|
546
|
+
expect(pr.kind).toBe(ParseKind.Ok);
|
|
547
|
+
expect(pr.args).toEqual(["s3", "a.txt", "b.txt"]);
|
|
548
|
+
expect(pr.opts.json).toBe("1");
|
|
549
|
+
});
|
|
550
|
+
|
|
373
551
|
/** Tests that options on routing groups are rejected at schema validation. */
|
|
374
552
|
test("rejects options on routing groups", () => {
|
|
375
553
|
const root = testProgram({
|
package/src/core/parse.ts
CHANGED
|
@@ -351,9 +351,47 @@ function finishLeaf(
|
|
|
351
351
|
const args: string[] = [];
|
|
352
352
|
let forcePositionals = forcePositionalsIn;
|
|
353
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Consumes any pending options, `--`, or help flags at the current argv index.
|
|
356
|
+
* Sets `forcePositionals = true` when `--` is encountered.
|
|
357
|
+
* Returns a help or error ParseResult if parsing halts, or null to continue positional consumption.
|
|
358
|
+
*/
|
|
359
|
+
function consumePendingOptions(): ParseResult | null {
|
|
360
|
+
while (!forcePositionals && idx < argv.length) {
|
|
361
|
+
const tok = argv[idx];
|
|
362
|
+
if (tok === "--") {
|
|
363
|
+
forcePositionals = true;
|
|
364
|
+
idx += 1;
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
if (isHelpTok(tok)) {
|
|
368
|
+
return helpResult(path, true, pathParams);
|
|
369
|
+
}
|
|
370
|
+
if (tok.startsWith("-")) {
|
|
371
|
+
const rep = consumeOptions(optionDefs, false, argv, idx, opts);
|
|
372
|
+
if (rep.report.err) {
|
|
373
|
+
return errorResult(rep.report.err, path, [], pathParams);
|
|
374
|
+
}
|
|
375
|
+
if (rep.report.sawDoubleDash) {
|
|
376
|
+
forcePositionals = true;
|
|
377
|
+
}
|
|
378
|
+
if (rep.nextIndex > idx) {
|
|
379
|
+
idx = rep.nextIndex;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
return errorResult(`Unexpected option token: ${tok}`, path, [], pathParams);
|
|
383
|
+
}
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
|
|
354
389
|
for (const p of node.positionals ?? []) {
|
|
355
390
|
const { argMin = 1, argMax = 1 } = p;
|
|
356
391
|
if (argMax === 1) {
|
|
392
|
+
const pendingErr = consumePendingOptions();
|
|
393
|
+
if (pendingErr) return pendingErr;
|
|
394
|
+
|
|
357
395
|
if (argMin >= 1) {
|
|
358
396
|
if (idx >= argv.length) {
|
|
359
397
|
return errorResult(`Missing positional argument: ${p.name}`, path, [], pathParams);
|
|
@@ -361,13 +399,8 @@ function finishLeaf(
|
|
|
361
399
|
args.push(argv[idx]);
|
|
362
400
|
idx += 1;
|
|
363
401
|
} else if (idx < argv.length) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
// Optional slot: leave `-` tokens for trailing option parsing.
|
|
367
|
-
} else {
|
|
368
|
-
args.push(tok);
|
|
369
|
-
idx += 1;
|
|
370
|
-
}
|
|
402
|
+
args.push(argv[idx]);
|
|
403
|
+
idx += 1;
|
|
371
404
|
}
|
|
372
405
|
continue;
|
|
373
406
|
}
|
|
@@ -375,40 +408,20 @@ function finishLeaf(
|
|
|
375
408
|
let count = 0;
|
|
376
409
|
if (argMax === 0) {
|
|
377
410
|
while (idx < argv.length) {
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
if (
|
|
381
|
-
forcePositionals = true;
|
|
382
|
-
idx++;
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
411
|
+
const pendingErr = consumePendingOptions();
|
|
412
|
+
if (pendingErr) return pendingErr;
|
|
413
|
+
if (idx >= argv.length) break;
|
|
385
414
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (!forcePositionals && tok.startsWith("-")) {
|
|
391
|
-
// MUST be false — lenient mode swallows unknown flags as positionals silently
|
|
392
|
-
const tailRep = consumeOptions(optionDefs, false, argv, idx, opts);
|
|
393
|
-
if (tailRep.report.err) {
|
|
394
|
-
return errorResult(tailRep.report.err, path, [], pathParams);
|
|
395
|
-
}
|
|
396
|
-
if (tailRep.report.sawDoubleDash) {
|
|
397
|
-
forcePositionals = true;
|
|
398
|
-
}
|
|
399
|
-
if (tailRep.nextIndex > idx) {
|
|
400
|
-
idx = tailRep.nextIndex;
|
|
401
|
-
continue;
|
|
402
|
-
}
|
|
403
|
-
return errorResult(`Unexpected option token: ${tok}`, path, [], pathParams);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
args.push(tok);
|
|
407
|
-
idx++;
|
|
408
|
-
count++;
|
|
415
|
+
args.push(argv[idx]);
|
|
416
|
+
idx += 1;
|
|
417
|
+
count += 1;
|
|
409
418
|
}
|
|
410
419
|
} else {
|
|
411
420
|
while (count < argMax && idx < argv.length) {
|
|
421
|
+
const pendingErr = consumePendingOptions();
|
|
422
|
+
if (pendingErr) return pendingErr;
|
|
423
|
+
if (idx >= argv.length) break;
|
|
424
|
+
|
|
412
425
|
args.push(argv[idx]);
|
|
413
426
|
idx += 1;
|
|
414
427
|
count += 1;
|
|
@@ -419,24 +432,11 @@ function finishLeaf(
|
|
|
419
432
|
}
|
|
420
433
|
}
|
|
421
434
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
return errorResult("Unexpected extra arguments", path, [], pathParams);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
if (isHelpTok(argv[idx])) {
|
|
428
|
-
return helpResult(path, true, pathParams);
|
|
429
|
-
}
|
|
435
|
+
const trailingErr = consumePendingOptions();
|
|
436
|
+
if (trailingErr) return trailingErr;
|
|
430
437
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
return errorResult(tailRep.report.err, path, [], pathParams);
|
|
434
|
-
}
|
|
435
|
-
idx = tailRep.nextIndex;
|
|
436
|
-
|
|
437
|
-
if (idx < argv.length) {
|
|
438
|
-
return errorResult("Unexpected extra arguments", path, [], pathParams);
|
|
439
|
-
}
|
|
438
|
+
if (idx < argv.length) {
|
|
439
|
+
return errorResult("Unexpected extra arguments", path, [], pathParams);
|
|
440
440
|
}
|
|
441
441
|
|
|
442
442
|
return {
|