@clo4/cli 2.0.0 → 3.0.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/dist/testing.js CHANGED
@@ -8,10 +8,64 @@ function makeArray(thing) {
8
8
  if (isArray(thing)) return thing.slice();
9
9
  return [thing];
10
10
  }
11
+ /**
12
+ * Like {@link makeArray}, but without copying when the input is already an
13
+ * array. Only safe when the caller treats the result as read-only — use it on
14
+ * the hot path to avoid slicing read-only spec metadata.
15
+ */
16
+ function asArray(thing) {
17
+ if (thing === void 0) return [];
18
+ if (isArray(thing)) return thing;
19
+ return [thing];
20
+ }
11
21
  function assert(expr, msg = "") {
12
22
  if (!expr) throw new Error(msg);
13
23
  }
14
- globalThis.process;
24
+ const nodeProcess = globalThis.process;
25
+ /**
26
+ * Write text to stdout (1) or stderr (2) with a synchronous `write` on the file
27
+ * descriptor.
28
+ *
29
+ * The first `console.log` in a Node process builds the stdout stream, which
30
+ * costs 1-3 ms. That's more than parsing takes, so all of this library's output
31
+ * goes through here instead. Without `node:fs` it falls back to `console`.
32
+ */
33
+ function writeFd(fd, text) {
34
+ const fs = nodeProcess?.getBuiltinModule?.("node:fs");
35
+ if (!fs) {
36
+ (fd === 1 ? console.log : console.error)(text.endsWith("\n") ? text.slice(0, -1) : text);
37
+ return;
38
+ }
39
+ const bytes = new TextEncoder().encode(text);
40
+ let offset = 0;
41
+ try {
42
+ while (offset < bytes.length) offset += fs.writeSync(fd, bytes, offset, bytes.length - offset);
43
+ } catch (error) {
44
+ if (error.code === "EAGAIN") (fd === 1 ? nodeProcess?.stdout : nodeProcess?.stderr)?.write(bytes.subarray(offset));
45
+ }
46
+ }
47
+ /**
48
+ * Where this library's output goes. Tests replace `write` to capture output.
49
+ *
50
+ * @internal
51
+ */
52
+ const output = { write: writeFd };
53
+ /** Print a message to stdout, trimming trailing whitespace. */
54
+ function print(text) {
55
+ output.write(1, `${text.trimEnd()}\n`);
56
+ }
57
+ /**
58
+ * Add the defaults of the arguments from index `from` on to `values`, stopping
59
+ * at the first argument without one. The parser and help both use this, so
60
+ * help only shows the defaults the parser uses.
61
+ */
62
+ function addDefaults(values, args, from = 0) {
63
+ for (let i = from; i < args.length; i++) {
64
+ const value = args[i].default;
65
+ if (value === void 0) return;
66
+ values.push(value);
67
+ }
68
+ }
15
69
  /**
16
70
  * Returns the minimum number of arguments that satisfies the args.
17
71
  *
@@ -35,6 +89,820 @@ function getMaxArgs(args) {
35
89
  for (let i = 0; i < args.length; i++) if (args[args.length - 1 - i].isVariadic) return Infinity;
36
90
  return args.length;
37
91
  }
92
+ // src/directives.ts
93
+ /** The directives in effect when no command sets any. */
94
+ function defaultDirectives() {
95
+ return {
96
+ optionArgSeparators: ["="],
97
+ flagsArePosixNoncompliant: false,
98
+ optionsMustPrecedeArguments: false,
99
+ subcommandsMatchUniquePrefix: false
100
+ };
101
+ }
102
+ /**
103
+ * Update `directives` for entering `command`. A subcommand inherits each
104
+ * directive until it sets its own.
105
+ */
106
+ function applyDirectives(directives, command) {
107
+ const configured = command.parserDirectives;
108
+ if (!configured) return;
109
+ if (configured.optionArgSeparators) directives.optionArgSeparators = asArray(configured.optionArgSeparators);
110
+ if (typeof configured.subcommandsMatchUniquePrefix === "boolean") directives.subcommandsMatchUniquePrefix = configured.subcommandsMatchUniquePrefix;
111
+ if (typeof configured.optionsMustPrecedeArguments === "boolean") directives.optionsMustPrecedeArguments = configured.optionsMustPrecedeArguments;
112
+ if (typeof configured.flagsArePosixNoncompliant === "boolean") directives.flagsArePosixNoncompliant = configured.flagsArePosixNoncompliant;
113
+ }
114
+ // src/scope.ts
115
+ /**
116
+ * The options and directives in effect at one command on a path.
117
+ *
118
+ * Moving to a subcommand replaces the local options and adds its persistent
119
+ * options to the inherited ones. A name refers to one option: a local option
120
+ * 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.
124
+ */
125
+ var OptionScope = class OptionScope {
126
+ /** Non-persistent options of the current command, in definition order. */
127
+ local = [];
128
+ /** Persistent options from every command on the path, in definition order. */
129
+ persistent = [];
130
+ /**
131
+ * Persistent subcommands of the commands above the current one, nearest
132
+ * command first, so a nearer one wins a name.
133
+ */
134
+ inheritedSubcommands = [];
135
+ /** The directives in effect. A subcommand inherits each until it sets its own. */
136
+ directives = defaultDirectives();
137
+ command = null;
138
+ /** The scope at the last command on `path`. */
139
+ static at(path) {
140
+ const scope = new OptionScope();
141
+ for (const command of path) scope.enter(command);
142
+ return scope;
143
+ }
144
+ /** Move to `command`, a subcommand of the current one. */
145
+ enter(command) {
146
+ const parentSubcommands = this.command?.subcommands;
147
+ if (parentSubcommands) {
148
+ const persistent = parentSubcommands.filter((subcommand) => subcommand.isPersistent);
149
+ if (persistent.length > 0) this.inheritedSubcommands.unshift(...persistent);
150
+ }
151
+ this.command = command;
152
+ this.local.length = 0;
153
+ if (command.options) for (const option of command.options) (option.isPersistent ? this.persistent : this.local).push(option);
154
+ applyDirectives(this.directives, command);
155
+ }
156
+ /** The option `name` refers to, if any. */
157
+ resolve(name) {
158
+ 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];
161
+ return null;
162
+ }
163
+ /**
164
+ * The options that can be used here, in definition order, each with only
165
+ * its live names: the names that still refer to it. Options with every name
166
+ * shadowed are left out.
167
+ */
168
+ live() {
169
+ const owner = /* @__PURE__ */ new Map();
170
+ 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
+ const pick = (options) => {
172
+ const picked = [];
173
+ for (const option of options) {
174
+ const names = asArray(option.name).filter((name) => owner.get(name) === option);
175
+ if (names.length > 0) picked.push({
176
+ option,
177
+ names
178
+ });
179
+ }
180
+ return picked;
181
+ };
182
+ return {
183
+ local: pick(this.local),
184
+ persistent: pick(this.persistent)
185
+ };
186
+ }
187
+ /** The options a user can see here: the {@link live} ones that aren't hidden. */
188
+ visible() {
189
+ const { local, persistent } = this.live();
190
+ const shown = (visible) => !visible.option.hidden;
191
+ return {
192
+ local: local.filter(shown),
193
+ persistent: persistent.filter(shown)
194
+ };
195
+ }
196
+ /** The live names of each option a user could have meant, for "did you mean" suggestions. */
197
+ suggestionNames() {
198
+ const { local, persistent } = this.visible();
199
+ return [...local, ...persistent].map((visible) => visible.names);
200
+ }
201
+ /** The options that must be used, local first. */
202
+ required() {
203
+ return [...this.local, ...this.persistent].filter((option) => option.isRequired);
204
+ }
205
+ /**
206
+ * How each of `names` is written with `option`'s value here. Help shows
207
+ * these, completion offers them, and the spec checker reads each one back
208
+ * through the parser.
209
+ */
210
+ forms(option, names = asArray(option.name)) {
211
+ const join = (name) => {
212
+ if (getMaxArgs(option.args) === 0) return null;
213
+ if (option.requiresSeparator) return this.requiredSeparator(option);
214
+ return !this.directives.flagsArePosixNoncompliant && (name === "-" || name === "+") ? "" : null;
215
+ };
216
+ return names.map((name) => ({
217
+ name,
218
+ join: join(name)
219
+ }));
220
+ }
221
+ /**
222
+ * The separator that must come between `option` and its value, or null if
223
+ * it can take a value as a separate token.
224
+ */
225
+ requiredSeparator(option) {
226
+ const required = option.requiresSeparator;
227
+ if (typeof required === "string") return required;
228
+ return required ? this.directives.optionArgSeparators[0] ?? "=" : null;
229
+ }
230
+ };
231
+ const optionKeys = /* @__PURE__ */ new WeakMap();
232
+ /**
233
+ * A key for the set of `option`'s names. Options with the same names, in any
234
+ * order, get the same key and are one option: the parser continues a
235
+ * repeatable option's uses across them, counts one as used when the other was,
236
+ * and stores their values under the same names, which is how they're read.
237
+ * Options that share only some names are different options, and each shared
238
+ * name refers to whichever one `resolve` picks.
239
+ */
240
+ function optionKey(option) {
241
+ let key = optionKeys.get(option);
242
+ if (key === void 0) {
243
+ key = typeof option.name === "string" ? option.name : [...option.name].sort().join("\0");
244
+ optionKeys.set(option, key);
245
+ }
246
+ return key;
247
+ }
248
+ function hasName$1(names, name) {
249
+ return typeof names === "string" ? names === name : names.includes(name);
250
+ }
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
+ /** The visible commands that `token` is a prefix of a name of, each listed once. */
284
+ function prefixMatches(subcommands, token) {
285
+ if (token === "") return [];
286
+ return subcommands.filter((candidate) => !candidate.hidden && asArray(candidate.name).some((name) => name.startsWith(token)));
287
+ }
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
+ // src/help.ts
331
+ /**
332
+ * Show an option's names, each written with its arguments the way `forms`
333
+ * says. Names written the same way share one copy of the arguments.
334
+ */
335
+ function optionToString(option, forms) {
336
+ const optionArgs = makeArray(option.args);
337
+ const sorted = [...forms].sort((a, b) => a.name.length - b.name.length);
338
+ return [...new Set(sorted.map((form) => form.join))].map((join) => {
339
+ return sorted.filter((form) => form.join === join).map((form) => form.name).join(", ") + argumentsAfterName(optionArgs, join);
340
+ }).join(", ");
341
+ }
342
+ /** The text after an option's name that shows its arguments, joined to the name by `join`. */
343
+ function argumentsAfterName(args, join) {
344
+ if (args.length === 0) return "";
345
+ if (join === null) return ` ${summarizeArguments(args)}`;
346
+ const [first, ...rest] = args;
347
+ const name = first.name || "argument";
348
+ const joined = (first.isOptional ? `[${join}${name}]` : `${join}<${name}>`) + (first.isVariadic ? "..." : "");
349
+ return rest.length > 0 ? `${joined} ${summarizeArguments(rest)}` : joined;
350
+ }
351
+ function summarizeArguments(args) {
352
+ return args.map((arg) => argToString(arg)).join(" ");
353
+ }
354
+ function argToString(arg) {
355
+ const name = arg.name || "argument";
356
+ let lhs = "<";
357
+ let rhs = ">";
358
+ let suffix = "";
359
+ if (arg.isOptional) {
360
+ lhs = "[";
361
+ rhs = "]";
362
+ }
363
+ if (arg.isVariadic) suffix = "...";
364
+ return `${lhs}${name}${rhs}${suffix}`;
365
+ }
366
+ // src/errors.ts
367
+ const repr$1 = JSON.stringify;
368
+ /**
369
+ * A command line the parser can't accept. `run` prints the message with the
370
+ * help for the command at `context.path`, and suggests the closest of
371
+ * `didYouMean.choices` if there is one.
372
+ */
373
+ var ParseError = class extends Error {
374
+ context;
375
+ didYouMean;
376
+ constructor(context, message, didYouMean) {
377
+ super(message);
378
+ this.context = context;
379
+ this.didYouMean = didYouMean;
380
+ this.name = this.constructor.name;
381
+ }
382
+ };
383
+ var UnknownOption = class extends ParseError {
384
+ option;
385
+ constructor(option, validOptions, context) {
386
+ const msg = `${repr$1(option)} looks like a flag, but is unknown in this context`;
387
+ super(context, msg, {
388
+ input: option,
389
+ choices: validOptions
390
+ });
391
+ this.option = option;
392
+ }
393
+ };
394
+ var InvalidOptionArg = class extends ParseError {
395
+ option;
396
+ constructor(option, context) {
397
+ super(context, `Option ${option} doesn't take arguments`);
398
+ this.option = option;
399
+ }
400
+ };
401
+ var TooManyArguments = class extends ParseError {
402
+ constructor(value, context) {
403
+ super(context, `Too many arguments, ${repr$1(value)} was unexpected`);
404
+ }
405
+ };
406
+ var TooFewOptionArguments = class extends ParseError {
407
+ constructor(option, min, max, context) {
408
+ const msg = min === max ? `Option ${option} needs ${min} argument${min === 1 ? "" : "s"}` : !Number.isFinite(max) ? `Option ${option} needs at least ${min} argument${min === 1 ? "" : "s"}` : `Option ${option} needs between ${min} and ${max} arguments`;
409
+ super(context, msg);
410
+ }
411
+ };
412
+ var TooFewArguments = class extends ParseError {
413
+ constructor(min, max, context) {
414
+ const msg = min === max ? `Expected ${min} argument${min === 1 ? "" : "s"}` : !Number.isFinite(max) ? `Expected at least ${min} argument${min === 1 ? "" : "s"}` : `Expected between ${min} and ${max} arguments`;
415
+ super(context, msg);
416
+ }
417
+ };
418
+ var MissingRequiredOption = class extends ParseError {
419
+ constructor(name, context) {
420
+ const msg = `Option ${name} is required, but wasn't found`;
421
+ super(context, msg);
422
+ }
423
+ };
424
+ // 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;
451
+ }
452
+ /**
453
+ * Throw if the options used break a `dependsOn` or `exclusiveOn` rule, or if
454
+ * any of `required` is missing.
455
+ */
456
+ function checkOptionRules(options, seenOptions, required, context) {
457
+ for (const option of seenOptions) {
458
+ if (!option.dependsOn) continue;
459
+ for (const name of option.dependsOn) if (!options.has(name)) throw new ParseError(context, `${asArray(option.name).join("/")} requires ${name}, add it to fix this error`);
460
+ }
461
+ for (const option of seenOptions) {
462
+ if (!option.exclusiveOn) continue;
463
+ for (const name of option.exclusiveOn) if (options.has(name)) throw new ParseError(context, `${asArray(option.name).join("/")} can't be used together with ${name}`);
464
+ }
465
+ for (const option of required) for (const name of asArray(option.name)) if (!options.has(name)) throw new MissingRequiredOption(name, context);
466
+ }
467
+ /**
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.
474
+ */
475
+ var Scanner = class {
476
+ strict;
477
+ path;
478
+ optionAction = void 0;
479
+ args = [];
480
+ options = /* @__PURE__ */ new Map();
481
+ seenOptions = [];
482
+ argSeparatorIndex = -1;
483
+ scope = new OptionScope();
484
+ subcommands = void 0;
485
+ commandArgsMin = 0;
486
+ commandArgsMax = 0;
487
+ directives = this.scope.directives;
488
+ hasFoundArg = false;
489
+ openOption = null;
490
+ openOptionName = "";
491
+ valueCount = 0;
492
+ valueMin = 0;
493
+ separateValueMax = 0;
494
+ values = null;
495
+ valuesStart = 0;
496
+ valuesMin = 0;
497
+ valuesMax = 0;
498
+ valueArgs = [];
499
+ requiredSeparator = false;
500
+ tooManyArgs = null;
501
+ repeatedUses = /* @__PURE__ */ new Map();
502
+ attached = null;
503
+ constructor(spec, strict) {
504
+ this.strict = strict;
505
+ this.path = [spec];
506
+ this.enterCommand(spec);
507
+ }
508
+ ctx() {
509
+ return { path: this.path };
510
+ }
511
+ /** Whether the open option can take the next token as a value. */
512
+ get hasRoomForValue() {
513
+ return this.valueCount < this.separateValueMax;
514
+ }
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
+ findOption(name) {
522
+ return this.scope.resolve(name);
523
+ }
524
+ unknownOption(option) {
525
+ return new UnknownOption(option, this.scope.suggestionNames(), this.ctx());
526
+ }
527
+ endOption() {
528
+ const values = this.values;
529
+ if (this.strict && values) {
530
+ 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);
533
+ }
534
+ this.values = null;
535
+ }
536
+ /** End the open option, so no later token can be its value. */
537
+ closeOption() {
538
+ this.endOption();
539
+ this.openOption = null;
540
+ this.valueCount = this.valueMin = this.separateValueMax = 0;
541
+ }
542
+ useOption(option, name) {
543
+ this.endOption();
544
+ this.openOption = option;
545
+ this.openOptionName = name;
546
+ this.valueCount = 0;
547
+ this.valueMin = getMinArgs(option.args);
548
+ this.separateValueMax = option.requiresSeparator ? 0 : getMaxArgs(option.args);
549
+ this.seenOptions.push(option);
550
+ if (option.action) this.optionAction = option.action;
551
+ const names = asArray(option.name);
552
+ const options = this.options;
553
+ let optionValues;
554
+ if (option.isRepeatable) {
555
+ const maxRepeat = option.isRepeatable === true ? Infinity : option.isRepeatable;
556
+ const key = optionKey(option);
557
+ let uses = this.repeatedUses.get(key);
558
+ if (!uses) {
559
+ uses = {
560
+ values: [],
561
+ count: 0
562
+ };
563
+ this.repeatedUses.set(key, uses);
564
+ }
565
+ if (this.strict && uses.count >= maxRepeat) throw new ParseError(this.ctx(), "Too many repetitions");
566
+ uses.count += 1;
567
+ for (const other of names) options.set(other, uses.values);
568
+ if (!option.args) {
569
+ uses.values.push("");
570
+ return;
571
+ }
572
+ optionValues = uses.values;
573
+ } else {
574
+ optionValues = [];
575
+ for (const other of names) options.set(other, optionValues);
576
+ }
577
+ this.valuesMin = this.valueMin;
578
+ this.valuesMax = getMaxArgs(option.args);
579
+ this.requiredSeparator = option.requiresSeparator ?? false;
580
+ if (this.valuesMax > 0) {
581
+ this.values = optionValues;
582
+ this.valuesStart = optionValues.length;
583
+ this.valueArgs = asArray(option.args);
584
+ }
585
+ }
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
+ */
590
+ addValue(value, separator) {
591
+ this.valueCount += 1;
592
+ if (!this.values) {
593
+ if (this.strict) throw new InvalidOptionArg(this.openOptionName, this.ctx());
594
+ return false;
595
+ }
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
+ }
609
+ this.values.push(value);
610
+ return accepted;
611
+ }
612
+ /**
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.
616
+ */
617
+ addAttachedValue(index, start, value, separator) {
618
+ if (this.addValue(value, separator)) this.attached = {
619
+ option: this.openOption,
620
+ index,
621
+ start
622
+ };
623
+ this.valueMin = this.separateValueMax = 0;
624
+ }
625
+ /**
626
+ * Whether the command already has all the arguments it takes. A command that
627
+ * requires a subcommand takes none.
628
+ */
629
+ get argsAreFull() {
630
+ const path = this.path;
631
+ return path[path.length - 1].requiresSubcommand === true || this.args.length >= this.commandArgsMax;
632
+ }
633
+ addArg(value) {
634
+ this.endOption();
635
+ if (this.strict && this.argsAreFull) this.tooManyArgs ??= new TooManyArguments(value, this.ctx());
636
+ this.args.push(value);
637
+ }
638
+ /** Read `--` and every token after it as arguments. */
639
+ argSeparator(input, index) {
640
+ 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?");
643
+ this.argSeparatorIndex = this.args.length;
644
+ for (index += 1; index < input.length; index++) this.addArg(input[index]);
645
+ }
646
+ /**
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.
651
+ */
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);
656
+ }
657
+ /**
658
+ * The subcommand the next token enters if it's `token`, or why it enters
659
+ * none. Null if the next token can't be a subcommand at all.
660
+ */
661
+ subcommandFor(token) {
662
+ if (!this.canMatchSubcommand) return null;
663
+ return matchSubcommand(this.subcommands ?? [], token, this.directives, this.hasRoomForValue, this.scope.inheritedSubcommands);
664
+ }
665
+ enterSubcommand(command) {
666
+ this.closeOption();
667
+ this.path.push(command);
668
+ this.enterCommand(command);
669
+ }
670
+ /** Read a token like `-abc`, `+x` or `-ovalue`. */
671
+ shortOptions(token, index) {
672
+ const leadingChar = token[0];
673
+ const dashOption = this.findOption(leadingChar);
674
+ if (dashOption?.args && !this.findOption(token.slice(0, 2))) {
675
+ this.useOption(dashOption, leadingChar);
676
+ this.addAttachedValue(index, 1, token.slice(1), "");
677
+ return;
678
+ }
679
+ for (let char = 1; char < token.length; char++) {
680
+ const optionName = leadingChar + token[char];
681
+ const option = this.findOption(optionName);
682
+ if (!option) {
683
+ if (char === 1 && this.hasRoomForValue) {
684
+ this.addValue(token, "");
685
+ return;
686
+ }
687
+ if (this.strict) throw this.unknownOption(optionName);
688
+ continue;
689
+ }
690
+ this.useOption(option, optionName);
691
+ if (char === token.length - 1) return;
692
+ const sep = leadingSeparator(token.slice(char + 1), this.directives.optionArgSeparators);
693
+ if (option.args || sep && !this.findOption(leadingChar + token[char + 1])) {
694
+ const argStart = char + sep.length + 1;
695
+ this.addAttachedValue(index, argStart, token.slice(argStart), sep);
696
+ return;
697
+ }
698
+ }
699
+ }
700
+ /**
701
+ * 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.
703
+ */
704
+ longOption(token, index) {
705
+ const found = firstSeparator(token, this.directives.optionArgSeparators);
706
+ const optionName = found ? token.slice(0, found.index) : token;
707
+ const option = this.findOption(optionName);
708
+ if (option) {
709
+ this.useOption(option, optionName);
710
+ if (found) {
711
+ const valueStart = found.index + found.sep.length;
712
+ this.addAttachedValue(index, valueStart, token.slice(valueStart), found.sep);
713
+ }
714
+ return true;
715
+ }
716
+ if (this.directives.flagsArePosixNoncompliant) return false;
717
+ if (this.hasRoomForValue) {
718
+ this.addValue(token, "");
719
+ return true;
720
+ }
721
+ if (this.strict) throw this.unknownOption(optionName);
722
+ return true;
723
+ }
724
+ /**
725
+ * Read a token that isn't `--`, a subcommand or an option. It's a value of
726
+ * the open option if that has room, and otherwise an argument, which stops
727
+ * later tokens from being subcommands. `unmatched` is why the token isn't a
728
+ * subcommand, if it could have been one.
729
+ */
730
+ 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) {
738
+ const { message, didYouMean } = unmatched.reason();
739
+ this.tooManyArgs ??= new ParseError(this.ctx(), message, didYouMean);
740
+ }
741
+ }
742
+ this.hasFoundArg = true;
743
+ this.addArg(token);
744
+ }
745
+ /**
746
+ * Throw if the finished command line is missing anything. Returns the action
747
+ * to run.
748
+ */
749
+ finish() {
750
+ const command = this.path[this.path.length - 1];
751
+ const showsUsage = command.requiresSubcommand && !command.action && this.args.length === 0;
752
+ 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());
755
+ this.endOption();
756
+ 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;
759
+ }
760
+ /** See {@link ParseResult.defaults}. */
761
+ defaults(name) {
762
+ if (this.options.has(name)) return;
763
+ const args = asArray(this.scope.resolve(name)?.args);
764
+ if (args.length === 0 || args[0].isOptional) return;
765
+ const values = [];
766
+ addDefaults(values, args);
767
+ return values.length > 0 ? values : void 0;
768
+ }
769
+ /** Whether using `option` next would go over its repeat limit or break an `exclusiveOn` rule. */
770
+ conflicts(option) {
771
+ const names = asArray(option.name);
772
+ 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
+ if (option.exclusiveOn?.some((name) => options.has(name))) return true;
777
+ return this.seenOptions.some((seen) => seen.exclusiveOn?.some((name) => names.includes(name)));
778
+ }
779
+ /** The options the next token can use. See {@link NextToken.options}. */
780
+ usableOptions() {
781
+ const used = new Set(this.seenOptions.map(optionKey));
782
+ const { local, persistent } = this.scope.live();
783
+ const usable = [];
784
+ for (const { option, names } of [...local, ...persistent]) {
785
+ if (this.conflicts(option)) continue;
786
+ const typeable = names.filter((name) => !this.subcommandFor(name)?.command);
787
+ if (typeable.length === 0) continue;
788
+ usable.push({
789
+ option,
790
+ forms: this.scope.forms(option, typeable),
791
+ used: used.has(optionKey(option))
792
+ });
793
+ }
794
+ return usable;
795
+ }
796
+ /** What the next token can be. See {@link readNext}. */
797
+ next() {
798
+ const open = this.openOption && this.hasRoomForValue ? this.openOption : null;
799
+ const command = this.path[this.path.length - 1];
800
+ const afterSeparator = this.argSeparatorIndex !== -1;
801
+ const optionsAllowed = !afterSeparator && this.optionsAllowed;
802
+ return {
803
+ path: this.path,
804
+ directives: this.directives,
805
+ optionsAllowed,
806
+ subcommands: !afterSeparator && this.canMatchSubcommand ? withInherited(this.subcommands ?? [], this.scope.inheritedSubcommands) : [],
807
+ options: optionsAllowed ? this.usableOptions() : [],
808
+ value: open && {
809
+ option: open,
810
+ arg: argAt(asArray(open.args), this.valueCount),
811
+ required: this.valueCount < this.valueMin
812
+ },
813
+ arg: open ? void 0 : argAt(asArray(command.args), this.args.length)
814
+ };
815
+ }
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
+ /** Whether the next token can be an option. Once options must stop, every token is an argument. */
852
+ get optionsAllowed() {
853
+ return !(this.directives.optionsMustPrecedeArguments && this.hasFoundArg);
854
+ }
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];
865
+ const optionsAllowed = this.optionsAllowed;
866
+ if (token === "--" && optionsAllowed) {
867
+ this.argSeparator(input, index);
868
+ return true;
869
+ }
870
+ const subcommand = this.subcommandFor(token);
871
+ if (subcommand?.command) {
872
+ this.enterSubcommand(subcommand.command);
873
+ return false;
874
+ }
875
+ if (optionsAllowed) {
876
+ const flagsArePosixNoncompliant = this.directives.flagsArePosixNoncompliant;
877
+ const first = token.charCodeAt(0);
878
+ 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
+ }
883
+ if (flagsArePosixNoncompliant || token.length > 2 && isDoubleDash) {
884
+ if (this.longOption(token, index)) return false;
885
+ }
886
+ }
887
+ this.valueOrArg(token, subcommand);
888
+ return false;
889
+ }
890
+ };
891
+ /** The argument at `index`, reusing a final variadic argument. */
892
+ function argAt(args, index) {
893
+ if (index < args.length) return args[index];
894
+ const last = args[args.length - 1];
895
+ return last?.isVariadic ? last : void 0;
896
+ }
897
+ /**
898
+ * Say what the parser reads `token` as when it comes after `before`. Reads
899
+ * leniently, like {@link readNext}.
900
+ */
901
+ function readToken(spec, before, token) {
902
+ const s = new Scanner(spec, false);
903
+ s.read(before);
904
+ return s.readAndClassify([...before, token], before.length);
905
+ }
38
906
  // src/testing.ts
39
907
  const repr = JSON.stringify;
40
908
  /**
@@ -48,12 +916,6 @@ function forEachArgArray(spec, fn, path = []) {
48
916
  for (const option of makeArray(spec.options)) fn(makeArray(option.args), path);
49
917
  for (const subcommand of makeArray(spec.subcommands)) forEachArgArray(subcommand, fn, path);
50
918
  }
51
- /** Run a function once for each `options` property in the spec (recursive) */
52
- function forEachOptionArray(spec, fn, path = []) {
53
- path = path.concat(spec);
54
- fn(makeArray(spec.options), path);
55
- for (const subcommand of makeArray(spec.subcommands)) forEachOptionArray(subcommand, fn, path);
56
- }
57
919
  /**
58
920
  * Run a function once for each `subcommands` property in the spec (recursive)
59
921
  *
@@ -67,10 +929,6 @@ function forEachSubcommand(spec, fn, path = []) {
67
929
  function joinNames(names) {
68
930
  return makeArray(names).sort((a, b) => a.length - b.length).join("|");
69
931
  }
70
- function isSameName(a, b) {
71
- if (isArray(a) && isArray(b)) return a.every((item) => b.includes(item)) && b.every((item) => a.includes(item));
72
- return a === b;
73
- }
74
932
  /** Compares spec values by shape. Any two functions count as the same shape. */
75
933
  function isSameStructure(a, b) {
76
934
  if (a === b) return true;
@@ -84,6 +942,54 @@ function isSameStructure(a, b) {
84
942
  function namedArrayToString(...parts) {
85
943
  return `\`${parts.map((named) => joinNames(named.name)).join(" ")}\``;
86
944
  }
945
+ /** Name the last command on `path` in a message, with its index among its siblings. */
946
+ function describeCommand(path) {
947
+ const command = path[path.length - 1];
948
+ const parent = path[path.length - 2];
949
+ if (!parent) return `Command ${namedArrayToString(command)}`;
950
+ const index = makeArray(parent.subcommands).indexOf(command);
951
+ return `Subcommand ${namedArrayToString(...path)} (index ${index})`;
952
+ }
953
+ /**
954
+ * Name the option at `index` on the last command on `definedIn` in a message,
955
+ * and the command it's used in when that isn't the one that defines it.
956
+ */
957
+ function describeOption(definedIn, index, usedIn = definedIn) {
958
+ const option = makeArray(definedIn[definedIn.length - 1].options)[index];
959
+ const name = `${namedArrayToString(...definedIn, option)} (index ${index})`;
960
+ return usedIn === definedIn ? `Option ${name}` : `In ${namedArrayToString(...usedIn)}, option ${name}`;
961
+ }
962
+ /**
963
+ * Run a function once for each option at each command it can be used in: the
964
+ * command's own options, in definition order, then the persistent ones it
965
+ * inherits. A check that is about how the option is written skips the uses
966
+ * that aren't `isDefinedHere`. A check that depends on the scope sees the
967
+ * option the way the parser does at each command.
968
+ */
969
+ 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
+ }));
978
+ for (const { option, definedIn, index } of [...own, ...inherited]) {
979
+ const where = describeOption(definedIn, index, path);
980
+ fn({
981
+ option,
982
+ path,
983
+ scope,
984
+ isDefinedHere: definedIn === path,
985
+ where
986
+ });
987
+ }
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], []);
992
+ }
87
993
  /** Asserts that required arguments do not come after an optional argument */
88
994
  function assertRequiredArgumentsDoNotFollowOptionalArguments(spec) {
89
995
  forEachArgArray(spec, (args, path) => {
@@ -96,14 +1002,38 @@ function assertRequiredArgumentsDoNotFollowOptionalArguments(spec) {
96
1002
  }
97
1003
  });
98
1004
  }
1005
+ /**
1006
+ * Asserts that the parser can use every default. A positional argument with a
1007
+ * default must be optional. A required option argument's default is only used
1008
+ * when the option is left out, so every argument before it needs a default
1009
+ * too, and the option can't be required.
1010
+ */
1011
+ function assertDefaultsCanBeUsed(spec) {
1012
+ forEachSubcommand(spec, (command, path) => {
1013
+ 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
+ });
1015
+ forEachOption(spec, ({ option, isDefinedHere, where }) => {
1016
+ if (!isDefinedHere) return;
1017
+ let firstWithout = null;
1018
+ for (const [index, arg] of makeArray(option.args).entries()) {
1019
+ if (arg.default === void 0) {
1020
+ firstWithout ??= index;
1021
+ continue;
1022
+ }
1023
+ if (arg.isOptional) continue;
1024
+ const name = `${where}, argument ${arg.name || index} has a default, but`;
1025
+ assert(!option.isRequired, `${name} the option is required, so the default is never used`);
1026
+ assert(firstWithout === null, `${name} the argument at index ${firstWithout} has none, so the default is never used`);
1027
+ }
1028
+ });
1029
+ }
99
1030
  /** Asserts that `isRepeatable` is only assigned a positive integer */
100
1031
  function assertRepeatableOptionsArePositiveIntegers(spec) {
101
- forEachOptionArray(spec, (options, path) => {
102
- for (const [index, option] of options.entries()) if (typeof option.isRepeatable === "number") {
103
- assert(Number.isSafeInteger(option.isRepeatable), `The option ${namedArrayToString(...path, option)} (index ${index}) has its \`isRepeatable\` value set to a number that is not an integer`);
104
- assert(!(option.isRepeatable < 1), `The option ${namedArrayToString(...path, option)} (index ${index}) has its \`isRepeatable\` value set to a number below 1, which means it can't be used at all`);
105
- assert(option.isRepeatable !== 1, `The option ${namedArrayToString(...path, option)} (index ${index}) has its \`isRepeatable\` value set to 1, which is no different that omitting the property entirely`);
106
- }
1032
+ forEachOption(spec, ({ option, isDefinedHere, where }) => {
1033
+ if (!isDefinedHere || typeof option.isRepeatable !== "number") return;
1034
+ assert(Number.isSafeInteger(option.isRepeatable), `${where} has its \`isRepeatable\` value set to a number that is not an integer`);
1035
+ assert(!(option.isRepeatable < 1), `${where} has its \`isRepeatable\` value set to a number below 1, which means it can't be used at all`);
1036
+ assert(option.isRepeatable !== 1, `${where} has its \`isRepeatable\` value set to 1, which is no different that omitting the property entirely`);
107
1037
  });
108
1038
  }
109
1039
  /** Asserts that options have unique names in their scope */
@@ -111,40 +1041,57 @@ function assertOptionsHaveLocallyUniqueNames(spec) {
111
1041
  forEachSubcommand(spec, (spec, path) => {
112
1042
  const optionNames = /* @__PURE__ */ new Set();
113
1043
  for (const [index, option] of makeArray(spec.options).entries()) for (const name of makeArray(option.name)) {
114
- assert(!optionNames.has(name), `Option ${namedArrayToString(...path, option)} (index ${index}) has a non-unique name, '${name}'. Option names must be unique.`);
1044
+ assert(!optionNames.has(name), `${describeOption(path, index)} has a non-unique name, '${name}'. Option names must be unique.`);
115
1045
  optionNames.add(name);
116
1046
  }
117
1047
  });
118
1048
  }
119
1049
  /** Asserts that options don't shadow a persistent option */
120
1050
  function assertOptionsDoNotShadowPersistentOptions(spec) {
121
- forEachOptionArray(spec, (options, path) => {
122
- const persistentOptionNames = new Set(path.slice(0, -1).flatMap((subcommand) => subcommand.options ? subcommand.options.filter((option) => option.isPersistent).flatMap((option) => option.name) : []));
123
- for (const [index, option] of makeArray(options).entries()) for (const name of makeArray(option.name)) assert(!persistentOptionNames.has(name), `The option ${namedArrayToString(...path, option)} (index ${index}) shadows the name of a persistent option, ${repr(name)}. Option names can't shadow persistent options.`);
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.`);
124
1055
  });
125
1056
  }
126
1057
  /**
127
- * Asserts that a subcommand's option isn't a different option with exactly the
128
- * same names as one on a parent.
1058
+ * Asserts that when a subcommand's option and a different option on a parent
1059
+ * share a name, each of them has a name of its own.
129
1060
  *
130
- * Parsed values are looked up by name, so that parent's value would be
131
- * unreadable. A shared name is fine when each option has a name of its own, and
132
- * when the options are identical, such as an option kept in a constant.
1061
+ * Parsed values are looked up by name, and an option sets every one of its
1062
+ * names when it's used. If every name of the parent's option is also the
1063
+ * subcommand's, using the subcommand's option replaces the parent's value, so
1064
+ * it can't be read. If every name of the subcommand's option is also the
1065
+ * parent's, its names hold the parent's value when it isn't used, so it can't
1066
+ * tell whether it was. Options with the same names are fine when they're
1067
+ * identical, such as an option kept in a constant, because the parser treats
1068
+ * them as one option.
133
1069
  *
134
1070
  * This skips persistent parent options, because
135
1071
  * {@link assertOptionsDoNotShadowPersistentOptions} covers those and
136
1072
  * `allowShadowingPersistentOptions` opts out of it.
137
1073
  */
138
1074
  function assertOptionsHaveDifferentNamesFromParents(spec) {
139
- forEachOptionArray(spec, (options, path) => {
140
- const parents = path.slice(0, -1);
141
- for (const [index, option] of options.entries()) for (const parent of parents) for (const parentOption of makeArray(parent.options)) assert(!(!parentOption.isPersistent && isSameName(parentOption.name, option.name) && !isSameStructure({
142
- ...parentOption,
143
- name: 0
144
- }, {
145
- ...option,
146
- name: 0
147
- })), `The option ${namedArrayToString(...path, option)} (index ${index}) has the same names as a different option, ${namedArrayToString(parent, parentOption)}, so the parent's value can't be read. Add a name that only one of them uses.`);
1075
+ forEachOption(spec, ({ option, path, isDefinedHere, where }) => {
1076
+ if (!isDefinedHere) return;
1077
+ const names = makeArray(option.name);
1078
+ for (const parent of path.slice(0, -1)) for (const parentOption of makeArray(parent.options)) {
1079
+ if (parentOption.isPersistent) continue;
1080
+ const parentNames = makeArray(parentOption.name);
1081
+ const other = namedArrayToString(parent, parentOption);
1082
+ if (optionKey(parentOption) === optionKey(option)) {
1083
+ assert(isSameStructure({
1084
+ ...parentOption,
1085
+ name: 0
1086
+ }, {
1087
+ ...option,
1088
+ name: 0
1089
+ }), `${where} has the same names as a different option, ${other}, so the parent's value can't be read. Add a name that only one of them uses.`);
1090
+ continue;
1091
+ }
1092
+ assert(!parentNames.every((name) => names.includes(name)), `${where} has every name of a different option, ${other}, so the parent's value can't be read. Add a name that only the parent's option uses.`);
1093
+ assert(!names.every((name) => parentNames.includes(name)), `${where} only has names of a different option, ${other}, so it can't tell whether it was used. Add a name that only the subcommand's option uses.`);
1094
+ }
148
1095
  });
149
1096
  }
150
1097
  /** Asserts that subcommands don't share a name with a sibling subcommand */
@@ -158,68 +1105,33 @@ function assertSubcommandsHaveLocallyUniqueNames(spec) {
158
1105
  });
159
1106
  }
160
1107
  /**
161
- * Asserts that option names starting with a single dash aren't longer
162
- * than two characters, unless parserDirectives.flagsArePosixNoncompliant is
163
- * set on an ancestor command.
164
- */
165
- function assertLongOptionNamesDoNotStartWithSingleDash(spec) {
166
- forEachOptionArray(spec, (options, path) => {
167
- if (path.map((command) => command.parserDirectives?.flagsArePosixNoncompliant).filter((value) => value !== void 0).at(-1) || false) return;
168
- for (const [index, option] of makeArray(options).entries()) for (const name of makeArray(option.name)) assert(!((name.startsWith("-") || name.startsWith("+")) && !name.startsWith("--") && name.length > 2), `Option ${namedArrayToString(...path, option)} (index ${index}) has a name that the parser is unable to parse, as it starts with a single '${name[0]}'. Tokens starting with that character are interpreted as short options, so passing '${name}' will be interpreted as '${Array.from(name.slice(1), (letter) => name[0] + letter).join(" ")}'. \`parserDirectives.flagsArePosixNoncompliant\` must be \`true\` on an ancestor command.`);
169
- });
170
- }
171
- /**
172
- * Asserts that all option names start with dashes
173
- */
174
- function assertOptionNamesStartWithDashes(spec) {
175
- forEachOptionArray(spec, (options, path) => {
176
- if (path.map((command) => command.parserDirectives?.flagsArePosixNoncompliant).filter((value) => value !== void 0).at(-1) || false) return;
177
- for (const [index, option] of makeArray(options).entries()) for (const name of makeArray(option.name)) assert(name.startsWith("-") || name.startsWith("+"), `Option ${namedArrayToString(...path, option)} (index ${index}) doesn't start with a dash or plus, the parser will be unable to find matching options without \`parserDirectives.flagsAreNonPosixCompliant\` being set to 'true' on an ancestor command`);
178
- });
179
- }
180
- /**
181
- * Asserts that nothing is named exactly '--'
182
- */
183
- function assertNothingIsNamedDashDash(spec) {
184
- forEachOptionArray(spec, (options, path) => {
185
- for (const [index, option] of makeArray(options).entries()) for (const name of makeArray(option.name)) assert(name !== "--", `Option ${namedArrayToString(...path, option)} (index ${index}) is named '--', which is a special instruction to the parser to treat all following tokens as arguments. This will never be matched`);
186
- });
187
- forEachSubcommand(spec, (subcommands, path) => {
188
- for (const [index, subcommand] of makeArray(subcommands).entries()) for (const name of makeArray(subcommand.name)) assert(name !== "--", `Subcommand ${namedArrayToString(...path, subcommand)} (index ${index}) is named '--', which is a special instruction to the parser to treat all following tokens as arguments. This will never be matched`);
189
- });
190
- }
191
- /**
192
1108
  * Asserts that no option arg separator is an empty string
193
1109
  */
194
1110
  function assertOptionArgSeparatorsHaveCharacters(spec) {
195
- forEachSubcommand(spec, (subcommands, path) => {
196
- for (const [index, subcommand] of makeArray(subcommands).entries()) {
197
- const separators = makeArray(subcommand.parserDirectives?.optionArgSeparators);
198
- for (const [sepIndex, separator] of separators.entries()) assert(separator !== "", `Subcommand ${namedArrayToString(...path, subcommand)} (index ${index}) has an empty string for an option arg separator at index ${sepIndex}. If you wanted to disable option arg separators, use an empty array.`);
199
- }
1111
+ forEachSubcommand(spec, (command, path) => {
1112
+ const separators = makeArray(command.parserDirectives?.optionArgSeparators);
1113
+ 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.`);
200
1114
  });
201
1115
  }
202
1116
  /**
203
- * Asserts that options named "+" or "-" takes one arg
1117
+ * Asserts that the parser reads options named "+" or "-" the way they're
1118
+ * written, which needs them to take one argument.
1119
+ *
1120
+ * @deprecated {@link assertNamesReadBack} checks this for every option.
204
1121
  */
205
1122
  function assertPlusMinusOptionsTakeOneArg(spec) {
206
- forEachOptionArray(spec, (options, path) => {
207
- if (path.map((command) => command.parserDirectives?.flagsArePosixNoncompliant).filter((value) => value !== void 0).at(-1) || false) return;
208
- for (const [index, option] of makeArray(options).entries()) for (const name of makeArray(option.name)) {
209
- if (name !== "-" && name !== "+") continue;
210
- assert(option.args && !Array.isArray(option.args), `Option ${namedArrayToString(...path, option)} (index ${index}) must take exactly one argument`);
211
- }
212
- });
1123
+ assertOptionFormsReadBack(spec, (_, name) => name === "-" || name === "+");
213
1124
  }
214
1125
  /**
215
1126
  * Asserts that names do not have leading or trailing whitespace
216
1127
  */
217
1128
  function assertNamesHaveNoExtraWhitespace(spec) {
218
- forEachOptionArray(spec, (options, path) => {
219
- for (const [index, option] of makeArray(options).entries()) for (const name of makeArray(option.name)) assert(name === name.trim(), `Option ${namedArrayToString(...path, option)} (index ${index}) has a name with extra whitespace`);
1129
+ forEachOption(spec, ({ option, isDefinedHere, where }) => {
1130
+ if (!isDefinedHere) return;
1131
+ for (const name of makeArray(option.name)) assert(name === name.trim(), `${where} has a name with extra whitespace`);
220
1132
  });
221
- forEachSubcommand(spec, (subcommands, path) => {
222
- for (const [index, subcommand] of makeArray(subcommands).entries()) for (const name of makeArray(subcommand.name)) assert(name === name.trim(), `Subcommand ${namedArrayToString(...path, subcommand)} (index ${index}) has a name with extra whitespace`);
1133
+ forEachSubcommand(spec, (command, path) => {
1134
+ for (const name of makeArray(command.name)) assert(name === name.trim(), `${describeCommand(path)} has a name with extra whitespace`);
223
1135
  });
224
1136
  forEachArgArray(spec, (args, path) => {
225
1137
  for (const [index, arg] of makeArray(args).entries()) for (const name of makeArray(arg.name)) assert(name === name.trim(), `Arg in ${namedArrayToString(...path)} (at index ${index}) has a name with extra whitespace`);
@@ -230,11 +1142,11 @@ function assertNamesHaveNoExtraWhitespace(spec) {
230
1142
  */
231
1143
  function assertEverythingHasDescription(spec) {
232
1144
  assert(spec.description, `Spec has no description`);
233
- forEachOptionArray(spec, (options, path) => {
234
- for (const [index, option] of makeArray(options).entries()) assert(option.description, `Option ${namedArrayToString(...path, option)} (index ${index}) has no description`);
1145
+ forEachOption(spec, ({ option, isDefinedHere, where }) => {
1146
+ if (isDefinedHere) assert(option.description, `${where} has no description`);
235
1147
  });
236
- forEachSubcommand(spec, (subcommands, path) => {
237
- for (const [index, subcommand] of makeArray(subcommands).entries()) assert(subcommand.description, `Subcommand ${namedArrayToString(...path, subcommand)} (index ${index}) has no description`);
1148
+ forEachSubcommand(spec, (command, path) => {
1149
+ assert(command.description, `${describeCommand(path)} has no description`);
238
1150
  });
239
1151
  }
240
1152
  /**
@@ -242,22 +1154,128 @@ function assertEverythingHasDescription(spec) {
242
1154
  */
243
1155
  function assertDescriptionLineLengthUnder69(spec) {
244
1156
  if (spec.description) assert(spec.description.split("\n").every((line) => line.length <= 68), `Spec has a description line over 68 characters`);
245
- forEachOptionArray(spec, (options, path) => {
246
- for (const [index, option] of makeArray(options).entries()) if (option.description) assert(option.description.split("\n").every((line) => line.length <= 68), `Option ${namedArrayToString(...path, option)} (index ${index}) has a description line over 68 characters`);
1157
+ forEachOption(spec, ({ option, isDefinedHere, where }) => {
1158
+ if (!isDefinedHere || !option.description) return;
1159
+ assert(option.description.split("\n").every((line) => line.length <= 68), `${where} has a description line over 68 characters`);
247
1160
  });
248
- forEachSubcommand(spec, (subcommands, path) => {
249
- for (const [index, subcommand] of makeArray(subcommands).entries()) if (subcommand.description) assert(subcommand.description.split("\n").every((line) => line.length <= 68), `Subcommand ${namedArrayToString(...path, subcommand)} (index ${index}) has a description line over 68 characters`);
1161
+ forEachSubcommand(spec, (command, path) => {
1162
+ if (command.description) assert(command.description.split("\n").every((line) => line.length <= 68), `${describeCommand(path)} has a description line over 68 characters`);
1163
+ 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`);
250
1164
  });
251
1165
  }
1166
+ /**
1167
+ * Asserts that the parser reads options with `requiresSeparator` the way
1168
+ * they're written, which needs them to take at most one argument.
1169
+ *
1170
+ * @deprecated {@link assertNamesReadBack} checks this for every option.
1171
+ */
252
1172
  function assertRequiresSeparatorTakesOneArg(spec) {
253
- forEachOptionArray(spec, (options, path) => {
254
- for (const [index, option] of makeArray(options).entries()) if (option.requiresSeparator) {
255
- const args = makeArray(option.args);
256
- assert(getMinArgs(args) <= 1, `Option ${namedArrayToString(...path, option)} (index ${index}) takes a minimum of ${getMinArgs(args)} arg(s), but because of requiresSeparator, it must instead be 0 or 1`);
257
- assert(getMaxArgs(args) === 1, `Option ${namedArrayToString(...path, option)} (index ${index}) takes a maximum of ${getMaxArgs(args)} arg(s), but because of requiresSeparator, it's only able to take 1`);
1173
+ assertOptionFormsReadBack(spec, (option) => Boolean(option.requiresSeparator));
1174
+ }
1175
+ /**
1176
+ * What the parser read a token as, for a message. `path` is the command the
1177
+ * token came after.
1178
+ */
1179
+ function describeReading(reading, path) {
1180
+ switch (reading.kind) {
1181
+ case "separator": return "the end of options, '--'";
1182
+ case "subcommand": return `the subcommand ${namedArrayToString(...path, reading.command)}`;
1183
+ case "options": return reading.options.length === 1 ? `the option ${namedArrayToString(reading.options[0])}` : `the options ${reading.options.map((option) => namedArrayToString(option)).join(", ")}`;
1184
+ case "value": return `a value of the option ${namedArrayToString(reading.option)}`;
1185
+ case "argument": return "an argument";
1186
+ case "unknown": return "an option that doesn't exist";
1187
+ }
1188
+ }
1189
+ /** Whether the parser read a token as `option` alone. */
1190
+ function readsAs(reading, option) {
1191
+ return reading.kind === "options" && reading.options.length === 1 && reading.options[0] === option;
1192
+ }
1193
+ /**
1194
+ * The tokens that reach the last command on `path`, using each command's first
1195
+ * name. {@link assertNamesReadBack} checks subcommand names from the root down
1196
+ * before options, so each of these is a name the parser reads.
1197
+ */
1198
+ function tokensTo(path) {
1199
+ return path.slice(1).map((command) => makeArray(command.name)[0]);
1200
+ }
1201
+ /**
1202
+ * A value for writing options with at the last command on `path`. No name
1203
+ * there starts with it, or with it after '-' or '+', so the parser can only
1204
+ * read it as a value.
1205
+ */
1206
+ function placeholderValue(path, scope) {
1207
+ const names = [...[...scope.local, ...scope.persistent].flatMap((option) => makeArray(option.name)), ...makeArray(path[path.length - 1].subcommands).flatMap((command) => makeArray(command.name))];
1208
+ for (const value of "0123456789abcdefghijklmnopqrstuvwxyz") {
1209
+ const starts = [
1210
+ value,
1211
+ `-${value}`,
1212
+ `+${value}`
1213
+ ];
1214
+ if (!names.some((name) => starts.some((start) => name.startsWith(start)))) return value;
1215
+ }
1216
+ return "0";
1217
+ }
1218
+ /**
1219
+ * The tokens that write `form` with every value `option` takes, or one more
1220
+ * than its minimum if it takes any number. The first value is joined to the
1221
+ * name if the form says so, and the rest are separate tokens.
1222
+ */
1223
+ function writeForm(option, form, value) {
1224
+ const max = getMaxArgs(option.args);
1225
+ const count = Number.isFinite(max) ? max : getMinArgs(option.args) + 1;
1226
+ const values = Array.from({ length: count }, () => value);
1227
+ if (form.join === null) return [form.name, ...values];
1228
+ return [form.name + form.join + values[0], ...values.slice(1)];
1229
+ }
1230
+ /**
1231
+ * Asserts that the parser reads each name of the options that `include`
1232
+ * picks the way the name is written. See {@link assertNamesReadBack}.
1233
+ */
1234
+ function assertOptionFormsReadBack(spec, include) {
1235
+ const root = spec;
1236
+ forEachOption(spec, ({ option, where, scope, path }) => {
1237
+ const before = tokensTo(path);
1238
+ const value = placeholderValue(path, scope);
1239
+ const names = makeArray(option.name).filter((name) => scope.resolve(name) === option);
1240
+ for (const form of scope.forms(option, names)) {
1241
+ if (!include(option, form.name)) continue;
1242
+ if (form.join !== "") {
1243
+ const reading = readToken(root, before, form.name);
1244
+ assert(readsAs(reading, option), `${where} has the name '${form.name}', but the parser reads it as ${describeReading(reading, path)}`);
1245
+ }
1246
+ const tokens = writeForm(option, form, value);
1247
+ const written = `${where} is written '${optionToString(option, [form])}'`;
1248
+ if (form.join !== null) {
1249
+ const first = readToken(root, before, tokens[0]);
1250
+ const joined = first.kind === "options" && first.attached?.option === option;
1251
+ assert(readsAs(first, option) && joined, `${written}, but the parser reads '${tokens[0]}' as ${describeReading(first, path)}${joined ? "" : " without a value"}`);
1252
+ }
1253
+ for (let index = 1; index < tokens.length; index++) {
1254
+ const reading = readToken(root, [...before, ...tokens.slice(0, index)], tokens[index]);
1255
+ assert(reading.kind === "value" && reading.option === option, `${written}, but in '${tokens.join(" ")}' the parser reads '${tokens[index]}' as ${describeReading(reading, path)}`);
1256
+ }
258
1257
  }
259
1258
  });
260
1259
  }
1260
+ /**
1261
+ * Asserts that the parser reads every name the way it's written: each name of
1262
+ * a subcommand enters that subcommand, and each name of an option uses that
1263
+ * option, at every command the option can be used in. Each option is also
1264
+ * written the way help shows it, with every value it takes, and the parser has
1265
+ * to read all of those values as the option's.
1266
+ */
1267
+ function assertNamesReadBack(spec) {
1268
+ const root = spec;
1269
+ forEachSubcommand(spec, (command, path) => {
1270
+ const before = tokensTo(path);
1271
+ for (const subcommand of makeArray(command.subcommands)) for (const name of makeArray(subcommand.name)) {
1272
+ const reading = readToken(root, before, name);
1273
+ if (reading.kind === "subcommand" && reading.command === subcommand) continue;
1274
+ assert(false, `${describeCommand([...path, subcommand])} has the name '${name}', but the parser reads it as ${describeReading(reading, path)}`);
1275
+ }
1276
+ });
1277
+ assertOptionFormsReadBack(spec, () => true);
1278
+ }
261
1279
  function assertCommonOptionsArePersistent(spec) {
262
1280
  const failures = [];
263
1281
  forEachSubcommand(spec, (subcommand, path) => {
@@ -265,7 +1283,8 @@ function assertCommonOptionsArePersistent(spec) {
265
1283
  for (const option of makeArray(subcommand.options)) {
266
1284
  const allChildSubcommands = [];
267
1285
  forEachSubcommand(subcommand, (cmd) => allChildSubcommands.push(cmd));
268
- if (allChildSubcommands.every((cmd) => makeArray(cmd.options).some((o) => isSameName(option.name, o.name))) && !failures.some(([name]) => isSameName(name, option.name))) failures.push([option.name, namedArrayToString(...path, option)]);
1286
+ 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)]);
269
1288
  }
270
1289
  });
271
1290
  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.
@@ -274,15 +1293,12 @@ ${failures.map(([, line]) => " * " + line).join("\n")}
274
1293
  }
275
1294
  /**
276
1295
  * Asserts that references to other options by name actually refer to options
277
- * that exist in scope
1296
+ * that exist in scope, at every command the option can be used in
278
1297
  */
279
1298
  function assertOptionNameReferencesExist(spec) {
280
- forEachOptionArray(spec, (options, path) => {
281
- const optionNames = /* @__PURE__ */ new Set([...path.slice(0, -1).flatMap((subcommand) => subcommand.options ? subcommand.options.filter((option) => option.isPersistent).flatMap((option) => option.name) : []), ...options.flatMap((option) => option.name)]);
282
- for (const [index, option] of makeArray(options).entries()) {
283
- for (const name of makeArray(option.dependsOn)) assert(optionNames.has(name), `The option ${namedArrayToString(...path, option)} (index ${index}) depends on an option named ${repr(name)}, which doesn't exist in its scope`);
284
- for (const name of makeArray(option.exclusiveOn)) assert(optionNames.has(name), `The option ${namedArrayToString(...path, option)} (index ${index}) is exclusive on an option named ${repr(name)}, which doesn't exist in its scope`);
285
- }
1299
+ forEachOption(spec, ({ option, where, scope }) => {
1300
+ for (const name of makeArray(option.dependsOn)) assert(scope.resolve(name) !== null, `${where} depends on an option named ${repr(name)}, which doesn't exist in its scope`);
1301
+ for (const name of makeArray(option.exclusiveOn)) assert(scope.resolve(name) !== null, `${where} is exclusive on an option named ${repr(name)}, which doesn't exist in its scope`);
286
1302
  });
287
1303
  }
288
1304
  /**
@@ -290,13 +1306,10 @@ function assertOptionNameReferencesExist(spec) {
290
1306
  */
291
1307
  function assertPrefixMatchCommandsHaveNoArguments(spec) {
292
1308
  forEachSubcommand(spec, (subcommand, path) => {
293
- if (!path.some((cmd) => cmd.parserDirectives?.subcommandsMatchUniquePrefix === true)) return;
1309
+ if (!OptionScope.at(path).directives.subcommandsMatchUniquePrefix) return;
294
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 }\``);
295
1311
  });
296
1312
  }
297
- function getName(namingStyle, names) {
298
- return names[namingStyle];
299
- }
300
1313
  async function runStep(context, name, fn) {
301
1314
  if (context) {
302
1315
  await context.step(name, fn);
@@ -313,118 +1326,106 @@ async function runStep(context, name, fn) {
313
1326
  */
314
1327
  function test(spec, options = {}) {
315
1328
  const { allowShadowingPersistentOptions = false, allowNoDescription = false, allowLongDescriptionLines = false, allowMatchingSubcommandPrefixAndArgs = false, namingStyle = "sentence" } = options;
1329
+ const checks = [
1330
+ [
1331
+ "Required arguments don't follow optional arguments",
1332
+ "assertRequiredArgumentsDoNotFollowOptionalArguments",
1333
+ assertRequiredArgumentsDoNotFollowOptionalArguments,
1334
+ true
1335
+ ],
1336
+ [
1337
+ "The parser can use every default",
1338
+ "assertDefaultsCanBeUsed",
1339
+ assertDefaultsCanBeUsed,
1340
+ true
1341
+ ],
1342
+ [
1343
+ "Repeatable options that are numbers are integers greater than 1",
1344
+ "assertRepeatableOptionsArePositiveIntegers",
1345
+ assertRepeatableOptionsArePositiveIntegers,
1346
+ true
1347
+ ],
1348
+ [
1349
+ "Options have locally unique names",
1350
+ "assertOptionsHaveLocallyUniqueNames",
1351
+ assertOptionsHaveLocallyUniqueNames,
1352
+ true
1353
+ ],
1354
+ [
1355
+ "Options don't shadow persistent options",
1356
+ "assertOptionsDoNotShadowPersistentOptions",
1357
+ assertOptionsDoNotShadowPersistentOptions,
1358
+ !allowShadowingPersistentOptions
1359
+ ],
1360
+ [
1361
+ "Different options on a subcommand and its parents have different names",
1362
+ "assertOptionsHaveDifferentNamesFromParents",
1363
+ assertOptionsHaveDifferentNamesFromParents,
1364
+ true
1365
+ ],
1366
+ [
1367
+ "Subcommands have locally unique names",
1368
+ "assertSubcommandsHaveLocallyUniqueNames",
1369
+ assertSubcommandsHaveLocallyUniqueNames,
1370
+ true
1371
+ ],
1372
+ [
1373
+ "The parser reads every option and subcommand the way it's written",
1374
+ "assertNamesReadBack",
1375
+ assertNamesReadBack,
1376
+ true
1377
+ ],
1378
+ [
1379
+ "Option arg separators are not an empty string",
1380
+ "assertOptionArgSeparatorsHaveCharacters",
1381
+ assertOptionArgSeparatorsHaveCharacters,
1382
+ true
1383
+ ],
1384
+ [
1385
+ "Names have no extra leading or trailing whitespace",
1386
+ "assertNamesHaveNoExtraWhitespace",
1387
+ assertNamesHaveNoExtraWhitespace,
1388
+ true
1389
+ ],
1390
+ [
1391
+ "Everything has a description",
1392
+ "assertEverythingHasDescription",
1393
+ assertEverythingHasDescription,
1394
+ !allowNoDescription
1395
+ ],
1396
+ [
1397
+ "Each line in descriptions is less than or equal to 68 characters",
1398
+ "assertDescriptionLineLengthUnder69",
1399
+ assertDescriptionLineLengthUnder69,
1400
+ !allowLongDescriptionLines
1401
+ ],
1402
+ [
1403
+ "Common options are not defined on every subcommand",
1404
+ "assertCommonOptionsArePersistent",
1405
+ assertCommonOptionsArePersistent,
1406
+ true
1407
+ ],
1408
+ [
1409
+ "References to other options by name are valid",
1410
+ "assertOptionNameReferencesExist",
1411
+ assertOptionNameReferencesExist,
1412
+ true
1413
+ ],
1414
+ [
1415
+ "Commands with `subcommandsMatchUniquePrefix` don't have arguments and subcommands",
1416
+ "assertPrefixMatchCommandsHaveNoArguments",
1417
+ assertPrefixMatchCommandsHaveNoArguments,
1418
+ !allowMatchingSubcommandPrefixAndArgs
1419
+ ]
1420
+ ];
316
1421
  return async (t) => {
317
- await runStep(t, getName(namingStyle, {
318
- sentence: "Required arguments don't follow optional arguments",
319
- function: "assertRequiredArgumentsDoNotFollowOptionalArguments"
320
- }), () => {
321
- assertRequiredArgumentsDoNotFollowOptionalArguments(spec);
322
- });
323
- await runStep(t, getName(namingStyle, {
324
- sentence: "Repeatable options that are numbers are integers greater than 1",
325
- function: "assertRepeatableOptionsArePositiveIntegers"
326
- }), () => {
327
- assertRepeatableOptionsArePositiveIntegers(spec);
328
- });
329
- await runStep(t, getName(namingStyle, {
330
- sentence: "Long option names don't start with a single dash",
331
- function: "assertLongOptionNamesDoNotStartWithSingleDash"
332
- }), () => {
333
- assertLongOptionNamesDoNotStartWithSingleDash(spec);
334
- });
335
- await runStep(t, getName(namingStyle, {
336
- sentence: "Option names all start with dashes",
337
- function: "assertOptionNamesStartWithDashes"
338
- }), () => {
339
- assertOptionNamesStartWithDashes(spec);
340
- });
341
- await runStep(t, getName(namingStyle, {
342
- sentence: "Options have locally unique names",
343
- function: "assertOptionsHaveLocallyUniqueNames"
344
- }), () => {
345
- assertOptionsHaveLocallyUniqueNames(spec);
346
- });
347
- if (!allowShadowingPersistentOptions) await runStep(t, getName(namingStyle, {
348
- sentence: "Options don't shadow persistent options",
349
- function: "assertOptionsDoNotShadowPersistentOptions"
350
- }), () => {
351
- assertOptionsDoNotShadowPersistentOptions(spec);
352
- });
353
- await runStep(t, getName(namingStyle, {
354
- sentence: "Different options on a subcommand and its parents have different names",
355
- function: "assertOptionsHaveDifferentNamesFromParents"
356
- }), () => {
357
- assertOptionsHaveDifferentNamesFromParents(spec);
358
- });
359
- await runStep(t, getName(namingStyle, {
360
- sentence: "Subcommands have locally unique names",
361
- function: "assertSubcommandsHaveLocallyUniqueNames"
362
- }), () => {
363
- assertSubcommandsHaveLocallyUniqueNames(spec);
364
- });
365
- await runStep(t, getName(namingStyle, {
366
- sentence: "Nothing is named '--'",
367
- function: "assertNothingIsNamedDashDash"
368
- }), () => {
369
- assertNothingIsNamedDashDash(spec);
370
- });
371
- await runStep(t, getName(namingStyle, {
372
- sentence: "Option arg separators are not an empty string",
373
- function: "assertOptionArgSeparatorsHaveCharacters"
374
- }), () => {
375
- assertOptionArgSeparatorsHaveCharacters(spec);
376
- });
377
- await runStep(t, getName(namingStyle, {
378
- sentence: "Options named '-' or '+' take exactly one argument",
379
- function: "assertPlusMinusOptionsTakeOneArg"
380
- }), () => {
381
- assertPlusMinusOptionsTakeOneArg(spec);
382
- });
383
- await runStep(t, getName(namingStyle, {
384
- sentence: "Names have no extra leading or trailing whitespace",
385
- function: "assertNamesHaveNoExtraWhitespace"
386
- }), () => {
387
- assertNamesHaveNoExtraWhitespace(spec);
388
- });
389
- if (!allowNoDescription) await runStep(t, getName(namingStyle, {
390
- sentence: "Everything has a description",
391
- function: "assertEverythingHasDescription"
392
- }), () => {
393
- assertEverythingHasDescription(spec);
394
- });
395
- if (!allowLongDescriptionLines) await runStep(t, getName(namingStyle, {
396
- sentence: "Each line in descriptions is less than or equal to 68 characters",
397
- function: "assertDescriptionLineLengthUnder69"
398
- }), () => {
399
- assertDescriptionLineLengthUnder69(spec);
400
- });
401
- await runStep(t, getName(namingStyle, {
402
- sentence: "Options with `requiresSeparator` take exactly one argument",
403
- function: "assertRequiresSeparatorTakesOneArg"
404
- }), () => {
405
- assertRequiresSeparatorTakesOneArg(spec);
406
- });
407
- await runStep(t, getName(namingStyle, {
408
- sentence: "Common options are not defined on every subcommand",
409
- function: "assertCommonOptionsArePersistent"
410
- }), () => {
411
- assertCommonOptionsArePersistent(spec);
412
- });
413
- await runStep(t, getName(namingStyle, {
414
- sentence: "References to other options by name are valid",
415
- function: "assertOptionNameReferencesExist"
416
- }), () => {
417
- assertOptionNameReferencesExist(spec);
418
- });
419
- if (!allowMatchingSubcommandPrefixAndArgs) await runStep(t, getName(namingStyle, {
420
- sentence: "Commands with `subcommandsMatchUniquePrefix` don't have arguments and subcommands",
421
- function: "assertPrefixMatchCommandsHaveNoArguments"
422
- }), () => {
423
- assertPrefixMatchCommandsHaveNoArguments(spec);
424
- });
1422
+ for (const [sentence, functionName, check, enabled] of checks) {
1423
+ if (!enabled) continue;
1424
+ await runStep(t, namingStyle === "function" ? functionName : sentence, () => check(spec));
1425
+ }
425
1426
  };
426
1427
  }
427
1428
  const testSpec = test;
428
- export { assertCommonOptionsArePersistent, assertDescriptionLineLengthUnder69, assertEverythingHasDescription, assertLongOptionNamesDoNotStartWithSingleDash, assertNamesHaveNoExtraWhitespace, assertNothingIsNamedDashDash, assertOptionArgSeparatorsHaveCharacters, assertOptionNameReferencesExist, assertOptionNamesStartWithDashes, assertOptionsDoNotShadowPersistentOptions, assertOptionsHaveDifferentNamesFromParents, assertOptionsHaveLocallyUniqueNames, assertPlusMinusOptionsTakeOneArg, assertPrefixMatchCommandsHaveNoArguments, assertRepeatableOptionsArePositiveIntegers, assertRequiredArgumentsDoNotFollowOptionalArguments, assertRequiresSeparatorTakesOneArg, assertSubcommandsHaveLocallyUniqueNames, test, testSpec };
1429
+ export { assertCommonOptionsArePersistent, assertDefaultsCanBeUsed, assertDescriptionLineLengthUnder69, assertEverythingHasDescription, assertNamesHaveNoExtraWhitespace, assertNamesReadBack, assertOptionArgSeparatorsHaveCharacters, assertOptionNameReferencesExist, assertOptionsDoNotShadowPersistentOptions, assertOptionsHaveDifferentNamesFromParents, assertOptionsHaveLocallyUniqueNames, assertPlusMinusOptionsTakeOneArg, assertPrefixMatchCommandsHaveNoArguments, assertRepeatableOptionsArePositiveIntegers, assertRequiredArgumentsDoNotFollowOptionalArguments, assertRequiresSeparatorTakesOneArg, assertSubcommandsHaveLocallyUniqueNames, test, testSpec };
429
1430
 
430
1431
  //# sourceMappingURL=testing.js.map