@clo4/cli 2.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
@@ -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,42 +89,897 @@ 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
  }
38
- // src/testing.ts
39
- const repr = JSON.stringify;
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
+ }
40
102
  /**
41
- * Run a function once for each `args` property in the spec (recursive)
42
- *
43
- * This will convert single args to an array with one item.
103
+ * Update `directives` for entering `command`. A subcommand inherits each
104
+ * directive until it sets its own.
44
105
  */
45
- function forEachArgArray(spec, fn, path = []) {
46
- path = path.concat(spec);
47
- fn(makeArray(spec.args), path);
48
- for (const option of makeArray(spec.options)) fn(makeArray(option.args), path);
49
- for (const subcommand of makeArray(spec.subcommands)) forEachArgArray(subcommand, fn, path);
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;
50
113
  }
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);
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) ?? [];
56
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
+ };
157
+ // src/scope.ts
57
158
  /**
58
- * Run a function once for each `subcommands` property in the spec (recursive)
159
+ * What a name means at one command on a path: the options, subcommands,
160
+ * directives and argument limits in effect there.
59
161
  *
60
- * This will also run the function for the root spec itself.
162
+ * Moving to a subcommand replaces the local options and adds its persistent
163
+ * options to the inherited ones. A name refers to one option: a local option
164
+ * wins over a persistent one, and a later option wins over an earlier one in
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.
168
+ */
169
+ var CommandScope = class CommandScope {
170
+ /** Non-persistent options of the current command, in definition order. */
171
+ local = [];
172
+ /** Persistent options from every command on the path, in definition order. */
173
+ persistent = [];
174
+ /**
175
+ * Persistent subcommands of the commands above the current one, nearest
176
+ * command first, so a nearer one wins a name.
177
+ */
178
+ inheritedSubcommands = [];
179
+ /** The directives in effect. A subcommand inherits each until it sets its own. */
180
+ directives = defaultDirectives();
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;
186
+ /** The scope at the last command on `path`. */
187
+ static at(path) {
188
+ const scope = new CommandScope();
189
+ for (const command of path) scope.enter(command);
190
+ return scope;
191
+ }
192
+ /** Move to `command`, a subcommand of the current one. */
193
+ enter(command) {
194
+ const parentSubcommands = this.current?.subcommands;
195
+ if (parentSubcommands) {
196
+ const persistent = parentSubcommands.filter((subcommand) => subcommand.isPersistent);
197
+ if (persistent.length > 0) this.inheritedSubcommands.unshift(...persistent);
198
+ }
199
+ this.current = command;
200
+ this.argsMin = getMinArgs(command.args);
201
+ this.argsMax = getMaxArgs(command.args);
202
+ this.local.length = 0;
203
+ if (command.options) for (const option of command.options) (option.isPersistent ? this.persistent : this.local).push(option);
204
+ applyDirectives(this.directives, command);
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
+ }
244
+ /** The option `name` refers to, if any. */
245
+ resolve(name) {
246
+ const { local, persistent } = this;
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];
249
+ return null;
250
+ }
251
+ /**
252
+ * The options that can be used here, in definition order, each with only
253
+ * its live names: the names that still refer to it. Options with every name
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.
256
+ */
257
+ live(subcommands = null) {
258
+ const owner = /* @__PURE__ */ new Map();
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);
260
+ const pick = (options) => {
261
+ const picked = [];
262
+ for (const option of options) {
263
+ const names = asArray(option.name).filter((name) => owner.get(name) === option && !(subcommands && this.matchSubcommand(name, subcommands.exactOnly).command));
264
+ if (names.length > 0) picked.push({
265
+ option,
266
+ names
267
+ });
268
+ }
269
+ return picked;
270
+ };
271
+ return {
272
+ local: pick(this.local),
273
+ persistent: pick(this.persistent)
274
+ };
275
+ }
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
+ */
280
+ visible() {
281
+ const { local, persistent } = this.live(this.hasSubcommands ? { exactOnly: false } : null);
282
+ const shown = (visible) => !visible.option.hidden;
283
+ return {
284
+ local: local.filter(shown),
285
+ persistent: persistent.filter(shown)
286
+ };
287
+ }
288
+ /** The live names of each option a user could have meant, for "did you mean" suggestions. */
289
+ suggestionNames() {
290
+ const { local, persistent } = this.live();
291
+ const shown = (visible) => !visible.option.hidden;
292
+ return [...local, ...persistent].filter(shown).map((visible) => visible.names);
293
+ }
294
+ /** The options that must be used, local first. */
295
+ required() {
296
+ return [...this.local, ...this.persistent].filter((option) => option.isRequired);
297
+ }
298
+ /**
299
+ * How each of `names` is written with `option`'s value here. Help shows
300
+ * these, completion offers them, and the spec checker reads each one back
301
+ * through the parser.
302
+ */
303
+ forms(option, names = asArray(option.name)) {
304
+ const join = (name) => {
305
+ if (getMaxArgs(option.args) === 0) return null;
306
+ if (option.requiresSeparator) return this.requiredSeparator(option);
307
+ return !this.directives.flagsArePosixNoncompliant && (name === "-" || name === "+") ? "" : null;
308
+ };
309
+ return names.map((name) => ({
310
+ name,
311
+ join: join(name)
312
+ }));
313
+ }
314
+ /**
315
+ * The separator that must come between `option` and its value, or null if
316
+ * it can take a value as a separate token.
317
+ */
318
+ requiredSeparator(option) {
319
+ const required = option.requiresSeparator;
320
+ if (typeof required === "string") return required;
321
+ return required ? this.directives.optionArgSeparators[0] ?? "=" : null;
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
+ }
363
+ };
364
+ const optionKeys = /* @__PURE__ */ new WeakMap();
365
+ /**
366
+ * A key for the set of `option`'s names. Options with the same names, in any
367
+ * order, get the same key and are one option: the parser continues a
368
+ * repeatable option's uses across them, counts one as used when the other was,
369
+ * and stores their values under the same names, which is how they're read.
370
+ * Options that share only some names are different options, and each shared
371
+ * name refers to whichever one `resolve` picks.
372
+ */
373
+ function optionKey(option) {
374
+ let key = optionKeys.get(option);
375
+ if (key === void 0) {
376
+ key = typeof option.name === "string" ? option.name : [...option.name].sort().join("\0");
377
+ optionKeys.set(option, key);
378
+ }
379
+ return key;
380
+ }
381
+ function hasName(names, name) {
382
+ return typeof names === "string" ? names === name : names.includes(name);
383
+ }
384
+ /** The visible commands that `token` is a prefix of a name of, each listed once. */
385
+ function prefixMatches(subcommands, token) {
386
+ if (token === "") return [];
387
+ return subcommands.filter((candidate) => !candidate.hidden && asArray(candidate.name).some((name) => name.startsWith(token)));
388
+ }
389
+ // src/help.ts
390
+ /**
391
+ * Show an option's names, each written with its arguments the way `forms`
392
+ * says. Names written the same way share one copy of the arguments.
61
393
  */
62
- function forEachSubcommand(spec, fn, path = []) {
63
- path = path.concat(spec);
64
- fn(spec, path);
65
- for (const subcommand of makeArray(spec.subcommands)) forEachSubcommand(subcommand, fn, path);
394
+ function optionToString(option, forms) {
395
+ const optionArgs = makeArray(option.args);
396
+ const sorted = [...forms].sort((a, b) => a.name.length - b.name.length);
397
+ return [...new Set(sorted.map((form) => form.join))].map((join) => {
398
+ return sorted.filter((form) => form.join === join).map((form) => form.name).join(", ") + argumentsAfterName(optionArgs, join);
399
+ }).join(", ");
400
+ }
401
+ /** The text after an option's name that shows its arguments, joined to the name by `join`. */
402
+ function argumentsAfterName(args, join) {
403
+ if (args.length === 0) return "";
404
+ if (join === null) return ` ${summarizeArguments(args)}`;
405
+ const [first, ...rest] = args;
406
+ const name = first.name || "argument";
407
+ const joined = (first.isOptional ? `[${join}${name}]` : `${join}<${name}>`) + (first.isVariadic ? "..." : "");
408
+ return rest.length > 0 ? `${joined} ${summarizeArguments(rest)}` : joined;
409
+ }
410
+ function summarizeArguments(args) {
411
+ return args.map((arg) => argToString(arg)).join(" ");
412
+ }
413
+ function argToString(arg) {
414
+ const name = arg.name || "argument";
415
+ let lhs = "<";
416
+ let rhs = ">";
417
+ let suffix = "";
418
+ if (arg.isOptional) {
419
+ lhs = "[";
420
+ rhs = "]";
421
+ }
422
+ if (arg.isVariadic) suffix = "...";
423
+ return `${lhs}${name}${rhs}${suffix}`;
424
+ }
425
+ // src/errors.ts
426
+ const repr$1 = JSON.stringify;
427
+ /**
428
+ * A command line the parser can't accept. `run` prints the message with the
429
+ * help for the command at `context.path`, and suggests the closest of
430
+ * `didYouMean.choices` if there is one.
431
+ */
432
+ var ParseError = class extends Error {
433
+ context;
434
+ didYouMean;
435
+ constructor(context, message, didYouMean) {
436
+ super(message);
437
+ this.context = context;
438
+ this.didYouMean = didYouMean;
439
+ this.name = this.constructor.name;
440
+ }
441
+ };
442
+ var UnknownOption = class extends ParseError {
443
+ option;
444
+ constructor(option, validOptions, context) {
445
+ const msg = `${repr$1(option)} looks like a flag, but is unknown in this context`;
446
+ super(context, msg, {
447
+ input: option,
448
+ choices: validOptions
449
+ });
450
+ this.option = option;
451
+ }
452
+ };
453
+ var InvalidOptionArg = class extends ParseError {
454
+ option;
455
+ constructor(option, context) {
456
+ super(context, `Option ${option} doesn't take arguments`);
457
+ this.option = option;
458
+ }
459
+ };
460
+ var TooManyArguments = class extends ParseError {
461
+ constructor(value, context) {
462
+ super(context, `Too many arguments, ${repr$1(value)} was unexpected`);
463
+ }
464
+ };
465
+ var TooFewOptionArguments = class extends ParseError {
466
+ constructor(option, min, max, context) {
467
+ 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`;
468
+ super(context, msg);
469
+ }
470
+ };
471
+ var TooFewArguments = class extends ParseError {
472
+ constructor(min, max, context) {
473
+ 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`;
474
+ super(context, msg);
475
+ }
476
+ };
477
+ var MissingRequiredOption = class extends ParseError {
478
+ constructor(name, context) {
479
+ const msg = `Option ${name} is required, but wasn't found`;
480
+ super(context, msg);
481
+ }
482
+ };
483
+ // src/parse.ts
484
+ /** How many times `option` can be used. */
485
+ function repeatLimit(option) {
486
+ return typeof option.isRepeatable === "number" ? option.isRepeatable : Infinity;
487
+ }
488
+ /**
489
+ * Throw if the options used break a `dependsOn` or `exclusiveOn` rule, or if
490
+ * any of `required` is missing.
491
+ */
492
+ function checkOptionRules(options, seenOptions, required, context) {
493
+ for (const option of seenOptions) {
494
+ if (!option.dependsOn) continue;
495
+ 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`);
496
+ }
497
+ for (const option of seenOptions) {
498
+ if (!option.exclusiveOn) continue;
499
+ 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}`);
500
+ }
501
+ for (const option of required) for (const name of asArray(option.name)) if (!options.has(name)) throw new MissingRequiredOption(name, context);
502
+ }
503
+ /**
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.
510
+ */
511
+ var Scanner = class {
512
+ path;
513
+ optionAction = void 0;
514
+ args = [];
515
+ options = /* @__PURE__ */ new Map();
516
+ seenOptions = [];
517
+ argSeparatorIndex = -1;
518
+ scope = new CommandScope();
519
+ directives = this.scope.directives;
520
+ hasFoundArg = false;
521
+ openOption = null;
522
+ openOptionName = "";
523
+ valueCount = 0;
524
+ valueMin = 0;
525
+ separateValueMax = 0;
526
+ values = null;
527
+ valuesStart = 0;
528
+ valuesMin = 0;
529
+ valuesMax = 0;
530
+ valueArgs = [];
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;
537
+ tooManyArgs = null;
538
+ repeatedUses = /* @__PURE__ */ new Map();
539
+ constructor(spec) {
540
+ this.path = [spec];
541
+ this.scope.enter(spec);
542
+ }
543
+ ctx() {
544
+ return { path: [...this.path] };
545
+ }
546
+ /** Whether the open option can take the next token as a value. */
547
+ get hasRoomForValue() {
548
+ return this.valueCount < this.separateValueMax;
549
+ }
550
+ findOption(name) {
551
+ return this.scope.resolve(name);
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
+ }
571
+ unknownOption(option) {
572
+ if (!this.problem) {
573
+ const names = this.scope.suggestionNames();
574
+ this.fail((ctx) => new UnknownOption(option, names, ctx));
575
+ }
576
+ }
577
+ endOption() {
578
+ const values = this.values;
579
+ if (values) {
580
+ const given = values.length - this.valuesStart;
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);
585
+ }
586
+ this.values = null;
587
+ }
588
+ /** End the open option, so no later token can be its value. */
589
+ closeOption() {
590
+ this.endOption();
591
+ this.openOption = null;
592
+ this.valueCount = this.valueMin = this.separateValueMax = 0;
593
+ }
594
+ useOption(option, name) {
595
+ this.endOption();
596
+ this.openOption = option;
597
+ this.openOptionName = name;
598
+ this.valueCount = 0;
599
+ this.valueMin = getMinArgs(option.args);
600
+ this.separateValueMax = option.requiresSeparator ? 0 : getMaxArgs(option.args);
601
+ this.seenOptions.push(option);
602
+ if (option.action) this.optionAction = option.action;
603
+ const names = asArray(option.name);
604
+ const options = this.options;
605
+ let optionValues;
606
+ if (option.isRepeatable) {
607
+ const key = optionKey(option);
608
+ let uses = this.repeatedUses.get(key);
609
+ if (!uses) {
610
+ uses = {
611
+ values: [],
612
+ count: 0
613
+ };
614
+ this.repeatedUses.set(key, uses);
615
+ }
616
+ if (uses.count >= repeatLimit(option)) this.fail((ctx) => new ParseError(ctx, "Too many repetitions"));
617
+ uses.count += 1;
618
+ for (const other of names) options.set(other, uses.values);
619
+ if (!option.args) {
620
+ uses.values.push("");
621
+ return;
622
+ }
623
+ optionValues = uses.values;
624
+ } else {
625
+ optionValues = [];
626
+ for (const other of names) options.set(other, optionValues);
627
+ }
628
+ this.valuesMin = this.valueMin;
629
+ this.valuesMax = getMaxArgs(option.args);
630
+ if (this.valuesMax > 0) {
631
+ this.values = optionValues;
632
+ this.valuesStart = optionValues.length;
633
+ this.valueArgs = asArray(option.args);
634
+ }
635
+ }
636
+ /** Add a value to the open option. Returns whether it's a value the option accepts. */
637
+ addValue(value, separator) {
638
+ this.valueCount += 1;
639
+ if (!this.values) {
640
+ const name = this.openOptionName;
641
+ this.fail((ctx) => new InvalidOptionArg(name, ctx));
642
+ return false;
643
+ }
644
+ const problem = this.scope.separatorProblem(this.openOption, this.openOptionName, separator);
645
+ if (problem) this.fail((ctx) => new ParseError(ctx, problem));
646
+ this.values.push(value);
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
+ };
656
+ }
657
+ /**
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.
661
+ */
662
+ addAttachedValue(start, value, separator) {
663
+ const option = this.openOption;
664
+ const accepted = this.addValue(value, separator);
665
+ this.valueMin = this.separateValueMax = 0;
666
+ return accepted ? {
667
+ option,
668
+ start
669
+ } : null;
670
+ }
671
+ /**
672
+ * Whether the command already has all the arguments it takes. A command that
673
+ * requires a subcommand takes none.
674
+ */
675
+ get argsAreFull() {
676
+ return this.scope.command.requiresSubcommand === true || this.args.length >= this.scope.argsMax;
677
+ }
678
+ addArg(value) {
679
+ this.endOption();
680
+ if (this.argsAreFull) this.failArgs((ctx) => new TooManyArguments(value, ctx));
681
+ this.args.push(value);
682
+ return { kind: "argument" };
683
+ }
684
+ /** Read `--`. Every token after it is an argument. */
685
+ argSeparator() {
686
+ this.closeOption();
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?"));
689
+ this.argSeparatorIndex = this.args.length;
690
+ return { kind: "separator" };
691
+ }
692
+ /**
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.
699
+ */
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 };
704
+ }
705
+ /**
706
+ * The subcommand the next token enters if it's `token`, or why it enters
707
+ * none. Null if the next token can't be a subcommand at all.
708
+ */
709
+ subcommandFor(token) {
710
+ const matching = this.subcommandMatching;
711
+ return matching && this.scope.matchSubcommand(token, matching.exactOnly);
712
+ }
713
+ enterSubcommand(command) {
714
+ this.closeOption();
715
+ this.path.push(command);
716
+ this.scope.enter(command);
717
+ return {
718
+ kind: "subcommand",
719
+ command
720
+ };
721
+ }
722
+ /** Read a token like `-abc`, `+x` or `-ovalue`. */
723
+ shortOptions(token) {
724
+ const leadingChar = token[0];
725
+ const dashOption = this.findOption(leadingChar);
726
+ if (dashOption?.args && !this.findOption(token.slice(0, 2))) {
727
+ this.useOption(dashOption, leadingChar);
728
+ const attached = this.addAttachedValue(1, token.slice(1), "");
729
+ return {
730
+ kind: "options",
731
+ options: [dashOption],
732
+ attached
733
+ };
734
+ }
735
+ const used = [];
736
+ let attached = null;
737
+ for (let char = 1; char < token.length; char++) {
738
+ const optionName = leadingChar + token[char];
739
+ const option = this.findOption(optionName);
740
+ if (!option) {
741
+ if (char === 1 && this.hasRoomForValue) return this.value(token);
742
+ this.unknownOption(optionName);
743
+ continue;
744
+ }
745
+ this.useOption(option, optionName);
746
+ used.push(option);
747
+ if (char === token.length - 1) break;
748
+ const sep = this.scope.leadingSeparator(token.slice(char + 1));
749
+ if (option.args || sep && !this.findOption(leadingChar + token[char + 1])) {
750
+ const argStart = char + sep.length + 1;
751
+ attached = this.addAttachedValue(argStart, token.slice(argStart), sep);
752
+ break;
753
+ }
754
+ }
755
+ return used.length > 0 ? {
756
+ kind: "options",
757
+ options: used,
758
+ attached
759
+ } : { kind: "unknown" };
760
+ }
761
+ /**
762
+ * Read a token like `--name` or `--name=value`, or any token when options
763
+ * aren't POSIX compliant. Returns null if the token should be an argument.
764
+ */
765
+ longOption(token) {
766
+ const found = this.scope.firstSeparator(token);
767
+ const optionName = found ? token.slice(0, found.index) : token;
768
+ const option = this.findOption(optionName);
769
+ if (option) {
770
+ this.useOption(option, optionName);
771
+ let attached = null;
772
+ if (found) {
773
+ const valueStart = found.index + found.separator.length;
774
+ attached = this.addAttachedValue(valueStart, token.slice(valueStart), found.separator);
775
+ }
776
+ return {
777
+ kind: "options",
778
+ options: [option],
779
+ attached
780
+ };
781
+ }
782
+ if (this.directives.flagsArePosixNoncompliant) return null;
783
+ if (this.hasRoomForValue) return this.value(token);
784
+ this.unknownOption(optionName);
785
+ return { kind: "unknown" };
786
+ }
787
+ /**
788
+ * Read a token that isn't `--`, a subcommand or an option. It's a value of
789
+ * the open option if that has room, and otherwise an argument, which stops
790
+ * later tokens from being subcommands. `unmatched` is why the token isn't a
791
+ * subcommand, if it could have been one.
792
+ */
793
+ valueOrArg(token, unmatched) {
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) => {
798
+ const { message, didYouMean } = unmatched.reason();
799
+ return new ParseError(ctx, message, didYouMean);
800
+ });
801
+ }
802
+ this.hasFoundArg = true;
803
+ return this.addArg(token);
804
+ }
805
+ /**
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}.
808
+ */
809
+ finish() {
810
+ const command = this.scope.command;
811
+ const showsUsage = command.requiresSubcommand && !command.action && this.args.length === 0;
812
+ const skipsChecks = this.optionAction !== void 0 || showsUsage;
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());
815
+ this.endOption();
816
+ if (this.problem) throw this.problem();
817
+ if (!skipsChecks) checkOptionRules(this.options, this.seenOptions, this.scope.required(), this.ctx());
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
+ };
827
+ }
828
+ /** See {@link ParseResult.defaults}. */
829
+ defaults(name) {
830
+ if (this.options.has(name)) return;
831
+ const args = asArray(this.scope.resolve(name)?.args);
832
+ if (args.length === 0 || args[0].isOptional) return;
833
+ const values = [];
834
+ addDefaults(values, args);
835
+ return values.length > 0 ? values : void 0;
836
+ }
837
+ /** Whether using `option` next would go over its repeat limit or break an `exclusiveOn` rule. */
838
+ conflicts(option) {
839
+ if ((this.repeatedUses.get(optionKey(option))?.count ?? 0) >= repeatLimit(option)) return true;
840
+ const options = this.options;
841
+ if (option.exclusiveOn?.some((name) => options.has(name))) return true;
842
+ const names = asArray(option.name);
843
+ return this.seenOptions.some((seen) => seen.exclusiveOn?.some((name) => names.includes(name)));
844
+ }
845
+ /** The options the next token can use. See {@link NextToken.options}. */
846
+ usableOptions() {
847
+ const used = new Set(this.seenOptions.map(optionKey));
848
+ const { local, persistent } = this.scope.live(this.subcommandMatching);
849
+ const usable = [];
850
+ for (const { option, names } of [...local, ...persistent]) {
851
+ if (this.conflicts(option)) continue;
852
+ usable.push({
853
+ option,
854
+ forms: this.scope.forms(option, names),
855
+ used: used.has(optionKey(option))
856
+ });
857
+ }
858
+ return usable;
859
+ }
860
+ /**
861
+ * What the next token can be. See {@link readNext}. Reading more tokens
862
+ * doesn't change what this returns.
863
+ */
864
+ next() {
865
+ const open = this.openOption && this.hasRoomForValue ? this.openOption : null;
866
+ const command = this.scope.command;
867
+ const afterSeparator = this.argSeparatorIndex !== -1;
868
+ const optionsAllowed = !afterSeparator && this.optionsAllowed;
869
+ return {
870
+ path: [...this.path],
871
+ directives: { ...this.directives },
872
+ optionsAllowed,
873
+ subcommands: !afterSeparator && this.subcommandMatching ? this.scope.subcommands() : [],
874
+ options: optionsAllowed ? this.usableOptions() : [],
875
+ value: open && {
876
+ option: open,
877
+ arg: argAt(asArray(open.args), this.valueCount),
878
+ required: this.valueCount < this.valueMin
879
+ },
880
+ arg: open ? void 0 : argAt(asArray(command.args), this.args.length)
881
+ };
882
+ }
883
+ /** Whether the next token can be an option. Once options must stop, every token is an argument. */
884
+ get optionsAllowed() {
885
+ return !(this.directives.optionsMustPrecedeArguments && this.hasFoundArg);
886
+ }
887
+ read(token) {
888
+ if (this.argSeparatorIndex !== -1) return this.addArg(token);
889
+ const optionsAllowed = this.optionsAllowed;
890
+ if (token === "--" && optionsAllowed) return this.argSeparator();
891
+ const subcommand = this.subcommandFor(token);
892
+ if (subcommand?.command) return this.enterSubcommand(subcommand.command);
893
+ if (optionsAllowed) {
894
+ const flagsArePosixNoncompliant = this.directives.flagsArePosixNoncompliant;
895
+ const first = token.charCodeAt(0);
896
+ const isDoubleDash = first === 45 && token.charCodeAt(1) === 45;
897
+ if (!flagsArePosixNoncompliant && token.length > 1 && (first === 45 || first === 43) && !isDoubleDash) return this.shortOptions(token);
898
+ if (flagsArePosixNoncompliant || token.length > 2 && isDoubleDash) {
899
+ const reading = this.longOption(token);
900
+ if (reading) return reading;
901
+ }
902
+ }
903
+ return this.valueOrArg(token, subcommand);
904
+ }
905
+ };
906
+ /** The argument at `index`, reusing a final variadic argument. */
907
+ function argAt(args, index) {
908
+ if (index < args.length) return args[index];
909
+ const last = args[args.length - 1];
910
+ return last?.isVariadic ? last : void 0;
911
+ }
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
+ /**
920
+ * Say what the parser reads `token` as when it comes after `before`. Reads
921
+ * leniently, like {@link readNext}. To read every token of a line, use
922
+ * {@link tokenReader}.
923
+ */
924
+ function readToken(spec, before, token) {
925
+ const s = new Scanner(spec);
926
+ for (const earlier of before) s.read(earlier);
927
+ return s.read(token);
928
+ }
929
+ // src/testing.ts
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
+ };
951
+ /**
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`.
955
+ */
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], []);
969
+ }
970
+ /**
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.
973
+ */
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
+ });
66
979
  }
67
980
  function joinNames(names) {
68
981
  return makeArray(names).sort((a, b) => a.length - b.length).join("|");
69
982
  }
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
983
  /** Compares spec values by shape. Any two functions count as the same shape. */
75
984
  function isSameStructure(a, b) {
76
985
  if (a === b) return true;
@@ -84,6 +993,44 @@ function isSameStructure(a, b) {
84
993
  function namedArrayToString(...parts) {
85
994
  return `\`${parts.map((named) => joinNames(named.name)).join(" ")}\``;
86
995
  }
996
+ /** Name the last command on `path` in a message, with its index among its siblings. */
997
+ function describeCommand(path) {
998
+ const command = path[path.length - 1];
999
+ const parent = path[path.length - 2];
1000
+ if (!parent) return `Command ${namedArrayToString(command)}`;
1001
+ const index = makeArray(parent.subcommands).indexOf(command);
1002
+ return `Subcommand ${namedArrayToString(...path)} (index ${index})`;
1003
+ }
1004
+ /**
1005
+ * Name the option at `index` on the last command on `definedIn` in a message,
1006
+ * and the command it's used in when that isn't the one that defines it.
1007
+ */
1008
+ function describeOption(definedIn, index, usedIn = definedIn) {
1009
+ const option = makeArray(definedIn[definedIn.length - 1].options)[index];
1010
+ const name = `${namedArrayToString(...definedIn, option)} (index ${index})`;
1011
+ return usedIn === definedIn ? `Option ${name}` : `In ${namedArrayToString(...usedIn)}, option ${name}`;
1012
+ }
1013
+ /**
1014
+ * Run a function once for each option at each command it can be used in: the
1015
+ * command's own options, in definition order, then the persistent ones it
1016
+ * inherits. A check that is about how the option is written skips the uses
1017
+ * that aren't `isDefinedHere`. A check that depends on the scope sees the
1018
+ * option the way the parser does at each command.
1019
+ */
1020
+ function forEachOption(spec, fn) {
1021
+ forEachCommand(spec, ({ path, own, inherited, scope }) => {
1022
+ for (const { option, definedIn, index } of [...own, ...inherited]) {
1023
+ const where = describeOption(definedIn, index, path);
1024
+ fn({
1025
+ option,
1026
+ path,
1027
+ scope,
1028
+ isDefinedHere: definedIn === path,
1029
+ where
1030
+ });
1031
+ }
1032
+ });
1033
+ }
87
1034
  /** Asserts that required arguments do not come after an optional argument */
88
1035
  function assertRequiredArgumentsDoNotFollowOptionalArguments(spec) {
89
1036
  forEachArgArray(spec, (args, path) => {
@@ -96,130 +1043,134 @@ function assertRequiredArgumentsDoNotFollowOptionalArguments(spec) {
96
1043
  }
97
1044
  });
98
1045
  }
1046
+ /**
1047
+ * Asserts that the parser can use every default. A positional argument with a
1048
+ * default must be optional. A required option argument's default is only used
1049
+ * when the option is left out, so every argument before it needs a default
1050
+ * too, and the option can't be required.
1051
+ */
1052
+ function assertDefaultsCanBeUsed(spec) {
1053
+ forEachCommand(spec, ({ command, path }) => {
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`);
1055
+ });
1056
+ forEachOption(spec, ({ option, isDefinedHere, where }) => {
1057
+ if (!isDefinedHere) return;
1058
+ let firstWithout = null;
1059
+ for (const [index, arg] of makeArray(option.args).entries()) {
1060
+ if (arg.default === void 0) {
1061
+ firstWithout ??= index;
1062
+ continue;
1063
+ }
1064
+ if (arg.isOptional) continue;
1065
+ const name = `${where}, argument ${arg.name || index} has a default, but`;
1066
+ assert(!option.isRequired, `${name} the option is required, so the default is never used`);
1067
+ assert(firstWithout === null, `${name} the argument at index ${firstWithout} has none, so the default is never used`);
1068
+ }
1069
+ });
1070
+ }
99
1071
  /** Asserts that `isRepeatable` is only assigned a positive integer */
100
1072
  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
- }
1073
+ forEachOption(spec, ({ option, isDefinedHere, where }) => {
1074
+ if (!isDefinedHere || typeof option.isRepeatable !== "number") return;
1075
+ assert(Number.isSafeInteger(option.isRepeatable), `${where} has its \`isRepeatable\` value set to a number that is not an integer`);
1076
+ assert(!(option.isRepeatable < 1), `${where} has its \`isRepeatable\` value set to a number below 1, which means it can't be used at all`);
1077
+ assert(option.isRepeatable !== 1, `${where} has its \`isRepeatable\` value set to 1, which is no different that omitting the property entirely`);
107
1078
  });
108
1079
  }
109
1080
  /** Asserts that options have unique names in their scope */
110
1081
  function assertOptionsHaveLocallyUniqueNames(spec) {
111
- forEachSubcommand(spec, (spec, path) => {
1082
+ forEachCommand(spec, ({ path, own }) => {
112
1083
  const optionNames = /* @__PURE__ */ new Set();
113
- 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.`);
1084
+ for (const { option, index } of own) for (const name of makeArray(option.name)) {
1085
+ assert(!optionNames.has(name), `${describeOption(path, index)} has a non-unique name, '${name}'. Option names must be unique.`);
115
1086
  optionNames.add(name);
116
1087
  }
117
1088
  });
118
1089
  }
119
1090
  /** Asserts that options don't shadow a persistent option */
120
1091
  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.`);
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.`);
124
1094
  });
125
1095
  }
126
1096
  /**
127
- * Asserts that a subcommand's option isn't a different option with exactly the
128
- * same names as one on a parent.
1097
+ * Asserts that when a subcommand's option and a different option on a parent
1098
+ * share a name, each of them has a name of its own.
129
1099
  *
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.
1100
+ * Parsed values are looked up by name, and an option sets every one of its
1101
+ * names when it's used. If every name of the parent's option is also the
1102
+ * subcommand's, using the subcommand's option replaces the parent's value, so
1103
+ * it can't be read. If every name of the subcommand's option is also the
1104
+ * parent's, its names hold the parent's value when it isn't used, so it can't
1105
+ * tell whether it was. Options with the same names are fine when they're
1106
+ * identical, such as an option kept in a constant, because the parser treats
1107
+ * them as one option.
133
1108
  *
134
1109
  * This skips persistent parent options, because
135
1110
  * {@link assertOptionsDoNotShadowPersistentOptions} covers those and
136
1111
  * `allowShadowingPersistentOptions` opts out of it.
137
1112
  */
138
1113
  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.`);
1114
+ forEachOption(spec, ({ option, path, isDefinedHere, where }) => {
1115
+ if (!isDefinedHere) return;
1116
+ const names = makeArray(option.name);
1117
+ for (const parent of path.slice(0, -1)) for (const parentOption of makeArray(parent.options)) {
1118
+ if (parentOption.isPersistent) continue;
1119
+ const parentNames = makeArray(parentOption.name);
1120
+ const other = namedArrayToString(parent, parentOption);
1121
+ if (optionKey(parentOption) === optionKey(option)) {
1122
+ assert(isSameStructure({
1123
+ ...parentOption,
1124
+ name: 0
1125
+ }, {
1126
+ ...option,
1127
+ name: 0
1128
+ }), `${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.`);
1129
+ continue;
1130
+ }
1131
+ 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.`);
1132
+ 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.`);
1133
+ }
148
1134
  });
149
1135
  }
150
1136
  /** Asserts that subcommands don't share a name with a sibling subcommand */
151
1137
  function assertSubcommandsHaveLocallyUniqueNames(spec) {
152
- forEachSubcommand(spec, (spec, path) => {
1138
+ forEachCommand(spec, ({ command, path }) => {
153
1139
  const names = /* @__PURE__ */ new Set();
154
- 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)) {
155
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.`);
156
1142
  names.add(name);
157
1143
  }
158
1144
  });
159
1145
  }
160
1146
  /**
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
1147
  * Asserts that no option arg separator is an empty string
193
1148
  */
194
1149
  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
- }
1150
+ forEachCommand(spec, ({ command, path }) => {
1151
+ const separators = makeArray(command.parserDirectives?.optionArgSeparators);
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.`);
200
1153
  });
201
1154
  }
202
1155
  /**
203
- * Asserts that options named "+" or "-" takes one arg
1156
+ * Asserts that the parser reads options named "+" or "-" the way they're
1157
+ * written, which needs them to take one argument.
1158
+ *
1159
+ * @deprecated {@link assertNamesReadBack} checks this for every option.
204
1160
  */
205
1161
  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
- });
1162
+ assertOptionFormsReadBack(spec, (_, name) => name === "-" || name === "+");
213
1163
  }
214
1164
  /**
215
1165
  * Asserts that names do not have leading or trailing whitespace
216
1166
  */
217
1167
  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`);
1168
+ forEachOption(spec, ({ option, isDefinedHere, where }) => {
1169
+ if (!isDefinedHere) return;
1170
+ for (const name of makeArray(option.name)) assert(name === name.trim(), `${where} has a name with extra whitespace`);
220
1171
  });
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`);
1172
+ forEachCommand(spec, ({ command, path }) => {
1173
+ for (const name of makeArray(command.name)) assert(name === name.trim(), `${describeCommand(path)} has a name with extra whitespace`);
223
1174
  });
224
1175
  forEachArgArray(spec, (args, path) => {
225
1176
  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 +1181,11 @@ function assertNamesHaveNoExtraWhitespace(spec) {
230
1181
  */
231
1182
  function assertEverythingHasDescription(spec) {
232
1183
  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`);
1184
+ forEachOption(spec, ({ option, isDefinedHere, where }) => {
1185
+ if (isDefinedHere) assert(option.description, `${where} has no description`);
235
1186
  });
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`);
1187
+ forEachCommand(spec, ({ command, path }) => {
1188
+ assert(command.description, `${describeCommand(path)} has no description`);
238
1189
  });
239
1190
  }
240
1191
  /**
@@ -242,30 +1193,143 @@ function assertEverythingHasDescription(spec) {
242
1193
  */
243
1194
  function assertDescriptionLineLengthUnder69(spec) {
244
1195
  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`);
1196
+ forEachOption(spec, ({ option, isDefinedHere, where }) => {
1197
+ if (!isDefinedHere || !option.description) return;
1198
+ assert(option.description.split("\n").every((line) => line.length <= 68), `${where} has a description line over 68 characters`);
247
1199
  });
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`);
1200
+ forEachCommand(spec, ({ command, path }) => {
1201
+ if (command.description) assert(command.description.split("\n").every((line) => line.length <= 68), `${describeCommand(path)} has a description line over 68 characters`);
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`);
250
1203
  });
251
1204
  }
1205
+ /**
1206
+ * Asserts that the parser reads options with `requiresSeparator` the way
1207
+ * they're written, which needs them to take at most one argument.
1208
+ *
1209
+ * @deprecated {@link assertNamesReadBack} checks this for every option.
1210
+ */
252
1211
  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`);
1212
+ assertOptionFormsReadBack(spec, (option) => Boolean(option.requiresSeparator));
1213
+ }
1214
+ /**
1215
+ * What the parser read a token as, for a message. `path` is the command the
1216
+ * token came after.
1217
+ */
1218
+ function describeReading(reading, path) {
1219
+ switch (reading.kind) {
1220
+ case "separator": return "the end of options, '--'";
1221
+ case "subcommand": return `the subcommand ${namedArrayToString(...path, reading.command)}`;
1222
+ case "options": return reading.options.length === 1 ? `the option ${namedArrayToString(reading.options[0])}` : `the options ${reading.options.map((option) => namedArrayToString(option)).join(", ")}`;
1223
+ case "value": return `a value of the option ${namedArrayToString(reading.option)}`;
1224
+ case "argument": return "an argument";
1225
+ case "unknown": return "an option that doesn't exist";
1226
+ }
1227
+ }
1228
+ /** Whether the parser read a token as `option` alone. */
1229
+ function readsAs(reading, option) {
1230
+ return reading.kind === "options" && reading.options.length === 1 && reading.options[0] === option;
1231
+ }
1232
+ /**
1233
+ * The tokens that reach the last command on `path`, using each command's first
1234
+ * name. {@link assertNamesReadBack} checks subcommand names from the root down
1235
+ * before options, so each of these is a name the parser reads.
1236
+ */
1237
+ function tokensTo(path) {
1238
+ return path.slice(1).map((command) => makeArray(command.name)[0]);
1239
+ }
1240
+ /**
1241
+ * A value for writing options with at the last command on `path`. No name
1242
+ * there starts with it, or with it after '-' or '+', so the parser can only
1243
+ * read it as a value.
1244
+ */
1245
+ function placeholderValue(path, scope) {
1246
+ const names = [...[...scope.local, ...scope.persistent].flatMap((option) => makeArray(option.name)), ...makeArray(path[path.length - 1].subcommands).flatMap((command) => makeArray(command.name))];
1247
+ for (const value of "0123456789abcdefghijklmnopqrstuvwxyz") {
1248
+ const starts = [
1249
+ value,
1250
+ `-${value}`,
1251
+ `+${value}`
1252
+ ];
1253
+ if (!names.some((name) => starts.some((start) => name.startsWith(start)))) return value;
1254
+ }
1255
+ return "0";
1256
+ }
1257
+ /**
1258
+ * The tokens that write `form` with every value `option` takes, or one more
1259
+ * than its minimum if it takes any number. The first value is joined to the
1260
+ * name if the form says so, and the rest are separate tokens.
1261
+ */
1262
+ function writeForm(option, form, value) {
1263
+ const max = getMaxArgs(option.args);
1264
+ const count = Number.isFinite(max) ? max : getMinArgs(option.args) + 1;
1265
+ const values = Array.from({ length: count }, () => value);
1266
+ if (form.join === null) return [form.name, ...values];
1267
+ return [form.name + form.join + values[0], ...values.slice(1)];
1268
+ }
1269
+ /**
1270
+ * Asserts that the parser reads each name of the options that `include`
1271
+ * picks the way the name is written. See {@link assertNamesReadBack}.
1272
+ */
1273
+ function assertOptionFormsReadBack(spec, include) {
1274
+ const root = spec;
1275
+ forEachOption(spec, ({ option, where, scope, path }) => {
1276
+ const before = tokensTo(path);
1277
+ const value = placeholderValue(path, scope);
1278
+ const names = makeArray(option.name).filter((name) => scope.resolve(name) === option);
1279
+ for (const form of scope.forms(option, names)) {
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]);
1285
+ if (form.join !== "") {
1286
+ const reading = form.join === null ? first : readToken(root, before, form.name);
1287
+ assert(readsAs(reading, option), `${where} has the name '${form.name}', but the parser reads it as ${describeReading(reading, path)}`);
1288
+ }
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\`.`);
1291
+ if (form.join !== null) {
1292
+ const joined = first.kind === "options" && first.attached?.option === option;
1293
+ assert(readsAs(first, option) && joined, `${written}, but the parser reads '${tokens[0]}' as ${describeReading(first, path)}${joined ? "" : " without a value"}`);
1294
+ }
1295
+ for (let index = 1; index < tokens.length; index++) {
1296
+ const reading = reader.read(tokens[index]);
1297
+ assert(reading.kind === "value" && reading.option === option, `${written}, but in '${tokens.join(" ")}' the parser reads '${tokens[index]}' as ${describeReading(reading, path)}`);
1298
+ }
258
1299
  }
259
1300
  });
260
1301
  }
1302
+ /**
1303
+ * Asserts that the parser reads every name the way it's written: each name of
1304
+ * a subcommand enters that subcommand, and each name of an option uses that
1305
+ * option, at every command the option can be used in. Each option is also
1306
+ * written the way help shows it, with every value it takes, and the parser has
1307
+ * to read all of those values as the option's.
1308
+ */
1309
+ function assertNamesReadBack(spec) {
1310
+ const root = spec;
1311
+ forEachCommand(spec, ({ command, path }) => {
1312
+ const before = tokensTo(path);
1313
+ for (const subcommand of makeArray(command.subcommands)) for (const name of makeArray(subcommand.name)) {
1314
+ const reading = readToken(root, before, name);
1315
+ if (reading.kind === "subcommand" && reading.command === subcommand) continue;
1316
+ assert(false, `${describeCommand([...path, subcommand])} has the name '${name}', but the parser reads it as ${describeReading(reading, path)}`);
1317
+ }
1318
+ });
1319
+ assertOptionFormsReadBack(spec, () => true);
1320
+ }
261
1321
  function assertCommonOptionsArePersistent(spec) {
262
1322
  const failures = [];
263
- forEachSubcommand(spec, (subcommand, path) => {
264
- if (!subcommand.subcommands) return;
265
- for (const option of makeArray(subcommand.options)) {
266
- const allChildSubcommands = [];
267
- 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)]);
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) {
1331
+ const key = optionKey(option);
1332
+ if (!failures.some(([failed]) => failed === key)) failures.push([key, namedArrayToString(...path, option)]);
269
1333
  }
270
1334
  });
271
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.
@@ -274,29 +1338,23 @@ ${failures.map(([, line]) => " * " + line).join("\n")}
274
1338
  }
275
1339
  /**
276
1340
  * Asserts that references to other options by name actually refer to options
277
- * that exist in scope
1341
+ * that exist in scope, at every command the option can be used in
278
1342
  */
279
1343
  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
- }
1344
+ forEachOption(spec, ({ option, where, scope }) => {
1345
+ 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`);
1346
+ 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
1347
  });
287
1348
  }
288
1349
  /**
289
1350
  * Asserts that commands that will match subcommands by prefix do not take arguments
290
1351
  */
291
1352
  function assertPrefixMatchCommandsHaveNoArguments(spec) {
292
- forEachSubcommand(spec, (subcommand, path) => {
293
- if (!path.some((cmd) => cmd.parserDirectives?.subcommandsMatchUniquePrefix === true)) return;
294
- 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 }\``);
295
1356
  });
296
1357
  }
297
- function getName(namingStyle, names) {
298
- return names[namingStyle];
299
- }
300
1358
  async function runStep(context, name, fn) {
301
1359
  if (context) {
302
1360
  await context.step(name, fn);
@@ -313,118 +1371,106 @@ async function runStep(context, name, fn) {
313
1371
  */
314
1372
  function test(spec, options = {}) {
315
1373
  const { allowShadowingPersistentOptions = false, allowNoDescription = false, allowLongDescriptionLines = false, allowMatchingSubcommandPrefixAndArgs = false, namingStyle = "sentence" } = options;
1374
+ const checks = [
1375
+ [
1376
+ "Required arguments don't follow optional arguments",
1377
+ "assertRequiredArgumentsDoNotFollowOptionalArguments",
1378
+ assertRequiredArgumentsDoNotFollowOptionalArguments,
1379
+ true
1380
+ ],
1381
+ [
1382
+ "The parser can use every default",
1383
+ "assertDefaultsCanBeUsed",
1384
+ assertDefaultsCanBeUsed,
1385
+ true
1386
+ ],
1387
+ [
1388
+ "Repeatable options that are numbers are integers greater than 1",
1389
+ "assertRepeatableOptionsArePositiveIntegers",
1390
+ assertRepeatableOptionsArePositiveIntegers,
1391
+ true
1392
+ ],
1393
+ [
1394
+ "Options have locally unique names",
1395
+ "assertOptionsHaveLocallyUniqueNames",
1396
+ assertOptionsHaveLocallyUniqueNames,
1397
+ true
1398
+ ],
1399
+ [
1400
+ "Options don't shadow persistent options",
1401
+ "assertOptionsDoNotShadowPersistentOptions",
1402
+ assertOptionsDoNotShadowPersistentOptions,
1403
+ !allowShadowingPersistentOptions
1404
+ ],
1405
+ [
1406
+ "Different options on a subcommand and its parents have different names",
1407
+ "assertOptionsHaveDifferentNamesFromParents",
1408
+ assertOptionsHaveDifferentNamesFromParents,
1409
+ true
1410
+ ],
1411
+ [
1412
+ "Subcommands have locally unique names",
1413
+ "assertSubcommandsHaveLocallyUniqueNames",
1414
+ assertSubcommandsHaveLocallyUniqueNames,
1415
+ true
1416
+ ],
1417
+ [
1418
+ "The parser reads every option and subcommand the way it's written",
1419
+ "assertNamesReadBack",
1420
+ assertNamesReadBack,
1421
+ true
1422
+ ],
1423
+ [
1424
+ "Option arg separators are not an empty string",
1425
+ "assertOptionArgSeparatorsHaveCharacters",
1426
+ assertOptionArgSeparatorsHaveCharacters,
1427
+ true
1428
+ ],
1429
+ [
1430
+ "Names have no extra leading or trailing whitespace",
1431
+ "assertNamesHaveNoExtraWhitespace",
1432
+ assertNamesHaveNoExtraWhitespace,
1433
+ true
1434
+ ],
1435
+ [
1436
+ "Everything has a description",
1437
+ "assertEverythingHasDescription",
1438
+ assertEverythingHasDescription,
1439
+ !allowNoDescription
1440
+ ],
1441
+ [
1442
+ "Each line in descriptions is less than or equal to 68 characters",
1443
+ "assertDescriptionLineLengthUnder69",
1444
+ assertDescriptionLineLengthUnder69,
1445
+ !allowLongDescriptionLines
1446
+ ],
1447
+ [
1448
+ "Common options are not defined on every subcommand",
1449
+ "assertCommonOptionsArePersistent",
1450
+ assertCommonOptionsArePersistent,
1451
+ true
1452
+ ],
1453
+ [
1454
+ "References to other options by name are valid",
1455
+ "assertOptionNameReferencesExist",
1456
+ assertOptionNameReferencesExist,
1457
+ true
1458
+ ],
1459
+ [
1460
+ "Commands with `subcommandsMatchUniquePrefix` don't have arguments and subcommands",
1461
+ "assertPrefixMatchCommandsHaveNoArguments",
1462
+ assertPrefixMatchCommandsHaveNoArguments,
1463
+ !allowMatchingSubcommandPrefixAndArgs
1464
+ ]
1465
+ ];
316
1466
  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
- });
1467
+ for (const [sentence, functionName, check, enabled] of checks) {
1468
+ if (!enabled) continue;
1469
+ await runStep(t, namingStyle === "function" ? functionName : sentence, () => check(spec));
1470
+ }
425
1471
  };
426
1472
  }
427
1473
  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 };
1474
+ export { assertCommonOptionsArePersistent, assertDefaultsCanBeUsed, assertDescriptionLineLengthUnder69, assertEverythingHasDescription, assertNamesHaveNoExtraWhitespace, assertNamesReadBack, assertOptionArgSeparatorsHaveCharacters, assertOptionNameReferencesExist, assertOptionsDoNotShadowPersistentOptions, assertOptionsHaveDifferentNamesFromParents, assertOptionsHaveLocallyUniqueNames, assertPlusMinusOptionsTakeOneArg, assertPrefixMatchCommandsHaveNoArguments, assertRepeatableOptionsArePositiveIntegers, assertRequiredArgumentsDoNotFollowOptionalArguments, assertRequiresSeparatorTakesOneArg, assertSubcommandsHaveLocallyUniqueNames, test, testSpec };
429
1475
 
430
1476
  //# sourceMappingURL=testing.js.map