@clo4/cli 3.0.0 → 3.0.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/testing.js CHANGED
@@ -111,18 +111,62 @@ function applyDirectives(directives, command) {
111
111
  if (typeof configured.optionsMustPrecedeArguments === "boolean") directives.optionsMustPrecedeArguments = configured.optionsMustPrecedeArguments;
112
112
  if (typeof configured.flagsArePosixNoncompliant === "boolean") directives.flagsArePosixNoncompliant = configured.flagsArePosixNoncompliant;
113
113
  }
114
+ // src/subcommands.ts
115
+ /**
116
+ * A token that names no subcommand. The reason is only worked out when asked
117
+ * for, since most tokens the parser tries aren't subcommands.
118
+ */
119
+ var NoSubcommand = class {
120
+ subcommands;
121
+ token;
122
+ prefixOf;
123
+ command = null;
124
+ /**
125
+ * `prefixOf` are the commands `token` is a prefix of a name of, where
126
+ * prefixes match. There is never exactly one of them.
127
+ */
128
+ constructor(subcommands, token, prefixOf) {
129
+ this.subcommands = subcommands;
130
+ this.token = token;
131
+ this.prefixOf = prefixOf;
132
+ }
133
+ /** Why the token names none of the subcommands, as an error message and suggestions. */
134
+ reason() {
135
+ const token = this.token;
136
+ if (token === "") return { message: "Expected a command, but the value was empty" };
137
+ if (this.prefixOf.length > 0) return { message: `Ambiguous command '${token}', could be: ${this.prefixOf.flatMap((command) => asArray(command.name).filter((name) => name.startsWith(token))).join(", ")}` };
138
+ const choices = visibleSubcommands(this.subcommands).map((command) => asArray(command.name));
139
+ return {
140
+ message: `Unknown command '${token}'`,
141
+ didYouMean: {
142
+ input: token,
143
+ choices
144
+ }
145
+ };
146
+ }
147
+ };
148
+ /** The subcommands that help, completion and suggestions show. */
149
+ function visibleSubcommands(subcommands) {
150
+ return subcommands?.filter((subcommand) => !subcommand.hidden) ?? [];
151
+ }
152
+ /** The action of a command that has none of its own: print the command's help. */
153
+ const usage = ({ help }) => {
154
+ print(help());
155
+ return 0;
156
+ };
114
157
  // src/scope.ts
115
158
  /**
116
- * The options and directives in effect at one command on a path.
159
+ * What a name means at one command on a path: the options, subcommands,
160
+ * directives and argument limits in effect there.
117
161
  *
118
162
  * Moving to a subcommand replaces the local options and adds its persistent
119
163
  * options to the inherited ones. A name refers to one option: a local option
120
164
  * wins over a persistent one, and a later option wins over an earlier one in
121
- * the same list. The parser, help, completion and the spec checker all ask
122
- * this module which option a name refers to, so they can't disagree. It also
123
- * holds the persistent subcommands the current command inherits.
165
+ * the same list. A subcommand wins over an option where the next token can
166
+ * name a subcommand. The parser, help, completion and the spec checker all
167
+ * ask this module what a name refers to, so they can't disagree.
124
168
  */
125
- var OptionScope = class OptionScope {
169
+ var CommandScope = class CommandScope {
126
170
  /** Non-persistent options of the current command, in definition order. */
127
171
  local = [];
128
172
  /** Persistent options from every command on the path, in definition order. */
@@ -134,44 +178,89 @@ var OptionScope = class OptionScope {
134
178
  inheritedSubcommands = [];
135
179
  /** The directives in effect. A subcommand inherits each until it sets its own. */
136
180
  directives = defaultDirectives();
137
- command = null;
181
+ current = null;
182
+ /** The fewest positional arguments the current command takes. */
183
+ argsMin = 0;
184
+ /** The most positional arguments the current command takes. */
185
+ argsMax = 0;
138
186
  /** The scope at the last command on `path`. */
139
187
  static at(path) {
140
- const scope = new OptionScope();
188
+ const scope = new CommandScope();
141
189
  for (const command of path) scope.enter(command);
142
190
  return scope;
143
191
  }
144
192
  /** Move to `command`, a subcommand of the current one. */
145
193
  enter(command) {
146
- const parentSubcommands = this.command?.subcommands;
194
+ const parentSubcommands = this.current?.subcommands;
147
195
  if (parentSubcommands) {
148
196
  const persistent = parentSubcommands.filter((subcommand) => subcommand.isPersistent);
149
197
  if (persistent.length > 0) this.inheritedSubcommands.unshift(...persistent);
150
198
  }
151
- this.command = command;
199
+ this.current = command;
200
+ this.argsMin = getMinArgs(command.args);
201
+ this.argsMax = getMaxArgs(command.args);
152
202
  this.local.length = 0;
153
203
  if (command.options) for (const option of command.options) (option.isPersistent ? this.persistent : this.local).push(option);
154
204
  applyDirectives(this.directives, command);
155
205
  }
206
+ /** The current command. */
207
+ get command() {
208
+ if (!this.current) throw new Error("The scope hasn't entered a command");
209
+ return this.current;
210
+ }
211
+ /** Whether the current command has subcommands of its own or inherits any. */
212
+ get hasSubcommands() {
213
+ return this.current?.subcommands !== void 0 || this.inheritedSubcommands.length > 0;
214
+ }
215
+ /**
216
+ * The subcommands the next token can name: the current command's own,
217
+ * followed by each inherited one that an earlier one doesn't shadow a name
218
+ * of. Hidden ones are included.
219
+ */
220
+ subcommands() {
221
+ const own = this.current?.subcommands ?? [];
222
+ if (this.inheritedSubcommands.length === 0) return own;
223
+ const all = [...own];
224
+ for (const candidate of this.inheritedSubcommands) if (!asArray(candidate.name).some((name) => all.some((earlier) => hasName(earlier.name, name)))) all.push(candidate);
225
+ return all;
226
+ }
227
+ /**
228
+ * Find the subcommand `token` names: one of the current command's own, or
229
+ * else an inherited persistent one. An exact name always wins. With
230
+ * `subcommandsMatchUniquePrefix`, a token that's a prefix of exactly one of
231
+ * the command's own names matches that command, unless `exactOnly`. A hidden
232
+ * or inherited command only matches its exact name, so it can't make a
233
+ * visible command's prefix ambiguous or take a prefix meant as an argument,
234
+ * and an empty token is no command's prefix.
235
+ */
236
+ matchSubcommand(token, exactOnly = false) {
237
+ const own = this.current?.subcommands ?? [];
238
+ for (const candidate of own) if (hasName(candidate.name, token)) return { command: candidate };
239
+ for (const candidate of this.inheritedSubcommands) if (hasName(candidate.name, token)) return { command: candidate };
240
+ const matches = this.directives.subcommandsMatchUniquePrefix && !exactOnly ? prefixMatches(own, token) : [];
241
+ if (matches.length === 1) return { command: matches[0] };
242
+ return new NoSubcommand(this.subcommands(), token, matches);
243
+ }
156
244
  /** The option `name` refers to, if any. */
157
245
  resolve(name) {
158
246
  const { local, persistent } = this;
159
- for (let i = local.length - 1; i >= 0; i--) if (hasName$1(local[i].name, name)) return local[i];
160
- for (let i = persistent.length - 1; i >= 0; i--) if (hasName$1(persistent[i].name, name)) return persistent[i];
247
+ for (let i = local.length - 1; i >= 0; i--) if (hasName(local[i].name, name)) return local[i];
248
+ for (let i = persistent.length - 1; i >= 0; i--) if (hasName(persistent[i].name, name)) return persistent[i];
161
249
  return null;
162
250
  }
163
251
  /**
164
252
  * The options that can be used here, in definition order, each with only
165
253
  * its live names: the names that still refer to it. Options with every name
166
- * shadowed are left out.
254
+ * shadowed are left out. Where the next token can name a subcommand, as
255
+ * `subcommands` says, a name that one matches is that subcommand's.
167
256
  */
168
- live() {
257
+ live(subcommands = null) {
169
258
  const owner = /* @__PURE__ */ new Map();
170
259
  for (const options of [this.persistent, this.local]) for (const option of options) for (const name of asArray(option.name)) owner.set(name, option);
171
260
  const pick = (options) => {
172
261
  const picked = [];
173
262
  for (const option of options) {
174
- const names = asArray(option.name).filter((name) => owner.get(name) === option);
263
+ const names = asArray(option.name).filter((name) => owner.get(name) === option && !(subcommands && this.matchSubcommand(name, subcommands.exactOnly).command));
175
264
  if (names.length > 0) picked.push({
176
265
  option,
177
266
  names
@@ -184,9 +273,12 @@ var OptionScope = class OptionScope {
184
273
  persistent: pick(this.persistent)
185
274
  };
186
275
  }
187
- /** The options a user can see here: the {@link live} ones that aren't hidden. */
276
+ /**
277
+ * The options a user can see at the start of the current command: the
278
+ * {@link live} ones that aren't hidden, without the names a subcommand takes.
279
+ */
188
280
  visible() {
189
- const { local, persistent } = this.live();
281
+ const { local, persistent } = this.live(this.hasSubcommands ? { exactOnly: false } : null);
190
282
  const shown = (visible) => !visible.option.hidden;
191
283
  return {
192
284
  local: local.filter(shown),
@@ -195,8 +287,9 @@ var OptionScope = class OptionScope {
195
287
  }
196
288
  /** The live names of each option a user could have meant, for "did you mean" suggestions. */
197
289
  suggestionNames() {
198
- const { local, persistent } = this.visible();
199
- return [...local, ...persistent].map((visible) => visible.names);
290
+ const { local, persistent } = this.live();
291
+ const shown = (visible) => !visible.option.hidden;
292
+ return [...local, ...persistent].filter(shown).map((visible) => visible.names);
200
293
  }
201
294
  /** The options that must be used, local first. */
202
295
  required() {
@@ -227,6 +320,46 @@ var OptionScope = class OptionScope {
227
320
  if (typeof required === "string") return required;
228
321
  return required ? this.directives.optionArgSeparators[0] ?? "=" : null;
229
322
  }
323
+ /** Whether the parser splits an option from its value at `separator` here. */
324
+ readsSeparator(separator) {
325
+ return this.directives.optionArgSeparators.includes(separator);
326
+ }
327
+ /** The separator that `text` starts with, or "" if none. Used after a short option. */
328
+ leadingSeparator(text) {
329
+ for (const separator of this.directives.optionArgSeparators) if (text.startsWith(separator)) return separator;
330
+ return "";
331
+ }
332
+ /**
333
+ * The separator that appears first in `token`, so the value after it can
334
+ * contain the other separators. When two separators start at the same place,
335
+ * the longer one wins. Null if there's none, or if the one found is empty.
336
+ */
337
+ firstSeparator(token) {
338
+ let found = "";
339
+ let index = -1;
340
+ for (const candidate of this.directives.optionArgSeparators) {
341
+ const at = token.indexOf(candidate);
342
+ if (at === -1) continue;
343
+ if (index === -1 || at < index || at === index && candidate.length > found.length) {
344
+ found = candidate;
345
+ index = at;
346
+ }
347
+ }
348
+ return found ? {
349
+ index,
350
+ separator: found
351
+ } : null;
352
+ }
353
+ /**
354
+ * Why `option`, used as `name`, can't take a value given after `separator`,
355
+ * or null if it can. A separate token has the separator "".
356
+ */
357
+ separatorProblem(option, name, separator) {
358
+ const required = option.requiresSeparator;
359
+ if (typeof required === "string" && separator !== required) return `Incorrect separator, use '${required}' instead of '${separator}'`;
360
+ if (required === true && separator === "") return `Missing separator, use '${this.requiredSeparator(option)}' between ${name} and its value`;
361
+ return null;
362
+ }
230
363
  };
231
364
  const optionKeys = /* @__PURE__ */ new WeakMap();
232
365
  /**
@@ -245,88 +378,14 @@ function optionKey(option) {
245
378
  }
246
379
  return key;
247
380
  }
248
- function hasName$1(names, name) {
381
+ function hasName(names, name) {
249
382
  return typeof names === "string" ? names === name : names.includes(name);
250
383
  }
251
- // src/subcommands.ts
252
- /**
253
- * Find the subcommand `token` names among `subcommands`, in a command where
254
- * `directives` are in effect, or else among the persistent subcommands the
255
- * command `inherited`. An exact name always wins. With
256
- * `subcommandsMatchUniquePrefix`, a token that's a prefix of exactly one
257
- * of the command's own names matches that command, unless `exactOnly`. A
258
- * hidden or inherited command only matches its exact name, so it can't make a
259
- * visible command's prefix ambiguous or take a prefix meant as an argument,
260
- * and an empty token is no command's prefix.
261
- */
262
- function matchSubcommand(subcommands, token, directives, exactOnly = false, inherited = []) {
263
- for (const candidate of subcommands) if (hasName(candidate, token)) return { command: candidate };
264
- for (const candidate of inherited) if (hasName(candidate, token)) return { command: candidate };
265
- const matches = directives.subcommandsMatchUniquePrefix && !exactOnly ? prefixMatches(subcommands, token) : [];
266
- if (matches.length === 1) return { command: matches[0] };
267
- return new NoSubcommand(withInherited(subcommands, inherited), token, matches);
268
- }
269
- /**
270
- * A command's own `subcommands` followed by the `inherited` ones it can use,
271
- * leaving out each inherited one that an earlier command shadows a name of.
272
- */
273
- function withInherited(subcommands, inherited) {
274
- if (inherited.length === 0) return subcommands;
275
- const all = [...subcommands];
276
- for (const candidate of inherited) if (!asArray(candidate.name).some((name) => all.some((earlier) => hasName(earlier, name)))) all.push(candidate);
277
- return all;
278
- }
279
- function hasName(command, token) {
280
- const names = command.name;
281
- return typeof names === "string" ? names === token : names.includes(token);
282
- }
283
384
  /** The visible commands that `token` is a prefix of a name of, each listed once. */
284
385
  function prefixMatches(subcommands, token) {
285
386
  if (token === "") return [];
286
387
  return subcommands.filter((candidate) => !candidate.hidden && asArray(candidate.name).some((name) => name.startsWith(token)));
287
388
  }
288
- /**
289
- * A token that names no subcommand. The reason is only worked out when asked
290
- * for, since most tokens the parser tries aren't subcommands.
291
- */
292
- var NoSubcommand = class {
293
- subcommands;
294
- token;
295
- prefixOf;
296
- command = null;
297
- /**
298
- * `prefixOf` are the commands `token` is a prefix of a name of, where
299
- * prefixes match. There is never exactly one of them.
300
- */
301
- constructor(subcommands, token, prefixOf) {
302
- this.subcommands = subcommands;
303
- this.token = token;
304
- this.prefixOf = prefixOf;
305
- }
306
- /** Why the token names none of the subcommands, as an error message and suggestions. */
307
- reason() {
308
- const token = this.token;
309
- if (token === "") return { message: "Expected a command, but the value was empty" };
310
- if (this.prefixOf.length > 0) return { message: `Ambiguous command '${token}', could be: ${this.prefixOf.flatMap((command) => asArray(command.name).filter((name) => name.startsWith(token))).join(", ")}` };
311
- const choices = visibleSubcommands(this.subcommands).map((command) => asArray(command.name));
312
- return {
313
- message: `Unknown command '${token}'`,
314
- didYouMean: {
315
- input: token,
316
- choices
317
- }
318
- };
319
- }
320
- };
321
- /** The subcommands that help, completion and suggestions show. */
322
- function visibleSubcommands(subcommands) {
323
- return subcommands?.filter((subcommand) => !subcommand.hidden) ?? [];
324
- }
325
- /** The action of a command that has none of its own: print the command's help. */
326
- const usage = ({ help }) => {
327
- print(help());
328
- return 0;
329
- };
330
389
  // src/help.ts
331
390
  /**
332
391
  * Show an option's names, each written with its arguments the way `forms`
@@ -422,32 +481,9 @@ var MissingRequiredOption = class extends ParseError {
422
481
  }
423
482
  };
424
483
  // src/parse.ts
425
- /** The first of `separators` that `text` starts with, or "" if none. */
426
- function leadingSeparator(text, separators) {
427
- for (const sep of separators) if (text.startsWith(sep)) return sep;
428
- return "";
429
- }
430
- /**
431
- * Find the separator that appears first in `token`, so the value after it can
432
- * contain the other separators. When two separators start at the same place,
433
- * the longer one wins. Returns null if there's no separator, or if the one
434
- * found is empty.
435
- */
436
- function firstSeparator(token, separators) {
437
- let sep = "";
438
- let index = -1;
439
- for (const candidate of separators) {
440
- const at = token.indexOf(candidate);
441
- if (at === -1) continue;
442
- if (index === -1 || at < index || at === index && candidate.length > sep.length) {
443
- sep = candidate;
444
- index = at;
445
- }
446
- }
447
- return sep ? {
448
- index,
449
- sep
450
- } : null;
484
+ /** How many times `option` can be used. */
485
+ function repeatLimit(option) {
486
+ return typeof option.isRepeatable === "number" ? option.isRepeatable : Infinity;
451
487
  }
452
488
  /**
453
489
  * Throw if the options used break a `dependsOn` or `exclusiveOn` rule, or if
@@ -465,25 +501,21 @@ function checkOptionRules(options, seenOptions, required, context) {
465
501
  for (const option of required) for (const name of asArray(option.name)) if (!options.has(name)) throw new MissingRequiredOption(name, context);
466
502
  }
467
503
  /**
468
- * The state of reading one command line in a single pass. This is the only
469
- * code that decides what each input token is. When `strict`, it throws on the
470
- * first problem, which is what {@link parse} needs. Otherwise it never throws:
471
- * it skips whatever is invalid and keeps going, so {@link readNext} can still
472
- * read a line that's wrong at the moment. Each method handles one kind of
473
- * token.
504
+ * The state of reading one command line, one token at a time. This is the
505
+ * only code that decides what each input token is. It never throws: it skips
506
+ * whatever is invalid and keeps going, so {@link readNext} can still read a
507
+ * line that's wrong at the moment, and it keeps the first problem it finds for
508
+ * {@link parse} to throw. Each method handles one kind of token, and says what
509
+ * the token was read as.
474
510
  */
475
511
  var Scanner = class {
476
- strict;
477
512
  path;
478
513
  optionAction = void 0;
479
514
  args = [];
480
515
  options = /* @__PURE__ */ new Map();
481
516
  seenOptions = [];
482
517
  argSeparatorIndex = -1;
483
- scope = new OptionScope();
484
- subcommands = void 0;
485
- commandArgsMin = 0;
486
- commandArgsMax = 0;
518
+ scope = new CommandScope();
487
519
  directives = this.scope.directives;
488
520
  hasFoundArg = false;
489
521
  openOption = null;
@@ -496,40 +528,60 @@ var Scanner = class {
496
528
  valuesMin = 0;
497
529
  valuesMax = 0;
498
530
  valueArgs = [];
499
- requiredSeparator = false;
531
+ /**
532
+ * Makes the error for the first token that can't be read the way it's
533
+ * written, like an unknown option or a value an option doesn't take.
534
+ * {@link parse} stops there.
535
+ */
536
+ problem = null;
500
537
  tooManyArgs = null;
501
538
  repeatedUses = /* @__PURE__ */ new Map();
502
- attached = null;
503
- constructor(spec, strict) {
504
- this.strict = strict;
539
+ constructor(spec) {
505
540
  this.path = [spec];
506
- this.enterCommand(spec);
541
+ this.scope.enter(spec);
507
542
  }
508
543
  ctx() {
509
- return { path: this.path };
544
+ return { path: [...this.path] };
510
545
  }
511
546
  /** Whether the open option can take the next token as a value. */
512
547
  get hasRoomForValue() {
513
548
  return this.valueCount < this.separateValueMax;
514
549
  }
515
- enterCommand(command) {
516
- this.scope.enter(command);
517
- this.subcommands = command.subcommands;
518
- this.commandArgsMin = getMinArgs(command.args);
519
- this.commandArgsMax = getMaxArgs(command.args);
520
- }
521
550
  findOption(name) {
522
551
  return this.scope.resolve(name);
523
552
  }
553
+ /**
554
+ * Keep the problem `make` describes if it's the first. The error is only
555
+ * made if {@link parse} throws it, since a lenient read never needs it and
556
+ * an error is slow to make. So `make` can't read anything later tokens change.
557
+ */
558
+ fail(make) {
559
+ if (!this.problem) {
560
+ const ctx = this.ctx();
561
+ this.problem = () => make(ctx);
562
+ }
563
+ }
564
+ /** Keep the first argument the command has no room for, like {@link fail}. */
565
+ failArgs(make) {
566
+ if (!this.tooManyArgs) {
567
+ const ctx = this.ctx();
568
+ this.tooManyArgs = () => make(ctx);
569
+ }
570
+ }
524
571
  unknownOption(option) {
525
- return new UnknownOption(option, this.scope.suggestionNames(), this.ctx());
572
+ if (!this.problem) {
573
+ const names = this.scope.suggestionNames();
574
+ this.fail((ctx) => new UnknownOption(option, names, ctx));
575
+ }
526
576
  }
527
577
  endOption() {
528
578
  const values = this.values;
529
- if (this.strict && values) {
579
+ if (values) {
530
580
  const given = values.length - this.valuesStart;
531
- if (given < this.valuesMin) throw new TooFewOptionArguments(this.openOptionName, this.valuesMin, this.valuesMax, this.ctx());
532
- addDefaults(values, this.valueArgs, given);
581
+ if (given < this.valuesMin) {
582
+ const { openOptionName, valuesMin, valuesMax } = this;
583
+ this.fail((ctx) => new TooFewOptionArguments(openOptionName, valuesMin, valuesMax, ctx));
584
+ } else addDefaults(values, this.valueArgs, given);
533
585
  }
534
586
  this.values = null;
535
587
  }
@@ -552,7 +604,6 @@ var Scanner = class {
552
604
  const options = this.options;
553
605
  let optionValues;
554
606
  if (option.isRepeatable) {
555
- const maxRepeat = option.isRepeatable === true ? Infinity : option.isRepeatable;
556
607
  const key = optionKey(option);
557
608
  let uses = this.repeatedUses.get(key);
558
609
  if (!uses) {
@@ -562,7 +613,7 @@ var Scanner = class {
562
613
  };
563
614
  this.repeatedUses.set(key, uses);
564
615
  }
565
- if (this.strict && uses.count >= maxRepeat) throw new ParseError(this.ctx(), "Too many repetitions");
616
+ if (uses.count >= repeatLimit(option)) this.fail((ctx) => new ParseError(ctx, "Too many repetitions"));
566
617
  uses.count += 1;
567
618
  for (const other of names) options.set(other, uses.values);
568
619
  if (!option.args) {
@@ -576,150 +627,162 @@ var Scanner = class {
576
627
  }
577
628
  this.valuesMin = this.valueMin;
578
629
  this.valuesMax = getMaxArgs(option.args);
579
- this.requiredSeparator = option.requiresSeparator ?? false;
580
630
  if (this.valuesMax > 0) {
581
631
  this.values = optionValues;
582
632
  this.valuesStart = optionValues.length;
583
633
  this.valueArgs = asArray(option.args);
584
634
  }
585
635
  }
586
- /**
587
- * Add a value to the open option. Returns whether it's a value the option
588
- * accepts. A lenient read keeps going either way.
589
- */
636
+ /** Add a value to the open option. Returns whether it's a value the option accepts. */
590
637
  addValue(value, separator) {
591
638
  this.valueCount += 1;
592
639
  if (!this.values) {
593
- if (this.strict) throw new InvalidOptionArg(this.openOptionName, this.ctx());
640
+ const name = this.openOptionName;
641
+ this.fail((ctx) => new InvalidOptionArg(name, ctx));
594
642
  return false;
595
643
  }
596
- const requiredSeparator = this.requiredSeparator;
597
- let accepted = true;
598
- if (typeof requiredSeparator === "string" && separator !== requiredSeparator) {
599
- if (this.strict) throw new ParseError(this.ctx(), `Incorrect separator, use '${requiredSeparator}' instead of '${separator}'`);
600
- accepted = false;
601
- }
602
- if (requiredSeparator === true && separator === "") {
603
- if (this.strict) {
604
- const suggested = this.scope.requiredSeparator(this.openOption);
605
- throw new ParseError(this.ctx(), `Missing separator, use '${suggested}' between ${this.openOptionName} and its value`);
606
- }
607
- accepted = false;
608
- }
644
+ const problem = this.scope.separatorProblem(this.openOption, this.openOptionName, separator);
645
+ if (problem) this.fail((ctx) => new ParseError(ctx, problem));
609
646
  this.values.push(value);
610
- return accepted;
647
+ return problem === null;
648
+ }
649
+ /** Add a separate token as a value of the open option. */
650
+ value(token) {
651
+ this.addValue(token, "");
652
+ return {
653
+ kind: "value",
654
+ option: this.openOption
655
+ };
611
656
  }
612
657
  /**
613
- * Add a value attached to the option's own token, the token at `index`,
614
- * starting at `start`. It's the option's only value, so no later token can be
615
- * one.
658
+ * Add a value attached to the option's own token, starting at `start`. It's
659
+ * the option's only value, so no later token can be one. Returns where the
660
+ * value starts, or null if the option doesn't accept it.
616
661
  */
617
- addAttachedValue(index, start, value, separator) {
618
- if (this.addValue(value, separator)) this.attached = {
619
- option: this.openOption,
620
- index,
621
- start
622
- };
662
+ addAttachedValue(start, value, separator) {
663
+ const option = this.openOption;
664
+ const accepted = this.addValue(value, separator);
623
665
  this.valueMin = this.separateValueMax = 0;
666
+ return accepted ? {
667
+ option,
668
+ start
669
+ } : null;
624
670
  }
625
671
  /**
626
672
  * Whether the command already has all the arguments it takes. A command that
627
673
  * requires a subcommand takes none.
628
674
  */
629
675
  get argsAreFull() {
630
- const path = this.path;
631
- return path[path.length - 1].requiresSubcommand === true || this.args.length >= this.commandArgsMax;
676
+ return this.scope.command.requiresSubcommand === true || this.args.length >= this.scope.argsMax;
632
677
  }
633
678
  addArg(value) {
634
679
  this.endOption();
635
- if (this.strict && this.argsAreFull) this.tooManyArgs ??= new TooManyArguments(value, this.ctx());
680
+ if (this.argsAreFull) this.failArgs((ctx) => new TooManyArguments(value, ctx));
636
681
  this.args.push(value);
682
+ return { kind: "argument" };
637
683
  }
638
- /** Read `--` and every token after it as arguments. */
639
- argSeparator(input, index) {
684
+ /** Read `--`. Every token after it is an argument. */
685
+ argSeparator() {
640
686
  this.closeOption();
641
- const command = this.path[this.path.length - 1];
642
- if (this.strict && this.argsAreFull && !command.requiresSubcommand) this.tooManyArgs ??= new ParseError(this.ctx(), "Unexpected argument '--', did you mean to use an option instead?");
687
+ const command = this.scope.command;
688
+ if (this.argsAreFull && !command.requiresSubcommand) this.failArgs((ctx) => new ParseError(ctx, "Unexpected argument '--', did you mean to use an option instead?"));
643
689
  this.argSeparatorIndex = this.args.length;
644
- for (index += 1; index < input.length; index++) this.addArg(input[index]);
690
+ return { kind: "separator" };
645
691
  }
646
692
  /**
647
- * Whether the next token can name a subcommand, its own or an inherited
648
- * one. It can't once an argument is found, or while the open option needs
649
- * the next token as its value. A required or variadic option value takes
650
- * priority over a subcommand.
693
+ * How the next token can name a subcommand, its own or an inherited one, or
694
+ * null if it can't. It can't once an argument is found, or while the open
695
+ * option needs the next token as its value. A required or variadic option
696
+ * value takes priority over a subcommand. A short optional value could be a
697
+ * prefix of a subcommand's name, so only an exact name can end an option
698
+ * that still has room.
651
699
  */
652
- get canMatchSubcommand() {
653
- if (this.hasFoundArg) return false;
654
- if (!this.subcommands && this.scope.inheritedSubcommands.length === 0) return false;
655
- return !(this.valueCount < this.valueMin || this.hasRoomForValue && this.separateValueMax === Infinity);
700
+ get subcommandMatching() {
701
+ if (this.hasFoundArg || !this.scope.hasSubcommands) return null;
702
+ if (this.valueCount < this.valueMin || this.hasRoomForValue && this.separateValueMax === Infinity) return null;
703
+ return { exactOnly: this.hasRoomForValue };
656
704
  }
657
705
  /**
658
706
  * The subcommand the next token enters if it's `token`, or why it enters
659
707
  * none. Null if the next token can't be a subcommand at all.
660
708
  */
661
709
  subcommandFor(token) {
662
- if (!this.canMatchSubcommand) return null;
663
- return matchSubcommand(this.subcommands ?? [], token, this.directives, this.hasRoomForValue, this.scope.inheritedSubcommands);
710
+ const matching = this.subcommandMatching;
711
+ return matching && this.scope.matchSubcommand(token, matching.exactOnly);
664
712
  }
665
713
  enterSubcommand(command) {
666
714
  this.closeOption();
667
715
  this.path.push(command);
668
- this.enterCommand(command);
716
+ this.scope.enter(command);
717
+ return {
718
+ kind: "subcommand",
719
+ command
720
+ };
669
721
  }
670
722
  /** Read a token like `-abc`, `+x` or `-ovalue`. */
671
- shortOptions(token, index) {
723
+ shortOptions(token) {
672
724
  const leadingChar = token[0];
673
725
  const dashOption = this.findOption(leadingChar);
674
726
  if (dashOption?.args && !this.findOption(token.slice(0, 2))) {
675
727
  this.useOption(dashOption, leadingChar);
676
- this.addAttachedValue(index, 1, token.slice(1), "");
677
- return;
728
+ const attached = this.addAttachedValue(1, token.slice(1), "");
729
+ return {
730
+ kind: "options",
731
+ options: [dashOption],
732
+ attached
733
+ };
678
734
  }
735
+ const used = [];
736
+ let attached = null;
679
737
  for (let char = 1; char < token.length; char++) {
680
738
  const optionName = leadingChar + token[char];
681
739
  const option = this.findOption(optionName);
682
740
  if (!option) {
683
- if (char === 1 && this.hasRoomForValue) {
684
- this.addValue(token, "");
685
- return;
686
- }
687
- if (this.strict) throw this.unknownOption(optionName);
741
+ if (char === 1 && this.hasRoomForValue) return this.value(token);
742
+ this.unknownOption(optionName);
688
743
  continue;
689
744
  }
690
745
  this.useOption(option, optionName);
691
- if (char === token.length - 1) return;
692
- const sep = leadingSeparator(token.slice(char + 1), this.directives.optionArgSeparators);
746
+ used.push(option);
747
+ if (char === token.length - 1) break;
748
+ const sep = this.scope.leadingSeparator(token.slice(char + 1));
693
749
  if (option.args || sep && !this.findOption(leadingChar + token[char + 1])) {
694
750
  const argStart = char + sep.length + 1;
695
- this.addAttachedValue(index, argStart, token.slice(argStart), sep);
696
- return;
751
+ attached = this.addAttachedValue(argStart, token.slice(argStart), sep);
752
+ break;
697
753
  }
698
754
  }
755
+ return used.length > 0 ? {
756
+ kind: "options",
757
+ options: used,
758
+ attached
759
+ } : { kind: "unknown" };
699
760
  }
700
761
  /**
701
762
  * Read a token like `--name` or `--name=value`, or any token when options
702
- * aren't POSIX compliant. Returns false if the token should be an argument.
763
+ * aren't POSIX compliant. Returns null if the token should be an argument.
703
764
  */
704
- longOption(token, index) {
705
- const found = firstSeparator(token, this.directives.optionArgSeparators);
765
+ longOption(token) {
766
+ const found = this.scope.firstSeparator(token);
706
767
  const optionName = found ? token.slice(0, found.index) : token;
707
768
  const option = this.findOption(optionName);
708
769
  if (option) {
709
770
  this.useOption(option, optionName);
771
+ let attached = null;
710
772
  if (found) {
711
- const valueStart = found.index + found.sep.length;
712
- this.addAttachedValue(index, valueStart, token.slice(valueStart), found.sep);
773
+ const valueStart = found.index + found.separator.length;
774
+ attached = this.addAttachedValue(valueStart, token.slice(valueStart), found.separator);
713
775
  }
714
- return true;
715
- }
716
- if (this.directives.flagsArePosixNoncompliant) return false;
717
- if (this.hasRoomForValue) {
718
- this.addValue(token, "");
719
- return true;
776
+ return {
777
+ kind: "options",
778
+ options: [option],
779
+ attached
780
+ };
720
781
  }
721
- if (this.strict) throw this.unknownOption(optionName);
722
- return true;
782
+ if (this.directives.flagsArePosixNoncompliant) return null;
783
+ if (this.hasRoomForValue) return this.value(token);
784
+ this.unknownOption(optionName);
785
+ return { kind: "unknown" };
723
786
  }
724
787
  /**
725
788
  * Read a token that isn't `--`, a subcommand or an option. It's a value of
@@ -728,34 +791,39 @@ var Scanner = class {
728
791
  * subcommand, if it could have been one.
729
792
  */
730
793
  valueOrArg(token, unmatched) {
731
- if (this.hasRoomForValue) {
732
- this.addValue(token, "");
733
- return;
734
- }
735
- if (this.strict && unmatched && this.argsAreFull) {
736
- const command = this.path[this.path.length - 1];
737
- if (command.subcommands?.length || command.requiresSubcommand) {
794
+ if (this.hasRoomForValue) return this.value(token);
795
+ if (unmatched && this.argsAreFull) {
796
+ const command = this.scope.command;
797
+ if (command.subcommands?.length || command.requiresSubcommand) this.failArgs((ctx) => {
738
798
  const { message, didYouMean } = unmatched.reason();
739
- this.tooManyArgs ??= new ParseError(this.ctx(), message, didYouMean);
740
- }
799
+ return new ParseError(ctx, message, didYouMean);
800
+ });
741
801
  }
742
802
  this.hasFoundArg = true;
743
- this.addArg(token);
803
+ return this.addArg(token);
744
804
  }
745
805
  /**
746
- * Throw if the finished command line is missing anything. Returns the action
747
- * to run.
806
+ * Throw if the finished command line is missing anything. Returns what it
807
+ * was parsed as. Only call this when there's no {@link problem}.
748
808
  */
749
809
  finish() {
750
- const command = this.path[this.path.length - 1];
810
+ const command = this.scope.command;
751
811
  const showsUsage = command.requiresSubcommand && !command.action && this.args.length === 0;
752
812
  const skipsChecks = this.optionAction !== void 0 || showsUsage;
753
- if (!skipsChecks && this.tooManyArgs) throw this.tooManyArgs;
754
- if (!skipsChecks && this.args.length < this.commandArgsMin) throw new TooFewArguments(this.commandArgsMin, this.commandArgsMax, this.ctx());
813
+ if (!skipsChecks && this.tooManyArgs) throw this.tooManyArgs();
814
+ if (!skipsChecks && this.args.length < this.scope.argsMin) throw new TooFewArguments(this.scope.argsMin, this.scope.argsMax, this.ctx());
755
815
  this.endOption();
816
+ if (this.problem) throw this.problem();
756
817
  if (!skipsChecks) checkOptionRules(this.options, this.seenOptions, this.scope.required(), this.ctx());
757
- if (!command.requiresSubcommand && this.args.length >= this.commandArgsMin) addDefaults(this.args, asArray(command.args), this.args.length);
758
- return this.optionAction ?? command.action ?? usage;
818
+ if (!command.requiresSubcommand && this.args.length >= this.scope.argsMin) addDefaults(this.args, asArray(command.args), this.args.length);
819
+ return {
820
+ path: this.path,
821
+ action: this.optionAction ?? command.action ?? usage,
822
+ args: this.args,
823
+ argSeparatorIndex: this.argSeparatorIndex,
824
+ options: this.options,
825
+ defaults: (name) => this.defaults(name)
826
+ };
759
827
  }
760
828
  /** See {@link ParseResult.defaults}. */
761
829
  defaults(name) {
@@ -768,42 +836,41 @@ var Scanner = class {
768
836
  }
769
837
  /** Whether using `option` next would go over its repeat limit or break an `exclusiveOn` rule. */
770
838
  conflicts(option) {
771
- const names = asArray(option.name);
839
+ if ((this.repeatedUses.get(optionKey(option))?.count ?? 0) >= repeatLimit(option)) return true;
772
840
  const options = this.options;
773
- if (typeof option.isRepeatable === "number") {
774
- if ((this.repeatedUses.get(optionKey(option))?.count ?? 0) >= option.isRepeatable) return true;
775
- }
776
841
  if (option.exclusiveOn?.some((name) => options.has(name))) return true;
842
+ const names = asArray(option.name);
777
843
  return this.seenOptions.some((seen) => seen.exclusiveOn?.some((name) => names.includes(name)));
778
844
  }
779
845
  /** The options the next token can use. See {@link NextToken.options}. */
780
846
  usableOptions() {
781
847
  const used = new Set(this.seenOptions.map(optionKey));
782
- const { local, persistent } = this.scope.live();
848
+ const { local, persistent } = this.scope.live(this.subcommandMatching);
783
849
  const usable = [];
784
850
  for (const { option, names } of [...local, ...persistent]) {
785
851
  if (this.conflicts(option)) continue;
786
- const typeable = names.filter((name) => !this.subcommandFor(name)?.command);
787
- if (typeable.length === 0) continue;
788
852
  usable.push({
789
853
  option,
790
- forms: this.scope.forms(option, typeable),
854
+ forms: this.scope.forms(option, names),
791
855
  used: used.has(optionKey(option))
792
856
  });
793
857
  }
794
858
  return usable;
795
859
  }
796
- /** What the next token can be. See {@link readNext}. */
860
+ /**
861
+ * What the next token can be. See {@link readNext}. Reading more tokens
862
+ * doesn't change what this returns.
863
+ */
797
864
  next() {
798
865
  const open = this.openOption && this.hasRoomForValue ? this.openOption : null;
799
- const command = this.path[this.path.length - 1];
866
+ const command = this.scope.command;
800
867
  const afterSeparator = this.argSeparatorIndex !== -1;
801
868
  const optionsAllowed = !afterSeparator && this.optionsAllowed;
802
869
  return {
803
- path: this.path,
804
- directives: this.directives,
870
+ path: [...this.path],
871
+ directives: { ...this.directives },
805
872
  optionsAllowed,
806
- subcommands: !afterSeparator && this.canMatchSubcommand ? withInherited(this.subcommands ?? [], this.scope.inheritedSubcommands) : [],
873
+ subcommands: !afterSeparator && this.subcommandMatching ? this.scope.subcommands() : [],
807
874
  options: optionsAllowed ? this.usableOptions() : [],
808
875
  value: open && {
809
876
  option: open,
@@ -813,79 +880,27 @@ var Scanner = class {
813
880
  arg: open ? void 0 : argAt(asArray(command.args), this.args.length)
814
881
  };
815
882
  }
816
- /** Read the token at `index`, the next one, and say what it was read as. */
817
- readAndClassify(input, index) {
818
- if (this.argSeparatorIndex !== -1) return { kind: "argument" };
819
- const depth = this.path.length;
820
- const seen = this.seenOptions.length;
821
- const args = this.args.length;
822
- const values = this.valueCount;
823
- if (this.readAt(input, index)) return { kind: "separator" };
824
- if (this.path.length > depth) return {
825
- kind: "subcommand",
826
- command: this.path[depth]
827
- };
828
- if (this.seenOptions.length > seen) {
829
- const found = this.attachedAt(index);
830
- const attached = found && {
831
- option: found.option,
832
- start: found.start
833
- };
834
- return {
835
- kind: "options",
836
- options: this.seenOptions.slice(seen),
837
- attached
838
- };
839
- }
840
- if (this.args.length > args) return { kind: "argument" };
841
- if (this.valueCount > values) return {
842
- kind: "value",
843
- option: this.openOption
844
- };
845
- return { kind: "unknown" };
846
- }
847
- /** The value attached to the token at `index`, if it has one the option accepts. */
848
- attachedAt(index) {
849
- return this.attached?.index === index ? this.attached : null;
850
- }
851
883
  /** Whether the next token can be an option. Once options must stop, every token is an argument. */
852
884
  get optionsAllowed() {
853
885
  return !(this.directives.optionsMustPrecedeArguments && this.hasFoundArg);
854
886
  }
855
- /** Read every token of `input`. */
856
- read(input) {
857
- for (let index = 0; index < input.length; index++) if (this.readAt(input, index)) return;
858
- }
859
- /**
860
- * Read the token at `index`. Returns true if it was `--`, which reads every
861
- * token after it too.
862
- */
863
- readAt(input, index) {
864
- const token = input[index];
887
+ read(token) {
888
+ if (this.argSeparatorIndex !== -1) return this.addArg(token);
865
889
  const optionsAllowed = this.optionsAllowed;
866
- if (token === "--" && optionsAllowed) {
867
- this.argSeparator(input, index);
868
- return true;
869
- }
890
+ if (token === "--" && optionsAllowed) return this.argSeparator();
870
891
  const subcommand = this.subcommandFor(token);
871
- if (subcommand?.command) {
872
- this.enterSubcommand(subcommand.command);
873
- return false;
874
- }
892
+ if (subcommand?.command) return this.enterSubcommand(subcommand.command);
875
893
  if (optionsAllowed) {
876
894
  const flagsArePosixNoncompliant = this.directives.flagsArePosixNoncompliant;
877
895
  const first = token.charCodeAt(0);
878
896
  const isDoubleDash = first === 45 && token.charCodeAt(1) === 45;
879
- if (!flagsArePosixNoncompliant && token.length > 1 && (first === 45 || first === 43) && !isDoubleDash) {
880
- this.shortOptions(token, index);
881
- return false;
882
- }
897
+ if (!flagsArePosixNoncompliant && token.length > 1 && (first === 45 || first === 43) && !isDoubleDash) return this.shortOptions(token);
883
898
  if (flagsArePosixNoncompliant || token.length > 2 && isDoubleDash) {
884
- if (this.longOption(token, index)) return false;
899
+ const reading = this.longOption(token);
900
+ if (reading) return reading;
885
901
  }
886
902
  }
887
- this.valueOrArg(token, subcommand);
888
- return false;
903
+ return this.valueOrArg(token, subcommand);
889
904
  }
890
905
  };
891
906
  /** The argument at `index`, reusing a final variadic argument. */
@@ -895,36 +910,72 @@ function argAt(args, index) {
895
910
  return last?.isVariadic ? last : void 0;
896
911
  }
897
912
  /**
913
+ * Start reading a command line for `spec`, to say what each token is read as
914
+ * in turn. Reads leniently, like {@link readNext}.
915
+ */
916
+ function tokenReader(spec) {
917
+ return new Scanner(spec);
918
+ }
919
+ /**
898
920
  * Say what the parser reads `token` as when it comes after `before`. Reads
899
- * leniently, like {@link readNext}.
921
+ * leniently, like {@link readNext}. To read every token of a line, use
922
+ * {@link tokenReader}.
900
923
  */
901
924
  function readToken(spec, before, token) {
902
- const s = new Scanner(spec, false);
903
- s.read(before);
904
- return s.readAndClassify([...before, token], before.length);
925
+ const s = new Scanner(spec);
926
+ for (const earlier of before) s.read(earlier);
927
+ return s.read(token);
905
928
  }
906
929
  // src/testing.ts
907
930
  const repr = JSON.stringify;
931
+ /** One command in the spec, with what's in effect there. */
932
+ var CommandVisit = class {
933
+ path;
934
+ own;
935
+ inherited;
936
+ built = null;
937
+ constructor(path, own, inherited) {
938
+ this.path = path;
939
+ this.own = own;
940
+ this.inherited = inherited;
941
+ }
942
+ /** The last command on the path. */
943
+ get command() {
944
+ return this.path[this.path.length - 1];
945
+ }
946
+ /** The options, subcommands and directives in effect here, built on first use. */
947
+ get scope() {
948
+ return this.built ??= CommandScope.at(this.path);
949
+ }
950
+ };
908
951
  /**
909
- * Run a function once for each `args` property in the spec (recursive)
910
- *
911
- * This will convert single args to an array with one item.
952
+ * Run a function once for each command in the spec, parents before their
953
+ * subcommands, starting with `spec`. Each visit's path and scope start at
954
+ * `spec`.
912
955
  */
913
- function forEachArgArray(spec, fn, path = []) {
914
- path = path.concat(spec);
915
- fn(makeArray(spec.args), path);
916
- for (const option of makeArray(spec.options)) fn(makeArray(option.args), path);
917
- for (const subcommand of makeArray(spec.subcommands)) forEachArgArray(subcommand, fn, path);
956
+ function forEachCommand(spec, fn) {
957
+ const visit = (path, inherited) => {
958
+ const command = path[path.length - 1];
959
+ const own = makeArray(command.options).map((option, index) => ({
960
+ option,
961
+ definedIn: path,
962
+ index
963
+ }));
964
+ fn(new CommandVisit(path, own, inherited));
965
+ const next = [...inherited, ...own.filter(({ option }) => option.isPersistent)];
966
+ for (const subcommand of makeArray(command.subcommands)) visit([...path, subcommand], next);
967
+ };
968
+ visit([spec], []);
918
969
  }
919
970
  /**
920
- * Run a function once for each `subcommands` property in the spec (recursive)
921
- *
922
- * This will also run the function for the root spec itself.
971
+ * Run a function once for each `args` property in the spec: each command's,
972
+ * then its options'. This will convert single args to an array with one item.
923
973
  */
924
- function forEachSubcommand(spec, fn, path = []) {
925
- path = path.concat(spec);
926
- fn(spec, path);
927
- for (const subcommand of makeArray(spec.subcommands)) forEachSubcommand(subcommand, fn, path);
974
+ function forEachArgArray(spec, fn) {
975
+ forEachCommand(spec, ({ command, path }) => {
976
+ fn(makeArray(command.args), path);
977
+ for (const option of makeArray(command.options)) fn(makeArray(option.args), path);
978
+ });
928
979
  }
929
980
  function joinNames(names) {
930
981
  return makeArray(names).sort((a, b) => a.length - b.length).join("|");
@@ -967,14 +1018,7 @@ function describeOption(definedIn, index, usedIn = definedIn) {
967
1018
  * option the way the parser does at each command.
968
1019
  */
969
1020
  function forEachOption(spec, fn) {
970
- const visit = (path, inherited) => {
971
- const command = path[path.length - 1];
972
- const scope = OptionScope.at(path);
973
- const own = makeArray(command.options).map((option, index) => ({
974
- option,
975
- definedIn: path,
976
- index
977
- }));
1021
+ forEachCommand(spec, ({ path, own, inherited, scope }) => {
978
1022
  for (const { option, definedIn, index } of [...own, ...inherited]) {
979
1023
  const where = describeOption(definedIn, index, path);
980
1024
  fn({
@@ -985,10 +1029,7 @@ function forEachOption(spec, fn) {
985
1029
  where
986
1030
  });
987
1031
  }
988
- const next = [...inherited, ...own.filter(({ option }) => option.isPersistent)];
989
- for (const subcommand of makeArray(command.subcommands)) visit([...path, subcommand], next);
990
- };
991
- visit([spec], []);
1032
+ });
992
1033
  }
993
1034
  /** Asserts that required arguments do not come after an optional argument */
994
1035
  function assertRequiredArgumentsDoNotFollowOptionalArguments(spec) {
@@ -1009,7 +1050,7 @@ function assertRequiredArgumentsDoNotFollowOptionalArguments(spec) {
1009
1050
  * too, and the option can't be required.
1010
1051
  */
1011
1052
  function assertDefaultsCanBeUsed(spec) {
1012
- forEachSubcommand(spec, (command, path) => {
1053
+ forEachCommand(spec, ({ command, path }) => {
1013
1054
  for (const [index, arg] of makeArray(command.args).entries()) assert(arg.default === void 0 || arg.isOptional, `In ${namedArrayToString(...path)}, argument ${arg.name || index} has a default, but it's required, so the default is never used`);
1014
1055
  });
1015
1056
  forEachOption(spec, ({ option, isDefinedHere, where }) => {
@@ -1038,9 +1079,9 @@ function assertRepeatableOptionsArePositiveIntegers(spec) {
1038
1079
  }
1039
1080
  /** Asserts that options have unique names in their scope */
1040
1081
  function assertOptionsHaveLocallyUniqueNames(spec) {
1041
- forEachSubcommand(spec, (spec, path) => {
1082
+ forEachCommand(spec, ({ path, own }) => {
1042
1083
  const optionNames = /* @__PURE__ */ new Set();
1043
- for (const [index, option] of makeArray(spec.options).entries()) for (const name of makeArray(option.name)) {
1084
+ for (const { option, index } of own) for (const name of makeArray(option.name)) {
1044
1085
  assert(!optionNames.has(name), `${describeOption(path, index)} has a non-unique name, '${name}'. Option names must be unique.`);
1045
1086
  optionNames.add(name);
1046
1087
  }
@@ -1048,10 +1089,8 @@ function assertOptionsHaveLocallyUniqueNames(spec) {
1048
1089
  }
1049
1090
  /** Asserts that options don't shadow a persistent option */
1050
1091
  function assertOptionsDoNotShadowPersistentOptions(spec) {
1051
- forEachOption(spec, ({ option, path, isDefinedHere, where }) => {
1052
- if (!isDefinedHere) return;
1053
- const inherited = OptionScope.at(path.slice(0, -1)).persistent;
1054
- for (const name of makeArray(option.name)) assert(!inherited.some((persistent) => makeArray(persistent.name).includes(name)), `${where} shadows the name of a persistent option, ${repr(name)}. Option names can't shadow persistent options.`);
1092
+ forEachCommand(spec, ({ path, own, inherited }) => {
1093
+ for (const { option, index } of own) for (const name of makeArray(option.name)) assert(!inherited.some((persistent) => makeArray(persistent.option.name).includes(name)), `${describeOption(path, index)} shadows the name of a persistent option, ${repr(name)}. Option names can't shadow persistent options.`);
1055
1094
  });
1056
1095
  }
1057
1096
  /**
@@ -1096,9 +1135,9 @@ function assertOptionsHaveDifferentNamesFromParents(spec) {
1096
1135
  }
1097
1136
  /** Asserts that subcommands don't share a name with a sibling subcommand */
1098
1137
  function assertSubcommandsHaveLocallyUniqueNames(spec) {
1099
- forEachSubcommand(spec, (spec, path) => {
1138
+ forEachCommand(spec, ({ command, path }) => {
1100
1139
  const names = /* @__PURE__ */ new Set();
1101
- for (const [index, subcommand] of makeArray(spec.subcommands).entries()) for (const name of makeArray(subcommand.name)) {
1140
+ for (const [index, subcommand] of makeArray(command.subcommands).entries()) for (const name of makeArray(subcommand.name)) {
1102
1141
  assert(!names.has(name), `The subcommand ${namedArrayToString(...path, subcommand)} (index ${index}) has a non-unique name, ${repr(name)}. Subcommand names must be unique among the subcommands in the same array.`);
1103
1142
  names.add(name);
1104
1143
  }
@@ -1108,7 +1147,7 @@ function assertSubcommandsHaveLocallyUniqueNames(spec) {
1108
1147
  * Asserts that no option arg separator is an empty string
1109
1148
  */
1110
1149
  function assertOptionArgSeparatorsHaveCharacters(spec) {
1111
- forEachSubcommand(spec, (command, path) => {
1150
+ forEachCommand(spec, ({ command, path }) => {
1112
1151
  const separators = makeArray(command.parserDirectives?.optionArgSeparators);
1113
1152
  for (const [sepIndex, separator] of separators.entries()) assert(separator !== "", `${describeCommand(path)} has an empty string for an option arg separator at index ${sepIndex}. If you wanted to disable option arg separators, use an empty array.`);
1114
1153
  });
@@ -1130,7 +1169,7 @@ function assertNamesHaveNoExtraWhitespace(spec) {
1130
1169
  if (!isDefinedHere) return;
1131
1170
  for (const name of makeArray(option.name)) assert(name === name.trim(), `${where} has a name with extra whitespace`);
1132
1171
  });
1133
- forEachSubcommand(spec, (command, path) => {
1172
+ forEachCommand(spec, ({ command, path }) => {
1134
1173
  for (const name of makeArray(command.name)) assert(name === name.trim(), `${describeCommand(path)} has a name with extra whitespace`);
1135
1174
  });
1136
1175
  forEachArgArray(spec, (args, path) => {
@@ -1145,7 +1184,7 @@ function assertEverythingHasDescription(spec) {
1145
1184
  forEachOption(spec, ({ option, isDefinedHere, where }) => {
1146
1185
  if (isDefinedHere) assert(option.description, `${where} has no description`);
1147
1186
  });
1148
- forEachSubcommand(spec, (command, path) => {
1187
+ forEachCommand(spec, ({ command, path }) => {
1149
1188
  assert(command.description, `${describeCommand(path)} has no description`);
1150
1189
  });
1151
1190
  }
@@ -1158,7 +1197,7 @@ function assertDescriptionLineLengthUnder69(spec) {
1158
1197
  if (!isDefinedHere || !option.description) return;
1159
1198
  assert(option.description.split("\n").every((line) => line.length <= 68), `${where} has a description line over 68 characters`);
1160
1199
  });
1161
- forEachSubcommand(spec, (command, path) => {
1200
+ forEachCommand(spec, ({ command, path }) => {
1162
1201
  if (command.description) assert(command.description.split("\n").every((line) => line.length <= 68), `${describeCommand(path)} has a description line over 68 characters`);
1163
1202
  for (const [index, arg] of makeArray(command.args).entries()) assert(!arg.description || arg.description.split("\n").every((line) => line.length <= 68), `In ${namedArrayToString(...path)}, argument ${arg.name || index} has a description line over 68 characters`);
1164
1203
  });
@@ -1239,19 +1278,22 @@ function assertOptionFormsReadBack(spec, include) {
1239
1278
  const names = makeArray(option.name).filter((name) => scope.resolve(name) === option);
1240
1279
  for (const form of scope.forms(option, names)) {
1241
1280
  if (!include(option, form.name)) continue;
1281
+ const tokens = writeForm(option, form, value);
1282
+ const reader = tokenReader(root);
1283
+ for (const token of before) reader.read(token);
1284
+ const first = reader.read(tokens[0]);
1242
1285
  if (form.join !== "") {
1243
- const reading = readToken(root, before, form.name);
1286
+ const reading = form.join === null ? first : readToken(root, before, form.name);
1244
1287
  assert(readsAs(reading, option), `${where} has the name '${form.name}', but the parser reads it as ${describeReading(reading, path)}`);
1245
1288
  }
1246
- const tokens = writeForm(option, form, value);
1247
1289
  const written = `${where} is written '${optionToString(option, [form])}'`;
1290
+ if (form.join) assert(scope.readsSeparator(form.join), `${written}, but '${form.join}' isn't one of the option arg separators there (${scope.directives.optionArgSeparators.map((separator) => `'${separator}'`).join(", ") || "none"}). Add it to \`parserDirectives.optionArgSeparators\`.`);
1248
1291
  if (form.join !== null) {
1249
- const first = readToken(root, before, tokens[0]);
1250
1292
  const joined = first.kind === "options" && first.attached?.option === option;
1251
1293
  assert(readsAs(first, option) && joined, `${written}, but the parser reads '${tokens[0]}' as ${describeReading(first, path)}${joined ? "" : " without a value"}`);
1252
1294
  }
1253
1295
  for (let index = 1; index < tokens.length; index++) {
1254
- const reading = readToken(root, [...before, ...tokens.slice(0, index)], tokens[index]);
1296
+ const reading = reader.read(tokens[index]);
1255
1297
  assert(reading.kind === "value" && reading.option === option, `${written}, but in '${tokens.join(" ")}' the parser reads '${tokens[index]}' as ${describeReading(reading, path)}`);
1256
1298
  }
1257
1299
  }
@@ -1266,7 +1308,7 @@ function assertOptionFormsReadBack(spec, include) {
1266
1308
  */
1267
1309
  function assertNamesReadBack(spec) {
1268
1310
  const root = spec;
1269
- forEachSubcommand(spec, (command, path) => {
1311
+ forEachCommand(spec, ({ command, path }) => {
1270
1312
  const before = tokensTo(path);
1271
1313
  for (const subcommand of makeArray(command.subcommands)) for (const name of makeArray(subcommand.name)) {
1272
1314
  const reading = readToken(root, before, name);
@@ -1278,13 +1320,16 @@ function assertNamesReadBack(spec) {
1278
1320
  }
1279
1321
  function assertCommonOptionsArePersistent(spec) {
1280
1322
  const failures = [];
1281
- forEachSubcommand(spec, (subcommand, path) => {
1282
- if (!subcommand.subcommands) return;
1283
- for (const option of makeArray(subcommand.options)) {
1284
- const allChildSubcommands = [];
1285
- forEachSubcommand(subcommand, (cmd) => allChildSubcommands.push(cmd));
1323
+ forEachCommand(spec, ({ command, path }) => {
1324
+ if (!command.subcommands) return;
1325
+ let common = makeArray(command.options);
1326
+ forEachCommand(command, ({ command: below }) => {
1327
+ const keys = makeArray(below.options).map(optionKey);
1328
+ common = common.filter((option) => keys.includes(optionKey(option)));
1329
+ });
1330
+ for (const option of common) {
1286
1331
  const key = optionKey(option);
1287
- if (allChildSubcommands.every((cmd) => makeArray(cmd.options).some((o) => optionKey(o) === key)) && !failures.some(([failed]) => failed === key)) failures.push([key, namedArrayToString(...path, option)]);
1332
+ if (!failures.some(([failed]) => failed === key)) failures.push([key, namedArrayToString(...path, option)]);
1288
1333
  }
1289
1334
  });
1290
1335
  assert(failures.length === 0, `The following options were also defined by all subcommands. Instead, define the option once, and use \`isPersistent: true\` to persist it across all subcommands.
@@ -1305,9 +1350,9 @@ function assertOptionNameReferencesExist(spec) {
1305
1350
  * Asserts that commands that will match subcommands by prefix do not take arguments
1306
1351
  */
1307
1352
  function assertPrefixMatchCommandsHaveNoArguments(spec) {
1308
- forEachSubcommand(spec, (subcommand, path) => {
1309
- if (!OptionScope.at(path).directives.subcommandsMatchUniquePrefix) return;
1310
- assert(!(subcommand.subcommands && subcommand.args), `The command ${namedArrayToString(...path)} has at least one argument and subcommand, but matches subcommands based on unique prefixes. To fix this, use \`parserDirectives: { subcommandsMatchUniquePrefix: false }\``);
1353
+ forEachCommand(spec, ({ command, path, scope }) => {
1354
+ if (!scope.directives.subcommandsMatchUniquePrefix) return;
1355
+ assert(!(command.subcommands && command.args), `The command ${namedArrayToString(...path)} has at least one argument and subcommand, but matches subcommands based on unique prefixes. To fix this, use \`parserDirectives: { subcommandsMatchUniquePrefix: false }\``);
1311
1356
  });
1312
1357
  }
1313
1358
  async function runStep(context, name, fn) {