@clo4/cli 3.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +324 -312
- package/dist/index.js.map +1 -1
- package/dist/testing.js +414 -369
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
package/dist/testing.js
CHANGED
|
@@ -111,18 +111,62 @@ function applyDirectives(directives, command) {
|
|
|
111
111
|
if (typeof configured.optionsMustPrecedeArguments === "boolean") directives.optionsMustPrecedeArguments = configured.optionsMustPrecedeArguments;
|
|
112
112
|
if (typeof configured.flagsArePosixNoncompliant === "boolean") directives.flagsArePosixNoncompliant = configured.flagsArePosixNoncompliant;
|
|
113
113
|
}
|
|
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) ?? [];
|
|
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
|
+
};
|
|
114
157
|
// src/scope.ts
|
|
115
158
|
/**
|
|
116
|
-
*
|
|
159
|
+
* What a name means at one command on a path: the options, subcommands,
|
|
160
|
+
* directives and argument limits in effect there.
|
|
117
161
|
*
|
|
118
162
|
* Moving to a subcommand replaces the local options and adds its persistent
|
|
119
163
|
* options to the inherited ones. A name refers to one option: a local option
|
|
120
164
|
* wins over a persistent one, and a later option wins over an earlier one in
|
|
121
|
-
* the same list.
|
|
122
|
-
*
|
|
123
|
-
*
|
|
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.
|
|
124
168
|
*/
|
|
125
|
-
var
|
|
169
|
+
var CommandScope = class CommandScope {
|
|
126
170
|
/** Non-persistent options of the current command, in definition order. */
|
|
127
171
|
local = [];
|
|
128
172
|
/** Persistent options from every command on the path, in definition order. */
|
|
@@ -134,44 +178,89 @@ var OptionScope = class OptionScope {
|
|
|
134
178
|
inheritedSubcommands = [];
|
|
135
179
|
/** The directives in effect. A subcommand inherits each until it sets its own. */
|
|
136
180
|
directives = defaultDirectives();
|
|
137
|
-
|
|
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;
|
|
138
186
|
/** The scope at the last command on `path`. */
|
|
139
187
|
static at(path) {
|
|
140
|
-
const scope = new
|
|
188
|
+
const scope = new CommandScope();
|
|
141
189
|
for (const command of path) scope.enter(command);
|
|
142
190
|
return scope;
|
|
143
191
|
}
|
|
144
192
|
/** Move to `command`, a subcommand of the current one. */
|
|
145
193
|
enter(command) {
|
|
146
|
-
const parentSubcommands = this.
|
|
194
|
+
const parentSubcommands = this.current?.subcommands;
|
|
147
195
|
if (parentSubcommands) {
|
|
148
196
|
const persistent = parentSubcommands.filter((subcommand) => subcommand.isPersistent);
|
|
149
197
|
if (persistent.length > 0) this.inheritedSubcommands.unshift(...persistent);
|
|
150
198
|
}
|
|
151
|
-
this.
|
|
199
|
+
this.current = command;
|
|
200
|
+
this.argsMin = getMinArgs(command.args);
|
|
201
|
+
this.argsMax = getMaxArgs(command.args);
|
|
152
202
|
this.local.length = 0;
|
|
153
203
|
if (command.options) for (const option of command.options) (option.isPersistent ? this.persistent : this.local).push(option);
|
|
154
204
|
applyDirectives(this.directives, command);
|
|
155
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
|
+
}
|
|
156
244
|
/** The option `name` refers to, if any. */
|
|
157
245
|
resolve(name) {
|
|
158
246
|
const { local, persistent } = this;
|
|
159
|
-
for (let i = local.length - 1; i >= 0; i--) if (hasName
|
|
160
|
-
for (let i = persistent.length - 1; i >= 0; i--) if (hasName
|
|
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];
|
|
161
249
|
return null;
|
|
162
250
|
}
|
|
163
251
|
/**
|
|
164
252
|
* The options that can be used here, in definition order, each with only
|
|
165
253
|
* its live names: the names that still refer to it. Options with every name
|
|
166
|
-
* shadowed are left out.
|
|
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.
|
|
167
256
|
*/
|
|
168
|
-
live() {
|
|
257
|
+
live(subcommands = null) {
|
|
169
258
|
const owner = /* @__PURE__ */ new Map();
|
|
170
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);
|
|
171
260
|
const pick = (options) => {
|
|
172
261
|
const picked = [];
|
|
173
262
|
for (const option of options) {
|
|
174
|
-
const names = asArray(option.name).filter((name) => owner.get(name) === option);
|
|
263
|
+
const names = asArray(option.name).filter((name) => owner.get(name) === option && !(subcommands && this.matchSubcommand(name, subcommands.exactOnly).command));
|
|
175
264
|
if (names.length > 0) picked.push({
|
|
176
265
|
option,
|
|
177
266
|
names
|
|
@@ -184,9 +273,12 @@ var OptionScope = class OptionScope {
|
|
|
184
273
|
persistent: pick(this.persistent)
|
|
185
274
|
};
|
|
186
275
|
}
|
|
187
|
-
/**
|
|
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
|
+
*/
|
|
188
280
|
visible() {
|
|
189
|
-
const { local, persistent } = this.live();
|
|
281
|
+
const { local, persistent } = this.live(this.hasSubcommands ? { exactOnly: false } : null);
|
|
190
282
|
const shown = (visible) => !visible.option.hidden;
|
|
191
283
|
return {
|
|
192
284
|
local: local.filter(shown),
|
|
@@ -195,8 +287,9 @@ var OptionScope = class OptionScope {
|
|
|
195
287
|
}
|
|
196
288
|
/** The live names of each option a user could have meant, for "did you mean" suggestions. */
|
|
197
289
|
suggestionNames() {
|
|
198
|
-
const { local, persistent } = this.
|
|
199
|
-
|
|
290
|
+
const { local, persistent } = this.live();
|
|
291
|
+
const shown = (visible) => !visible.option.hidden;
|
|
292
|
+
return [...local, ...persistent].filter(shown).map((visible) => visible.names);
|
|
200
293
|
}
|
|
201
294
|
/** The options that must be used, local first. */
|
|
202
295
|
required() {
|
|
@@ -227,6 +320,46 @@ var OptionScope = class OptionScope {
|
|
|
227
320
|
if (typeof required === "string") return required;
|
|
228
321
|
return required ? this.directives.optionArgSeparators[0] ?? "=" : null;
|
|
229
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
|
+
}
|
|
230
363
|
};
|
|
231
364
|
const optionKeys = /* @__PURE__ */ new WeakMap();
|
|
232
365
|
/**
|
|
@@ -245,88 +378,14 @@ function optionKey(option) {
|
|
|
245
378
|
}
|
|
246
379
|
return key;
|
|
247
380
|
}
|
|
248
|
-
function hasName
|
|
381
|
+
function hasName(names, name) {
|
|
249
382
|
return typeof names === "string" ? names === name : names.includes(name);
|
|
250
383
|
}
|
|
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
384
|
/** The visible commands that `token` is a prefix of a name of, each listed once. */
|
|
284
385
|
function prefixMatches(subcommands, token) {
|
|
285
386
|
if (token === "") return [];
|
|
286
387
|
return subcommands.filter((candidate) => !candidate.hidden && asArray(candidate.name).some((name) => name.startsWith(token)));
|
|
287
388
|
}
|
|
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
389
|
// src/help.ts
|
|
331
390
|
/**
|
|
332
391
|
* Show an option's names, each written with its arguments the way `forms`
|
|
@@ -422,32 +481,9 @@ var MissingRequiredOption = class extends ParseError {
|
|
|
422
481
|
}
|
|
423
482
|
};
|
|
424
483
|
// src/parse.ts
|
|
425
|
-
/**
|
|
426
|
-
function
|
|
427
|
-
|
|
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;
|
|
484
|
+
/** How many times `option` can be used. */
|
|
485
|
+
function repeatLimit(option) {
|
|
486
|
+
return typeof option.isRepeatable === "number" ? option.isRepeatable : Infinity;
|
|
451
487
|
}
|
|
452
488
|
/**
|
|
453
489
|
* Throw if the options used break a `dependsOn` or `exclusiveOn` rule, or if
|
|
@@ -465,25 +501,21 @@ function checkOptionRules(options, seenOptions, required, context) {
|
|
|
465
501
|
for (const option of required) for (const name of asArray(option.name)) if (!options.has(name)) throw new MissingRequiredOption(name, context);
|
|
466
502
|
}
|
|
467
503
|
/**
|
|
468
|
-
* The state of reading one command line
|
|
469
|
-
* code that decides what each input token is.
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
* token.
|
|
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.
|
|
474
510
|
*/
|
|
475
511
|
var Scanner = class {
|
|
476
|
-
strict;
|
|
477
512
|
path;
|
|
478
513
|
optionAction = void 0;
|
|
479
514
|
args = [];
|
|
480
515
|
options = /* @__PURE__ */ new Map();
|
|
481
516
|
seenOptions = [];
|
|
482
517
|
argSeparatorIndex = -1;
|
|
483
|
-
scope = new
|
|
484
|
-
subcommands = void 0;
|
|
485
|
-
commandArgsMin = 0;
|
|
486
|
-
commandArgsMax = 0;
|
|
518
|
+
scope = new CommandScope();
|
|
487
519
|
directives = this.scope.directives;
|
|
488
520
|
hasFoundArg = false;
|
|
489
521
|
openOption = null;
|
|
@@ -496,40 +528,60 @@ var Scanner = class {
|
|
|
496
528
|
valuesMin = 0;
|
|
497
529
|
valuesMax = 0;
|
|
498
530
|
valueArgs = [];
|
|
499
|
-
|
|
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;
|
|
500
537
|
tooManyArgs = null;
|
|
501
538
|
repeatedUses = /* @__PURE__ */ new Map();
|
|
502
|
-
|
|
503
|
-
constructor(spec, strict) {
|
|
504
|
-
this.strict = strict;
|
|
539
|
+
constructor(spec) {
|
|
505
540
|
this.path = [spec];
|
|
506
|
-
this.
|
|
541
|
+
this.scope.enter(spec);
|
|
507
542
|
}
|
|
508
543
|
ctx() {
|
|
509
|
-
return { path: this.path };
|
|
544
|
+
return { path: [...this.path] };
|
|
510
545
|
}
|
|
511
546
|
/** Whether the open option can take the next token as a value. */
|
|
512
547
|
get hasRoomForValue() {
|
|
513
548
|
return this.valueCount < this.separateValueMax;
|
|
514
549
|
}
|
|
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
550
|
findOption(name) {
|
|
522
551
|
return this.scope.resolve(name);
|
|
523
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
|
+
}
|
|
524
571
|
unknownOption(option) {
|
|
525
|
-
|
|
572
|
+
if (!this.problem) {
|
|
573
|
+
const names = this.scope.suggestionNames();
|
|
574
|
+
this.fail((ctx) => new UnknownOption(option, names, ctx));
|
|
575
|
+
}
|
|
526
576
|
}
|
|
527
577
|
endOption() {
|
|
528
578
|
const values = this.values;
|
|
529
|
-
if (
|
|
579
|
+
if (values) {
|
|
530
580
|
const given = values.length - this.valuesStart;
|
|
531
|
-
if (given < this.valuesMin)
|
|
532
|
-
|
|
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);
|
|
533
585
|
}
|
|
534
586
|
this.values = null;
|
|
535
587
|
}
|
|
@@ -552,7 +604,6 @@ var Scanner = class {
|
|
|
552
604
|
const options = this.options;
|
|
553
605
|
let optionValues;
|
|
554
606
|
if (option.isRepeatable) {
|
|
555
|
-
const maxRepeat = option.isRepeatable === true ? Infinity : option.isRepeatable;
|
|
556
607
|
const key = optionKey(option);
|
|
557
608
|
let uses = this.repeatedUses.get(key);
|
|
558
609
|
if (!uses) {
|
|
@@ -562,7 +613,7 @@ var Scanner = class {
|
|
|
562
613
|
};
|
|
563
614
|
this.repeatedUses.set(key, uses);
|
|
564
615
|
}
|
|
565
|
-
if (
|
|
616
|
+
if (uses.count >= repeatLimit(option)) this.fail((ctx) => new ParseError(ctx, "Too many repetitions"));
|
|
566
617
|
uses.count += 1;
|
|
567
618
|
for (const other of names) options.set(other, uses.values);
|
|
568
619
|
if (!option.args) {
|
|
@@ -576,150 +627,162 @@ var Scanner = class {
|
|
|
576
627
|
}
|
|
577
628
|
this.valuesMin = this.valueMin;
|
|
578
629
|
this.valuesMax = getMaxArgs(option.args);
|
|
579
|
-
this.requiredSeparator = option.requiresSeparator ?? false;
|
|
580
630
|
if (this.valuesMax > 0) {
|
|
581
631
|
this.values = optionValues;
|
|
582
632
|
this.valuesStart = optionValues.length;
|
|
583
633
|
this.valueArgs = asArray(option.args);
|
|
584
634
|
}
|
|
585
635
|
}
|
|
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
|
-
*/
|
|
636
|
+
/** Add a value to the open option. Returns whether it's a value the option accepts. */
|
|
590
637
|
addValue(value, separator) {
|
|
591
638
|
this.valueCount += 1;
|
|
592
639
|
if (!this.values) {
|
|
593
|
-
|
|
640
|
+
const name = this.openOptionName;
|
|
641
|
+
this.fail((ctx) => new InvalidOptionArg(name, ctx));
|
|
594
642
|
return false;
|
|
595
643
|
}
|
|
596
|
-
const
|
|
597
|
-
|
|
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
|
-
}
|
|
644
|
+
const problem = this.scope.separatorProblem(this.openOption, this.openOptionName, separator);
|
|
645
|
+
if (problem) this.fail((ctx) => new ParseError(ctx, problem));
|
|
609
646
|
this.values.push(value);
|
|
610
|
-
return
|
|
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
|
+
};
|
|
611
656
|
}
|
|
612
657
|
/**
|
|
613
|
-
* Add a value attached to the option's own token,
|
|
614
|
-
*
|
|
615
|
-
*
|
|
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.
|
|
616
661
|
*/
|
|
617
|
-
addAttachedValue(
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
index,
|
|
621
|
-
start
|
|
622
|
-
};
|
|
662
|
+
addAttachedValue(start, value, separator) {
|
|
663
|
+
const option = this.openOption;
|
|
664
|
+
const accepted = this.addValue(value, separator);
|
|
623
665
|
this.valueMin = this.separateValueMax = 0;
|
|
666
|
+
return accepted ? {
|
|
667
|
+
option,
|
|
668
|
+
start
|
|
669
|
+
} : null;
|
|
624
670
|
}
|
|
625
671
|
/**
|
|
626
672
|
* Whether the command already has all the arguments it takes. A command that
|
|
627
673
|
* requires a subcommand takes none.
|
|
628
674
|
*/
|
|
629
675
|
get argsAreFull() {
|
|
630
|
-
|
|
631
|
-
return path[path.length - 1].requiresSubcommand === true || this.args.length >= this.commandArgsMax;
|
|
676
|
+
return this.scope.command.requiresSubcommand === true || this.args.length >= this.scope.argsMax;
|
|
632
677
|
}
|
|
633
678
|
addArg(value) {
|
|
634
679
|
this.endOption();
|
|
635
|
-
if (this.
|
|
680
|
+
if (this.argsAreFull) this.failArgs((ctx) => new TooManyArguments(value, ctx));
|
|
636
681
|
this.args.push(value);
|
|
682
|
+
return { kind: "argument" };
|
|
637
683
|
}
|
|
638
|
-
/** Read
|
|
639
|
-
argSeparator(
|
|
684
|
+
/** Read `--`. Every token after it is an argument. */
|
|
685
|
+
argSeparator() {
|
|
640
686
|
this.closeOption();
|
|
641
|
-
const command = this.
|
|
642
|
-
if (this.
|
|
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?"));
|
|
643
689
|
this.argSeparatorIndex = this.args.length;
|
|
644
|
-
|
|
690
|
+
return { kind: "separator" };
|
|
645
691
|
}
|
|
646
692
|
/**
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
* the next token as its value. A required or variadic option
|
|
650
|
-
* priority over a subcommand.
|
|
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.
|
|
651
699
|
*/
|
|
652
|
-
get
|
|
653
|
-
if (this.hasFoundArg) return
|
|
654
|
-
if (
|
|
655
|
-
return
|
|
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 };
|
|
656
704
|
}
|
|
657
705
|
/**
|
|
658
706
|
* The subcommand the next token enters if it's `token`, or why it enters
|
|
659
707
|
* none. Null if the next token can't be a subcommand at all.
|
|
660
708
|
*/
|
|
661
709
|
subcommandFor(token) {
|
|
662
|
-
|
|
663
|
-
return
|
|
710
|
+
const matching = this.subcommandMatching;
|
|
711
|
+
return matching && this.scope.matchSubcommand(token, matching.exactOnly);
|
|
664
712
|
}
|
|
665
713
|
enterSubcommand(command) {
|
|
666
714
|
this.closeOption();
|
|
667
715
|
this.path.push(command);
|
|
668
|
-
this.
|
|
716
|
+
this.scope.enter(command);
|
|
717
|
+
return {
|
|
718
|
+
kind: "subcommand",
|
|
719
|
+
command
|
|
720
|
+
};
|
|
669
721
|
}
|
|
670
722
|
/** Read a token like `-abc`, `+x` or `-ovalue`. */
|
|
671
|
-
shortOptions(token
|
|
723
|
+
shortOptions(token) {
|
|
672
724
|
const leadingChar = token[0];
|
|
673
725
|
const dashOption = this.findOption(leadingChar);
|
|
674
726
|
if (dashOption?.args && !this.findOption(token.slice(0, 2))) {
|
|
675
727
|
this.useOption(dashOption, leadingChar);
|
|
676
|
-
this.addAttachedValue(
|
|
677
|
-
return
|
|
728
|
+
const attached = this.addAttachedValue(1, token.slice(1), "");
|
|
729
|
+
return {
|
|
730
|
+
kind: "options",
|
|
731
|
+
options: [dashOption],
|
|
732
|
+
attached
|
|
733
|
+
};
|
|
678
734
|
}
|
|
735
|
+
const used = [];
|
|
736
|
+
let attached = null;
|
|
679
737
|
for (let char = 1; char < token.length; char++) {
|
|
680
738
|
const optionName = leadingChar + token[char];
|
|
681
739
|
const option = this.findOption(optionName);
|
|
682
740
|
if (!option) {
|
|
683
|
-
if (char === 1 && this.hasRoomForValue)
|
|
684
|
-
|
|
685
|
-
return;
|
|
686
|
-
}
|
|
687
|
-
if (this.strict) throw this.unknownOption(optionName);
|
|
741
|
+
if (char === 1 && this.hasRoomForValue) return this.value(token);
|
|
742
|
+
this.unknownOption(optionName);
|
|
688
743
|
continue;
|
|
689
744
|
}
|
|
690
745
|
this.useOption(option, optionName);
|
|
691
|
-
|
|
692
|
-
|
|
746
|
+
used.push(option);
|
|
747
|
+
if (char === token.length - 1) break;
|
|
748
|
+
const sep = this.scope.leadingSeparator(token.slice(char + 1));
|
|
693
749
|
if (option.args || sep && !this.findOption(leadingChar + token[char + 1])) {
|
|
694
750
|
const argStart = char + sep.length + 1;
|
|
695
|
-
this.addAttachedValue(
|
|
696
|
-
|
|
751
|
+
attached = this.addAttachedValue(argStart, token.slice(argStart), sep);
|
|
752
|
+
break;
|
|
697
753
|
}
|
|
698
754
|
}
|
|
755
|
+
return used.length > 0 ? {
|
|
756
|
+
kind: "options",
|
|
757
|
+
options: used,
|
|
758
|
+
attached
|
|
759
|
+
} : { kind: "unknown" };
|
|
699
760
|
}
|
|
700
761
|
/**
|
|
701
762
|
* Read a token like `--name` or `--name=value`, or any token when options
|
|
702
|
-
* aren't POSIX compliant. Returns
|
|
763
|
+
* aren't POSIX compliant. Returns null if the token should be an argument.
|
|
703
764
|
*/
|
|
704
|
-
longOption(token
|
|
705
|
-
const found = firstSeparator(token
|
|
765
|
+
longOption(token) {
|
|
766
|
+
const found = this.scope.firstSeparator(token);
|
|
706
767
|
const optionName = found ? token.slice(0, found.index) : token;
|
|
707
768
|
const option = this.findOption(optionName);
|
|
708
769
|
if (option) {
|
|
709
770
|
this.useOption(option, optionName);
|
|
771
|
+
let attached = null;
|
|
710
772
|
if (found) {
|
|
711
|
-
const valueStart = found.index + found.
|
|
712
|
-
this.addAttachedValue(
|
|
773
|
+
const valueStart = found.index + found.separator.length;
|
|
774
|
+
attached = this.addAttachedValue(valueStart, token.slice(valueStart), found.separator);
|
|
713
775
|
}
|
|
714
|
-
return
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
return true;
|
|
776
|
+
return {
|
|
777
|
+
kind: "options",
|
|
778
|
+
options: [option],
|
|
779
|
+
attached
|
|
780
|
+
};
|
|
720
781
|
}
|
|
721
|
-
if (this.
|
|
722
|
-
return
|
|
782
|
+
if (this.directives.flagsArePosixNoncompliant) return null;
|
|
783
|
+
if (this.hasRoomForValue) return this.value(token);
|
|
784
|
+
this.unknownOption(optionName);
|
|
785
|
+
return { kind: "unknown" };
|
|
723
786
|
}
|
|
724
787
|
/**
|
|
725
788
|
* Read a token that isn't `--`, a subcommand or an option. It's a value of
|
|
@@ -728,34 +791,39 @@ var Scanner = class {
|
|
|
728
791
|
* subcommand, if it could have been one.
|
|
729
792
|
*/
|
|
730
793
|
valueOrArg(token, unmatched) {
|
|
731
|
-
if (this.hasRoomForValue)
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
if (this.strict && unmatched && this.argsAreFull) {
|
|
736
|
-
const command = this.path[this.path.length - 1];
|
|
737
|
-
if (command.subcommands?.length || command.requiresSubcommand) {
|
|
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) => {
|
|
738
798
|
const { message, didYouMean } = unmatched.reason();
|
|
739
|
-
|
|
740
|
-
}
|
|
799
|
+
return new ParseError(ctx, message, didYouMean);
|
|
800
|
+
});
|
|
741
801
|
}
|
|
742
802
|
this.hasFoundArg = true;
|
|
743
|
-
this.addArg(token);
|
|
803
|
+
return this.addArg(token);
|
|
744
804
|
}
|
|
745
805
|
/**
|
|
746
|
-
* Throw if the finished command line is missing anything. Returns
|
|
747
|
-
*
|
|
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}.
|
|
748
808
|
*/
|
|
749
809
|
finish() {
|
|
750
|
-
const command = this.
|
|
810
|
+
const command = this.scope.command;
|
|
751
811
|
const showsUsage = command.requiresSubcommand && !command.action && this.args.length === 0;
|
|
752
812
|
const skipsChecks = this.optionAction !== void 0 || showsUsage;
|
|
753
|
-
if (!skipsChecks && this.tooManyArgs) throw this.tooManyArgs;
|
|
754
|
-
if (!skipsChecks && this.args.length < this.
|
|
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());
|
|
755
815
|
this.endOption();
|
|
816
|
+
if (this.problem) throw this.problem();
|
|
756
817
|
if (!skipsChecks) checkOptionRules(this.options, this.seenOptions, this.scope.required(), this.ctx());
|
|
757
|
-
if (!command.requiresSubcommand && this.args.length >= this.
|
|
758
|
-
return
|
|
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
|
+
};
|
|
759
827
|
}
|
|
760
828
|
/** See {@link ParseResult.defaults}. */
|
|
761
829
|
defaults(name) {
|
|
@@ -768,42 +836,41 @@ var Scanner = class {
|
|
|
768
836
|
}
|
|
769
837
|
/** Whether using `option` next would go over its repeat limit or break an `exclusiveOn` rule. */
|
|
770
838
|
conflicts(option) {
|
|
771
|
-
|
|
839
|
+
if ((this.repeatedUses.get(optionKey(option))?.count ?? 0) >= repeatLimit(option)) return true;
|
|
772
840
|
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
841
|
if (option.exclusiveOn?.some((name) => options.has(name))) return true;
|
|
842
|
+
const names = asArray(option.name);
|
|
777
843
|
return this.seenOptions.some((seen) => seen.exclusiveOn?.some((name) => names.includes(name)));
|
|
778
844
|
}
|
|
779
845
|
/** The options the next token can use. See {@link NextToken.options}. */
|
|
780
846
|
usableOptions() {
|
|
781
847
|
const used = new Set(this.seenOptions.map(optionKey));
|
|
782
|
-
const { local, persistent } = this.scope.live();
|
|
848
|
+
const { local, persistent } = this.scope.live(this.subcommandMatching);
|
|
783
849
|
const usable = [];
|
|
784
850
|
for (const { option, names } of [...local, ...persistent]) {
|
|
785
851
|
if (this.conflicts(option)) continue;
|
|
786
|
-
const typeable = names.filter((name) => !this.subcommandFor(name)?.command);
|
|
787
|
-
if (typeable.length === 0) continue;
|
|
788
852
|
usable.push({
|
|
789
853
|
option,
|
|
790
|
-
forms: this.scope.forms(option,
|
|
854
|
+
forms: this.scope.forms(option, names),
|
|
791
855
|
used: used.has(optionKey(option))
|
|
792
856
|
});
|
|
793
857
|
}
|
|
794
858
|
return usable;
|
|
795
859
|
}
|
|
796
|
-
/**
|
|
860
|
+
/**
|
|
861
|
+
* What the next token can be. See {@link readNext}. Reading more tokens
|
|
862
|
+
* doesn't change what this returns.
|
|
863
|
+
*/
|
|
797
864
|
next() {
|
|
798
865
|
const open = this.openOption && this.hasRoomForValue ? this.openOption : null;
|
|
799
|
-
const command = this.
|
|
866
|
+
const command = this.scope.command;
|
|
800
867
|
const afterSeparator = this.argSeparatorIndex !== -1;
|
|
801
868
|
const optionsAllowed = !afterSeparator && this.optionsAllowed;
|
|
802
869
|
return {
|
|
803
|
-
path: this.path,
|
|
804
|
-
directives: this.directives,
|
|
870
|
+
path: [...this.path],
|
|
871
|
+
directives: { ...this.directives },
|
|
805
872
|
optionsAllowed,
|
|
806
|
-
subcommands: !afterSeparator && this.
|
|
873
|
+
subcommands: !afterSeparator && this.subcommandMatching ? this.scope.subcommands() : [],
|
|
807
874
|
options: optionsAllowed ? this.usableOptions() : [],
|
|
808
875
|
value: open && {
|
|
809
876
|
option: open,
|
|
@@ -813,79 +880,27 @@ var Scanner = class {
|
|
|
813
880
|
arg: open ? void 0 : argAt(asArray(command.args), this.args.length)
|
|
814
881
|
};
|
|
815
882
|
}
|
|
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
883
|
/** Whether the next token can be an option. Once options must stop, every token is an argument. */
|
|
852
884
|
get optionsAllowed() {
|
|
853
885
|
return !(this.directives.optionsMustPrecedeArguments && this.hasFoundArg);
|
|
854
886
|
}
|
|
855
|
-
|
|
856
|
-
|
|
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];
|
|
887
|
+
read(token) {
|
|
888
|
+
if (this.argSeparatorIndex !== -1) return this.addArg(token);
|
|
865
889
|
const optionsAllowed = this.optionsAllowed;
|
|
866
|
-
if (token === "--" && optionsAllowed)
|
|
867
|
-
this.argSeparator(input, index);
|
|
868
|
-
return true;
|
|
869
|
-
}
|
|
890
|
+
if (token === "--" && optionsAllowed) return this.argSeparator();
|
|
870
891
|
const subcommand = this.subcommandFor(token);
|
|
871
|
-
if (subcommand?.command)
|
|
872
|
-
this.enterSubcommand(subcommand.command);
|
|
873
|
-
return false;
|
|
874
|
-
}
|
|
892
|
+
if (subcommand?.command) return this.enterSubcommand(subcommand.command);
|
|
875
893
|
if (optionsAllowed) {
|
|
876
894
|
const flagsArePosixNoncompliant = this.directives.flagsArePosixNoncompliant;
|
|
877
895
|
const first = token.charCodeAt(0);
|
|
878
896
|
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
|
-
}
|
|
897
|
+
if (!flagsArePosixNoncompliant && token.length > 1 && (first === 45 || first === 43) && !isDoubleDash) return this.shortOptions(token);
|
|
883
898
|
if (flagsArePosixNoncompliant || token.length > 2 && isDoubleDash) {
|
|
884
|
-
|
|
899
|
+
const reading = this.longOption(token);
|
|
900
|
+
if (reading) return reading;
|
|
885
901
|
}
|
|
886
902
|
}
|
|
887
|
-
this.valueOrArg(token, subcommand);
|
|
888
|
-
return false;
|
|
903
|
+
return this.valueOrArg(token, subcommand);
|
|
889
904
|
}
|
|
890
905
|
};
|
|
891
906
|
/** The argument at `index`, reusing a final variadic argument. */
|
|
@@ -895,36 +910,72 @@ function argAt(args, index) {
|
|
|
895
910
|
return last?.isVariadic ? last : void 0;
|
|
896
911
|
}
|
|
897
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
|
+
/**
|
|
898
920
|
* Say what the parser reads `token` as when it comes after `before`. Reads
|
|
899
|
-
* leniently, like {@link readNext}.
|
|
921
|
+
* leniently, like {@link readNext}. To read every token of a line, use
|
|
922
|
+
* {@link tokenReader}.
|
|
900
923
|
*/
|
|
901
924
|
function readToken(spec, before, token) {
|
|
902
|
-
const s = new Scanner(spec
|
|
903
|
-
s.read(
|
|
904
|
-
return s.
|
|
925
|
+
const s = new Scanner(spec);
|
|
926
|
+
for (const earlier of before) s.read(earlier);
|
|
927
|
+
return s.read(token);
|
|
905
928
|
}
|
|
906
929
|
// src/testing.ts
|
|
907
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
|
+
};
|
|
908
951
|
/**
|
|
909
|
-
* Run a function once for each
|
|
910
|
-
*
|
|
911
|
-
*
|
|
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`.
|
|
912
955
|
*/
|
|
913
|
-
function
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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], []);
|
|
918
969
|
}
|
|
919
970
|
/**
|
|
920
|
-
* Run a function once for each `
|
|
921
|
-
*
|
|
922
|
-
* This will also run the function for the root spec itself.
|
|
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.
|
|
923
973
|
*/
|
|
924
|
-
function
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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
|
+
});
|
|
928
979
|
}
|
|
929
980
|
function joinNames(names) {
|
|
930
981
|
return makeArray(names).sort((a, b) => a.length - b.length).join("|");
|
|
@@ -967,14 +1018,7 @@ function describeOption(definedIn, index, usedIn = definedIn) {
|
|
|
967
1018
|
* option the way the parser does at each command.
|
|
968
1019
|
*/
|
|
969
1020
|
function forEachOption(spec, fn) {
|
|
970
|
-
|
|
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
|
-
}));
|
|
1021
|
+
forEachCommand(spec, ({ path, own, inherited, scope }) => {
|
|
978
1022
|
for (const { option, definedIn, index } of [...own, ...inherited]) {
|
|
979
1023
|
const where = describeOption(definedIn, index, path);
|
|
980
1024
|
fn({
|
|
@@ -985,10 +1029,7 @@ function forEachOption(spec, fn) {
|
|
|
985
1029
|
where
|
|
986
1030
|
});
|
|
987
1031
|
}
|
|
988
|
-
|
|
989
|
-
for (const subcommand of makeArray(command.subcommands)) visit([...path, subcommand], next);
|
|
990
|
-
};
|
|
991
|
-
visit([spec], []);
|
|
1032
|
+
});
|
|
992
1033
|
}
|
|
993
1034
|
/** Asserts that required arguments do not come after an optional argument */
|
|
994
1035
|
function assertRequiredArgumentsDoNotFollowOptionalArguments(spec) {
|
|
@@ -1009,7 +1050,7 @@ function assertRequiredArgumentsDoNotFollowOptionalArguments(spec) {
|
|
|
1009
1050
|
* too, and the option can't be required.
|
|
1010
1051
|
*/
|
|
1011
1052
|
function assertDefaultsCanBeUsed(spec) {
|
|
1012
|
-
|
|
1053
|
+
forEachCommand(spec, ({ command, path }) => {
|
|
1013
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`);
|
|
1014
1055
|
});
|
|
1015
1056
|
forEachOption(spec, ({ option, isDefinedHere, where }) => {
|
|
@@ -1038,9 +1079,9 @@ function assertRepeatableOptionsArePositiveIntegers(spec) {
|
|
|
1038
1079
|
}
|
|
1039
1080
|
/** Asserts that options have unique names in their scope */
|
|
1040
1081
|
function assertOptionsHaveLocallyUniqueNames(spec) {
|
|
1041
|
-
|
|
1082
|
+
forEachCommand(spec, ({ path, own }) => {
|
|
1042
1083
|
const optionNames = /* @__PURE__ */ new Set();
|
|
1043
|
-
for (const
|
|
1084
|
+
for (const { option, index } of own) for (const name of makeArray(option.name)) {
|
|
1044
1085
|
assert(!optionNames.has(name), `${describeOption(path, index)} has a non-unique name, '${name}'. Option names must be unique.`);
|
|
1045
1086
|
optionNames.add(name);
|
|
1046
1087
|
}
|
|
@@ -1048,10 +1089,8 @@ function assertOptionsHaveLocallyUniqueNames(spec) {
|
|
|
1048
1089
|
}
|
|
1049
1090
|
/** Asserts that options don't shadow a persistent option */
|
|
1050
1091
|
function assertOptionsDoNotShadowPersistentOptions(spec) {
|
|
1051
|
-
|
|
1052
|
-
|
|
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.`);
|
|
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.`);
|
|
1055
1094
|
});
|
|
1056
1095
|
}
|
|
1057
1096
|
/**
|
|
@@ -1096,9 +1135,9 @@ function assertOptionsHaveDifferentNamesFromParents(spec) {
|
|
|
1096
1135
|
}
|
|
1097
1136
|
/** Asserts that subcommands don't share a name with a sibling subcommand */
|
|
1098
1137
|
function assertSubcommandsHaveLocallyUniqueNames(spec) {
|
|
1099
|
-
|
|
1138
|
+
forEachCommand(spec, ({ command, path }) => {
|
|
1100
1139
|
const names = /* @__PURE__ */ new Set();
|
|
1101
|
-
for (const [index, subcommand] of makeArray(
|
|
1140
|
+
for (const [index, subcommand] of makeArray(command.subcommands).entries()) for (const name of makeArray(subcommand.name)) {
|
|
1102
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.`);
|
|
1103
1142
|
names.add(name);
|
|
1104
1143
|
}
|
|
@@ -1108,7 +1147,7 @@ function assertSubcommandsHaveLocallyUniqueNames(spec) {
|
|
|
1108
1147
|
* Asserts that no option arg separator is an empty string
|
|
1109
1148
|
*/
|
|
1110
1149
|
function assertOptionArgSeparatorsHaveCharacters(spec) {
|
|
1111
|
-
|
|
1150
|
+
forEachCommand(spec, ({ command, path }) => {
|
|
1112
1151
|
const separators = makeArray(command.parserDirectives?.optionArgSeparators);
|
|
1113
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.`);
|
|
1114
1153
|
});
|
|
@@ -1130,7 +1169,7 @@ function assertNamesHaveNoExtraWhitespace(spec) {
|
|
|
1130
1169
|
if (!isDefinedHere) return;
|
|
1131
1170
|
for (const name of makeArray(option.name)) assert(name === name.trim(), `${where} has a name with extra whitespace`);
|
|
1132
1171
|
});
|
|
1133
|
-
|
|
1172
|
+
forEachCommand(spec, ({ command, path }) => {
|
|
1134
1173
|
for (const name of makeArray(command.name)) assert(name === name.trim(), `${describeCommand(path)} has a name with extra whitespace`);
|
|
1135
1174
|
});
|
|
1136
1175
|
forEachArgArray(spec, (args, path) => {
|
|
@@ -1145,7 +1184,7 @@ function assertEverythingHasDescription(spec) {
|
|
|
1145
1184
|
forEachOption(spec, ({ option, isDefinedHere, where }) => {
|
|
1146
1185
|
if (isDefinedHere) assert(option.description, `${where} has no description`);
|
|
1147
1186
|
});
|
|
1148
|
-
|
|
1187
|
+
forEachCommand(spec, ({ command, path }) => {
|
|
1149
1188
|
assert(command.description, `${describeCommand(path)} has no description`);
|
|
1150
1189
|
});
|
|
1151
1190
|
}
|
|
@@ -1158,7 +1197,7 @@ function assertDescriptionLineLengthUnder69(spec) {
|
|
|
1158
1197
|
if (!isDefinedHere || !option.description) return;
|
|
1159
1198
|
assert(option.description.split("\n").every((line) => line.length <= 68), `${where} has a description line over 68 characters`);
|
|
1160
1199
|
});
|
|
1161
|
-
|
|
1200
|
+
forEachCommand(spec, ({ command, path }) => {
|
|
1162
1201
|
if (command.description) assert(command.description.split("\n").every((line) => line.length <= 68), `${describeCommand(path)} has a description line over 68 characters`);
|
|
1163
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`);
|
|
1164
1203
|
});
|
|
@@ -1239,19 +1278,22 @@ function assertOptionFormsReadBack(spec, include) {
|
|
|
1239
1278
|
const names = makeArray(option.name).filter((name) => scope.resolve(name) === option);
|
|
1240
1279
|
for (const form of scope.forms(option, names)) {
|
|
1241
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]);
|
|
1242
1285
|
if (form.join !== "") {
|
|
1243
|
-
const reading = readToken(root, before, form.name);
|
|
1286
|
+
const reading = form.join === null ? first : readToken(root, before, form.name);
|
|
1244
1287
|
assert(readsAs(reading, option), `${where} has the name '${form.name}', but the parser reads it as ${describeReading(reading, path)}`);
|
|
1245
1288
|
}
|
|
1246
|
-
const tokens = writeForm(option, form, value);
|
|
1247
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\`.`);
|
|
1248
1291
|
if (form.join !== null) {
|
|
1249
|
-
const first = readToken(root, before, tokens[0]);
|
|
1250
1292
|
const joined = first.kind === "options" && first.attached?.option === option;
|
|
1251
1293
|
assert(readsAs(first, option) && joined, `${written}, but the parser reads '${tokens[0]}' as ${describeReading(first, path)}${joined ? "" : " without a value"}`);
|
|
1252
1294
|
}
|
|
1253
1295
|
for (let index = 1; index < tokens.length; index++) {
|
|
1254
|
-
const reading =
|
|
1296
|
+
const reading = reader.read(tokens[index]);
|
|
1255
1297
|
assert(reading.kind === "value" && reading.option === option, `${written}, but in '${tokens.join(" ")}' the parser reads '${tokens[index]}' as ${describeReading(reading, path)}`);
|
|
1256
1298
|
}
|
|
1257
1299
|
}
|
|
@@ -1266,7 +1308,7 @@ function assertOptionFormsReadBack(spec, include) {
|
|
|
1266
1308
|
*/
|
|
1267
1309
|
function assertNamesReadBack(spec) {
|
|
1268
1310
|
const root = spec;
|
|
1269
|
-
|
|
1311
|
+
forEachCommand(spec, ({ command, path }) => {
|
|
1270
1312
|
const before = tokensTo(path);
|
|
1271
1313
|
for (const subcommand of makeArray(command.subcommands)) for (const name of makeArray(subcommand.name)) {
|
|
1272
1314
|
const reading = readToken(root, before, name);
|
|
@@ -1278,13 +1320,16 @@ function assertNamesReadBack(spec) {
|
|
|
1278
1320
|
}
|
|
1279
1321
|
function assertCommonOptionsArePersistent(spec) {
|
|
1280
1322
|
const failures = [];
|
|
1281
|
-
|
|
1282
|
-
if (!
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
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) {
|
|
1286
1331
|
const key = optionKey(option);
|
|
1287
|
-
if (
|
|
1332
|
+
if (!failures.some(([failed]) => failed === key)) failures.push([key, namedArrayToString(...path, option)]);
|
|
1288
1333
|
}
|
|
1289
1334
|
});
|
|
1290
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.
|
|
@@ -1305,9 +1350,9 @@ function assertOptionNameReferencesExist(spec) {
|
|
|
1305
1350
|
* Asserts that commands that will match subcommands by prefix do not take arguments
|
|
1306
1351
|
*/
|
|
1307
1352
|
function assertPrefixMatchCommandsHaveNoArguments(spec) {
|
|
1308
|
-
|
|
1309
|
-
if (!
|
|
1310
|
-
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 }\``);
|
|
1311
1356
|
});
|
|
1312
1357
|
}
|
|
1313
1358
|
async function runStep(context, name, fn) {
|