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