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