@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/index.js
CHANGED
|
@@ -56,9 +56,21 @@ const output = { write: writeFd };
|
|
|
56
56
|
function print(text) {
|
|
57
57
|
output.write(1, `${text.trimEnd()}\n`);
|
|
58
58
|
}
|
|
59
|
-
/** Print
|
|
60
|
-
function
|
|
61
|
-
output.write(2,
|
|
59
|
+
/** Print `Error:` and the given values to stderr, separated by spaces. */
|
|
60
|
+
function printError(...strings) {
|
|
61
|
+
output.write(2, `${["Error:", ...strings].map(String).join(" ")}\n`);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Add the defaults of the arguments from index `from` on to `values`, stopping
|
|
65
|
+
* at the first argument without one. The parser and help both use this, so
|
|
66
|
+
* help only shows the defaults the parser uses.
|
|
67
|
+
*/
|
|
68
|
+
function addDefaults(values, args, from = 0) {
|
|
69
|
+
for (let i = from; i < args.length; i++) {
|
|
70
|
+
const value = args[i].default;
|
|
71
|
+
if (value === void 0) return;
|
|
72
|
+
values.push(value);
|
|
73
|
+
}
|
|
62
74
|
}
|
|
63
75
|
/**
|
|
64
76
|
* Returns the minimum number of arguments that satisfies the args.
|
|
@@ -83,76 +95,6 @@ function getMaxArgs(args) {
|
|
|
83
95
|
for (let i = 0; i < args.length; i++) if (args[args.length - 1 - i].isVariadic) return Infinity;
|
|
84
96
|
return args.length;
|
|
85
97
|
}
|
|
86
|
-
// src/errors.ts
|
|
87
|
-
const repr = JSON.stringify;
|
|
88
|
-
var ParseError = class extends Error {
|
|
89
|
-
context;
|
|
90
|
-
constructor(context, ...args) {
|
|
91
|
-
super(...args);
|
|
92
|
-
this.context = context;
|
|
93
|
-
this.name = this.constructor.name;
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
var UnknownOption = class extends ParseError {
|
|
97
|
-
index;
|
|
98
|
-
start;
|
|
99
|
-
end;
|
|
100
|
-
option;
|
|
101
|
-
validOptions;
|
|
102
|
-
constructor(found, context) {
|
|
103
|
-
const msg = `${repr(found.option)} looks like a flag, but is unknown in this context`;
|
|
104
|
-
super(context, msg);
|
|
105
|
-
this.option = found.option;
|
|
106
|
-
this.validOptions = found.validOptions;
|
|
107
|
-
this.index = found.index;
|
|
108
|
-
this.start = found.start;
|
|
109
|
-
this.end = found.end;
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
var InvalidOptionArg = class extends ParseError {
|
|
113
|
-
index;
|
|
114
|
-
start;
|
|
115
|
-
end;
|
|
116
|
-
arg;
|
|
117
|
-
option;
|
|
118
|
-
constructor(found, context) {
|
|
119
|
-
const msg = `Option ${found.option} doesn't take arguments`;
|
|
120
|
-
super(context, msg);
|
|
121
|
-
this.index = found.index;
|
|
122
|
-
this.start = found.start;
|
|
123
|
-
this.end = found.end;
|
|
124
|
-
this.option = found.option;
|
|
125
|
-
this.arg = found.arg;
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
var TooManyArguments = class extends ParseError {
|
|
129
|
-
index;
|
|
130
|
-
value;
|
|
131
|
-
constructor(found, context) {
|
|
132
|
-
const msg = `Too many arguments, ${repr(found.value)} was unexpected`;
|
|
133
|
-
super(context, msg);
|
|
134
|
-
this.index = found.index;
|
|
135
|
-
this.value = found.value;
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
var TooFewOptionArguments = class extends ParseError {
|
|
139
|
-
constructor(min, max, context) {
|
|
140
|
-
const msg = min === max ? `Option needs ${min} argument${min === 1 ? "" : "s"}` : !Number.isFinite(max) ? `Option needs at least ${min} argument${min === 1 ? "" : "s"}` : `Option needs between ${min} and ${max} arguments`;
|
|
141
|
-
super(context, msg);
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
var TooFewArguments = class extends ParseError {
|
|
145
|
-
constructor(min, max, context) {
|
|
146
|
-
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`;
|
|
147
|
-
super(context, msg);
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
var MissingRequiredOption = class extends ParseError {
|
|
151
|
-
constructor(name, context) {
|
|
152
|
-
const msg = `Option ${name} is required, but wasn't found`;
|
|
153
|
-
super(context, msg);
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
98
|
// src/suggest.ts
|
|
157
99
|
/**
|
|
158
100
|
* "Did you mean" suggestions for unknown commands and options.
|
|
@@ -202,61 +144,350 @@ function editDistance(a, b, limit) {
|
|
|
202
144
|
}
|
|
203
145
|
return previous[b.length];
|
|
204
146
|
}
|
|
147
|
+
/** The first name of the only choice with a name that `matches`, if exactly one has one. */
|
|
148
|
+
function onlyMatch(choices, matches) {
|
|
149
|
+
let match;
|
|
150
|
+
for (const choice of choices) {
|
|
151
|
+
const name = (typeof choice === "string" ? [choice] : choice).find(matches);
|
|
152
|
+
if (name === void 0) continue;
|
|
153
|
+
if (match !== void 0) return;
|
|
154
|
+
match = name;
|
|
155
|
+
}
|
|
156
|
+
return match;
|
|
157
|
+
}
|
|
205
158
|
/**
|
|
206
|
-
* The
|
|
159
|
+
* The name the user most likely meant by `input`, or `undefined` if none is
|
|
207
160
|
* close enough to be worth suggesting. In order:
|
|
208
161
|
*
|
|
209
162
|
* 1. The only choice that starts with the input (`--verb` for `--verbose`).
|
|
210
163
|
* 2. The only choice that contains every character of the input in order, for
|
|
211
164
|
* inputs of 3 or more characters (`bld` for `build`).
|
|
212
|
-
* 3. The
|
|
165
|
+
* 3. The name with the fewest edits, allowing one edit per three characters
|
|
213
166
|
* of the input. Leading dashes don't count toward the length or the edits.
|
|
214
167
|
*/
|
|
215
168
|
function closestChoice(input, choices) {
|
|
216
169
|
const inputDashes = dashCount(input);
|
|
217
170
|
const bare = input.slice(inputDashes);
|
|
218
171
|
if (bare.length >= 2) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
for (const choice of choices) if (choice.startsWith(input)) {
|
|
222
|
-
match = choice;
|
|
223
|
-
count++;
|
|
224
|
-
}
|
|
225
|
-
if (count === 1) return match;
|
|
172
|
+
const match = onlyMatch(choices, (name) => name.startsWith(input));
|
|
173
|
+
if (match !== void 0) return match;
|
|
226
174
|
}
|
|
227
175
|
if (bare.length >= 3) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
for (const choice of choices) if (isAbbreviation(bare, choice.slice(dashCount(choice)))) {
|
|
231
|
-
match = choice;
|
|
232
|
-
count++;
|
|
233
|
-
}
|
|
234
|
-
if (count === 1) return match;
|
|
176
|
+
const match = onlyMatch(choices, (name) => isAbbreviation(bare, name.slice(dashCount(name))));
|
|
177
|
+
if (match !== void 0) return match;
|
|
235
178
|
}
|
|
236
179
|
let best;
|
|
237
180
|
let limit = Math.floor(bare.length / 3);
|
|
238
|
-
for (const
|
|
239
|
-
const other =
|
|
181
|
+
for (const name of choices.flat()) {
|
|
182
|
+
const other = name.slice(dashCount(name));
|
|
240
183
|
if (Math.abs(other.length - bare.length) > limit) continue;
|
|
241
184
|
const distance = editDistance(bare, other, limit);
|
|
242
185
|
if (distance <= limit && (best === void 0 || distance < limit)) {
|
|
243
|
-
best =
|
|
186
|
+
best = name;
|
|
244
187
|
limit = distance;
|
|
245
188
|
if (distance === 0) break;
|
|
246
189
|
}
|
|
247
190
|
}
|
|
248
191
|
return best;
|
|
249
192
|
}
|
|
250
|
-
// src/
|
|
251
|
-
|
|
252
|
-
|
|
193
|
+
// src/subcommands.ts
|
|
194
|
+
/**
|
|
195
|
+
* A token that names no subcommand. The reason is only worked out when asked
|
|
196
|
+
* for, since most tokens the parser tries aren't subcommands.
|
|
197
|
+
*/
|
|
198
|
+
var NoSubcommand = class {
|
|
199
|
+
subcommands;
|
|
200
|
+
token;
|
|
201
|
+
prefixOf;
|
|
202
|
+
command = null;
|
|
203
|
+
/**
|
|
204
|
+
* `prefixOf` are the commands `token` is a prefix of a name of, where
|
|
205
|
+
* prefixes match. There is never exactly one of them.
|
|
206
|
+
*/
|
|
207
|
+
constructor(subcommands, token, prefixOf) {
|
|
208
|
+
this.subcommands = subcommands;
|
|
209
|
+
this.token = token;
|
|
210
|
+
this.prefixOf = prefixOf;
|
|
211
|
+
}
|
|
212
|
+
/** Why the token names none of the subcommands, as an error message and suggestions. */
|
|
213
|
+
reason() {
|
|
214
|
+
const token = this.token;
|
|
215
|
+
if (token === "") return { message: "Expected a command, but the value was empty" };
|
|
216
|
+
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(", ")}` };
|
|
217
|
+
const choices = visibleSubcommands(this.subcommands).map((command) => asArray(command.name));
|
|
218
|
+
return {
|
|
219
|
+
message: `Unknown command '${token}'`,
|
|
220
|
+
didYouMean: {
|
|
221
|
+
input: token,
|
|
222
|
+
choices
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
/** The subcommands that help, completion and suggestions show. */
|
|
228
|
+
function visibleSubcommands(subcommands) {
|
|
229
|
+
return subcommands?.filter((subcommand) => !subcommand.hidden) ?? [];
|
|
253
230
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
231
|
+
/** The action of a command that has none of its own: print the command's help. */
|
|
232
|
+
const usage = ({ help }) => {
|
|
233
|
+
print(help());
|
|
234
|
+
return 0;
|
|
235
|
+
};
|
|
236
|
+
// src/directives.ts
|
|
237
|
+
/** The directives in effect when no command sets any. */
|
|
238
|
+
function defaultDirectives() {
|
|
239
|
+
return {
|
|
240
|
+
optionArgSeparators: ["="],
|
|
241
|
+
flagsArePosixNoncompliant: false,
|
|
242
|
+
optionsMustPrecedeArguments: false,
|
|
243
|
+
subcommandsMatchUniquePrefix: false
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Update `directives` for entering `command`. A subcommand inherits each
|
|
248
|
+
* directive until it sets its own.
|
|
249
|
+
*/
|
|
250
|
+
function applyDirectives(directives, command) {
|
|
251
|
+
const configured = command.parserDirectives;
|
|
252
|
+
if (!configured) return;
|
|
253
|
+
if (configured.optionArgSeparators) directives.optionArgSeparators = asArray(configured.optionArgSeparators);
|
|
254
|
+
if (typeof configured.subcommandsMatchUniquePrefix === "boolean") directives.subcommandsMatchUniquePrefix = configured.subcommandsMatchUniquePrefix;
|
|
255
|
+
if (typeof configured.optionsMustPrecedeArguments === "boolean") directives.optionsMustPrecedeArguments = configured.optionsMustPrecedeArguments;
|
|
256
|
+
if (typeof configured.flagsArePosixNoncompliant === "boolean") directives.flagsArePosixNoncompliant = configured.flagsArePosixNoncompliant;
|
|
257
|
+
}
|
|
258
|
+
// src/scope.ts
|
|
259
|
+
/**
|
|
260
|
+
* What a name means at one command on a path: the options, subcommands,
|
|
261
|
+
* directives and argument limits in effect there.
|
|
262
|
+
*
|
|
263
|
+
* Moving to a subcommand replaces the local options and adds its persistent
|
|
264
|
+
* options to the inherited ones. A name refers to one option: a local option
|
|
265
|
+
* wins over a persistent one, and a later option wins over an earlier one in
|
|
266
|
+
* the same list. A subcommand wins over an option where the next token can
|
|
267
|
+
* name a subcommand. The parser, help, completion and the spec checker all
|
|
268
|
+
* ask this module what a name refers to, so they can't disagree.
|
|
269
|
+
*/
|
|
270
|
+
var CommandScope = class CommandScope {
|
|
271
|
+
/** Non-persistent options of the current command, in definition order. */
|
|
272
|
+
local = [];
|
|
273
|
+
/** Persistent options from every command on the path, in definition order. */
|
|
274
|
+
persistent = [];
|
|
275
|
+
/**
|
|
276
|
+
* Persistent subcommands of the commands above the current one, nearest
|
|
277
|
+
* command first, so a nearer one wins a name.
|
|
278
|
+
*/
|
|
279
|
+
inheritedSubcommands = [];
|
|
280
|
+
/** The directives in effect. A subcommand inherits each until it sets its own. */
|
|
281
|
+
directives = defaultDirectives();
|
|
282
|
+
current = null;
|
|
283
|
+
/** The fewest positional arguments the current command takes. */
|
|
284
|
+
argsMin = 0;
|
|
285
|
+
/** The most positional arguments the current command takes. */
|
|
286
|
+
argsMax = 0;
|
|
287
|
+
/** The scope at the last command on `path`. */
|
|
288
|
+
static at(path) {
|
|
289
|
+
const scope = new CommandScope();
|
|
290
|
+
for (const command of path) scope.enter(command);
|
|
291
|
+
return scope;
|
|
258
292
|
}
|
|
293
|
+
/** Move to `command`, a subcommand of the current one. */
|
|
294
|
+
enter(command) {
|
|
295
|
+
const parentSubcommands = this.current?.subcommands;
|
|
296
|
+
if (parentSubcommands) {
|
|
297
|
+
const persistent = parentSubcommands.filter((subcommand) => subcommand.isPersistent);
|
|
298
|
+
if (persistent.length > 0) this.inheritedSubcommands.unshift(...persistent);
|
|
299
|
+
}
|
|
300
|
+
this.current = command;
|
|
301
|
+
this.argsMin = getMinArgs(command.args);
|
|
302
|
+
this.argsMax = getMaxArgs(command.args);
|
|
303
|
+
this.local.length = 0;
|
|
304
|
+
if (command.options) for (const option of command.options) (option.isPersistent ? this.persistent : this.local).push(option);
|
|
305
|
+
applyDirectives(this.directives, command);
|
|
306
|
+
}
|
|
307
|
+
/** The current command. */
|
|
308
|
+
get command() {
|
|
309
|
+
if (!this.current) throw new Error("The scope hasn't entered a command");
|
|
310
|
+
return this.current;
|
|
311
|
+
}
|
|
312
|
+
/** Whether the current command has subcommands of its own or inherits any. */
|
|
313
|
+
get hasSubcommands() {
|
|
314
|
+
return this.current?.subcommands !== void 0 || this.inheritedSubcommands.length > 0;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* The subcommands the next token can name: the current command's own,
|
|
318
|
+
* followed by each inherited one that an earlier one doesn't shadow a name
|
|
319
|
+
* of. Hidden ones are included.
|
|
320
|
+
*/
|
|
321
|
+
subcommands() {
|
|
322
|
+
const own = this.current?.subcommands ?? [];
|
|
323
|
+
if (this.inheritedSubcommands.length === 0) return own;
|
|
324
|
+
const all = [...own];
|
|
325
|
+
for (const candidate of this.inheritedSubcommands) if (!asArray(candidate.name).some((name) => all.some((earlier) => hasName(earlier.name, name)))) all.push(candidate);
|
|
326
|
+
return all;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Find the subcommand `token` names: one of the current command's own, or
|
|
330
|
+
* else an inherited persistent one. An exact name always wins. With
|
|
331
|
+
* `subcommandsMatchUniquePrefix`, a token that's a prefix of exactly one of
|
|
332
|
+
* the command's own names matches that command, unless `exactOnly`. A hidden
|
|
333
|
+
* or inherited command only matches its exact name, so it can't make a
|
|
334
|
+
* visible command's prefix ambiguous or take a prefix meant as an argument,
|
|
335
|
+
* and an empty token is no command's prefix.
|
|
336
|
+
*/
|
|
337
|
+
matchSubcommand(token, exactOnly = false) {
|
|
338
|
+
const own = this.current?.subcommands ?? [];
|
|
339
|
+
for (const candidate of own) if (hasName(candidate.name, token)) return { command: candidate };
|
|
340
|
+
for (const candidate of this.inheritedSubcommands) if (hasName(candidate.name, token)) return { command: candidate };
|
|
341
|
+
const matches = this.directives.subcommandsMatchUniquePrefix && !exactOnly ? prefixMatches(own, token) : [];
|
|
342
|
+
if (matches.length === 1) return { command: matches[0] };
|
|
343
|
+
return new NoSubcommand(this.subcommands(), token, matches);
|
|
344
|
+
}
|
|
345
|
+
/** The option `name` refers to, if any. */
|
|
346
|
+
resolve(name) {
|
|
347
|
+
const { local, persistent } = this;
|
|
348
|
+
for (let i = local.length - 1; i >= 0; i--) if (hasName(local[i].name, name)) return local[i];
|
|
349
|
+
for (let i = persistent.length - 1; i >= 0; i--) if (hasName(persistent[i].name, name)) return persistent[i];
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* The options that can be used here, in definition order, each with only
|
|
354
|
+
* its live names: the names that still refer to it. Options with every name
|
|
355
|
+
* shadowed are left out. Where the next token can name a subcommand, as
|
|
356
|
+
* `subcommands` says, a name that one matches is that subcommand's.
|
|
357
|
+
*/
|
|
358
|
+
live(subcommands = null) {
|
|
359
|
+
const owner = /* @__PURE__ */ new Map();
|
|
360
|
+
for (const options of [this.persistent, this.local]) for (const option of options) for (const name of asArray(option.name)) owner.set(name, option);
|
|
361
|
+
const pick = (options) => {
|
|
362
|
+
const picked = [];
|
|
363
|
+
for (const option of options) {
|
|
364
|
+
const names = asArray(option.name).filter((name) => owner.get(name) === option && !(subcommands && this.matchSubcommand(name, subcommands.exactOnly).command));
|
|
365
|
+
if (names.length > 0) picked.push({
|
|
366
|
+
option,
|
|
367
|
+
names
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return picked;
|
|
371
|
+
};
|
|
372
|
+
return {
|
|
373
|
+
local: pick(this.local),
|
|
374
|
+
persistent: pick(this.persistent)
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* The options a user can see at the start of the current command: the
|
|
379
|
+
* {@link live} ones that aren't hidden, without the names a subcommand takes.
|
|
380
|
+
*/
|
|
381
|
+
visible() {
|
|
382
|
+
const { local, persistent } = this.live(this.hasSubcommands ? { exactOnly: false } : null);
|
|
383
|
+
const shown = (visible) => !visible.option.hidden;
|
|
384
|
+
return {
|
|
385
|
+
local: local.filter(shown),
|
|
386
|
+
persistent: persistent.filter(shown)
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
/** The live names of each option a user could have meant, for "did you mean" suggestions. */
|
|
390
|
+
suggestionNames() {
|
|
391
|
+
const { local, persistent } = this.live();
|
|
392
|
+
const shown = (visible) => !visible.option.hidden;
|
|
393
|
+
return [...local, ...persistent].filter(shown).map((visible) => visible.names);
|
|
394
|
+
}
|
|
395
|
+
/** The options that must be used, local first. */
|
|
396
|
+
required() {
|
|
397
|
+
return [...this.local, ...this.persistent].filter((option) => option.isRequired);
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* How each of `names` is written with `option`'s value here. Help shows
|
|
401
|
+
* these, completion offers them, and the spec checker reads each one back
|
|
402
|
+
* through the parser.
|
|
403
|
+
*/
|
|
404
|
+
forms(option, names = asArray(option.name)) {
|
|
405
|
+
const join = (name) => {
|
|
406
|
+
if (getMaxArgs(option.args) === 0) return null;
|
|
407
|
+
if (option.requiresSeparator) return this.requiredSeparator(option);
|
|
408
|
+
return !this.directives.flagsArePosixNoncompliant && (name === "-" || name === "+") ? "" : null;
|
|
409
|
+
};
|
|
410
|
+
return names.map((name) => ({
|
|
411
|
+
name,
|
|
412
|
+
join: join(name)
|
|
413
|
+
}));
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* The separator that must come between `option` and its value, or null if
|
|
417
|
+
* it can take a value as a separate token.
|
|
418
|
+
*/
|
|
419
|
+
requiredSeparator(option) {
|
|
420
|
+
const required = option.requiresSeparator;
|
|
421
|
+
if (typeof required === "string") return required;
|
|
422
|
+
return required ? this.directives.optionArgSeparators[0] ?? "=" : null;
|
|
423
|
+
}
|
|
424
|
+
/** Whether the parser splits an option from its value at `separator` here. */
|
|
425
|
+
readsSeparator(separator) {
|
|
426
|
+
return this.directives.optionArgSeparators.includes(separator);
|
|
427
|
+
}
|
|
428
|
+
/** The separator that `text` starts with, or "" if none. Used after a short option. */
|
|
429
|
+
leadingSeparator(text) {
|
|
430
|
+
for (const separator of this.directives.optionArgSeparators) if (text.startsWith(separator)) return separator;
|
|
431
|
+
return "";
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* The separator that appears first in `token`, so the value after it can
|
|
435
|
+
* contain the other separators. When two separators start at the same place,
|
|
436
|
+
* the longer one wins. Null if there's none, or if the one found is empty.
|
|
437
|
+
*/
|
|
438
|
+
firstSeparator(token) {
|
|
439
|
+
let found = "";
|
|
440
|
+
let index = -1;
|
|
441
|
+
for (const candidate of this.directives.optionArgSeparators) {
|
|
442
|
+
const at = token.indexOf(candidate);
|
|
443
|
+
if (at === -1) continue;
|
|
444
|
+
if (index === -1 || at < index || at === index && candidate.length > found.length) {
|
|
445
|
+
found = candidate;
|
|
446
|
+
index = at;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return found ? {
|
|
450
|
+
index,
|
|
451
|
+
separator: found
|
|
452
|
+
} : null;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Why `option`, used as `name`, can't take a value given after `separator`,
|
|
456
|
+
* or null if it can. A separate token has the separator "".
|
|
457
|
+
*/
|
|
458
|
+
separatorProblem(option, name, separator) {
|
|
459
|
+
const required = option.requiresSeparator;
|
|
460
|
+
if (typeof required === "string" && separator !== required) return `Incorrect separator, use '${required}' instead of '${separator}'`;
|
|
461
|
+
if (required === true && separator === "") return `Missing separator, use '${this.requiredSeparator(option)}' between ${name} and its value`;
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
const optionKeys = /* @__PURE__ */ new WeakMap();
|
|
466
|
+
/**
|
|
467
|
+
* A key for the set of `option`'s names. Options with the same names, in any
|
|
468
|
+
* order, get the same key and are one option: the parser continues a
|
|
469
|
+
* repeatable option's uses across them, counts one as used when the other was,
|
|
470
|
+
* and stores their values under the same names, which is how they're read.
|
|
471
|
+
* Options that share only some names are different options, and each shared
|
|
472
|
+
* name refers to whichever one `resolve` picks.
|
|
473
|
+
*/
|
|
474
|
+
function optionKey(option) {
|
|
475
|
+
let key = optionKeys.get(option);
|
|
476
|
+
if (key === void 0) {
|
|
477
|
+
key = typeof option.name === "string" ? option.name : [...option.name].sort().join("\0");
|
|
478
|
+
optionKeys.set(option, key);
|
|
479
|
+
}
|
|
480
|
+
return key;
|
|
259
481
|
}
|
|
482
|
+
function hasName(names, name) {
|
|
483
|
+
return typeof names === "string" ? names === name : names.includes(name);
|
|
484
|
+
}
|
|
485
|
+
/** The visible commands that `token` is a prefix of a name of, each listed once. */
|
|
486
|
+
function prefixMatches(subcommands, token) {
|
|
487
|
+
if (token === "") return [];
|
|
488
|
+
return subcommands.filter((candidate) => !candidate.hidden && asArray(candidate.name).some((name) => name.startsWith(token)));
|
|
489
|
+
}
|
|
490
|
+
// src/help.ts
|
|
260
491
|
function dedent(str) {
|
|
261
492
|
if (str[0] !== "\n") return str;
|
|
262
493
|
const lines = str.split("\n").slice(1, -1);
|
|
@@ -273,80 +504,53 @@ function getHelp(path, config = {}) {
|
|
|
273
504
|
return formatHelpSections(sections);
|
|
274
505
|
}
|
|
275
506
|
/**
|
|
276
|
-
*
|
|
507
|
+
* Print `message` as an error, then the usage and flags of the last command on
|
|
508
|
+
* `path`, suggesting the closest of `didYouMean.choices` if one is close.
|
|
509
|
+
*/
|
|
510
|
+
function printUsageError(path, message, didYouMean) {
|
|
511
|
+
printError(`${message}\n\n${getHelp(path, {
|
|
512
|
+
description: false,
|
|
513
|
+
didYouMean
|
|
514
|
+
})}`);
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* A subcommand that prints help for its parent, or for the command the names
|
|
518
|
+
* after it lead to from there, so `cli help a b` prints the help of `cli a b`.
|
|
519
|
+
* It reads names until one isn't a subcommand. A token where the command takes
|
|
520
|
+
* arguments, or where it has no subcommands, ends the names, so
|
|
521
|
+
* `cli help completion zsh` prints the help of `completion`. Any other token
|
|
522
|
+
* is an unknown command. Add `{ ...helpCommand, isPersistent: true }` instead
|
|
523
|
+
* to make `cli a b help` work too.
|
|
277
524
|
*/
|
|
278
|
-
const usage = ({ path, args: [command], help }) => {
|
|
279
|
-
if (command === void 0) {
|
|
280
|
-
print(help());
|
|
281
|
-
return 0;
|
|
282
|
-
}
|
|
283
|
-
if (command === "") {
|
|
284
|
-
printActionError(`Expected a command, but the value was empty\n\n${help({ description: false })}`);
|
|
285
|
-
return 1;
|
|
286
|
-
}
|
|
287
|
-
const subcommands = path.at(-1)?.subcommands?.filter((cmd) => !cmd.hidden).flatMap((cmd) => cmd.name);
|
|
288
|
-
if (subcommands && subcommands.length > 0) {
|
|
289
|
-
if (getParserDirective(path, "subcommandsMatchUniquePrefix")) {
|
|
290
|
-
const possible = subcommands.filter((name) => name.startsWith(command));
|
|
291
|
-
if (possible.length > 0) {
|
|
292
|
-
printActionError(`Ambiguous command '${command}', could be: ${possible.join(", ")}\n\n${help({ description: false })}`);
|
|
293
|
-
return 1;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
printActionError(`Unknown command '${command}'\n\n${help({
|
|
297
|
-
description: false,
|
|
298
|
-
didYouMean: {
|
|
299
|
-
input: command,
|
|
300
|
-
choices: subcommands
|
|
301
|
-
}
|
|
302
|
-
})}`);
|
|
303
|
-
return 1;
|
|
304
|
-
}
|
|
305
|
-
printActionError(`Unknown command '${command}'\n\n${help({ description: false })}`);
|
|
306
|
-
return 1;
|
|
307
|
-
};
|
|
308
|
-
/** A subcommand that prints help for its parent or a sibling command. */
|
|
309
525
|
const helpCommand = {
|
|
310
526
|
name: "help",
|
|
311
527
|
description: "Print a help message",
|
|
312
528
|
args: {
|
|
313
529
|
name: "command",
|
|
314
|
-
isOptional: true
|
|
530
|
+
isOptional: true,
|
|
531
|
+
isVariadic: true
|
|
315
532
|
},
|
|
316
|
-
action({ path, args
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
const command = getNamed(parent.subcommands, commandName);
|
|
331
|
-
if (!command) {
|
|
332
|
-
const subcommands = parent.subcommands.filter((cmd) => !cmd.hidden).flatMap((cmd) => cmd.name);
|
|
333
|
-
if (subcommands.length > 0) {
|
|
334
|
-
printActionError(`There is no subcommand named '${commandName}'\n\n${help({
|
|
335
|
-
path: helpRoot,
|
|
336
|
-
didYouMean: {
|
|
337
|
-
input: commandName,
|
|
338
|
-
choices: subcommands
|
|
339
|
-
}
|
|
340
|
-
})}`);
|
|
341
|
-
return 1;
|
|
533
|
+
action({ path, args, help }) {
|
|
534
|
+
const self = path[path.length - 1];
|
|
535
|
+
const helpPath = path.slice(0, -1);
|
|
536
|
+
const scope = CommandScope.at(helpPath);
|
|
537
|
+
for (const name of args) {
|
|
538
|
+
const command = helpPath[helpPath.length - 1];
|
|
539
|
+
if (!command.subcommands) break;
|
|
540
|
+
const match = scope.matchSubcommand(name);
|
|
541
|
+
if (match.command === self && !command.subcommands.includes(self)) break;
|
|
542
|
+
if (match.command) {
|
|
543
|
+
helpPath.push(match.command);
|
|
544
|
+
scope.enter(match.command);
|
|
545
|
+
continue;
|
|
342
546
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
})}`);
|
|
547
|
+
if (!command.requiresSubcommand && getMaxArgs(command.args) > 0) break;
|
|
548
|
+
const { message, didYouMean } = match.reason();
|
|
549
|
+
printUsageError(helpPath, message, didYouMean);
|
|
347
550
|
return 1;
|
|
348
551
|
}
|
|
349
|
-
print(help({ path:
|
|
552
|
+
print(help({ path: helpPath }));
|
|
553
|
+
return 0;
|
|
350
554
|
}
|
|
351
555
|
};
|
|
352
556
|
/** A persistent help option that prints help for the selected command. */
|
|
@@ -362,46 +566,60 @@ const help = {
|
|
|
362
566
|
function getSubcommandPathString(commands) {
|
|
363
567
|
return commands.map((command) => getLongestString(makeArray1(command.name))).join(" ");
|
|
364
568
|
}
|
|
365
|
-
function nonNullable(value) {
|
|
366
|
-
return value !== null && value !== void 0;
|
|
367
|
-
}
|
|
368
569
|
function getSections(commands) {
|
|
369
570
|
const command = commands[commands.length - 1];
|
|
571
|
+
const scope = CommandScope.at(commands);
|
|
572
|
+
const show = ({ option, names }) => optionToString(option, scope.forms(option, names));
|
|
370
573
|
const description = command.description;
|
|
371
574
|
const defaultDesc = "No description";
|
|
372
|
-
const subcommands = command.subcommands
|
|
373
|
-
|
|
374
|
-
let optionObjects =
|
|
375
|
-
let
|
|
376
|
-
let options = [];
|
|
575
|
+
const subcommands = visibleSubcommands(command.subcommands).map((subcommand) => [makeArray1(subcommand.name).sort((a, b) => a.length - b.length).join(", "), subcommand.description ?? defaultDesc]);
|
|
576
|
+
const visible = scope.visible();
|
|
577
|
+
let optionObjects = visible.local;
|
|
578
|
+
let persistentOptionObjects = visible.persistent;
|
|
377
579
|
if (commands.length === 1 && subcommands.length === 0) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
persistentOptionObjects = commands.map((command) => command.options).filter(nonNullable).flat().filter((option) => option.isPersistent && !option.hidden);
|
|
382
|
-
persistentOptions = persistentOptionObjects.map((option) => [optionToString(option), option.description ?? defaultDesc]);
|
|
383
|
-
optionObjects = (command.options ?? []).filter((option) => !option.hidden && !option.isPersistent);
|
|
384
|
-
options = optionObjects.map((option) => [optionToString(option), option.description ?? defaultDesc]);
|
|
580
|
+
const byOption = new Map([...optionObjects, ...persistentOptionObjects].map((v) => [v.option, v]));
|
|
581
|
+
optionObjects = (command.options ?? []).flatMap((option) => byOption.get(option) ?? []);
|
|
582
|
+
persistentOptionObjects = [];
|
|
385
583
|
}
|
|
584
|
+
const row = (visible) => {
|
|
585
|
+
const defaults = [];
|
|
586
|
+
addDefaults(defaults, asArray(visible.option.args));
|
|
587
|
+
return [show(visible), withDefaults(visible.option.description, defaults, defaultDesc)];
|
|
588
|
+
};
|
|
589
|
+
const options = optionObjects.map(row);
|
|
590
|
+
const persistentOptions = persistentOptionObjects.map(row);
|
|
591
|
+
const usage = (() => {
|
|
592
|
+
const parts = [getSubcommandPathString(commands)];
|
|
593
|
+
for (const visible of [...optionObjects, ...persistentOptionObjects]) if (visible.option.isRequired) parts.push(show(visible));
|
|
594
|
+
if (options.length || persistentOptions.length) parts.push("[flags]");
|
|
595
|
+
if (command.requiresSubcommand) parts.push("<command>");
|
|
596
|
+
else {
|
|
597
|
+
const args = makeArray(command.args);
|
|
598
|
+
if (args.length > 0) parts.push(summarizeArguments(args));
|
|
599
|
+
}
|
|
600
|
+
return parts.join(" ");
|
|
601
|
+
})();
|
|
602
|
+
const positionals = command.requiresSubcommand ? [] : asArray(command.args);
|
|
386
603
|
return {
|
|
387
|
-
usage
|
|
388
|
-
const parts = [getSubcommandPathString(commands)];
|
|
389
|
-
const required = [...optionObjects.filter((option) => option.isRequired), ...persistentOptionObjects.filter((option) => option.isRequired)];
|
|
390
|
-
for (const option of required) parts.push(optionToString(option));
|
|
391
|
-
if (options.length || persistentOptions.length) parts.push("[flags]");
|
|
392
|
-
if (command.requiresSubcommand) parts.push("<command>");
|
|
393
|
-
else {
|
|
394
|
-
const args = makeArray(command.args);
|
|
395
|
-
if (args.length > 0) parts.push(summarizeArguments(args));
|
|
396
|
-
}
|
|
397
|
-
return parts.join(" ");
|
|
398
|
-
})(),
|
|
604
|
+
usage,
|
|
399
605
|
description,
|
|
606
|
+
args: positionals.some((arg) => arg.description !== void 0 || arg.default !== void 0) ? positionals.map((arg) => [(arg.name || "argument") + (arg.isVariadic ? "..." : ""), withDefaults(arg.description, arg.default === void 0 ? [] : [arg.default], "")]) : [],
|
|
400
607
|
persistentOptions,
|
|
401
608
|
options,
|
|
402
609
|
subcommands
|
|
403
610
|
};
|
|
404
611
|
}
|
|
612
|
+
/**
|
|
613
|
+
* A description ending with the defaults the parser uses. They go on a line of
|
|
614
|
+
* their own when the last line would be too long for the 68 characters the
|
|
615
|
+
* spec checks allow.
|
|
616
|
+
*/
|
|
617
|
+
function withDefaults(description, defaults, defaultDesc) {
|
|
618
|
+
if (defaults.length === 0) return description ?? defaultDesc;
|
|
619
|
+
const shown = `(default: ${defaults.join(" ")})`;
|
|
620
|
+
if (description === void 0) return shown;
|
|
621
|
+
return `${description}${description.slice(description.lastIndexOf("\n") + 1).length + 1 + shown.length <= 68 ? " " : "\n"}${shown}`;
|
|
622
|
+
}
|
|
405
623
|
function formatHelpSections(sections) {
|
|
406
624
|
const parts = [];
|
|
407
625
|
const indent = " ";
|
|
@@ -410,17 +628,26 @@ function formatHelpSections(sections) {
|
|
|
410
628
|
if (sections.usage) parts.push(`Usage:\n${indent}${sections.usage}`);
|
|
411
629
|
const longestOptionName = Math.max(0, ...sections.options.map(([name]) => name.length), ...sections.persistentOptions.map(([name]) => name.length));
|
|
412
630
|
const minSpacingBetweenNameAndDesc = 2;
|
|
413
|
-
const
|
|
631
|
+
const formatRows = (rows, width) => {
|
|
414
632
|
const lines = [];
|
|
415
|
-
const newline = indent + " ".repeat(
|
|
416
|
-
for (const [name, desc] of
|
|
417
|
-
|
|
633
|
+
const newline = indent + " ".repeat(width + minSpacingBetweenNameAndDesc);
|
|
634
|
+
for (const [name, desc] of rows) {
|
|
635
|
+
if (desc === "") {
|
|
636
|
+
lines.push(`${indent}${name}`);
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
const spaces = " ".repeat(width - name.length + minSpacingBetweenNameAndDesc);
|
|
418
640
|
const [head, ...tail] = desc.split("\n");
|
|
419
641
|
const description = [head, ...tail.map((line) => newline + line)].join("\n");
|
|
420
642
|
lines.push(`${indent}${name}${spaces}${description}`);
|
|
421
643
|
}
|
|
422
644
|
return lines;
|
|
423
645
|
};
|
|
646
|
+
const formatOptions = (options) => formatRows(options, longestOptionName);
|
|
647
|
+
if (sections.args.length > 0) {
|
|
648
|
+
const longestArgName = Math.max(...sections.args.map(([name]) => name.length));
|
|
649
|
+
parts.push(["Arguments:", ...formatRows(sections.args, longestArgName)].join("\n"));
|
|
650
|
+
}
|
|
424
651
|
if (sections.subcommands.length > 0) {
|
|
425
652
|
const lines = ["Commands:"];
|
|
426
653
|
const longestName = Math.max(...sections.subcommands.map(([name]) => name.length));
|
|
@@ -451,16 +678,25 @@ function getLongestString(strings) {
|
|
|
451
678
|
}
|
|
452
679
|
return longest;
|
|
453
680
|
}
|
|
454
|
-
|
|
455
|
-
|
|
681
|
+
/**
|
|
682
|
+
* Show an option's names, each written with its arguments the way `forms`
|
|
683
|
+
* says. Names written the same way share one copy of the arguments.
|
|
684
|
+
*/
|
|
685
|
+
function optionToString(option, forms) {
|
|
456
686
|
const optionArgs = makeArray(option.args);
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
687
|
+
const sorted = [...forms].sort((a, b) => a.name.length - b.name.length);
|
|
688
|
+
return [...new Set(sorted.map((form) => form.join))].map((join) => {
|
|
689
|
+
return sorted.filter((form) => form.join === join).map((form) => form.name).join(", ") + argumentsAfterName(optionArgs, join);
|
|
690
|
+
}).join(", ");
|
|
691
|
+
}
|
|
692
|
+
/** The text after an option's name that shows its arguments, joined to the name by `join`. */
|
|
693
|
+
function argumentsAfterName(args, join) {
|
|
694
|
+
if (args.length === 0) return "";
|
|
695
|
+
if (join === null) return ` ${summarizeArguments(args)}`;
|
|
696
|
+
const [first, ...rest] = args;
|
|
697
|
+
const name = first.name || "argument";
|
|
698
|
+
const joined = (first.isOptional ? `[${join}${name}]` : `${join}<${name}>`) + (first.isVariadic ? "..." : "");
|
|
699
|
+
return rest.length > 0 ? `${joined} ${summarizeArguments(rest)}` : joined;
|
|
464
700
|
}
|
|
465
701
|
function summarizeArguments(args) {
|
|
466
702
|
return args.map((arg) => argToString(arg)).join(" ");
|
|
@@ -477,358 +713,547 @@ function argToString(arg) {
|
|
|
477
713
|
if (arg.isVariadic) suffix = "...";
|
|
478
714
|
return `${lhs}${name}${rhs}${suffix}`;
|
|
479
715
|
}
|
|
716
|
+
// src/errors.ts
|
|
717
|
+
const repr = JSON.stringify;
|
|
718
|
+
/**
|
|
719
|
+
* A command line the parser can't accept. `run` prints the message with the
|
|
720
|
+
* help for the command at `context.path`, and suggests the closest of
|
|
721
|
+
* `didYouMean.choices` if there is one.
|
|
722
|
+
*/
|
|
723
|
+
var ParseError = class extends Error {
|
|
724
|
+
context;
|
|
725
|
+
didYouMean;
|
|
726
|
+
constructor(context, message, didYouMean) {
|
|
727
|
+
super(message);
|
|
728
|
+
this.context = context;
|
|
729
|
+
this.didYouMean = didYouMean;
|
|
730
|
+
this.name = this.constructor.name;
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
var UnknownOption = class extends ParseError {
|
|
734
|
+
option;
|
|
735
|
+
constructor(option, validOptions, context) {
|
|
736
|
+
const msg = `${repr(option)} looks like a flag, but is unknown in this context`;
|
|
737
|
+
super(context, msg, {
|
|
738
|
+
input: option,
|
|
739
|
+
choices: validOptions
|
|
740
|
+
});
|
|
741
|
+
this.option = option;
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
var InvalidOptionArg = class extends ParseError {
|
|
745
|
+
option;
|
|
746
|
+
constructor(option, context) {
|
|
747
|
+
super(context, `Option ${option} doesn't take arguments`);
|
|
748
|
+
this.option = option;
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
var TooManyArguments = class extends ParseError {
|
|
752
|
+
constructor(value, context) {
|
|
753
|
+
super(context, `Too many arguments, ${repr(value)} was unexpected`);
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
var TooFewOptionArguments = class extends ParseError {
|
|
757
|
+
constructor(option, min, max, context) {
|
|
758
|
+
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`;
|
|
759
|
+
super(context, msg);
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
var TooFewArguments = class extends ParseError {
|
|
763
|
+
constructor(min, max, context) {
|
|
764
|
+
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`;
|
|
765
|
+
super(context, msg);
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
var MissingRequiredOption = class extends ParseError {
|
|
769
|
+
constructor(name, context) {
|
|
770
|
+
const msg = `Option ${name} is required, but wasn't found`;
|
|
771
|
+
super(context, msg);
|
|
772
|
+
}
|
|
773
|
+
};
|
|
480
774
|
// src/parse.ts
|
|
481
|
-
|
|
482
|
-
|
|
775
|
+
/** How many times `option` can be used. */
|
|
776
|
+
function repeatLimit(option) {
|
|
777
|
+
return typeof option.isRepeatable === "number" ? option.isRepeatable : Infinity;
|
|
483
778
|
}
|
|
484
779
|
/**
|
|
485
|
-
*
|
|
486
|
-
*
|
|
780
|
+
* Throw if the options used break a `dependsOn` or `exclusiveOn` rule, or if
|
|
781
|
+
* any of `required` is missing.
|
|
487
782
|
*/
|
|
488
|
-
function
|
|
489
|
-
for (
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
for (const options of [local, persistent]) {
|
|
497
|
-
const seen = /* @__PURE__ */ new Set();
|
|
498
|
-
for (const option of options) for (const name of asArray(option.name)) if (!seen.has(name)) {
|
|
499
|
-
seen.add(name);
|
|
500
|
-
names.push(name);
|
|
501
|
-
}
|
|
783
|
+
function checkOptionRules(options, seenOptions, required, context) {
|
|
784
|
+
for (const option of seenOptions) {
|
|
785
|
+
if (!option.dependsOn) continue;
|
|
786
|
+
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`);
|
|
787
|
+
}
|
|
788
|
+
for (const option of seenOptions) {
|
|
789
|
+
if (!option.exclusiveOn) continue;
|
|
790
|
+
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}`);
|
|
502
791
|
}
|
|
503
|
-
|
|
792
|
+
for (const option of required) for (const name of asArray(option.name)) if (!options.has(name)) throw new MissingRequiredOption(name, context);
|
|
504
793
|
}
|
|
505
794
|
/**
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
* This runs once per process, so it's written for a cold start: one loop, few
|
|
514
|
-
* helper functions and plain arrays, because every extra function is compiled
|
|
515
|
-
* the first time it runs.
|
|
795
|
+
* The state of reading one command line, one token at a time. This is the
|
|
796
|
+
* only code that decides what each input token is. It never throws: it skips
|
|
797
|
+
* whatever is invalid and keeps going, so {@link readNext} can still read a
|
|
798
|
+
* line that's wrong at the moment, and it keeps the first problem it finds for
|
|
799
|
+
* {@link parse} to throw. Each method handles one kind of token, and says what
|
|
800
|
+
* the token was read as.
|
|
516
801
|
*/
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
802
|
+
var Scanner = class {
|
|
803
|
+
path;
|
|
804
|
+
optionAction = void 0;
|
|
805
|
+
args = [];
|
|
806
|
+
options = /* @__PURE__ */ new Map();
|
|
807
|
+
seenOptions = [];
|
|
808
|
+
argSeparatorIndex = -1;
|
|
809
|
+
scope = new CommandScope();
|
|
810
|
+
directives = this.scope.directives;
|
|
811
|
+
hasFoundArg = false;
|
|
812
|
+
openOption = null;
|
|
813
|
+
openOptionName = "";
|
|
814
|
+
valueCount = 0;
|
|
815
|
+
valueMin = 0;
|
|
816
|
+
separateValueMax = 0;
|
|
817
|
+
values = null;
|
|
818
|
+
valuesStart = 0;
|
|
819
|
+
valuesMin = 0;
|
|
820
|
+
valuesMax = 0;
|
|
821
|
+
valueArgs = [];
|
|
822
|
+
/**
|
|
823
|
+
* Makes the error for the first token that can't be read the way it's
|
|
824
|
+
* written, like an unknown option or a value an option doesn't take.
|
|
825
|
+
* {@link parse} stops there.
|
|
826
|
+
*/
|
|
827
|
+
problem = null;
|
|
828
|
+
tooManyArgs = null;
|
|
829
|
+
repeatedUses = /* @__PURE__ */ new Map();
|
|
830
|
+
constructor(spec) {
|
|
831
|
+
this.path = [spec];
|
|
832
|
+
this.scope.enter(spec);
|
|
833
|
+
}
|
|
834
|
+
ctx() {
|
|
835
|
+
return { path: [...this.path] };
|
|
836
|
+
}
|
|
837
|
+
/** Whether the open option can take the next token as a value. */
|
|
838
|
+
get hasRoomForValue() {
|
|
839
|
+
return this.valueCount < this.separateValueMax;
|
|
840
|
+
}
|
|
841
|
+
findOption(name) {
|
|
842
|
+
return this.scope.resolve(name);
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Keep the problem `make` describes if it's the first. The error is only
|
|
846
|
+
* made if {@link parse} throws it, since a lenient read never needs it and
|
|
847
|
+
* an error is slow to make. So `make` can't read anything later tokens change.
|
|
848
|
+
*/
|
|
849
|
+
fail(make) {
|
|
850
|
+
if (!this.problem) {
|
|
851
|
+
const ctx = this.ctx();
|
|
852
|
+
this.problem = () => make(ctx);
|
|
558
853
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
if (
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
if (directives) {
|
|
566
|
-
if (directives.optionArgSeparators) separators = asArray(directives.optionArgSeparators);
|
|
567
|
-
if (typeof directives.subcommandsMatchUniquePrefix === "boolean") subcommandsMatchUniquePrefix = directives.subcommandsMatchUniquePrefix;
|
|
568
|
-
if (typeof directives.optionsMustPrecedeArguments === "boolean") optionsMustPrecedeArguments = directives.optionsMustPrecedeArguments;
|
|
569
|
-
if (typeof directives.flagsArePosixNoncompliant === "boolean") posixCompliantOptions = !directives.flagsArePosixNoncompliant;
|
|
854
|
+
}
|
|
855
|
+
/** Keep the first argument the command has no room for, like {@link fail}. */
|
|
856
|
+
failArgs(make) {
|
|
857
|
+
if (!this.tooManyArgs) {
|
|
858
|
+
const ctx = this.ctx();
|
|
859
|
+
this.tooManyArgs = () => make(ctx);
|
|
570
860
|
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
if (
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
861
|
+
}
|
|
862
|
+
unknownOption(option) {
|
|
863
|
+
if (!this.problem) {
|
|
864
|
+
const names = this.scope.suggestionNames();
|
|
865
|
+
this.fail((ctx) => new UnknownOption(option, names, ctx));
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
endOption() {
|
|
869
|
+
const values = this.values;
|
|
870
|
+
if (values) {
|
|
871
|
+
const given = values.length - this.valuesStart;
|
|
872
|
+
if (given < this.valuesMin) {
|
|
873
|
+
const { openOptionName, valuesMin, valuesMax } = this;
|
|
874
|
+
this.fail((ctx) => new TooFewOptionArguments(openOptionName, valuesMin, valuesMax, ctx));
|
|
875
|
+
} else addDefaults(values, this.valueArgs, given);
|
|
876
|
+
}
|
|
877
|
+
this.values = null;
|
|
878
|
+
}
|
|
879
|
+
/** End the open option, so no later token can be its value. */
|
|
880
|
+
closeOption() {
|
|
881
|
+
this.endOption();
|
|
882
|
+
this.openOption = null;
|
|
883
|
+
this.valueCount = this.valueMin = this.separateValueMax = 0;
|
|
884
|
+
}
|
|
885
|
+
useOption(option, name) {
|
|
886
|
+
this.endOption();
|
|
887
|
+
this.openOption = option;
|
|
888
|
+
this.openOptionName = name;
|
|
889
|
+
this.valueCount = 0;
|
|
890
|
+
this.valueMin = getMinArgs(option.args);
|
|
891
|
+
this.separateValueMax = option.requiresSeparator ? 0 : getMaxArgs(option.args);
|
|
892
|
+
this.seenOptions.push(option);
|
|
893
|
+
if (option.action) this.optionAction = option.action;
|
|
585
894
|
const names = asArray(option.name);
|
|
895
|
+
const options = this.options;
|
|
586
896
|
let optionValues;
|
|
587
897
|
if (option.isRepeatable) {
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
let isSharedCount = true;
|
|
591
|
-
for (const other of names) if (options.get(other) !== existing) isSharedCount = false;
|
|
592
|
-
let uses = isSharedCount ? existing : void 0;
|
|
898
|
+
const key = optionKey(option);
|
|
899
|
+
let uses = this.repeatedUses.get(key);
|
|
593
900
|
if (!uses) {
|
|
594
|
-
uses =
|
|
595
|
-
|
|
901
|
+
uses = {
|
|
902
|
+
values: [],
|
|
903
|
+
count: 0
|
|
904
|
+
};
|
|
905
|
+
this.repeatedUses.set(key, uses);
|
|
596
906
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
907
|
+
if (uses.count >= repeatLimit(option)) this.fail((ctx) => new ParseError(ctx, "Too many repetitions"));
|
|
908
|
+
uses.count += 1;
|
|
909
|
+
for (const other of names) options.set(other, uses.values);
|
|
600
910
|
if (!option.args) {
|
|
601
|
-
uses.push("");
|
|
911
|
+
uses.values.push("");
|
|
602
912
|
return;
|
|
603
913
|
}
|
|
604
|
-
optionValues = uses;
|
|
914
|
+
optionValues = uses.values;
|
|
605
915
|
} else {
|
|
606
916
|
optionValues = [];
|
|
607
917
|
for (const other of names) options.set(other, optionValues);
|
|
608
918
|
}
|
|
609
|
-
valuesMin = valueMin;
|
|
610
|
-
valuesMax = getMaxArgs(option.args);
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
919
|
+
this.valuesMin = this.valueMin;
|
|
920
|
+
this.valuesMax = getMaxArgs(option.args);
|
|
921
|
+
if (this.valuesMax > 0) {
|
|
922
|
+
this.values = optionValues;
|
|
923
|
+
this.valuesStart = optionValues.length;
|
|
924
|
+
this.valueArgs = asArray(option.args);
|
|
615
925
|
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
start,
|
|
625
|
-
end
|
|
626
|
-
}, ctx());
|
|
627
|
-
return;
|
|
926
|
+
}
|
|
927
|
+
/** Add a value to the open option. Returns whether it's a value the option accepts. */
|
|
928
|
+
addValue(value, separator) {
|
|
929
|
+
this.valueCount += 1;
|
|
930
|
+
if (!this.values) {
|
|
931
|
+
const name = this.openOptionName;
|
|
932
|
+
this.fail((ctx) => new InvalidOptionArg(name, ctx));
|
|
933
|
+
return false;
|
|
628
934
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
935
|
+
const problem = this.scope.separatorProblem(this.openOption, this.openOptionName, separator);
|
|
936
|
+
if (problem) this.fail((ctx) => new ParseError(ctx, problem));
|
|
937
|
+
this.values.push(value);
|
|
938
|
+
return problem === null;
|
|
939
|
+
}
|
|
940
|
+
/** Add a separate token as a value of the open option. */
|
|
941
|
+
value(token) {
|
|
942
|
+
this.addValue(token, "");
|
|
943
|
+
return {
|
|
944
|
+
kind: "value",
|
|
945
|
+
option: this.openOption
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Add a value attached to the option's own token, starting at `start`. It's
|
|
950
|
+
* the option's only value, so no later token can be one. Returns where the
|
|
951
|
+
* value starts, or null if the option doesn't accept it.
|
|
952
|
+
*/
|
|
953
|
+
addAttachedValue(start, value, separator) {
|
|
954
|
+
const option = this.openOption;
|
|
955
|
+
const accepted = this.addValue(value, separator);
|
|
956
|
+
this.valueMin = this.separateValueMax = 0;
|
|
957
|
+
return accepted ? {
|
|
958
|
+
option,
|
|
959
|
+
start
|
|
960
|
+
} : null;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Whether the command already has all the arguments it takes. A command that
|
|
964
|
+
* requires a subcommand takes none.
|
|
965
|
+
*/
|
|
966
|
+
get argsAreFull() {
|
|
967
|
+
return this.scope.command.requiresSubcommand === true || this.args.length >= this.scope.argsMax;
|
|
968
|
+
}
|
|
969
|
+
addArg(value) {
|
|
970
|
+
this.endOption();
|
|
971
|
+
if (this.argsAreFull) this.failArgs((ctx) => new TooManyArguments(value, ctx));
|
|
972
|
+
this.args.push(value);
|
|
973
|
+
return { kind: "argument" };
|
|
974
|
+
}
|
|
975
|
+
/** Read `--`. Every token after it is an argument. */
|
|
976
|
+
argSeparator() {
|
|
977
|
+
this.closeOption();
|
|
978
|
+
const command = this.scope.command;
|
|
979
|
+
if (this.argsAreFull && !command.requiresSubcommand) this.failArgs((ctx) => new ParseError(ctx, "Unexpected argument '--', did you mean to use an option instead?"));
|
|
980
|
+
this.argSeparatorIndex = this.args.length;
|
|
981
|
+
return { kind: "separator" };
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* How the next token can name a subcommand, its own or an inherited one, or
|
|
985
|
+
* null if it can't. It can't once an argument is found, or while the open
|
|
986
|
+
* option needs the next token as its value. A required or variadic option
|
|
987
|
+
* value takes priority over a subcommand. A short optional value could be a
|
|
988
|
+
* prefix of a subcommand's name, so only an exact name can end an option
|
|
989
|
+
* that still has room.
|
|
990
|
+
*/
|
|
991
|
+
get subcommandMatching() {
|
|
992
|
+
if (this.hasFoundArg || !this.scope.hasSubcommands) return null;
|
|
993
|
+
if (this.valueCount < this.valueMin || this.hasRoomForValue && this.separateValueMax === Infinity) return null;
|
|
994
|
+
return { exactOnly: this.hasRoomForValue };
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* The subcommand the next token enters if it's `token`, or why it enters
|
|
998
|
+
* none. Null if the next token can't be a subcommand at all.
|
|
999
|
+
*/
|
|
1000
|
+
subcommandFor(token) {
|
|
1001
|
+
const matching = this.subcommandMatching;
|
|
1002
|
+
return matching && this.scope.matchSubcommand(token, matching.exactOnly);
|
|
1003
|
+
}
|
|
1004
|
+
enterSubcommand(command) {
|
|
1005
|
+
this.closeOption();
|
|
1006
|
+
this.path.push(command);
|
|
1007
|
+
this.scope.enter(command);
|
|
1008
|
+
return {
|
|
1009
|
+
kind: "subcommand",
|
|
1010
|
+
command
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
/** Read a token like `-abc`, `+x` or `-ovalue`. */
|
|
1014
|
+
shortOptions(token) {
|
|
1015
|
+
const leadingChar = token[0];
|
|
1016
|
+
const dashOption = this.findOption(leadingChar);
|
|
1017
|
+
if (dashOption?.args && !this.findOption(token.slice(0, 2))) {
|
|
1018
|
+
this.useOption(dashOption, leadingChar);
|
|
1019
|
+
const attached = this.addAttachedValue(1, token.slice(1), "");
|
|
1020
|
+
return {
|
|
1021
|
+
kind: "options",
|
|
1022
|
+
options: [dashOption],
|
|
1023
|
+
attached
|
|
1024
|
+
};
|
|
651
1025
|
}
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
command = candidate;
|
|
662
|
-
prefixCount = -1;
|
|
663
|
-
break commands;
|
|
664
|
-
}
|
|
665
|
-
if (allowPrefix && names.startsWith(token)) {
|
|
666
|
-
command = candidate;
|
|
667
|
-
prefixCount += 1;
|
|
668
|
-
}
|
|
669
|
-
continue commands;
|
|
670
|
-
}
|
|
671
|
-
let prefixMatched = false;
|
|
672
|
-
for (const name of names) {
|
|
673
|
-
if (name === token) {
|
|
674
|
-
command = candidate;
|
|
675
|
-
prefixCount = -1;
|
|
676
|
-
break commands;
|
|
677
|
-
}
|
|
678
|
-
if (allowPrefix && !prefixMatched && name.startsWith(token)) {
|
|
679
|
-
prefixMatched = true;
|
|
680
|
-
command = candidate;
|
|
681
|
-
prefixCount += 1;
|
|
682
|
-
}
|
|
683
|
-
}
|
|
1026
|
+
const used = [];
|
|
1027
|
+
let attached = null;
|
|
1028
|
+
for (let char = 1; char < token.length; char++) {
|
|
1029
|
+
const optionName = leadingChar + token[char];
|
|
1030
|
+
const option = this.findOption(optionName);
|
|
1031
|
+
if (!option) {
|
|
1032
|
+
if (char === 1 && this.hasRoomForValue) return this.value(token);
|
|
1033
|
+
this.unknownOption(optionName);
|
|
1034
|
+
continue;
|
|
684
1035
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
1036
|
+
this.useOption(option, optionName);
|
|
1037
|
+
used.push(option);
|
|
1038
|
+
if (char === token.length - 1) break;
|
|
1039
|
+
const sep = this.scope.leadingSeparator(token.slice(char + 1));
|
|
1040
|
+
if (option.args || sep && !this.findOption(leadingChar + token[char + 1])) {
|
|
1041
|
+
const argStart = char + sep.length + 1;
|
|
1042
|
+
attached = this.addAttachedValue(argStart, token.slice(argStart), sep);
|
|
1043
|
+
break;
|
|
692
1044
|
}
|
|
693
1045
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
option: optionName,
|
|
715
|
-
validOptions: optionNames(local, persistent),
|
|
716
|
-
index,
|
|
717
|
-
start: char,
|
|
718
|
-
end: char + 1
|
|
719
|
-
}, ctx());
|
|
720
|
-
continue;
|
|
721
|
-
}
|
|
722
|
-
useOption(option, optionName);
|
|
723
|
-
if (option.args && char < token.length - 1) {
|
|
724
|
-
const remainingChars = token.slice(char + 1);
|
|
725
|
-
let sep = "";
|
|
726
|
-
for (const candidate of separators) if (remainingChars.startsWith(candidate)) {
|
|
727
|
-
sep = candidate;
|
|
728
|
-
break;
|
|
729
|
-
}
|
|
730
|
-
const argStart = char + sep.length + 1;
|
|
731
|
-
addValue(index, argStart, token.length, token.slice(argStart), sep);
|
|
732
|
-
break;
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
continue tokens;
|
|
736
|
-
}
|
|
737
|
-
if (!posixCompliantOptions || token.length > 2 && isDoubleDash) {
|
|
738
|
-
let foundSep = "";
|
|
739
|
-
let foundSepIndex = -1;
|
|
740
|
-
for (const sep of separators) {
|
|
741
|
-
const at = token.indexOf(sep);
|
|
742
|
-
if (at === -1) continue;
|
|
743
|
-
if (foundSepIndex === -1 || at < foundSepIndex || at === foundSepIndex && sep.length > foundSep.length) {
|
|
744
|
-
foundSep = sep;
|
|
745
|
-
foundSepIndex = at;
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
const optionName = foundSep ? token.slice(0, foundSepIndex) : token;
|
|
749
|
-
const option = findOption(local, persistent, optionName);
|
|
750
|
-
if (option) {
|
|
751
|
-
useOption(option, optionName);
|
|
752
|
-
if (foundSep) {
|
|
753
|
-
const valueStart = foundSepIndex + foundSep.length;
|
|
754
|
-
addValue(index, valueStart, token.length, token.slice(valueStart), foundSep);
|
|
755
|
-
}
|
|
756
|
-
continue tokens;
|
|
757
|
-
}
|
|
758
|
-
if (posixCompliantOptions) {
|
|
759
|
-
if (valueCount < separateValueMax) {
|
|
760
|
-
addValue(index, 0, token.length, token, "");
|
|
761
|
-
continue tokens;
|
|
762
|
-
}
|
|
763
|
-
if (strict) throw new UnknownOption({
|
|
764
|
-
option: optionName,
|
|
765
|
-
validOptions: optionNames(local, persistent),
|
|
766
|
-
index,
|
|
767
|
-
start: 0,
|
|
768
|
-
end: optionName.length
|
|
769
|
-
}, ctx());
|
|
770
|
-
continue tokens;
|
|
771
|
-
}
|
|
1046
|
+
return used.length > 0 ? {
|
|
1047
|
+
kind: "options",
|
|
1048
|
+
options: used,
|
|
1049
|
+
attached
|
|
1050
|
+
} : { kind: "unknown" };
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Read a token like `--name` or `--name=value`, or any token when options
|
|
1054
|
+
* aren't POSIX compliant. Returns null if the token should be an argument.
|
|
1055
|
+
*/
|
|
1056
|
+
longOption(token) {
|
|
1057
|
+
const found = this.scope.firstSeparator(token);
|
|
1058
|
+
const optionName = found ? token.slice(0, found.index) : token;
|
|
1059
|
+
const option = this.findOption(optionName);
|
|
1060
|
+
if (option) {
|
|
1061
|
+
this.useOption(option, optionName);
|
|
1062
|
+
let attached = null;
|
|
1063
|
+
if (found) {
|
|
1064
|
+
const valueStart = found.index + found.separator.length;
|
|
1065
|
+
attached = this.addAttachedValue(valueStart, token.slice(valueStart), found.separator);
|
|
772
1066
|
}
|
|
1067
|
+
return {
|
|
1068
|
+
kind: "options",
|
|
1069
|
+
options: [option],
|
|
1070
|
+
attached
|
|
1071
|
+
};
|
|
773
1072
|
}
|
|
774
|
-
if (
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
if
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
1073
|
+
if (this.directives.flagsArePosixNoncompliant) return null;
|
|
1074
|
+
if (this.hasRoomForValue) return this.value(token);
|
|
1075
|
+
this.unknownOption(optionName);
|
|
1076
|
+
return { kind: "unknown" };
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Read a token that isn't `--`, a subcommand or an option. It's a value of
|
|
1080
|
+
* the open option if that has room, and otherwise an argument, which stops
|
|
1081
|
+
* later tokens from being subcommands. `unmatched` is why the token isn't a
|
|
1082
|
+
* subcommand, if it could have been one.
|
|
1083
|
+
*/
|
|
1084
|
+
valueOrArg(token, unmatched) {
|
|
1085
|
+
if (this.hasRoomForValue) return this.value(token);
|
|
1086
|
+
if (unmatched && this.argsAreFull) {
|
|
1087
|
+
const command = this.scope.command;
|
|
1088
|
+
if (command.subcommands?.length || command.requiresSubcommand) this.failArgs((ctx) => {
|
|
1089
|
+
const { message, didYouMean } = unmatched.reason();
|
|
1090
|
+
return new ParseError(ctx, message, didYouMean);
|
|
1091
|
+
});
|
|
788
1092
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1093
|
+
this.hasFoundArg = true;
|
|
1094
|
+
return this.addArg(token);
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Throw if the finished command line is missing anything. Returns what it
|
|
1098
|
+
* was parsed as. Only call this when there's no {@link problem}.
|
|
1099
|
+
*/
|
|
1100
|
+
finish() {
|
|
1101
|
+
const command = this.scope.command;
|
|
1102
|
+
const showsUsage = command.requiresSubcommand && !command.action && this.args.length === 0;
|
|
1103
|
+
const skipsChecks = this.optionAction !== void 0 || showsUsage;
|
|
1104
|
+
if (!skipsChecks && this.tooManyArgs) throw this.tooManyArgs();
|
|
1105
|
+
if (!skipsChecks && this.args.length < this.scope.argsMin) throw new TooFewArguments(this.scope.argsMin, this.scope.argsMax, this.ctx());
|
|
1106
|
+
this.endOption();
|
|
1107
|
+
if (this.problem) throw this.problem();
|
|
1108
|
+
if (!skipsChecks) checkOptionRules(this.options, this.seenOptions, this.scope.required(), this.ctx());
|
|
1109
|
+
if (!command.requiresSubcommand && this.args.length >= this.scope.argsMin) addDefaults(this.args, asArray(command.args), this.args.length);
|
|
1110
|
+
return {
|
|
1111
|
+
path: this.path,
|
|
1112
|
+
action: this.optionAction ?? command.action ?? usage,
|
|
1113
|
+
args: this.args,
|
|
1114
|
+
argSeparatorIndex: this.argSeparatorIndex,
|
|
1115
|
+
options: this.options,
|
|
1116
|
+
defaults: (name) => this.defaults(name)
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
/** See {@link ParseResult.defaults}. */
|
|
1120
|
+
defaults(name) {
|
|
1121
|
+
if (this.options.has(name)) return;
|
|
1122
|
+
const args = asArray(this.scope.resolve(name)?.args);
|
|
1123
|
+
if (args.length === 0 || args[0].isOptional) return;
|
|
1124
|
+
const values = [];
|
|
1125
|
+
addDefaults(values, args);
|
|
1126
|
+
return values.length > 0 ? values : void 0;
|
|
1127
|
+
}
|
|
1128
|
+
/** Whether using `option` next would go over its repeat limit or break an `exclusiveOn` rule. */
|
|
1129
|
+
conflicts(option) {
|
|
1130
|
+
if ((this.repeatedUses.get(optionKey(option))?.count ?? 0) >= repeatLimit(option)) return true;
|
|
1131
|
+
const options = this.options;
|
|
1132
|
+
if (option.exclusiveOn?.some((name) => options.has(name))) return true;
|
|
1133
|
+
const names = asArray(option.name);
|
|
1134
|
+
return this.seenOptions.some((seen) => seen.exclusiveOn?.some((name) => names.includes(name)));
|
|
1135
|
+
}
|
|
1136
|
+
/** The options the next token can use. See {@link NextToken.options}. */
|
|
1137
|
+
usableOptions() {
|
|
1138
|
+
const used = new Set(this.seenOptions.map(optionKey));
|
|
1139
|
+
const { local, persistent } = this.scope.live(this.subcommandMatching);
|
|
1140
|
+
const usable = [];
|
|
1141
|
+
for (const { option, names } of [...local, ...persistent]) {
|
|
1142
|
+
if (this.conflicts(option)) continue;
|
|
1143
|
+
usable.push({
|
|
1144
|
+
option,
|
|
1145
|
+
forms: this.scope.forms(option, names),
|
|
1146
|
+
used: used.has(optionKey(option))
|
|
1147
|
+
});
|
|
792
1148
|
}
|
|
793
|
-
|
|
794
|
-
|
|
1149
|
+
return usable;
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* What the next token can be. See {@link readNext}. Reading more tokens
|
|
1153
|
+
* doesn't change what this returns.
|
|
1154
|
+
*/
|
|
1155
|
+
next() {
|
|
1156
|
+
const open = this.openOption && this.hasRoomForValue ? this.openOption : null;
|
|
1157
|
+
const command = this.scope.command;
|
|
1158
|
+
const afterSeparator = this.argSeparatorIndex !== -1;
|
|
1159
|
+
const optionsAllowed = !afterSeparator && this.optionsAllowed;
|
|
1160
|
+
return {
|
|
1161
|
+
path: [...this.path],
|
|
1162
|
+
directives: { ...this.directives },
|
|
1163
|
+
optionsAllowed,
|
|
1164
|
+
subcommands: !afterSeparator && this.subcommandMatching ? this.scope.subcommands() : [],
|
|
1165
|
+
options: optionsAllowed ? this.usableOptions() : [],
|
|
1166
|
+
value: open && {
|
|
1167
|
+
option: open,
|
|
1168
|
+
arg: argAt(asArray(open.args), this.valueCount),
|
|
1169
|
+
required: this.valueCount < this.valueMin
|
|
1170
|
+
},
|
|
1171
|
+
arg: open ? void 0 : argAt(asArray(command.args), this.args.length)
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
/** Whether the next token can be an option. Once options must stop, every token is an argument. */
|
|
1175
|
+
get optionsAllowed() {
|
|
1176
|
+
return !(this.directives.optionsMustPrecedeArguments && this.hasFoundArg);
|
|
1177
|
+
}
|
|
1178
|
+
read(token) {
|
|
1179
|
+
if (this.argSeparatorIndex !== -1) return this.addArg(token);
|
|
1180
|
+
const optionsAllowed = this.optionsAllowed;
|
|
1181
|
+
if (token === "--" && optionsAllowed) return this.argSeparator();
|
|
1182
|
+
const subcommand = this.subcommandFor(token);
|
|
1183
|
+
if (subcommand?.command) return this.enterSubcommand(subcommand.command);
|
|
1184
|
+
if (optionsAllowed) {
|
|
1185
|
+
const flagsArePosixNoncompliant = this.directives.flagsArePosixNoncompliant;
|
|
1186
|
+
const first = token.charCodeAt(0);
|
|
1187
|
+
const isDoubleDash = first === 45 && token.charCodeAt(1) === 45;
|
|
1188
|
+
if (!flagsArePosixNoncompliant && token.length > 1 && (first === 45 || first === 43) && !isDoubleDash) return this.shortOptions(token);
|
|
1189
|
+
if (flagsArePosixNoncompliant || token.length > 2 && isDoubleDash) {
|
|
1190
|
+
const reading = this.longOption(token);
|
|
1191
|
+
if (reading) return reading;
|
|
1192
|
+
}
|
|
795
1193
|
}
|
|
1194
|
+
return this.valueOrArg(token, subcommand);
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
/** The argument at `index`, reusing a final variadic argument. */
|
|
1198
|
+
function argAt(args, index) {
|
|
1199
|
+
if (index < args.length) return args[index];
|
|
1200
|
+
const last = args[args.length - 1];
|
|
1201
|
+
return last?.isVariadic ? last : void 0;
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Work out what the token after `before` can be, and how the parser reads
|
|
1205
|
+
* `partial`: whether it's an option, and if so, whether it has a value
|
|
1206
|
+
* attached. Reads leniently, so a line that's wrong at the moment still has
|
|
1207
|
+
* an answer. Completion is built on this.
|
|
1208
|
+
*/
|
|
1209
|
+
function readNext(spec, before, partial) {
|
|
1210
|
+
const s = new Scanner(spec);
|
|
1211
|
+
for (const token of before) s.read(token);
|
|
1212
|
+
const next = s.next();
|
|
1213
|
+
let attached = null;
|
|
1214
|
+
let partialIsOption = false;
|
|
1215
|
+
if (partial !== "" && next.optionsAllowed) {
|
|
1216
|
+
const reading = s.read(partial);
|
|
1217
|
+
partialIsOption = reading.kind === "options" || reading.kind === "unknown" || reading.kind === "separator";
|
|
1218
|
+
const found = reading.kind === "options" ? reading.attached : null;
|
|
1219
|
+
const arg = found && asArray(found.option.args)[0];
|
|
1220
|
+
if (found && arg) attached = {
|
|
1221
|
+
option: found.option,
|
|
1222
|
+
arg,
|
|
1223
|
+
prefix: partial.slice(0, found.start),
|
|
1224
|
+
value: partial.slice(found.start)
|
|
1225
|
+
};
|
|
796
1226
|
}
|
|
797
|
-
const open = openOption;
|
|
798
1227
|
return {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
args,
|
|
803
|
-
argSeparatorIndex,
|
|
804
|
-
options,
|
|
805
|
-
seenOptions,
|
|
806
|
-
localOptions: local,
|
|
807
|
-
persistentOptions: persistent,
|
|
808
|
-
openOption: open && valueCount < separateValueMax ? {
|
|
809
|
-
option: open,
|
|
810
|
-
valueCount
|
|
811
|
-
} : null
|
|
1228
|
+
...next,
|
|
1229
|
+
attached,
|
|
1230
|
+
partialIsOption
|
|
812
1231
|
};
|
|
813
1232
|
}
|
|
814
1233
|
/**
|
|
815
1234
|
* Parse the input tokens using the given CLI spec.
|
|
816
1235
|
*
|
|
817
1236
|
* This returns a `ParseResult`, which contains the array of
|
|
818
|
-
* subcommands used, the provided options, and the
|
|
1237
|
+
* subcommands used, the provided options, the arguments, and the action to
|
|
1238
|
+
* run.
|
|
819
1239
|
*
|
|
820
1240
|
* If parsing fails, this function throws.
|
|
821
|
-
*
|
|
822
|
-
* This function is exposed to make it easier to build a custom runner.
|
|
823
1241
|
*/
|
|
824
1242
|
function parse(input, spec) {
|
|
825
|
-
|
|
1243
|
+
const s = new Scanner(spec);
|
|
1244
|
+
for (const token of input) {
|
|
1245
|
+
s.read(token);
|
|
1246
|
+
if (s.problem) throw s.problem();
|
|
1247
|
+
}
|
|
1248
|
+
return s.finish();
|
|
826
1249
|
}
|
|
827
1250
|
// src/completion.ts
|
|
828
1251
|
const SHELLS = [
|
|
829
1252
|
"bash",
|
|
830
1253
|
"zsh",
|
|
831
|
-
"fish"
|
|
1254
|
+
"fish",
|
|
1255
|
+
"powershell",
|
|
1256
|
+
"nushell"
|
|
832
1257
|
];
|
|
833
1258
|
function isShell(value) {
|
|
834
1259
|
return SHELLS.includes(value);
|
|
@@ -844,73 +1269,30 @@ function shQuote(value) {
|
|
|
844
1269
|
function fishQuote(value) {
|
|
845
1270
|
return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
|
|
846
1271
|
}
|
|
1272
|
+
/** Single-quote a string for PowerShell (a quote is written twice). */
|
|
1273
|
+
function psQuote(value) {
|
|
1274
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
1275
|
+
}
|
|
847
1276
|
/**
|
|
848
|
-
*
|
|
849
|
-
*
|
|
1277
|
+
* Quote a string for Nushell. A single-quoted string has no escapes, so a name
|
|
1278
|
+
* with a quote in it is written as a double-quoted string instead.
|
|
850
1279
|
*/
|
|
851
|
-
function
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
const presentNames = /* @__PURE__ */ new Set();
|
|
855
|
-
const countByOption = /* @__PURE__ */ new Map();
|
|
856
|
-
for (const option of scanned.seenOptions) {
|
|
857
|
-
for (const name of makeArray(option.name)) presentNames.add(name);
|
|
858
|
-
countByOption.set(option, (countByOption.get(option) ?? 0) + 1);
|
|
859
|
-
}
|
|
860
|
-
const open = scanned.openOption;
|
|
861
|
-
const pending = open ? {
|
|
862
|
-
option: open.option,
|
|
863
|
-
argIndex: open.valueCount
|
|
864
|
-
} : null;
|
|
865
|
-
const availableOptions = [];
|
|
866
|
-
const seen = /* @__PURE__ */ new Set();
|
|
867
|
-
for (const list of [scanned.localOptions, scanned.persistentOptions]) {
|
|
868
|
-
const byName = /* @__PURE__ */ new Map();
|
|
869
|
-
for (const option of list) for (const name of makeArray(option.name)) byName.set(name, option);
|
|
870
|
-
for (const option of byName.values()) if (!seen.has(option)) {
|
|
871
|
-
seen.add(option);
|
|
872
|
-
availableOptions.push(option);
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
return {
|
|
876
|
-
path,
|
|
877
|
-
current: path[path.length - 1],
|
|
878
|
-
availableOptions,
|
|
879
|
-
pending,
|
|
880
|
-
positionalIndex: scanned.args.length,
|
|
881
|
-
presentNames,
|
|
882
|
-
countByOption,
|
|
883
|
-
forceArgs: scanned.argSeparatorIndex !== -1,
|
|
884
|
-
sawPositional: scanned.args.length > 0
|
|
885
|
-
};
|
|
886
|
-
}
|
|
887
|
-
/** A directive's value in effect (the deepest command that sets it). */
|
|
888
|
-
function directiveOnPath(path, key) {
|
|
889
|
-
for (let i = path.length - 1; i >= 0; i--) {
|
|
890
|
-
const value = path[i].parserDirectives?.[key];
|
|
891
|
-
if (typeof value === "boolean") return value;
|
|
892
|
-
}
|
|
893
|
-
return false;
|
|
894
|
-
}
|
|
895
|
-
/** The option-arg separators in effect (the deepest command that sets them). */
|
|
896
|
-
function separatorsOnPath(path) {
|
|
897
|
-
let separators = ["="];
|
|
898
|
-
for (const command of path) {
|
|
899
|
-
const configured = command.parserDirectives?.optionArgSeparators;
|
|
900
|
-
if (configured) separators = makeArray(configured);
|
|
901
|
-
}
|
|
902
|
-
return separators;
|
|
1280
|
+
function nuQuote(value) {
|
|
1281
|
+
if (!value.includes("'")) return `'${value}'`;
|
|
1282
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
|
|
903
1283
|
}
|
|
904
1284
|
/** Enumerate filesystem entries matching `partial` for a path template. */
|
|
905
1285
|
async function enumeratePaths(partial, template) {
|
|
906
1286
|
const { readdirSync } = await import("node:fs");
|
|
907
|
-
const
|
|
1287
|
+
const isWindows = globalThis.process?.platform === "win32";
|
|
1288
|
+
const slash = Math.max(partial.lastIndexOf("/"), isWindows ? partial.lastIndexOf("\\") : -1);
|
|
908
1289
|
const dir = slash >= 0 ? partial.slice(0, slash + 1) : "";
|
|
909
1290
|
const base = slash >= 0 ? partial.slice(slash + 1) : partial;
|
|
1291
|
+
const separator = slash >= 0 ? partial[slash] : "/";
|
|
910
1292
|
const foldersOnly = template === "folders";
|
|
911
1293
|
let entries;
|
|
912
1294
|
try {
|
|
913
|
-
entries = readdirSync(dir || ".", { withFileTypes: true });
|
|
1295
|
+
entries = readdirSync((isWindows ? dir.replaceAll("\\", "/") : dir) || ".", { withFileTypes: true });
|
|
914
1296
|
} catch {
|
|
915
1297
|
return [];
|
|
916
1298
|
}
|
|
@@ -919,7 +1301,7 @@ async function enumeratePaths(partial, template) {
|
|
|
919
1301
|
if (!entry.name.startsWith(base)) continue;
|
|
920
1302
|
const isDir = entry.isDirectory();
|
|
921
1303
|
if (foldersOnly && !isDir) continue;
|
|
922
|
-
candidates.push({ value: `${dir}${entry.name}${isDir ?
|
|
1304
|
+
candidates.push({ value: `${dir}${entry.name}${isDir ? separator : ""}` });
|
|
923
1305
|
}
|
|
924
1306
|
return candidates;
|
|
925
1307
|
}
|
|
@@ -941,116 +1323,75 @@ async function argValues(arg, tokens, partial) {
|
|
|
941
1323
|
if (arg.template) candidates.push(...await enumeratePaths(partial, arg.template));
|
|
942
1324
|
return candidates;
|
|
943
1325
|
}
|
|
944
|
-
/** Pick the argument at an index, reusing a final variadic argument. */
|
|
945
|
-
function argAt(args, index) {
|
|
946
|
-
if (args.length === 0) return void 0;
|
|
947
|
-
if (index < args.length) return args[index];
|
|
948
|
-
const last = args[args.length - 1];
|
|
949
|
-
return last?.isVariadic ? last : void 0;
|
|
950
|
-
}
|
|
951
|
-
/** Pick the argument being completed at a positional index (honors variadic). */
|
|
952
|
-
function positionalArg(command, index) {
|
|
953
|
-
return argAt(asArray(command.args), index);
|
|
954
|
-
}
|
|
955
1326
|
/** Decide which kinds of candidate the word being completed can use. */
|
|
956
|
-
function wantedCandidates(
|
|
957
|
-
|
|
958
|
-
|
|
1327
|
+
function wantedCandidates(next, req) {
|
|
1328
|
+
if (!req.gateOptions || next.directives.flagsArePosixNoncompliant) return {
|
|
1329
|
+
wantValues: true,
|
|
1330
|
+
wantOptions: true,
|
|
1331
|
+
wantSubcommand: () => true
|
|
1332
|
+
};
|
|
1333
|
+
const partial = req.partial;
|
|
1334
|
+
const wantValues = !next.partialIsOption;
|
|
1335
|
+
const starts = (name) => partial !== "" && name.startsWith(partial);
|
|
959
1336
|
return {
|
|
960
|
-
wantValues
|
|
961
|
-
wantOptions:
|
|
1337
|
+
wantValues,
|
|
1338
|
+
wantOptions: next.options.some(({ forms }) => forms.some(({ name }) => starts(name))),
|
|
1339
|
+
wantSubcommand: (name) => wantValues || starts(name)
|
|
962
1340
|
};
|
|
963
1341
|
}
|
|
964
1342
|
/**
|
|
965
|
-
* Resolve completion candidates for a partial command line
|
|
966
|
-
*
|
|
967
|
-
*
|
|
1343
|
+
* Resolve completion candidates for a partial command line. The parser works
|
|
1344
|
+
* out what the word can be with {@link readNext}, and this offers candidates
|
|
1345
|
+
* for each of those.
|
|
968
1346
|
*/
|
|
969
1347
|
async function completeRuntime(req) {
|
|
970
|
-
const
|
|
971
|
-
const separators = separatorsOnPath(state.path);
|
|
972
|
-
const optionValue = await resolveOptionValue(state, req, separators);
|
|
973
|
-
if (optionValue) return optionValue;
|
|
974
|
-
return freshTokenCandidates(state, req, separators);
|
|
975
|
-
}
|
|
976
|
-
/**
|
|
977
|
-
* Candidates when the word being completed is a option's value — either attached
|
|
978
|
-
* (`--option=value`) or the separate token after a option that expects one. Returns
|
|
979
|
-
* null when no option value is being completed.
|
|
980
|
-
*/
|
|
981
|
-
async function resolveOptionValue(state, req, separators) {
|
|
1348
|
+
const next = readNext(req.spec, req.before, req.partial);
|
|
982
1349
|
const tokens = [...req.before];
|
|
983
|
-
|
|
984
|
-
const
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
const option = state.availableOptions.find((candidate) => makeArray(candidate.name).includes(optionName));
|
|
988
|
-
const arg = option && asArray(option.args)[0];
|
|
989
|
-
if (option && arg) return (await argValues(arg, tokens, req.partial.slice(at + separator.length))).map((candidate) => ({
|
|
990
|
-
value: `${optionName}${separator}${candidate.value}`,
|
|
1350
|
+
if (next.attached) {
|
|
1351
|
+
const { prefix, arg, value } = next.attached;
|
|
1352
|
+
return (await argValues(arg, tokens, value)).map((candidate) => ({
|
|
1353
|
+
value: prefix + candidate.value,
|
|
991
1354
|
description: candidate.description
|
|
992
1355
|
}));
|
|
993
1356
|
}
|
|
994
|
-
if (
|
|
995
|
-
const { option, argIndex } = state.pending;
|
|
996
|
-
const args = asArray(option.args);
|
|
997
|
-
const arg = argAt(args, argIndex);
|
|
998
|
-
if (argIndex < args.length) return arg ? argValues(arg, tokens, req.partial) : [];
|
|
999
|
-
const { wantValues, wantOptions } = wantedCandidates(state, req);
|
|
1000
|
-
return [...arg && wantValues ? await argValues(arg, tokens, req.partial) : [], ...wantOptions ? allowedOptionCandidates(state, separators) : []];
|
|
1001
|
-
}
|
|
1002
|
-
return null;
|
|
1003
|
-
}
|
|
1004
|
-
/** Candidates when completing a fresh token: subcommand / positional / option. */
|
|
1005
|
-
async function freshTokenCandidates(state, req, separators) {
|
|
1357
|
+
if (next.value?.required) return next.value.arg ? argValues(next.value.arg, tokens, req.partial) : [];
|
|
1006
1358
|
const candidates = [];
|
|
1007
|
-
const
|
|
1008
|
-
const { wantValues, wantOptions } = wantedCandidates(state, req);
|
|
1359
|
+
const { wantValues, wantOptions, wantSubcommand } = wantedCandidates(next, req);
|
|
1009
1360
|
if (wantValues) {
|
|
1010
|
-
const
|
|
1011
|
-
if (
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
} else if (state.forceArgs) return candidates;
|
|
1022
|
-
if (wantOptions) candidates.push(...allowedOptionCandidates(state, separators));
|
|
1361
|
+
const arg = next.value ? next.value.arg : next.arg;
|
|
1362
|
+
if (arg) candidates.push(...await argValues(arg, tokens, req.partial));
|
|
1363
|
+
}
|
|
1364
|
+
for (const sub of visibleSubcommands(next.subcommands)) {
|
|
1365
|
+
const description = firstLine(sub.description);
|
|
1366
|
+
for (const token of makeArray(sub.name)) if (wantSubcommand(token)) candidates.push({
|
|
1367
|
+
value: token,
|
|
1368
|
+
description
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
if (wantOptions) candidates.push(...optionCandidates(next));
|
|
1023
1372
|
return candidates;
|
|
1024
1373
|
}
|
|
1025
|
-
/**
|
|
1026
|
-
function
|
|
1027
|
-
return state.sawPositional && directiveOnPath(state.path, "optionsMustPrecedeArguments") ? [] : optionCandidates(state, separators);
|
|
1028
|
-
}
|
|
1029
|
-
/** Eligible option candidates after relationship filtering. */
|
|
1030
|
-
function optionCandidates(state, separators) {
|
|
1374
|
+
/** The options a user can use next, required ones first. */
|
|
1375
|
+
function optionCandidates(next) {
|
|
1031
1376
|
const required = [];
|
|
1032
1377
|
const optional = [];
|
|
1033
|
-
for (const option of
|
|
1378
|
+
for (const { option, forms, used } of next.options) {
|
|
1034
1379
|
if (option.hidden) continue;
|
|
1035
|
-
|
|
1036
|
-
if (count > 0) {
|
|
1037
|
-
if (!option.isRepeatable) continue;
|
|
1038
|
-
if (typeof option.isRepeatable === "number" && count >= option.isRepeatable) continue;
|
|
1039
|
-
}
|
|
1040
|
-
if (option.exclusiveOn?.some((name) => state.presentNames.has(name))) continue;
|
|
1041
|
-
if (state.availableOptions.some((other) => other !== option && makeArray(other.name).some((name) => state.presentNames.has(name)) && other.exclusiveOn?.some((name) => makeArray(option.name).includes(name)))) continue;
|
|
1380
|
+
if (used && !option.isRepeatable) continue;
|
|
1042
1381
|
let description = firstLine(option.description);
|
|
1043
1382
|
if (option.dependsOn) {
|
|
1044
1383
|
const needs = option.dependsOn.join(", ");
|
|
1045
1384
|
description = description ? `${description} (requires ${needs})` : `requires ${needs}`;
|
|
1046
1385
|
}
|
|
1047
1386
|
if (option.isRequired) description = description ? `${description} [required]` : "[required]";
|
|
1048
|
-
const sep = option.requiresSeparator === true ? separators[0] : typeof option.requiresSeparator === "string" ? option.requiresSeparator : void 0;
|
|
1049
1387
|
const bucket = option.isRequired ? required : optional;
|
|
1050
|
-
for (const
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1388
|
+
for (const { name, join } of forms) {
|
|
1389
|
+
if (join === "") continue;
|
|
1390
|
+
bucket.push({
|
|
1391
|
+
value: name + (join ?? ""),
|
|
1392
|
+
description
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1054
1395
|
}
|
|
1055
1396
|
return [...required, ...optional];
|
|
1056
1397
|
}
|
|
@@ -1088,6 +1429,85 @@ function renderRuntimeShim(spec, shell) {
|
|
|
1088
1429
|
`}`,
|
|
1089
1430
|
`_${id}_complete "$@"`
|
|
1090
1431
|
].join("\n");
|
|
1432
|
+
if (shell === "powershell") {
|
|
1433
|
+
const stringAst = "[System.Management.Automation.Language.StringConstantExpressionAst]";
|
|
1434
|
+
return [
|
|
1435
|
+
`Register-ArgumentCompleter -Native -CommandName ${psQuote(name)} -ScriptBlock {`,
|
|
1436
|
+
` param($wordToComplete, $commandAst, $cursorPosition)`,
|
|
1437
|
+
` $elements = $commandAst.CommandElements`,
|
|
1438
|
+
` $first = $elements[0]`,
|
|
1439
|
+
` $bin = if ($first -is ${stringAst}) { $first.Value } else { $first.Extent.Text }`,
|
|
1440
|
+
` if (-not (Get-Command $bin -ErrorAction SilentlyContinue)) { return }`,
|
|
1441
|
+
` $partial = $wordToComplete`,
|
|
1442
|
+
` if ($partial.StartsWith("'")) { $partial = $partial.Substring(1).TrimEnd("'").Replace("''", "'") }`,
|
|
1443
|
+
` elseif ($partial.StartsWith('"')) { $partial = $partial.Substring(1).TrimEnd('"') }`,
|
|
1444
|
+
` $arguments = @('completion', 'complete', '--gate-options')`,
|
|
1445
|
+
` if ($partial -eq '') { $arguments += '--empty-word' }`,
|
|
1446
|
+
` $arguments += '--'`,
|
|
1447
|
+
` for ($i = 1; $i -lt $elements.Count; $i++) {`,
|
|
1448
|
+
` $element = $elements[$i]`,
|
|
1449
|
+
` if ($element.Extent.EndOffset -ge $cursorPosition) { break }`,
|
|
1450
|
+
` if ($element -is ${stringAst}) {`,
|
|
1451
|
+
` $arguments += $element.Value`,
|
|
1452
|
+
` } else {`,
|
|
1453
|
+
` $arguments += $element.Extent.Text`,
|
|
1454
|
+
` }`,
|
|
1455
|
+
` }`,
|
|
1456
|
+
` if ($partial -ne '') { $arguments += $partial }`,
|
|
1457
|
+
` & $bin @arguments 2>$null | ForEach-Object {`,
|
|
1458
|
+
" $value, $description = $_ -split \"`t\", 2",
|
|
1459
|
+
` if (-not $value.StartsWith($partial, [System.StringComparison]::OrdinalIgnoreCase)) { return }`,
|
|
1460
|
+
" $text = if ($value -match '[\\s''\"`$&|;,(){}<>@#]') { \"'$($value.Replace(\"'\", \"''\"))'\" } else { $value }",
|
|
1461
|
+
` if (-not $description) { $description = $value }`,
|
|
1462
|
+
` [System.Management.Automation.CompletionResult]::new($text, $value, 'ParameterValue', $description)`,
|
|
1463
|
+
` }`,
|
|
1464
|
+
`}`
|
|
1465
|
+
].join("\n");
|
|
1466
|
+
}
|
|
1467
|
+
if (shell === "nushell") {
|
|
1468
|
+
const previous = `$__${id}_previous_completer`;
|
|
1469
|
+
const names = [
|
|
1470
|
+
"",
|
|
1471
|
+
".exe",
|
|
1472
|
+
".cmd",
|
|
1473
|
+
".bat"
|
|
1474
|
+
].map((extension) => nuQuote(name + extension)).join(" ");
|
|
1475
|
+
return [
|
|
1476
|
+
`let __${id}_previous_completer = $env.config.completions.external.completer?`,
|
|
1477
|
+
`$env.config.completions.external.completer = {|spans|`,
|
|
1478
|
+
` let words = $spans | each {|span|`,
|
|
1479
|
+
` let quote = $span | str substring 0..0`,
|
|
1480
|
+
` if $quote in ["'", '"', '\`'] { $span | str substring 1.. | str trim --right --char $quote } else { $span }`,
|
|
1481
|
+
` }`,
|
|
1482
|
+
` let command = $words.0`,
|
|
1483
|
+
` if ($command | path basename) not-in [${names}] {`,
|
|
1484
|
+
` return (if ${previous} != null { do ${previous} $spans })`,
|
|
1485
|
+
` }`,
|
|
1486
|
+
` if (which $command | is-empty) { return }`,
|
|
1487
|
+
` let args = $words | skip 1`,
|
|
1488
|
+
` let partial = $args | last | default ""`,
|
|
1489
|
+
` ^$command completion complete --gate-options -- ...$args`,
|
|
1490
|
+
` | complete`,
|
|
1491
|
+
` | get stdout`,
|
|
1492
|
+
` | lines`,
|
|
1493
|
+
` | each {|line|`,
|
|
1494
|
+
` let parts = $line | split row "\\t"`,
|
|
1495
|
+
` { value: $parts.0, description: ($parts | get 1? | default "") }`,
|
|
1496
|
+
` }`,
|
|
1497
|
+
` | where { $in.value | str starts-with --ignore-case $partial }`,
|
|
1498
|
+
" | update value {|candidate|",
|
|
1499
|
+
" let value = $candidate.value",
|
|
1500
|
+
" if ($value !~ \"[\\\\s'\\\"`$|;#()\\\\[\\\\]{}]\") {",
|
|
1501
|
+
" $value",
|
|
1502
|
+
" } else if ($value | str contains \"'\") {",
|
|
1503
|
+
" $value | to json",
|
|
1504
|
+
" } else {",
|
|
1505
|
+
" $\"'($value)'\"",
|
|
1506
|
+
" }",
|
|
1507
|
+
" }",
|
|
1508
|
+
`}`
|
|
1509
|
+
].join("\n");
|
|
1510
|
+
}
|
|
1091
1511
|
return [
|
|
1092
1512
|
`function __${id}_complete`,
|
|
1093
1513
|
` set -l toks (commandline -opc)`,
|
|
@@ -1110,7 +1530,8 @@ const completion = {
|
|
|
1110
1530
|
args: {
|
|
1111
1531
|
name: "shell",
|
|
1112
1532
|
isOptional: true,
|
|
1113
|
-
suggestions: SHELLS
|
|
1533
|
+
suggestions: SHELLS,
|
|
1534
|
+
description: `One of: ${SHELLS.join(", ")}`
|
|
1114
1535
|
},
|
|
1115
1536
|
subcommands: [{
|
|
1116
1537
|
name: "complete",
|
|
@@ -1119,6 +1540,9 @@ const completion = {
|
|
|
1119
1540
|
options: [{
|
|
1120
1541
|
name: "--gate-options",
|
|
1121
1542
|
description: "Only suggest options after a leading dash"
|
|
1543
|
+
}, {
|
|
1544
|
+
name: "--empty-word",
|
|
1545
|
+
description: "The word being completed is empty, so every token is before it"
|
|
1122
1546
|
}],
|
|
1123
1547
|
args: {
|
|
1124
1548
|
name: "tokens",
|
|
@@ -1127,10 +1551,11 @@ const completion = {
|
|
|
1127
1551
|
},
|
|
1128
1552
|
async action({ path, args, options }) {
|
|
1129
1553
|
const spec = path[0];
|
|
1130
|
-
const
|
|
1554
|
+
const emptyWord = options.has("--empty-word");
|
|
1555
|
+
const partial = emptyWord || args.length === 0 ? "" : args[args.length - 1];
|
|
1131
1556
|
const lines = (await completeRuntime({
|
|
1132
1557
|
spec,
|
|
1133
|
-
before: args.slice(0, -1),
|
|
1558
|
+
before: emptyWord ? args : args.slice(0, -1),
|
|
1134
1559
|
partial,
|
|
1135
1560
|
gateOptions: options.has("--gate-options")
|
|
1136
1561
|
})).map((candidate) => candidate.description ? `${candidate.value}\t${candidate.description}` : candidate.value);
|
|
@@ -1138,19 +1563,17 @@ const completion = {
|
|
|
1138
1563
|
return 0;
|
|
1139
1564
|
}
|
|
1140
1565
|
}],
|
|
1141
|
-
action({ path
|
|
1566
|
+
action({ path, args: [shell] }) {
|
|
1567
|
+
const spec = path[0];
|
|
1142
1568
|
if (shell === void 0) {
|
|
1143
1569
|
print(`Supported shells: ${SHELLS.join(", ")}`);
|
|
1144
1570
|
return 0;
|
|
1145
1571
|
}
|
|
1146
1572
|
if (!isShell(shell)) {
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
choices: SHELLS
|
|
1152
|
-
}
|
|
1153
|
-
})}`);
|
|
1573
|
+
printUsageError(path, `Unsupported shell '${shell}'`, {
|
|
1574
|
+
input: shell,
|
|
1575
|
+
choices: SHELLS
|
|
1576
|
+
});
|
|
1154
1577
|
return 1;
|
|
1155
1578
|
}
|
|
1156
1579
|
print(getCompletion(spec, shell));
|
|
@@ -1161,31 +1584,21 @@ const completion = {
|
|
|
1161
1584
|
/**
|
|
1162
1585
|
* Parse argv and execute the selected action.
|
|
1163
1586
|
*
|
|
1164
|
-
* The
|
|
1165
|
-
* actions take precedence so helpers like `--help` can run without
|
|
1166
|
-
* the selected command's required arguments.
|
|
1587
|
+
* The selected command's action runs unless a parsed option has an action.
|
|
1588
|
+
* Option actions take precedence so helpers like `--help` can run without
|
|
1589
|
+
* satisfying the selected command's required arguments. A command without an
|
|
1590
|
+
* action prints its help.
|
|
1167
1591
|
*/
|
|
1168
1592
|
async function run(spec, args = defaultArgs()) {
|
|
1169
1593
|
let result;
|
|
1170
1594
|
try {
|
|
1171
1595
|
result = parse(args, spec);
|
|
1172
1596
|
} catch (error) {
|
|
1173
|
-
if (error instanceof
|
|
1174
|
-
const helpMessage = getHelp(error.context.path, {
|
|
1175
|
-
description: false,
|
|
1176
|
-
didYouMean: {
|
|
1177
|
-
input: error.option,
|
|
1178
|
-
choices: error.validOptions
|
|
1179
|
-
}
|
|
1180
|
-
});
|
|
1181
|
-
printError(`${error.message}\n\n${helpMessage}`);
|
|
1182
|
-
} else if (error instanceof ParseError) printError(`${error.message}\n\n${getHelp(error.context.path, { description: false })}`);
|
|
1597
|
+
if (error instanceof ParseError) printUsageError(error.context.path, error.message, error.didYouMean);
|
|
1183
1598
|
else printError(`${error}`);
|
|
1184
1599
|
return 1;
|
|
1185
1600
|
}
|
|
1186
|
-
const action
|
|
1187
|
-
if (!action) return 1;
|
|
1188
|
-
const options = result.options;
|
|
1601
|
+
const { action, options } = result;
|
|
1189
1602
|
const actionInit = {
|
|
1190
1603
|
help: (helpOptions) => getHelp(helpOptions?.path ?? result.path, helpOptions),
|
|
1191
1604
|
args: result.args,
|
|
@@ -1194,8 +1607,8 @@ async function run(spec, args = defaultArgs()) {
|
|
|
1194
1607
|
options: {
|
|
1195
1608
|
options,
|
|
1196
1609
|
has: (name) => options.has(name),
|
|
1197
|
-
get: (name) => options.get(name)?.[0],
|
|
1198
|
-
all: (name) => options.get(name),
|
|
1610
|
+
get: (name) => (options.get(name) ?? result.defaults(name))?.[0],
|
|
1611
|
+
all: (name) => options.get(name) ?? result.defaults(name),
|
|
1199
1612
|
count: (name) => options.get(name)?.length ?? 0
|
|
1200
1613
|
}
|
|
1201
1614
|
};
|
|
@@ -1204,14 +1617,22 @@ async function run(spec, args = defaultArgs()) {
|
|
|
1204
1617
|
const actionResult = action(actionInit);
|
|
1205
1618
|
exitCode = typeof actionResult === "object" ? await actionResult : actionResult;
|
|
1206
1619
|
} catch (error) {
|
|
1207
|
-
printError(
|
|
1620
|
+
printError(describeThrown(error));
|
|
1208
1621
|
return 1;
|
|
1209
1622
|
}
|
|
1210
1623
|
return Math.min(Math.max(exitCode ?? 0, 0), 255);
|
|
1211
1624
|
}
|
|
1212
|
-
/**
|
|
1213
|
-
|
|
1214
|
-
|
|
1625
|
+
/**
|
|
1626
|
+
* The message for a value an action threw. A value that isn't an Error is shown
|
|
1627
|
+
* as JSON where it can be, since `String` turns an object into
|
|
1628
|
+
* "[object Object]".
|
|
1629
|
+
*/
|
|
1630
|
+
function describeThrown(value) {
|
|
1631
|
+
if (value instanceof Error) return value.message;
|
|
1632
|
+
if (typeof value === "object" && value !== null) try {
|
|
1633
|
+
return JSON.stringify(value);
|
|
1634
|
+
} catch {}
|
|
1635
|
+
return String(value);
|
|
1215
1636
|
}
|
|
1216
1637
|
function defaultArgs() {
|
|
1217
1638
|
return globalThis.process?.argv?.slice(2) ?? [];
|