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