@clo4/cli 1.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
@@ -1,5 +1,3 @@
1
- // \0rolldown/runtime.js
2
- var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod, mod.exports), cb = null), mod.exports);
3
1
  // src/util.ts
4
2
  function isArray(o) {
5
3
  return Array.isArray(o);
@@ -10,125 +8,64 @@ function makeArray(thing) {
10
8
  if (isArray(thing)) return thing.slice();
11
9
  return [thing];
12
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
+ }
13
21
  function assert(expr, msg = "") {
14
22
  if (!expr) throw new Error(msg);
15
23
  }
16
- (/* @__PURE__ */ __commonJSMin(((exports) => {
17
- exports.__esModule = true;
18
- exports.distance = exports.closest = void 0;
19
- var peq = /* @__PURE__ */ new Uint32Array(65536);
20
- var myers_32 = function(a, b) {
21
- var n = a.length;
22
- var m = b.length;
23
- var lst = 1 << n - 1;
24
- var pv = -1;
25
- var mv = 0;
26
- var sc = n;
27
- var i = n;
28
- while (i--) peq[a.charCodeAt(i)] |= 1 << i;
29
- for (i = 0; i < m; i++) {
30
- var eq = peq[b.charCodeAt(i)];
31
- var xv = eq | mv;
32
- eq |= (eq & pv) + pv ^ pv;
33
- mv |= ~(eq | pv);
34
- pv &= eq;
35
- if (mv & lst) sc++;
36
- if (pv & lst) sc--;
37
- mv = mv << 1 | 1;
38
- pv = pv << 1 | ~(xv | mv);
39
- mv &= xv;
40
- }
41
- i = n;
42
- while (i--) peq[a.charCodeAt(i)] = 0;
43
- return sc;
44
- };
45
- var myers_x = function(b, a) {
46
- var n = a.length;
47
- var m = b.length;
48
- var mhc = [];
49
- var phc = [];
50
- var hsize = Math.ceil(n / 32);
51
- var vsize = Math.ceil(m / 32);
52
- for (var i = 0; i < hsize; i++) {
53
- phc[i] = -1;
54
- mhc[i] = 0;
55
- }
56
- var j = 0;
57
- for (; j < vsize - 1; j++) {
58
- var mv_1 = 0;
59
- var pv_1 = -1;
60
- var start_1 = j * 32;
61
- var vlen_1 = Math.min(32, m) + start_1;
62
- for (var k = start_1; k < vlen_1; k++) peq[b.charCodeAt(k)] |= 1 << k;
63
- for (var i = 0; i < n; i++) {
64
- var eq = peq[a.charCodeAt(i)];
65
- var pb = phc[i / 32 | 0] >>> i & 1;
66
- var mb = mhc[i / 32 | 0] >>> i & 1;
67
- var xv = eq | mv_1;
68
- var xh = ((eq | mb) & pv_1) + pv_1 ^ pv_1 | eq | mb;
69
- var ph = mv_1 | ~(xh | pv_1);
70
- var mh = pv_1 & xh;
71
- if (ph >>> 31 ^ pb) phc[i / 32 | 0] ^= 1 << i;
72
- if (mh >>> 31 ^ mb) mhc[i / 32 | 0] ^= 1 << i;
73
- ph = ph << 1 | pb;
74
- mh = mh << 1 | mb;
75
- pv_1 = mh | ~(xv | ph);
76
- mv_1 = ph & xv;
77
- }
78
- for (var k = start_1; k < vlen_1; k++) peq[b.charCodeAt(k)] = 0;
79
- }
80
- var mv = 0;
81
- var pv = -1;
82
- var start = j * 32;
83
- var vlen = Math.min(32, m - start) + start;
84
- for (var k = start; k < vlen; k++) peq[b.charCodeAt(k)] |= 1 << k;
85
- var score = m;
86
- for (var i = 0; i < n; i++) {
87
- var eq = peq[a.charCodeAt(i)];
88
- var pb = phc[i / 32 | 0] >>> i & 1;
89
- var mb = mhc[i / 32 | 0] >>> i & 1;
90
- var xv = eq | mv;
91
- var xh = ((eq | mb) & pv) + pv ^ pv | eq | mb;
92
- var ph = mv | ~(xh | pv);
93
- var mh = pv & xh;
94
- score += ph >>> m - 1 & 1;
95
- score -= mh >>> m - 1 & 1;
96
- if (ph >>> 31 ^ pb) phc[i / 32 | 0] ^= 1 << i;
97
- if (mh >>> 31 ^ mb) mhc[i / 32 | 0] ^= 1 << i;
98
- ph = ph << 1 | pb;
99
- mh = mh << 1 | mb;
100
- pv = mh | ~(xv | ph);
101
- mv = ph & xv;
102
- }
103
- for (var k = start; k < vlen; k++) peq[b.charCodeAt(k)] = 0;
104
- return score;
105
- };
106
- var distance = function(a, b) {
107
- if (a.length < b.length) {
108
- var tmp = b;
109
- b = a;
110
- a = tmp;
111
- }
112
- if (b.length === 0) return a.length;
113
- if (a.length <= 32) return myers_32(a, b);
114
- return myers_x(a, b);
115
- };
116
- exports.distance = distance;
117
- var closest = function(str, arr) {
118
- var min_distance = Infinity;
119
- var min_index = 0;
120
- for (var i = 0; i < arr.length; i++) {
121
- var dist = distance(str, arr[i]);
122
- if (dist < min_distance) {
123
- min_distance = dist;
124
- min_index = i;
125
- }
126
- }
127
- return arr[min_index];
128
- };
129
- exports.closest = closest;
130
- })))();
131
- // src/parse.ts
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
+ }
132
69
  /**
133
70
  * Returns the minimum number of arguments that satisfies the args.
134
71
  *
@@ -152,6 +89,820 @@ function getMaxArgs(args) {
152
89
  for (let i = 0; i < args.length; i++) if (args[args.length - 1 - i].isVariadic) return Infinity;
153
90
  return args.length;
154
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
+ }
155
906
  // src/testing.ts
156
907
  const repr = JSON.stringify;
157
908
  /**
@@ -165,12 +916,6 @@ function forEachArgArray(spec, fn, path = []) {
165
916
  for (const option of makeArray(spec.options)) fn(makeArray(option.args), path);
166
917
  for (const subcommand of makeArray(spec.subcommands)) forEachArgArray(subcommand, fn, path);
167
918
  }
168
- /** Run a function once for each `options` property in the spec (recursive) */
169
- function forEachOptionArray(spec, fn, path = []) {
170
- path = path.concat(spec);
171
- fn(makeArray(spec.options), path);
172
- for (const subcommand of makeArray(spec.subcommands)) forEachOptionArray(subcommand, fn, path);
173
- }
174
919
  /**
175
920
  * Run a function once for each `subcommands` property in the spec (recursive)
176
921
  *
@@ -184,9 +929,67 @@ function forEachSubcommand(spec, fn, path = []) {
184
929
  function joinNames(names) {
185
930
  return makeArray(names).sort((a, b) => a.length - b.length).join("|");
186
931
  }
932
+ /** Compares spec values by shape. Any two functions count as the same shape. */
933
+ function isSameStructure(a, b) {
934
+ if (a === b) return true;
935
+ if (typeof a === "function" && typeof b === "function") return true;
936
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
937
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
938
+ const left = a;
939
+ const right = b;
940
+ return [.../* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)])].every((key) => isSameStructure(left[key], right[key]));
941
+ }
187
942
  function namedArrayToString(...parts) {
188
943
  return `\`${parts.map((named) => joinNames(named.name)).join(" ")}\``;
189
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
+ }
190
993
  /** Asserts that required arguments do not come after an optional argument */
191
994
  function assertRequiredArgumentsDoNotFollowOptionalArguments(spec) {
192
995
  forEachArgArray(spec, (args, path) => {
@@ -199,20 +1002,38 @@ function assertRequiredArgumentsDoNotFollowOptionalArguments(spec) {
199
1002
  }
200
1003
  });
201
1004
  }
202
- /** Asserts that options with `isRepeatable: true` don't have arguments */
203
- function assertRepeatableOptionsHaveNoArguments(spec) {
204
- forEachOptionArray(spec, (options, path) => {
205
- for (const [index, option] of options.entries()) assert(!(option.args && option.isRepeatable), `The option ${namedArrayToString(...path, option)} (index ${index}) has arguments and is repeatable. Repeatable options cannot have arguments, but you can use a variadic argument instead.`);
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
+ }
206
1028
  });
207
1029
  }
208
1030
  /** Asserts that `isRepeatable` is only assigned a positive integer */
209
1031
  function assertRepeatableOptionsArePositiveIntegers(spec) {
210
- forEachOptionArray(spec, (options, path) => {
211
- for (const [index, option] of options.entries()) if (typeof option.isRepeatable === "number") {
212
- 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`);
213
- 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`);
214
- 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`);
215
- }
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`);
216
1037
  });
217
1038
  }
218
1039
  /** Asserts that options have unique names in their scope */
@@ -220,16 +1041,57 @@ function assertOptionsHaveLocallyUniqueNames(spec) {
220
1041
  forEachSubcommand(spec, (spec, path) => {
221
1042
  const optionNames = /* @__PURE__ */ new Set();
222
1043
  for (const [index, option] of makeArray(spec.options).entries()) for (const name of makeArray(option.name)) {
223
- 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.`);
224
1045
  optionNames.add(name);
225
1046
  }
226
1047
  });
227
1048
  }
228
1049
  /** Asserts that options don't shadow a persistent option */
229
1050
  function assertOptionsDoNotShadowPersistentOptions(spec) {
230
- forEachOptionArray(spec, (options, path) => {
231
- const persistentOptionNames = new Set(path.slice(0, -1).flatMap((subcommand) => subcommand.options ? subcommand.options.filter((option) => option.isPersistent).flatMap((option) => option.name) : []));
232
- 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.`);
1055
+ });
1056
+ }
1057
+ /**
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.
1060
+ *
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.
1069
+ *
1070
+ * This skips persistent parent options, because
1071
+ * {@link assertOptionsDoNotShadowPersistentOptions} covers those and
1072
+ * `allowShadowingPersistentOptions` opts out of it.
1073
+ */
1074
+ function assertOptionsHaveDifferentNamesFromParents(spec) {
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
+ }
233
1095
  });
234
1096
  }
235
1097
  /** Asserts that subcommands don't share a name with a sibling subcommand */
@@ -243,68 +1105,33 @@ function assertSubcommandsHaveLocallyUniqueNames(spec) {
243
1105
  });
244
1106
  }
245
1107
  /**
246
- * Asserts that option names starting with a single dash aren't longer
247
- * than two characters, unless parserDirectives.flagsArePosixNoncompliant is
248
- * set on an ancestor command.
249
- */
250
- function assertLongOptionNamesDoNotStartWithSingleDash(spec) {
251
- forEachOptionArray(spec, (options, path) => {
252
- if (path.map((command) => command.parserDirectives?.flagsArePosixNoncompliant).filter((value) => value !== void 0).at(-1) || false) return;
253
- 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.`);
254
- });
255
- }
256
- /**
257
- * Asserts that all option names start with dashes
258
- */
259
- function assertOptionNamesStartWithDashes(spec) {
260
- forEachOptionArray(spec, (options, path) => {
261
- if (path.map((command) => command.parserDirectives?.flagsArePosixNoncompliant).filter((value) => value !== void 0).at(-1) || false) return;
262
- 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`);
263
- });
264
- }
265
- /**
266
- * Asserts that nothing is named exactly '--'
267
- */
268
- function assertNothingIsNamedDashDash(spec) {
269
- forEachOptionArray(spec, (options, path) => {
270
- 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`);
271
- });
272
- forEachSubcommand(spec, (subcommands, path) => {
273
- 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`);
274
- });
275
- }
276
- /**
277
1108
  * Asserts that no option arg separator is an empty string
278
1109
  */
279
1110
  function assertOptionArgSeparatorsHaveCharacters(spec) {
280
- forEachSubcommand(spec, (subcommands, path) => {
281
- for (const [index, subcommand] of makeArray(subcommands).entries()) {
282
- const separators = makeArray(subcommand.parserDirectives?.optionArgSeparators);
283
- 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.`);
284
- }
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.`);
285
1114
  });
286
1115
  }
287
1116
  /**
288
- * 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.
289
1121
  */
290
1122
  function assertPlusMinusOptionsTakeOneArg(spec) {
291
- forEachOptionArray(spec, (options, path) => {
292
- if (path.map((command) => command.parserDirectives?.flagsArePosixNoncompliant).filter((value) => value !== void 0).at(-1) || false) return;
293
- for (const [index, option] of makeArray(options).entries()) for (const name of makeArray(option.name)) {
294
- if (name !== "-" && name !== "+") continue;
295
- assert(option.args && !Array.isArray(option.args), `Option ${namedArrayToString(...path, option)} (index ${index}) must take exactly one argument`);
296
- }
297
- });
1123
+ assertOptionFormsReadBack(spec, (_, name) => name === "-" || name === "+");
298
1124
  }
299
1125
  /**
300
1126
  * Asserts that names do not have leading or trailing whitespace
301
1127
  */
302
1128
  function assertNamesHaveNoExtraWhitespace(spec) {
303
- forEachOptionArray(spec, (options, path) => {
304
- 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`);
305
1132
  });
306
- forEachSubcommand(spec, (subcommands, path) => {
307
- 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`);
308
1135
  });
309
1136
  forEachArgArray(spec, (args, path) => {
310
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`);
@@ -315,11 +1142,11 @@ function assertNamesHaveNoExtraWhitespace(spec) {
315
1142
  */
316
1143
  function assertEverythingHasDescription(spec) {
317
1144
  assert(spec.description, `Spec has no description`);
318
- forEachOptionArray(spec, (options, path) => {
319
- 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`);
320
1147
  });
321
- forEachSubcommand(spec, (subcommands, path) => {
322
- 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`);
323
1150
  });
324
1151
  }
325
1152
  /**
@@ -327,34 +1154,137 @@ function assertEverythingHasDescription(spec) {
327
1154
  */
328
1155
  function assertDescriptionLineLengthUnder69(spec) {
329
1156
  if (spec.description) assert(spec.description.split("\n").every((line) => line.length <= 68), `Spec has a description line over 68 characters`);
330
- forEachOptionArray(spec, (options, path) => {
331
- 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`);
332
1160
  });
333
- forEachSubcommand(spec, (subcommands, path) => {
334
- 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`);
335
1164
  });
336
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
+ */
337
1172
  function assertRequiresSeparatorTakesOneArg(spec) {
338
- forEachOptionArray(spec, (options, path) => {
339
- for (const [index, option] of makeArray(options).entries()) if (option.requiresSeparator) {
340
- const args = makeArray(option.args);
341
- 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`);
342
- 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
+ }
1257
+ }
1258
+ });
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)}`);
343
1275
  }
344
1276
  });
1277
+ assertOptionFormsReadBack(spec, () => true);
345
1278
  }
346
1279
  function assertCommonOptionsArePersistent(spec) {
347
- const isSameName = (a, b) => {
348
- if (isArray(a) && isArray(b)) return a.every((item) => b.includes(item)) && b.every((item) => a.includes(item));
349
- return a === b;
350
- };
351
1280
  const failures = [];
352
1281
  forEachSubcommand(spec, (subcommand, path) => {
353
1282
  if (!subcommand.subcommands) return;
354
1283
  for (const option of makeArray(subcommand.options)) {
355
1284
  const allChildSubcommands = [];
356
1285
  forEachSubcommand(subcommand, (cmd) => allChildSubcommands.push(cmd));
357
- 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)]);
358
1288
  }
359
1289
  });
360
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.
@@ -363,15 +1293,12 @@ ${failures.map(([, line]) => " * " + line).join("\n")}
363
1293
  }
364
1294
  /**
365
1295
  * Asserts that references to other options by name actually refer to options
366
- * that exist in scope
1296
+ * that exist in scope, at every command the option can be used in
367
1297
  */
368
1298
  function assertOptionNameReferencesExist(spec) {
369
- forEachOptionArray(spec, (options, path) => {
370
- 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)]);
371
- for (const [index, option] of makeArray(options).entries()) {
372
- 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`);
373
- 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`);
374
- }
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`);
375
1302
  });
376
1303
  }
377
1304
  /**
@@ -379,13 +1306,10 @@ function assertOptionNameReferencesExist(spec) {
379
1306
  */
380
1307
  function assertPrefixMatchCommandsHaveNoArguments(spec) {
381
1308
  forEachSubcommand(spec, (subcommand, path) => {
382
- if (!path.some((cmd) => cmd.parserDirectives?.subcommandsMatchUniquePrefix === true)) return;
1309
+ if (!OptionScope.at(path).directives.subcommandsMatchUniquePrefix) return;
383
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 }\``);
384
1311
  });
385
1312
  }
386
- function getName(namingStyle, names) {
387
- return names[namingStyle];
388
- }
389
1313
  async function runStep(context, name, fn) {
390
1314
  if (context) {
391
1315
  await context.step(name, fn);
@@ -402,118 +1326,106 @@ async function runStep(context, name, fn) {
402
1326
  */
403
1327
  function test(spec, options = {}) {
404
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
+ ];
405
1421
  return async (t) => {
406
- await runStep(t, getName(namingStyle, {
407
- sentence: "Required arguments don't follow optional arguments",
408
- function: "assertRequiredArgumentsDoNotFollowOptionalArguments"
409
- }), () => {
410
- assertRequiredArgumentsDoNotFollowOptionalArguments(spec);
411
- });
412
- await runStep(t, getName(namingStyle, {
413
- sentence: "Repeatable options don't have arguments",
414
- function: "assertRepeatableOptionsHaveNoArguments"
415
- }), () => {
416
- assertRepeatableOptionsHaveNoArguments(spec);
417
- });
418
- await runStep(t, getName(namingStyle, {
419
- sentence: "Repeatable options that are numbers are integers greater than 1",
420
- function: "assertRepeatableOptionsArePositiveIntegers"
421
- }), () => {
422
- assertRepeatableOptionsArePositiveIntegers(spec);
423
- });
424
- await runStep(t, getName(namingStyle, {
425
- sentence: "Long option names don't start with a single dash",
426
- function: "assertLongOptionNamesDoNotStartWithSingleDash"
427
- }), () => {
428
- assertLongOptionNamesDoNotStartWithSingleDash(spec);
429
- });
430
- await runStep(t, getName(namingStyle, {
431
- sentence: "Option names all start with dashes",
432
- function: "assertOptionNamesStartWithDashes"
433
- }), () => {
434
- assertOptionNamesStartWithDashes(spec);
435
- });
436
- await runStep(t, getName(namingStyle, {
437
- sentence: "Options have locally unique names",
438
- function: "assertOptionsHaveLocallyUniqueNames"
439
- }), () => {
440
- assertOptionsHaveLocallyUniqueNames(spec);
441
- });
442
- if (!allowShadowingPersistentOptions) await runStep(t, getName(namingStyle, {
443
- sentence: "Options don't shadow persistent options",
444
- function: "assertOptionsDoNotShadowPersistentOptions"
445
- }), () => {
446
- assertOptionsDoNotShadowPersistentOptions(spec);
447
- });
448
- await runStep(t, getName(namingStyle, {
449
- sentence: "Subcommands have locally unique names",
450
- function: "assertSubcommandsHaveLocallyUniqueNames"
451
- }), () => {
452
- assertSubcommandsHaveLocallyUniqueNames(spec);
453
- });
454
- await runStep(t, getName(namingStyle, {
455
- sentence: "Nothing is named '--'",
456
- function: "assertNothingIsNamedDashDash"
457
- }), () => {
458
- assertNothingIsNamedDashDash(spec);
459
- });
460
- await runStep(t, getName(namingStyle, {
461
- sentence: "Option arg separators are not an empty string",
462
- function: "assertOptionArgSeparatorsHaveCharacters"
463
- }), () => {
464
- assertOptionArgSeparatorsHaveCharacters(spec);
465
- });
466
- await runStep(t, getName(namingStyle, {
467
- sentence: "Options named '-' or '+' take exactly one argument",
468
- function: "assertPlusMinusOptionsTakeOneArg"
469
- }), () => {
470
- assertPlusMinusOptionsTakeOneArg(spec);
471
- });
472
- await runStep(t, getName(namingStyle, {
473
- sentence: "Names have no extra leading or trailing whitespace",
474
- function: "assertNamesHaveNoExtraWhitespace"
475
- }), () => {
476
- assertNamesHaveNoExtraWhitespace(spec);
477
- });
478
- if (!allowNoDescription) await runStep(t, getName(namingStyle, {
479
- sentence: "Everything has a description",
480
- function: "assertEverythingHasDescription"
481
- }), () => {
482
- assertEverythingHasDescription(spec);
483
- });
484
- if (!allowLongDescriptionLines) await runStep(t, getName(namingStyle, {
485
- sentence: "Each line in descriptions is less than or equal to 68 characters",
486
- function: "assertDescriptionLineLengthUnder69"
487
- }), () => {
488
- assertDescriptionLineLengthUnder69(spec);
489
- });
490
- await runStep(t, getName(namingStyle, {
491
- sentence: "Options with `requiresSeparator` take exactly one argument",
492
- function: "assertRequiresSeparatorTakesOneArg"
493
- }), () => {
494
- assertRequiresSeparatorTakesOneArg(spec);
495
- });
496
- await runStep(t, getName(namingStyle, {
497
- sentence: "Common options are not defined on every subcommand",
498
- function: "assertCommonOptionsArePersistent"
499
- }), () => {
500
- assertCommonOptionsArePersistent(spec);
501
- });
502
- await runStep(t, getName(namingStyle, {
503
- sentence: "References to other options by name are valid",
504
- function: "assertOptionNameReferencesExist"
505
- }), () => {
506
- assertOptionNameReferencesExist(spec);
507
- });
508
- if (!allowMatchingSubcommandPrefixAndArgs) await runStep(t, getName(namingStyle, {
509
- sentence: "Commands with `subcommandsMatchUniquePrefix` don't have arguments and subcommands",
510
- function: "assertPrefixMatchCommandsHaveNoArguments"
511
- }), () => {
512
- assertPrefixMatchCommandsHaveNoArguments(spec);
513
- });
1422
+ for (const [sentence, functionName, check, enabled] of checks) {
1423
+ if (!enabled) continue;
1424
+ await runStep(t, namingStyle === "function" ? functionName : sentence, () => check(spec));
1425
+ }
514
1426
  };
515
1427
  }
516
1428
  const testSpec = test;
517
- export { assertCommonOptionsArePersistent, assertDescriptionLineLengthUnder69, assertEverythingHasDescription, assertLongOptionNamesDoNotStartWithSingleDash, assertNamesHaveNoExtraWhitespace, assertNothingIsNamedDashDash, assertOptionArgSeparatorsHaveCharacters, assertOptionNameReferencesExist, assertOptionNamesStartWithDashes, assertOptionsDoNotShadowPersistentOptions, assertOptionsHaveLocallyUniqueNames, assertPlusMinusOptionsTakeOneArg, assertPrefixMatchCommandsHaveNoArguments, assertRepeatableOptionsArePositiveIntegers, assertRepeatableOptionsHaveNoArguments, 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 };
518
1430
 
519
1431
  //# sourceMappingURL=testing.js.map