@clo4/cli 1.0.0 → 2.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 +4 -6
- package/dist/index.d.ts +9 -4
- package/dist/index.js +537 -1013
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +21 -6
- package/dist/testing.js +46 -135
- package/dist/testing.js.map +1 -1
- package/package.json +1 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// \0rolldown/runtime.js
|
|
2
|
-
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod, mod.exports), cb = null), mod.exports);
|
|
3
1
|
// src/util.ts
|
|
4
2
|
function isArray(o) {
|
|
5
3
|
return Array.isArray(o);
|
|
@@ -25,247 +23,65 @@ function makeArray1(thing) {
|
|
|
25
23
|
if (isArray(thing)) return thing.slice();
|
|
26
24
|
return [thing];
|
|
27
25
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
const nodeProcess = globalThis.process;
|
|
27
|
+
/**
|
|
28
|
+
* Write text to stdout (1) or stderr (2) with a synchronous `write` on the file
|
|
29
|
+
* descriptor.
|
|
30
|
+
*
|
|
31
|
+
* The first `console.log` in a Node process builds the stdout stream, which
|
|
32
|
+
* costs 1-3 ms. That's more than parsing takes, so all of this library's output
|
|
33
|
+
* goes through here instead. Without `node:fs` it falls back to `console`.
|
|
34
|
+
*/
|
|
35
|
+
function writeFd(fd, text) {
|
|
36
|
+
const fs = nodeProcess?.getBuiltinModule?.("node:fs");
|
|
37
|
+
if (!fs) {
|
|
38
|
+
(fd === 1 ? console.log : console.error)(text.endsWith("\n") ? text.slice(0, -1) : text);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const bytes = new TextEncoder().encode(text);
|
|
42
|
+
let offset = 0;
|
|
43
|
+
try {
|
|
44
|
+
while (offset < bytes.length) offset += fs.writeSync(fd, bytes, offset, bytes.length - offset);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error.code === "EAGAIN") (fd === 1 ? nodeProcess?.stdout : nodeProcess?.stderr)?.write(bytes.subarray(offset));
|
|
47
|
+
}
|
|
35
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Where this library's output goes. Tests replace `write` to capture output.
|
|
51
|
+
*
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
const output = { write: writeFd };
|
|
36
55
|
/** Print a message to stdout, trimming trailing whitespace. */
|
|
37
56
|
function print(text) {
|
|
38
|
-
|
|
57
|
+
output.write(1, `${text.trimEnd()}\n`);
|
|
39
58
|
}
|
|
40
59
|
/** Print an error message to stderr with the shared `Error:` prefix. */
|
|
41
60
|
function printActionError(message) {
|
|
42
|
-
|
|
61
|
+
output.write(2, `Error: ${message}\n`);
|
|
43
62
|
}
|
|
44
|
-
// src/analyze.ts
|
|
45
|
-
const $optionArg = (index, start, end, literal, option, separator) => ({
|
|
46
|
-
kind: "option-arg",
|
|
47
|
-
index,
|
|
48
|
-
literal,
|
|
49
|
-
start,
|
|
50
|
-
end,
|
|
51
|
-
option,
|
|
52
|
-
separator
|
|
53
|
-
});
|
|
54
|
-
const $unknownOption = (index, start, end, literal, validOptions) => ({
|
|
55
|
-
kind: "unknown-option",
|
|
56
|
-
index,
|
|
57
|
-
start,
|
|
58
|
-
end,
|
|
59
|
-
literal,
|
|
60
|
-
validOptions
|
|
61
|
-
});
|
|
62
|
-
const $arg = (index, start, end, literal) => ({
|
|
63
|
-
kind: "arg",
|
|
64
|
-
index,
|
|
65
|
-
start,
|
|
66
|
-
end,
|
|
67
|
-
literal
|
|
68
|
-
});
|
|
69
|
-
const $argSeparator = (index, start, end, literal) => ({
|
|
70
|
-
kind: "arg-separator",
|
|
71
|
-
index,
|
|
72
|
-
start,
|
|
73
|
-
end,
|
|
74
|
-
literal
|
|
75
|
-
});
|
|
76
|
-
const $subcommand = (index, start, end, literal, subcommand) => {
|
|
77
|
-
return {
|
|
78
|
-
kind: "subcommand",
|
|
79
|
-
index,
|
|
80
|
-
start,
|
|
81
|
-
end,
|
|
82
|
-
literal,
|
|
83
|
-
subcommand
|
|
84
|
-
};
|
|
85
|
-
};
|
|
86
|
-
const $option = (index, start, end, literal, option) => {
|
|
87
|
-
return {
|
|
88
|
-
kind: "option",
|
|
89
|
-
index,
|
|
90
|
-
start,
|
|
91
|
-
end,
|
|
92
|
-
literal,
|
|
93
|
-
option
|
|
94
|
-
};
|
|
95
|
-
};
|
|
96
63
|
/**
|
|
97
|
-
*
|
|
64
|
+
* Returns the minimum number of arguments that satisfies the args.
|
|
98
65
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* don't match an option or subcommand are yielded as arguments.
|
|
66
|
+
* Accepts the raw spec value (a single arg, an array, or `undefined`) so the
|
|
67
|
+
* hot path doesn't have to copy it into an array first.
|
|
102
68
|
*/
|
|
103
|
-
function
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (spec.options) for (const option of spec.options) if (option.isPersistent) {
|
|
111
|
-
setEach(persistentOptions, option.name, option);
|
|
112
|
-
if (option.isRequired) setEach(persistentRequiredOptions, option.name, option);
|
|
113
|
-
} else {
|
|
114
|
-
setEach(localOptions, option.name, option);
|
|
115
|
-
if (option.isRequired) setEach(localRequiredOptions, option.name, option);
|
|
116
|
-
}
|
|
117
|
-
return {
|
|
118
|
-
finalState: {
|
|
119
|
-
localOptions,
|
|
120
|
-
persistentOptions,
|
|
121
|
-
localRequiredOptions,
|
|
122
|
-
persistentRequiredOptions
|
|
123
|
-
},
|
|
124
|
-
tokens
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
let localSubcommands;
|
|
128
|
-
const localOptions = /* @__PURE__ */ new Map();
|
|
129
|
-
const persistentOptions = /* @__PURE__ */ new Map();
|
|
130
|
-
const localRequiredOptions = /* @__PURE__ */ new Map();
|
|
131
|
-
const persistentRequiredOptions = /* @__PURE__ */ new Map();
|
|
132
|
-
let separators = ["="];
|
|
133
|
-
let hasFoundArg = false;
|
|
134
|
-
let posixCompliantOptions = true;
|
|
135
|
-
let subcommandsMatchUniquePrefix = false;
|
|
136
|
-
let optionsMustPrecedeArguments = false;
|
|
137
|
-
let hasUsedNonPersistentOption = false;
|
|
138
|
-
const getOption = (name) => localOptions.get(name) ?? persistentOptions.get(name) ?? null;
|
|
139
|
-
const hasOption = (name) => localOptions.has(name) || persistentOptions.has(name);
|
|
140
|
-
const getSubcommand = (name) => {
|
|
141
|
-
if (!localSubcommands) return null;
|
|
142
|
-
let prefixMatch = null;
|
|
143
|
-
let prefixCount = 0;
|
|
144
|
-
for (const command of localSubcommands) {
|
|
145
|
-
const names = typeof command.name === "string" ? [command.name] : command.name;
|
|
146
|
-
for (const cmdName of names) {
|
|
147
|
-
if (cmdName === name) return command;
|
|
148
|
-
if (subcommandsMatchUniquePrefix && cmdName.startsWith(name)) {
|
|
149
|
-
prefixMatch = command;
|
|
150
|
-
prefixCount += 1;
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return prefixCount === 1 ? prefixMatch : null;
|
|
156
|
-
};
|
|
157
|
-
const updateCurrentCommand = (command) => {
|
|
158
|
-
if (command.options) for (const option of command.options) if (option.isPersistent) {
|
|
159
|
-
setEach(persistentOptions, option.name, option);
|
|
160
|
-
if (option.isRequired) setEach(persistentRequiredOptions, option.name, option);
|
|
161
|
-
} else {
|
|
162
|
-
setEach(localOptions, option.name, option);
|
|
163
|
-
if (option.isRequired) setEach(localRequiredOptions, option.name, option);
|
|
164
|
-
}
|
|
165
|
-
localSubcommands = command.subcommands;
|
|
166
|
-
const directives = command.parserDirectives;
|
|
167
|
-
if (directives?.optionArgSeparators) separators = asArray(directives.optionArgSeparators);
|
|
168
|
-
if (directives?.subcommandsMatchUniquePrefix) subcommandsMatchUniquePrefix = directives.subcommandsMatchUniquePrefix;
|
|
169
|
-
if (directives?.optionsMustPrecedeArguments) optionsMustPrecedeArguments = directives.optionsMustPrecedeArguments;
|
|
170
|
-
if (typeof directives?.flagsArePosixNoncompliant === "boolean") posixCompliantOptions = !directives.flagsArePosixNoncompliant;
|
|
171
|
-
};
|
|
172
|
-
updateCurrentCommand(spec);
|
|
173
|
-
tokens: for (let index = 0; index < input.length; index++) {
|
|
174
|
-
const token = input[index];
|
|
175
|
-
if (token === "--") {
|
|
176
|
-
tokens.push($argSeparator(index, 0, token.length, token));
|
|
177
|
-
index += 1;
|
|
178
|
-
for (; index < input.length; index++) {
|
|
179
|
-
const tok = input[index];
|
|
180
|
-
tokens.push($arg(index, 0, tok.length, tok));
|
|
181
|
-
}
|
|
182
|
-
break tokens;
|
|
183
|
-
}
|
|
184
|
-
if (!hasFoundArg && !hasUsedNonPersistentOption && localSubcommands) {
|
|
185
|
-
const command = getSubcommand(token);
|
|
186
|
-
if (command) {
|
|
187
|
-
localOptions.clear();
|
|
188
|
-
updateCurrentCommand(command);
|
|
189
|
-
tokens.push($subcommand(index, 0, token.length, token, command));
|
|
190
|
-
continue tokens;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
if (!(optionsMustPrecedeArguments && hasFoundArg)) {
|
|
194
|
-
if (posixCompliantOptions && token.length > 1 && (token.startsWith("-") || token.startsWith("+")) && !token.startsWith("--")) {
|
|
195
|
-
const leadingChar = token[0];
|
|
196
|
-
const dashOption = getOption(leadingChar);
|
|
197
|
-
if (dashOption?.args && !hasOption(token.slice(0, 2))) {
|
|
198
|
-
tokens.push($option(index, 0, 1, leadingChar, dashOption));
|
|
199
|
-
tokens.push($optionArg(index, 1, token.length, token.slice(1), leadingChar, ""));
|
|
200
|
-
continue tokens;
|
|
201
|
-
}
|
|
202
|
-
chars: for (let char = 1; char < token.length; char++) {
|
|
203
|
-
const optionName = `${leadingChar}${token[char]}`;
|
|
204
|
-
const option = getOption(optionName);
|
|
205
|
-
if (!option) {
|
|
206
|
-
tokens.push($unknownOption(index, char, char + 1, optionName, [...localOptions.keys(), ...persistentOptions.keys()]));
|
|
207
|
-
continue chars;
|
|
208
|
-
}
|
|
209
|
-
if (!hasUsedNonPersistentOption && !option.isPersistent) hasUsedNonPersistentOption = true;
|
|
210
|
-
tokens.push($option(index, char, char + 1, optionName, option));
|
|
211
|
-
if (option.args && char < token.length - 1) {
|
|
212
|
-
const remainingChars = token.slice(char + 1);
|
|
213
|
-
const sep = separators.find((sep) => remainingChars.startsWith(sep)) || "";
|
|
214
|
-
const argStartOffset = char + sep.length;
|
|
215
|
-
tokens.push($optionArg(index, argStartOffset + 1, token.length, token.slice(argStartOffset + 1), optionName, sep));
|
|
216
|
-
break;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
continue tokens;
|
|
220
|
-
}
|
|
221
|
-
if (!posixCompliantOptions || token.length > 2 && token.startsWith("--")) {
|
|
222
|
-
let foundSep = null;
|
|
223
|
-
let foundSepIndex = -1;
|
|
224
|
-
separators: for (const sep of separators) {
|
|
225
|
-
const index = token.indexOf(sep);
|
|
226
|
-
if (index === -1) continue separators;
|
|
227
|
-
foundSep = sep;
|
|
228
|
-
foundSepIndex = index;
|
|
229
|
-
}
|
|
230
|
-
if (foundSep) {
|
|
231
|
-
const optionName = token.slice(0, foundSepIndex);
|
|
232
|
-
const option = getOption(optionName);
|
|
233
|
-
if (option) {
|
|
234
|
-
if (!hasUsedNonPersistentOption && !option.isPersistent) hasUsedNonPersistentOption = true;
|
|
235
|
-
tokens.push($option(index, 0, foundSepIndex, token.slice(0, foundSepIndex), option));
|
|
236
|
-
tokens.push($optionArg(index, foundSepIndex + foundSep.length, token.length, token.slice(foundSepIndex + foundSep.length), optionName, foundSep));
|
|
237
|
-
continue tokens;
|
|
238
|
-
} else if (posixCompliantOptions) {
|
|
239
|
-
const optionName = token.slice(0, foundSepIndex);
|
|
240
|
-
tokens.push($unknownOption(index, 0, foundSepIndex, optionName, [...localOptions.keys(), ...persistentOptions.keys()]));
|
|
241
|
-
tokens.push($optionArg(index, foundSepIndex + foundSep.length, token.length, token.slice(foundSepIndex + foundSep.length), optionName, foundSep));
|
|
242
|
-
continue tokens;
|
|
243
|
-
}
|
|
244
|
-
} else {
|
|
245
|
-
const option = getOption(token);
|
|
246
|
-
if (option) {
|
|
247
|
-
if (!hasUsedNonPersistentOption && !option.isPersistent) hasUsedNonPersistentOption = true;
|
|
248
|
-
tokens.push($option(index, 0, token.length, token, option));
|
|
249
|
-
continue tokens;
|
|
250
|
-
} else if (posixCompliantOptions) {
|
|
251
|
-
tokens.push($unknownOption(index, 0, token.length, token, [...localOptions.keys(), ...persistentOptions.keys()]));
|
|
252
|
-
continue tokens;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
if (!hasFoundArg) hasFoundArg = true;
|
|
258
|
-
tokens.push($arg(index, 0, token.length, token));
|
|
69
|
+
function getMinArgs(args) {
|
|
70
|
+
if (args === void 0) return 0;
|
|
71
|
+
if (!isArray(args)) return args.isOptional ? 0 : 1;
|
|
72
|
+
let count = 0;
|
|
73
|
+
for (let i = 0; i < args.length; i++) {
|
|
74
|
+
if (args[i].isOptional) break;
|
|
75
|
+
count++;
|
|
259
76
|
}
|
|
260
|
-
return
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
};
|
|
77
|
+
return count;
|
|
78
|
+
}
|
|
79
|
+
/** Returns the maximum number of arguments that satisfies the args. */
|
|
80
|
+
function getMaxArgs(args) {
|
|
81
|
+
if (args === void 0) return 0;
|
|
82
|
+
if (!isArray(args)) return args.isVariadic ? Infinity : 1;
|
|
83
|
+
for (let i = 0; i < args.length; i++) if (args[args.length - 1 - i].isVariadic) return Infinity;
|
|
84
|
+
return args.length;
|
|
269
85
|
}
|
|
270
86
|
// src/errors.ts
|
|
271
87
|
const repr = JSON.stringify;
|
|
@@ -283,14 +99,14 @@ var UnknownOption = class extends ParseError {
|
|
|
283
99
|
end;
|
|
284
100
|
option;
|
|
285
101
|
validOptions;
|
|
286
|
-
constructor(
|
|
287
|
-
const msg = `${repr(
|
|
102
|
+
constructor(found, context) {
|
|
103
|
+
const msg = `${repr(found.option)} looks like a flag, but is unknown in this context`;
|
|
288
104
|
super(context, msg);
|
|
289
|
-
this.option =
|
|
290
|
-
this.validOptions =
|
|
291
|
-
this.index =
|
|
292
|
-
this.start =
|
|
293
|
-
this.end =
|
|
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;
|
|
294
110
|
}
|
|
295
111
|
};
|
|
296
112
|
var InvalidOptionArg = class extends ParseError {
|
|
@@ -299,24 +115,24 @@ var InvalidOptionArg = class extends ParseError {
|
|
|
299
115
|
end;
|
|
300
116
|
arg;
|
|
301
117
|
option;
|
|
302
|
-
constructor(
|
|
303
|
-
const msg = `Option ${
|
|
118
|
+
constructor(found, context) {
|
|
119
|
+
const msg = `Option ${found.option} doesn't take arguments`;
|
|
304
120
|
super(context, msg);
|
|
305
|
-
this.index =
|
|
306
|
-
this.start =
|
|
307
|
-
this.end =
|
|
308
|
-
this.option =
|
|
309
|
-
this.arg =
|
|
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;
|
|
310
126
|
}
|
|
311
127
|
};
|
|
312
128
|
var TooManyArguments = class extends ParseError {
|
|
313
129
|
index;
|
|
314
130
|
value;
|
|
315
|
-
constructor(
|
|
316
|
-
const msg = `Too many arguments, ${repr(
|
|
131
|
+
constructor(found, context) {
|
|
132
|
+
const msg = `Too many arguments, ${repr(found.value)} was unexpected`;
|
|
317
133
|
super(context, msg);
|
|
318
|
-
this.index =
|
|
319
|
-
this.value =
|
|
134
|
+
this.index = found.index;
|
|
135
|
+
this.value = found.value;
|
|
320
136
|
}
|
|
321
137
|
};
|
|
322
138
|
var TooFewOptionArguments = class extends ParseError {
|
|
@@ -337,122 +153,101 @@ var MissingRequiredOption = class extends ParseError {
|
|
|
337
153
|
super(context, msg);
|
|
338
154
|
}
|
|
339
155
|
};
|
|
340
|
-
// src/
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
var j = 0;
|
|
382
|
-
for (; j < vsize - 1; j++) {
|
|
383
|
-
var mv_1 = 0;
|
|
384
|
-
var pv_1 = -1;
|
|
385
|
-
var start_1 = j * 32;
|
|
386
|
-
var vlen_1 = Math.min(32, m) + start_1;
|
|
387
|
-
for (var k = start_1; k < vlen_1; k++) peq[b.charCodeAt(k)] |= 1 << k;
|
|
388
|
-
for (var i = 0; i < n; i++) {
|
|
389
|
-
var eq = peq[a.charCodeAt(i)];
|
|
390
|
-
var pb = phc[i / 32 | 0] >>> i & 1;
|
|
391
|
-
var mb = mhc[i / 32 | 0] >>> i & 1;
|
|
392
|
-
var xv = eq | mv_1;
|
|
393
|
-
var xh = ((eq | mb) & pv_1) + pv_1 ^ pv_1 | eq | mb;
|
|
394
|
-
var ph = mv_1 | ~(xh | pv_1);
|
|
395
|
-
var mh = pv_1 & xh;
|
|
396
|
-
if (ph >>> 31 ^ pb) phc[i / 32 | 0] ^= 1 << i;
|
|
397
|
-
if (mh >>> 31 ^ mb) mhc[i / 32 | 0] ^= 1 << i;
|
|
398
|
-
ph = ph << 1 | pb;
|
|
399
|
-
mh = mh << 1 | mb;
|
|
400
|
-
pv_1 = mh | ~(xv | ph);
|
|
401
|
-
mv_1 = ph & xv;
|
|
402
|
-
}
|
|
403
|
-
for (var k = start_1; k < vlen_1; k++) peq[b.charCodeAt(k)] = 0;
|
|
156
|
+
// src/suggest.ts
|
|
157
|
+
/**
|
|
158
|
+
* "Did you mean" suggestions for unknown commands and options.
|
|
159
|
+
*
|
|
160
|
+
* This only runs after a mistake, but it's loaded on every run, so it's kept
|
|
161
|
+
* small and does nothing until it's called.
|
|
162
|
+
*
|
|
163
|
+
* @module
|
|
164
|
+
*/
|
|
165
|
+
/** How many `-` or `+` characters `name` starts with. */
|
|
166
|
+
function dashCount(name) {
|
|
167
|
+
let i = 0;
|
|
168
|
+
while (i < name.length && (name[i] === "-" || name[i] === "+")) i++;
|
|
169
|
+
return i;
|
|
170
|
+
}
|
|
171
|
+
/** Whether every character of `short` appears in `long`, in order. */
|
|
172
|
+
function isAbbreviation(short, long) {
|
|
173
|
+
let i = 0;
|
|
174
|
+
for (let j = 0; j < long.length && i < short.length; j++) if (long[j] === short[i]) i++;
|
|
175
|
+
return i === short.length;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Levenshtein distance where swapping two adjacent characters also counts as
|
|
179
|
+
* one edit (optimal string alignment). Returns `limit + 1` as soon as the
|
|
180
|
+
* distance must be larger than `limit`.
|
|
181
|
+
*/
|
|
182
|
+
function editDistance(a, b, limit) {
|
|
183
|
+
let before = new Uint32Array(b.length + 1);
|
|
184
|
+
let previous = new Uint32Array(b.length + 1);
|
|
185
|
+
let current = new Uint32Array(b.length + 1);
|
|
186
|
+
for (let j = 0; j <= b.length; j++) previous[j] = j;
|
|
187
|
+
for (let i = 1; i <= a.length; i++) {
|
|
188
|
+
current[0] = i;
|
|
189
|
+
let rowMin = i;
|
|
190
|
+
for (let j = 1; j <= b.length; j++) {
|
|
191
|
+
const substitution = previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1);
|
|
192
|
+
let d = Math.min(previous[j] + 1, current[j - 1] + 1, substitution);
|
|
193
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) d = Math.min(d, before[j - 2] + 1);
|
|
194
|
+
current[j] = d;
|
|
195
|
+
if (d < rowMin) rowMin = d;
|
|
404
196
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
197
|
+
if (rowMin > limit) return limit + 1;
|
|
198
|
+
const recycled = before;
|
|
199
|
+
before = previous;
|
|
200
|
+
previous = current;
|
|
201
|
+
current = recycled;
|
|
202
|
+
}
|
|
203
|
+
return previous[b.length];
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The choice the user most likely meant by `input`, or `undefined` if none is
|
|
207
|
+
* close enough to be worth suggesting. In order:
|
|
208
|
+
*
|
|
209
|
+
* 1. The only choice that starts with the input (`--verb` for `--verbose`).
|
|
210
|
+
* 2. The only choice that contains every character of the input in order, for
|
|
211
|
+
* inputs of 3 or more characters (`bld` for `build`).
|
|
212
|
+
* 3. The choice with the fewest edits, allowing one edit per three characters
|
|
213
|
+
* of the input. Leading dashes don't count toward the length or the edits.
|
|
214
|
+
*/
|
|
215
|
+
function closestChoice(input, choices) {
|
|
216
|
+
const inputDashes = dashCount(input);
|
|
217
|
+
const bare = input.slice(inputDashes);
|
|
218
|
+
if (bare.length >= 2) {
|
|
219
|
+
let match;
|
|
220
|
+
let count = 0;
|
|
221
|
+
for (const choice of choices) if (choice.startsWith(input)) {
|
|
222
|
+
match = choice;
|
|
223
|
+
count++;
|
|
427
224
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
225
|
+
if (count === 1) return match;
|
|
226
|
+
}
|
|
227
|
+
if (bare.length >= 3) {
|
|
228
|
+
let match;
|
|
229
|
+
let count = 0;
|
|
230
|
+
for (const choice of choices) if (isAbbreviation(bare, choice.slice(dashCount(choice)))) {
|
|
231
|
+
match = choice;
|
|
232
|
+
count++;
|
|
436
233
|
}
|
|
437
|
-
if (
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
min_index = i;
|
|
450
|
-
}
|
|
234
|
+
if (count === 1) return match;
|
|
235
|
+
}
|
|
236
|
+
let best;
|
|
237
|
+
let limit = Math.floor(bare.length / 3);
|
|
238
|
+
for (const choice of choices) {
|
|
239
|
+
const other = choice.slice(dashCount(choice));
|
|
240
|
+
if (Math.abs(other.length - bare.length) > limit) continue;
|
|
241
|
+
const distance = editDistance(bare, other, limit);
|
|
242
|
+
if (distance <= limit && (best === void 0 || distance < limit)) {
|
|
243
|
+
best = choice;
|
|
244
|
+
limit = distance;
|
|
245
|
+
if (distance === 0) break;
|
|
451
246
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
247
|
+
}
|
|
248
|
+
return best;
|
|
249
|
+
}
|
|
250
|
+
// src/help.ts
|
|
456
251
|
function getNamed(named, name) {
|
|
457
252
|
return named.find((obj) => makeArray(obj.name).includes(name)) ?? null;
|
|
458
253
|
}
|
|
@@ -472,7 +267,7 @@ function dedent(str) {
|
|
|
472
267
|
function getHelp(path, config = {}) {
|
|
473
268
|
const { description = true, usage = true, didYouMean } = config;
|
|
474
269
|
const sections = getSections(path);
|
|
475
|
-
if (didYouMean) sections.didYouMean = (
|
|
270
|
+
if (didYouMean) sections.didYouMean = closestChoice(didYouMean.input, didYouMean.choices);
|
|
476
271
|
if (!description) sections.description = void 0;
|
|
477
272
|
if (!usage) sections.usage = void 0;
|
|
478
273
|
return formatHelpSections(sections);
|
|
@@ -683,211 +478,352 @@ function argToString(arg) {
|
|
|
683
478
|
return `${lhs}${name}${rhs}${suffix}`;
|
|
684
479
|
}
|
|
685
480
|
// src/parse.ts
|
|
481
|
+
function hasName(names, name) {
|
|
482
|
+
return typeof names === "string" ? names === name : names.includes(name);
|
|
483
|
+
}
|
|
686
484
|
/**
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
* Accepts the raw spec value (a single arg, an array, or `undefined`) so the
|
|
690
|
-
* hot path doesn't have to copy it into an array first.
|
|
485
|
+
* Find the option `name` refers to. Local options win over persistent ones, and
|
|
486
|
+
* within each list a later definition wins over an earlier one.
|
|
691
487
|
*/
|
|
692
|
-
function
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
for (let i = 0; i < args.length; i++) {
|
|
697
|
-
if (args[i].isOptional) break;
|
|
698
|
-
count++;
|
|
699
|
-
}
|
|
700
|
-
return count;
|
|
701
|
-
}
|
|
702
|
-
/** Returns the maximum number of arguments that satisfies the args. */
|
|
703
|
-
function getMaxArgs(args) {
|
|
704
|
-
if (args === void 0) return 0;
|
|
705
|
-
if (!isArray(args)) return args.isVariadic ? Infinity : 1;
|
|
706
|
-
for (let i = 0; i < args.length; i++) if (args[args.length - 1 - i].isVariadic) return Infinity;
|
|
707
|
-
return args.length;
|
|
708
|
-
}
|
|
709
|
-
/** Check if a map has a key or any key from an array */
|
|
710
|
-
function has(map, key) {
|
|
711
|
-
if (isArray(key)) return map.has(key[0]);
|
|
712
|
-
return map.has(key);
|
|
488
|
+
function findOption(local, persistent, name) {
|
|
489
|
+
for (let i = local.length - 1; i >= 0; i--) if (hasName(local[i].name, name)) return local[i];
|
|
490
|
+
for (let i = persistent.length - 1; i >= 0; i--) if (hasName(persistent[i].name, name)) return persistent[i];
|
|
491
|
+
return null;
|
|
713
492
|
}
|
|
714
|
-
/**
|
|
715
|
-
function
|
|
716
|
-
|
|
717
|
-
|
|
493
|
+
/** Every name that's valid as an option here, for "did you mean" suggestions. */
|
|
494
|
+
function optionNames(local, persistent) {
|
|
495
|
+
const names = [];
|
|
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
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return names;
|
|
718
504
|
}
|
|
719
505
|
/**
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
* This returns a `ParseResult`, which contains the array of
|
|
723
|
-
* subcommands used, the provided options, and the arguments.
|
|
506
|
+
* Read a command line against a spec in one pass.
|
|
724
507
|
*
|
|
725
|
-
*
|
|
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.
|
|
726
512
|
*
|
|
727
|
-
* This
|
|
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.
|
|
728
516
|
*/
|
|
729
|
-
function
|
|
517
|
+
function scan(input, spec, strict) {
|
|
730
518
|
const path = [spec];
|
|
731
519
|
const actions = [];
|
|
732
520
|
const optionActions = [];
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
let commandArgsMax = getMaxArgs(spec.args);
|
|
737
|
-
const foundArgs = [];
|
|
738
|
-
const foundOptions = /* @__PURE__ */ new Map();
|
|
739
|
-
let state = 0;
|
|
740
|
-
let optionArgs = null;
|
|
741
|
-
let optionArgsMin = 0;
|
|
742
|
-
let optionArgsMax = 0;
|
|
743
|
-
let requiredSeparator = false;
|
|
521
|
+
const args = [];
|
|
522
|
+
const options = /* @__PURE__ */ new Map();
|
|
523
|
+
const seenOptions = [];
|
|
744
524
|
let argSeparatorIndex = -1;
|
|
745
|
-
|
|
746
|
-
const
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
525
|
+
let local = [];
|
|
526
|
+
const persistent = [];
|
|
527
|
+
const localRequired = [];
|
|
528
|
+
const persistentRequired = [];
|
|
529
|
+
let subcommands;
|
|
530
|
+
let commandArgsMin = 0;
|
|
531
|
+
let commandArgsMax = 0;
|
|
532
|
+
let separators = ["="];
|
|
533
|
+
let posixCompliantOptions = true;
|
|
534
|
+
let subcommandsMatchUniquePrefix = false;
|
|
535
|
+
let optionsMustPrecedeArguments = false;
|
|
536
|
+
let hasFoundArg = false;
|
|
537
|
+
let openOption = null;
|
|
538
|
+
let openOptionName = "";
|
|
539
|
+
let valueCount = 0;
|
|
540
|
+
let valueMin = 0;
|
|
541
|
+
let separateValueMax = 0;
|
|
542
|
+
let values = null;
|
|
543
|
+
let valuesStart = 0;
|
|
544
|
+
let valuesMin = 0;
|
|
545
|
+
let valuesMax = 0;
|
|
546
|
+
let requiredSeparator = false;
|
|
547
|
+
const repeatCounts = /* @__PURE__ */ new Map();
|
|
548
|
+
const ctx = () => ({ path });
|
|
549
|
+
const enterCommand = (command) => {
|
|
550
|
+
local = [];
|
|
551
|
+
localRequired.length = 0;
|
|
552
|
+
if (command.options) for (const option of command.options) if (option.isPersistent) {
|
|
553
|
+
persistent.push(option);
|
|
554
|
+
if (option.isRequired) persistentRequired.push(option);
|
|
555
|
+
} else {
|
|
556
|
+
local.push(option);
|
|
557
|
+
if (option.isRequired) localRequired.push(option);
|
|
558
|
+
}
|
|
559
|
+
subcommands = command.subcommands;
|
|
560
|
+
commandArgsMin = getMinArgs(command.args);
|
|
561
|
+
commandArgsMax = getMaxArgs(command.args);
|
|
562
|
+
if (command.action) actions.push(command.action);
|
|
563
|
+
else if (command.requiresSubcommand) actions.push(usage);
|
|
564
|
+
const directives = command.parserDirectives;
|
|
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;
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
const endOption = () => {
|
|
573
|
+
if (strict && values && values.length - valuesStart < valuesMin) throw new TooFewOptionArguments(valuesMin, valuesMax, ctx());
|
|
574
|
+
values = null;
|
|
575
|
+
};
|
|
576
|
+
const useOption = (option, name) => {
|
|
577
|
+
endOption();
|
|
578
|
+
openOption = option;
|
|
579
|
+
openOptionName = name;
|
|
580
|
+
valueCount = 0;
|
|
581
|
+
valueMin = getMinArgs(option.args);
|
|
582
|
+
separateValueMax = option.requiresSeparator ? 0 : getMaxArgs(option.args);
|
|
583
|
+
seenOptions.push(option);
|
|
752
584
|
if (option.action) optionActions.push(option.action);
|
|
585
|
+
const names = asArray(option.name);
|
|
586
|
+
let optionValues;
|
|
753
587
|
if (option.isRepeatable) {
|
|
754
588
|
const maxRepeat = option.isRepeatable === true ? Infinity : option.isRepeatable;
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
589
|
+
const existing = options.get(names[0]);
|
|
590
|
+
let isSharedCount = true;
|
|
591
|
+
for (const other of names) if (options.get(other) !== existing) isSharedCount = false;
|
|
592
|
+
let uses = isSharedCount ? existing : void 0;
|
|
593
|
+
if (!uses) {
|
|
594
|
+
uses = [];
|
|
595
|
+
for (const other of names) options.set(other, uses);
|
|
759
596
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
597
|
+
const count = repeatCounts.get(uses) ?? uses.length;
|
|
598
|
+
if (strict && count >= maxRepeat) throw new ParseError(ctx(), "Too many repetitions");
|
|
599
|
+
repeatCounts.set(uses, count + 1);
|
|
600
|
+
if (!option.args) {
|
|
601
|
+
uses.push("");
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
optionValues = uses;
|
|
605
|
+
} else {
|
|
606
|
+
optionValues = [];
|
|
607
|
+
for (const other of names) options.set(other, optionValues);
|
|
763
608
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
state = 0;
|
|
771
|
-
return;
|
|
609
|
+
valuesMin = valueMin;
|
|
610
|
+
valuesMax = getMaxArgs(option.args);
|
|
611
|
+
requiredSeparator = option.requiresSeparator ?? false;
|
|
612
|
+
if (valuesMax > 0) {
|
|
613
|
+
values = optionValues;
|
|
614
|
+
valuesStart = optionValues.length;
|
|
772
615
|
}
|
|
773
|
-
if (option.requiresSeparator) {
|
|
774
|
-
state = 2;
|
|
775
|
-
requiredSeparator = option.requiresSeparator;
|
|
776
|
-
} else state = 1;
|
|
777
616
|
};
|
|
778
|
-
const
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
617
|
+
const addValue = (index, start, end, value, separator) => {
|
|
618
|
+
valueCount += 1;
|
|
619
|
+
if (!values) {
|
|
620
|
+
if (strict) throw new InvalidOptionArg({
|
|
621
|
+
option: openOptionName,
|
|
622
|
+
arg: value,
|
|
623
|
+
index,
|
|
624
|
+
start,
|
|
625
|
+
end
|
|
626
|
+
}, ctx());
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (strict && typeof requiredSeparator === "string" && separator !== requiredSeparator) throw new ParseError(ctx(), `Incorrect separator, use '${requiredSeparator}' instead of '${separator}'`);
|
|
630
|
+
values.push(value);
|
|
786
631
|
};
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
632
|
+
const addArg = (index, value) => {
|
|
633
|
+
endOption();
|
|
634
|
+
if (strict && !path[path.length - 1].requiresSubcommand && args.length >= commandArgsMax) throw new TooManyArguments({
|
|
635
|
+
value,
|
|
636
|
+
index
|
|
637
|
+
}, ctx());
|
|
638
|
+
args.push(value);
|
|
793
639
|
};
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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;
|
|
651
|
+
}
|
|
652
|
+
const hasRoomForValue = valueCount < separateValueMax;
|
|
653
|
+
if (!hasFoundArg && !(valueCount < valueMin || hasRoomForValue && separateValueMax === Infinity) && subcommands) {
|
|
654
|
+
const allowPrefix = subcommandsMatchUniquePrefix && !hasRoomForValue && token !== "";
|
|
655
|
+
let command = null;
|
|
656
|
+
let prefixCount = 0;
|
|
657
|
+
commands: for (const candidate of subcommands) {
|
|
658
|
+
const names = candidate.name;
|
|
659
|
+
if (typeof names === "string") {
|
|
660
|
+
if (names === token) {
|
|
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
|
+
}
|
|
814
684
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
685
|
+
if (command && (prefixCount === -1 || prefixCount === 1)) {
|
|
686
|
+
endOption();
|
|
687
|
+
openOption = null;
|
|
688
|
+
valueCount = valueMin = separateValueMax = 0;
|
|
689
|
+
path.push(command);
|
|
690
|
+
enterCommand(command);
|
|
691
|
+
continue tokens;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (!(optionsMustPrecedeArguments && hasFoundArg)) {
|
|
695
|
+
const first = token.charCodeAt(0);
|
|
696
|
+
const isDoubleDash = first === 45 && token.charCodeAt(1) === 45;
|
|
697
|
+
if (posixCompliantOptions && token.length > 1 && (first === 45 || first === 43) && !isDoubleDash) {
|
|
698
|
+
const leadingChar = token[0];
|
|
699
|
+
const dashOption = findOption(local, persistent, leadingChar);
|
|
700
|
+
if (dashOption?.args && !findOption(local, persistent, token.slice(0, 2))) {
|
|
701
|
+
useOption(dashOption, leadingChar);
|
|
702
|
+
addValue(index, 1, token.length, token.slice(1), "");
|
|
703
|
+
continue tokens;
|
|
704
|
+
}
|
|
705
|
+
for (let char = 1; char < token.length; char++) {
|
|
706
|
+
const optionName = leadingChar + token[char];
|
|
707
|
+
const option = findOption(local, persistent, optionName);
|
|
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;
|
|
826
721
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
default: throw new ParseError(ctx(), "Unreachable");
|
|
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;
|
|
842
736
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
if (
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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
|
+
}
|
|
871
772
|
}
|
|
773
|
+
}
|
|
774
|
+
if (valueCount < separateValueMax) {
|
|
775
|
+
addValue(index, 0, token.length, token, "");
|
|
776
|
+
continue tokens;
|
|
777
|
+
}
|
|
778
|
+
hasFoundArg = true;
|
|
779
|
+
addArg(index, token);
|
|
872
780
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
781
|
+
if (strict) {
|
|
782
|
+
const hasOptionAction = optionActions.length > 0;
|
|
783
|
+
if (!hasOptionAction && args.length < commandArgsMin) throw new TooFewArguments(commandArgsMin, commandArgsMax, ctx());
|
|
784
|
+
endOption();
|
|
785
|
+
for (const option of seenOptions) {
|
|
786
|
+
if (!option.dependsOn) continue;
|
|
787
|
+
for (const name of option.dependsOn) if (!options.has(name)) throw new ParseError(ctx(), `${asArray(option.name).join("/")} requires ${name}, add it to fix this error`);
|
|
788
|
+
}
|
|
789
|
+
for (const option of seenOptions) {
|
|
790
|
+
if (!option.exclusiveOn) continue;
|
|
791
|
+
for (const name of option.exclusiveOn) if (options.has(name)) throw new ParseError(ctx(), `${asArray(option.name).join("/")} can't be used together with ${name}`);
|
|
792
|
+
}
|
|
793
|
+
if (!hasOptionAction) {
|
|
794
|
+
for (const required of [localRequired, persistentRequired]) for (const option of required) for (const name of asArray(option.name)) if (!options.has(name)) throw new MissingRequiredOption(name, ctx());
|
|
795
|
+
}
|
|
881
796
|
}
|
|
797
|
+
const open = openOption;
|
|
882
798
|
return {
|
|
883
799
|
path,
|
|
884
800
|
actions,
|
|
885
801
|
optionActions,
|
|
802
|
+
args,
|
|
886
803
|
argSeparatorIndex,
|
|
887
|
-
|
|
888
|
-
|
|
804
|
+
options,
|
|
805
|
+
seenOptions,
|
|
806
|
+
localOptions: local,
|
|
807
|
+
persistentOptions: persistent,
|
|
808
|
+
openOption: open && valueCount < separateValueMax ? {
|
|
809
|
+
option: open,
|
|
810
|
+
valueCount
|
|
811
|
+
} : null
|
|
889
812
|
};
|
|
890
813
|
}
|
|
814
|
+
/**
|
|
815
|
+
* Parse the input tokens using the given CLI spec.
|
|
816
|
+
*
|
|
817
|
+
* This returns a `ParseResult`, which contains the array of
|
|
818
|
+
* subcommands used, the provided options, and the arguments.
|
|
819
|
+
*
|
|
820
|
+
* If parsing fails, this function throws.
|
|
821
|
+
*
|
|
822
|
+
* This function is exposed to make it easier to build a custom runner.
|
|
823
|
+
*/
|
|
824
|
+
function parse(input, spec) {
|
|
825
|
+
return scan(input, spec, true);
|
|
826
|
+
}
|
|
891
827
|
// src/completion.ts
|
|
892
828
|
const SHELLS = [
|
|
893
829
|
"bash",
|
|
@@ -900,97 +836,6 @@ function isShell(value) {
|
|
|
900
836
|
const firstLine = (description) => description ? description.split("\n", 1)[0].trim() : "";
|
|
901
837
|
/** Turn an arbitrary name into a safe shell identifier fragment. */
|
|
902
838
|
const sanitize = (value) => value.replace(/[^A-Za-z0-9]+/g, "_");
|
|
903
|
-
function encodeLocation(location) {
|
|
904
|
-
const bytes = new TextEncoder().encode(JSON.stringify(location));
|
|
905
|
-
let binary = "";
|
|
906
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
907
|
-
return btoa(binary);
|
|
908
|
-
}
|
|
909
|
-
function decodeLocation(encoded) {
|
|
910
|
-
const binary = atob(encoded);
|
|
911
|
-
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
912
|
-
return JSON.parse(new TextDecoder().decode(bytes));
|
|
913
|
-
}
|
|
914
|
-
/** Build {@link ArgComplete} for one argument at a known spec location. */
|
|
915
|
-
function argCompleteFor(arg, location) {
|
|
916
|
-
const staticValues = (arg.suggestions ?? []).map((suggestion) => typeof suggestion === "string" ? suggestion : makeArray(suggestion.name)[0]);
|
|
917
|
-
const genIndex = makeArray(arg.generators).findIndex((generator) => generator.custom);
|
|
918
|
-
const customLocation = genIndex >= 0 ? encodeLocation({
|
|
919
|
-
...location,
|
|
920
|
-
genIndex
|
|
921
|
-
}) : void 0;
|
|
922
|
-
if (!arg.template && staticValues.length === 0 && !customLocation) return;
|
|
923
|
-
return {
|
|
924
|
-
template: arg.template,
|
|
925
|
-
staticValues,
|
|
926
|
-
customLocation
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
/** Completion for a option's first argument, if it has any metadata. */
|
|
930
|
-
function optionArgComplete(option, path) {
|
|
931
|
-
const arg = asArray(option.args)[0];
|
|
932
|
-
if (!arg) return void 0;
|
|
933
|
-
return argCompleteFor(arg, {
|
|
934
|
-
path,
|
|
935
|
-
option: makeArray(option.name)[0],
|
|
936
|
-
argIndex: 0
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
/** Completion for a command's first positional arg that has metadata. */
|
|
940
|
-
function positionalComplete(command, path) {
|
|
941
|
-
const args = asArray(command.args);
|
|
942
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
943
|
-
const ac = argCompleteFor(args[index], {
|
|
944
|
-
path,
|
|
945
|
-
argIndex: index
|
|
946
|
-
});
|
|
947
|
-
if (ac) return ac;
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
function toOptionInfo(option, path) {
|
|
951
|
-
return {
|
|
952
|
-
tokens: makeArray(option.name),
|
|
953
|
-
description: firstLine(option.description),
|
|
954
|
-
takesArg: asArray(option.args).length > 0,
|
|
955
|
-
argComplete: optionArgComplete(option, path)
|
|
956
|
-
};
|
|
957
|
-
}
|
|
958
|
-
/**
|
|
959
|
-
* Flatten the spec into a list of command nodes. Hidden options and subcommands
|
|
960
|
-
* are excluded; persistent options are propagated to descendants.
|
|
961
|
-
*/
|
|
962
|
-
function walk(spec) {
|
|
963
|
-
const nodes = [];
|
|
964
|
-
const recurse = (command, key, path, inheritedPersistent) => {
|
|
965
|
-
const ownOptions = (command.options ?? []).filter((option) => !option.hidden);
|
|
966
|
-
const children = (command.subcommands ?? []).filter((sub) => !sub.hidden);
|
|
967
|
-
const node = {
|
|
968
|
-
key,
|
|
969
|
-
path,
|
|
970
|
-
subcommands: [],
|
|
971
|
-
options: [...inheritedPersistent, ...ownOptions].map((option) => toOptionInfo(option, path)),
|
|
972
|
-
positional: positionalComplete(command, path),
|
|
973
|
-
transitions: []
|
|
974
|
-
};
|
|
975
|
-
const childPersistent = [...inheritedPersistent, ...ownOptions.filter((option) => option.isPersistent)];
|
|
976
|
-
for (const child of children) {
|
|
977
|
-
const tokens = makeArray(child.name);
|
|
978
|
-
const childKey = `${key}_${sanitize(tokens[0])}`;
|
|
979
|
-
node.subcommands.push({
|
|
980
|
-
tokens,
|
|
981
|
-
description: firstLine(child.description)
|
|
982
|
-
});
|
|
983
|
-
for (const token of tokens) node.transitions.push({
|
|
984
|
-
token,
|
|
985
|
-
childKey
|
|
986
|
-
});
|
|
987
|
-
recurse(child, childKey, [...path, tokens[0]], childPersistent);
|
|
988
|
-
}
|
|
989
|
-
nodes.push(node);
|
|
990
|
-
};
|
|
991
|
-
recurse(spec, "cmd", [], []);
|
|
992
|
-
return nodes;
|
|
993
|
-
}
|
|
994
839
|
/** Single-quote a string for bash/zsh. */
|
|
995
840
|
function shQuote(value) {
|
|
996
841
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
@@ -999,318 +844,53 @@ function shQuote(value) {
|
|
|
999
844
|
function fishQuote(value) {
|
|
1000
845
|
return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
|
|
1001
846
|
}
|
|
1002
|
-
/** Every candidate token offered at a node (subcommand aliases + option aliases). */
|
|
1003
|
-
function candidateTokens(node) {
|
|
1004
|
-
const tokens = [];
|
|
1005
|
-
for (const sub of node.subcommands) tokens.push(...sub.tokens);
|
|
1006
|
-
for (const option of node.options) tokens.push(...option.tokens);
|
|
1007
|
-
return tokens;
|
|
1008
|
-
}
|
|
1009
|
-
function renderTransitionCases(nodes, indent) {
|
|
1010
|
-
const lines = [];
|
|
1011
|
-
for (const node of nodes) for (const { token, childKey } of node.transitions) lines.push(`${indent}${shQuote(`${node.key}::${token}`)}) cmd=${shQuote(childKey)} ;;`);
|
|
1012
|
-
return lines;
|
|
1013
|
-
}
|
|
1014
847
|
/**
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
1017
|
-
* The generator callback uses the typed command and is guarded for resilience.
|
|
848
|
+
* Read the line typed so far with the parser in lenient mode, which skips
|
|
849
|
+
* anything invalid instead of throwing.
|
|
1018
850
|
*/
|
|
1019
|
-
function
|
|
1020
|
-
const
|
|
1021
|
-
const
|
|
1022
|
-
const hasWords = ac.staticValues.length > 0 || ac.customLocation !== void 0;
|
|
1023
|
-
if (hasWords) {
|
|
1024
|
-
lines.push(`${indent}local words=${ac.staticValues.length > 0 ? shQuote(ac.staticValues.join(" ")) : "\"\""}`);
|
|
1025
|
-
if (ac.customLocation) {
|
|
1026
|
-
lines.push(`${indent}if command -v "\${COMP_WORDS[0]}" >/dev/null 2>&1; then`);
|
|
1027
|
-
lines.push(`${indent} words="$words $("\${COMP_WORDS[0]}" completion generate-suggestions ${shQuote(ac.customLocation)} -- "\${COMP_WORDS[@]:1}" 2>/dev/null)"`);
|
|
1028
|
-
lines.push(`${indent}fi`);
|
|
1029
|
-
}
|
|
1030
|
-
lines.push(`${indent}${op}( $(compgen -W "$words" -- "$cur") )`);
|
|
1031
|
-
}
|
|
1032
|
-
if (ac.template) {
|
|
1033
|
-
const option = ac.template === "folders" ? "-d" : "-f";
|
|
1034
|
-
const fileOp = hasWords || append ? "COMPREPLY+=" : "COMPREPLY=";
|
|
1035
|
-
lines.push(`${indent}${fileOp}( $(compgen ${option} -- "$cur") )`);
|
|
1036
|
-
}
|
|
1037
|
-
return lines;
|
|
1038
|
-
}
|
|
1039
|
-
function renderBash(spec, nodes) {
|
|
1040
|
-
const id = sanitize(spec.name);
|
|
1041
|
-
const lines = [];
|
|
1042
|
-
lines.push(`_${id}() {`);
|
|
1043
|
-
lines.push(` local cur="\${COMP_WORDS[COMP_CWORD]}"`);
|
|
1044
|
-
lines.push(` local prev="\${COMP_WORDS[COMP_CWORD-1]}"`);
|
|
1045
|
-
lines.push(` local cmd="cmd" i w`);
|
|
1046
|
-
lines.push(` for (( i=1; i < COMP_CWORD; i++ )); do`);
|
|
1047
|
-
lines.push(` w="\${COMP_WORDS[i]}"`);
|
|
1048
|
-
lines.push(` case "$w" in -* ) continue ;; esac`);
|
|
1049
|
-
lines.push(` case "\${cmd}::\${w}" in`);
|
|
1050
|
-
lines.push(...renderTransitionCases(nodes, " "));
|
|
1051
|
-
lines.push(` esac`);
|
|
1052
|
-
lines.push(` done`);
|
|
1053
|
-
const argCases = [];
|
|
1054
|
-
for (const node of nodes) for (const option of node.options) {
|
|
1055
|
-
if (!option.argComplete) continue;
|
|
1056
|
-
for (const token of option.tokens) {
|
|
1057
|
-
argCases.push(` ${shQuote(`${node.key}::${token}`)})`);
|
|
1058
|
-
argCases.push(...bashArgLines(option.argComplete, " ", false));
|
|
1059
|
-
argCases.push(` return ;;`);
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
if (argCases.length > 0) {
|
|
1063
|
-
lines.push(` case "\${cmd}::\${prev}" in`);
|
|
1064
|
-
lines.push(...argCases);
|
|
1065
|
-
lines.push(` esac`);
|
|
1066
|
-
}
|
|
1067
|
-
lines.push(` local opts=""`);
|
|
1068
|
-
lines.push(` case "$cmd" in`);
|
|
1069
|
-
for (const node of nodes) lines.push(` ${shQuote(node.key)}) opts=${shQuote(candidateTokens(node).join(" "))} ;;`);
|
|
1070
|
-
lines.push(` esac`);
|
|
1071
|
-
lines.push(` COMPREPLY=( $(compgen -W "$opts" -- "$cur") )`);
|
|
1072
|
-
const posCases = [];
|
|
1073
|
-
for (const node of nodes) {
|
|
1074
|
-
if (!node.positional) continue;
|
|
1075
|
-
posCases.push(` ${shQuote(node.key)})`);
|
|
1076
|
-
posCases.push(...bashArgLines(node.positional, " ", true));
|
|
1077
|
-
posCases.push(` ;;`);
|
|
1078
|
-
}
|
|
1079
|
-
if (posCases.length > 0) {
|
|
1080
|
-
lines.push(` case "$cmd" in`);
|
|
1081
|
-
lines.push(...posCases);
|
|
1082
|
-
lines.push(` esac`);
|
|
1083
|
-
}
|
|
1084
|
-
lines.push(`}`);
|
|
1085
|
-
lines.push(`complete -F _${id} ${shQuote(spec.name)}`);
|
|
1086
|
-
return lines.join("\n");
|
|
1087
|
-
}
|
|
1088
|
-
/** zsh candidate lines for an arg (used for both option values and positionals). */
|
|
1089
|
-
function zshArgLines(ac, indent) {
|
|
1090
|
-
const lines = [];
|
|
1091
|
-
if (ac.staticValues.length > 0 || ac.customLocation !== void 0) {
|
|
1092
|
-
lines.push(`${indent}local -a vals`);
|
|
1093
|
-
lines.push(`${indent}vals=( ${ac.staticValues.map(shQuote).join(" ")} )`);
|
|
1094
|
-
if (ac.customLocation) {
|
|
1095
|
-
lines.push(`${indent}if command -v "$words[1]" >/dev/null 2>&1; then`);
|
|
1096
|
-
lines.push(`${indent} vals+=( \${(f)"$("$words[1]" completion generate-suggestions ${shQuote(ac.customLocation)} -- \${words[2,-1]} 2>/dev/null)"} )`);
|
|
1097
|
-
lines.push(`${indent}fi`);
|
|
1098
|
-
}
|
|
1099
|
-
lines.push(`${indent}compadd -a vals`);
|
|
1100
|
-
}
|
|
1101
|
-
if (ac.template) lines.push(`${indent}${ac.template === "folders" ? "_files -/" : "_files"}`);
|
|
1102
|
-
return lines;
|
|
1103
|
-
}
|
|
1104
|
-
function renderZsh(spec, nodes) {
|
|
1105
|
-
const id = sanitize(spec.name);
|
|
1106
|
-
const lines = [];
|
|
1107
|
-
lines.push(`#compdef ${spec.name}`);
|
|
1108
|
-
lines.push(`_${id}() {`);
|
|
1109
|
-
lines.push(` local cmd=cmd i w prev`);
|
|
1110
|
-
lines.push(` for (( i=2; i < CURRENT; i++ )); do`);
|
|
1111
|
-
lines.push(` w="\${words[i]}"`);
|
|
1112
|
-
lines.push(` case "$w" in -* ) continue ;; esac`);
|
|
1113
|
-
lines.push(` case "\${cmd}::\${w}" in`);
|
|
1114
|
-
lines.push(...renderTransitionCases(nodes, " "));
|
|
1115
|
-
lines.push(` esac`);
|
|
1116
|
-
lines.push(` done`);
|
|
1117
|
-
lines.push(` prev="\${words[CURRENT-1]}"`);
|
|
1118
|
-
const argCases = [];
|
|
1119
|
-
for (const node of nodes) for (const option of node.options) {
|
|
1120
|
-
if (!option.argComplete) continue;
|
|
1121
|
-
for (const token of option.tokens) {
|
|
1122
|
-
argCases.push(` ${shQuote(`${node.key}::${token}`)})`);
|
|
1123
|
-
argCases.push(...zshArgLines(option.argComplete, " "));
|
|
1124
|
-
argCases.push(` return ;;`);
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
if (argCases.length > 0) {
|
|
1128
|
-
lines.push(` case "\${cmd}::\${prev}" in`);
|
|
1129
|
-
lines.push(...argCases);
|
|
1130
|
-
lines.push(` esac`);
|
|
1131
|
-
}
|
|
1132
|
-
lines.push(` local -a opts`);
|
|
1133
|
-
lines.push(` case "$cmd" in`);
|
|
1134
|
-
for (const node of nodes) {
|
|
1135
|
-
const entries = [];
|
|
1136
|
-
for (const sub of node.subcommands) for (const token of sub.tokens) entries.push(shQuote(`${token}:${sub.description}`));
|
|
1137
|
-
for (const option of node.options) for (const token of option.tokens) entries.push(shQuote(`${token}:${option.description}`));
|
|
1138
|
-
lines.push(` ${shQuote(node.key)}) opts=( ${entries.join(" ")} ) ;;`);
|
|
1139
|
-
}
|
|
1140
|
-
lines.push(` esac`);
|
|
1141
|
-
lines.push(` _describe -t values ${shQuote(spec.name)} opts`);
|
|
1142
|
-
const posCases = [];
|
|
1143
|
-
for (const node of nodes) {
|
|
1144
|
-
if (!node.positional) continue;
|
|
1145
|
-
posCases.push(` ${shQuote(node.key)})`);
|
|
1146
|
-
posCases.push(...zshArgLines(node.positional, " "));
|
|
1147
|
-
posCases.push(` ;;`);
|
|
1148
|
-
}
|
|
1149
|
-
if (posCases.length > 0) {
|
|
1150
|
-
lines.push(` case "$cmd" in`);
|
|
1151
|
-
lines.push(...posCases);
|
|
1152
|
-
lines.push(` esac`);
|
|
1153
|
-
}
|
|
1154
|
-
lines.push(`}`);
|
|
1155
|
-
lines.push(`_${id} "$@"`);
|
|
1156
|
-
return lines.join("\n");
|
|
1157
|
-
}
|
|
1158
|
-
/** Build the fish `complete` option selectors (`-s x`, `-l long`, `-o old`). */
|
|
1159
|
-
function fishOptionSelectors(tokens) {
|
|
1160
|
-
const parts = [];
|
|
1161
|
-
for (const token of tokens) if (token.startsWith("--")) parts.push(`-l ${fishQuote(token.slice(2))}`);
|
|
1162
|
-
else if (token.startsWith("-") && token.length === 2) parts.push(`-s ${fishQuote(token.slice(1))}`);
|
|
1163
|
-
else if (token.startsWith("-") && token.length > 2) parts.push(`-o ${fishQuote(token.slice(1))}`);
|
|
1164
|
-
else parts.push(`-a ${fishQuote(token)}`);
|
|
1165
|
-
return parts.join(" ");
|
|
1166
|
-
}
|
|
1167
|
-
/** A guarded fish command substitution that runs a generator by location. */
|
|
1168
|
-
function fishGuardedCallback(location) {
|
|
1169
|
-
return `(set -l c (commandline -opc)[1]; if command -q -- $c; or test -x "$c"; $c completion generate-suggestions ${location} -- (commandline -opc) 2>/dev/null; end)`;
|
|
1170
|
-
}
|
|
1171
|
-
/** The `-a` candidate bits and whether the shell's file completion is wanted. */
|
|
1172
|
-
function fishValueBits(ac) {
|
|
1173
|
-
const parts = [...ac.staticValues];
|
|
1174
|
-
if (ac.template === "folders" || Array.isArray(ac.template)) parts.push("(__fish_complete_directories)");
|
|
1175
|
-
if (ac.customLocation) parts.push(fishGuardedCallback(ac.customLocation));
|
|
1176
|
-
return {
|
|
1177
|
-
aBits: parts.length > 0 ? ` -a ${fishQuote(parts.join(" "))}` : "",
|
|
1178
|
-
wantsFiles: ac.template === "filepaths" || Array.isArray(ac.template)
|
|
1179
|
-
};
|
|
1180
|
-
}
|
|
1181
|
-
/**
|
|
1182
|
-
* The suffix after a option's selectors. Uses `-x` (exclusive: requires a value
|
|
1183
|
-
* and suppresses fish's file fallback) when the arg has its own candidates and
|
|
1184
|
-
* isn't a filepaths template; `-r` lets files through when they're wanted.
|
|
1185
|
-
*/
|
|
1186
|
-
function fishOptionArgSuffix(option) {
|
|
1187
|
-
if (!option.takesArg) return "";
|
|
1188
|
-
if (!option.argComplete) return " -r";
|
|
1189
|
-
const { aBits, wantsFiles } = fishValueBits(option.argComplete);
|
|
1190
|
-
return `${wantsFiles ? " -r" : " -x"}${aBits}`;
|
|
1191
|
-
}
|
|
1192
|
-
function renderFish(spec, nodes) {
|
|
1193
|
-
const id = sanitize(spec.name);
|
|
1194
|
-
const name = spec.name;
|
|
1195
|
-
const lines = [];
|
|
1196
|
-
lines.push(`function __fish_${id}_at`);
|
|
1197
|
-
lines.push(` set -l cmd cmd`);
|
|
1198
|
-
lines.push(` set -l toks (commandline -opc)`);
|
|
1199
|
-
lines.push(` for w in $toks[2..-1]`);
|
|
1200
|
-
lines.push(` if string match -qr '^-' -- $w`);
|
|
1201
|
-
lines.push(` continue`);
|
|
1202
|
-
lines.push(` end`);
|
|
1203
|
-
lines.push(` switch "$cmd $w"`);
|
|
1204
|
-
for (const node of nodes) for (const { token, childKey } of node.transitions) {
|
|
1205
|
-
lines.push(` case ${fishQuote(`${node.key} ${token}`)}`);
|
|
1206
|
-
lines.push(` set cmd ${fishQuote(childKey)}`);
|
|
1207
|
-
}
|
|
1208
|
-
lines.push(` end`);
|
|
1209
|
-
lines.push(` end`);
|
|
1210
|
-
lines.push(` test "$cmd" = "$argv[1]"`);
|
|
1211
|
-
lines.push(`end`);
|
|
1212
|
-
lines.push("");
|
|
1213
|
-
lines.push(`complete -c ${fishQuote(name)} -f`);
|
|
1214
|
-
for (const node of nodes) {
|
|
1215
|
-
const guard = `-n ${fishQuote(`__fish_${id}_at ${node.key}`)}`;
|
|
1216
|
-
for (const sub of node.subcommands) for (const token of sub.tokens) {
|
|
1217
|
-
const desc = sub.description ? ` -d ${fishQuote(sub.description)}` : "";
|
|
1218
|
-
lines.push(`complete -c ${fishQuote(name)} ${guard} -a ${fishQuote(token)}${desc}`);
|
|
1219
|
-
}
|
|
1220
|
-
for (const option of node.options) {
|
|
1221
|
-
const desc = option.description ? ` -d ${fishQuote(option.description)}` : "";
|
|
1222
|
-
const suffix = fishOptionArgSuffix(option);
|
|
1223
|
-
lines.push(`complete -c ${fishQuote(name)} ${guard} ${fishOptionSelectors(option.tokens)}${suffix}${desc}`);
|
|
1224
|
-
}
|
|
1225
|
-
if (node.positional) {
|
|
1226
|
-
const { aBits, wantsFiles } = fishValueBits(node.positional);
|
|
1227
|
-
lines.push(`complete -c ${fishQuote(name)} ${guard}${aBits}${wantsFiles ? " -F" : ""}`);
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
return lines.join("\n");
|
|
1231
|
-
}
|
|
1232
|
-
function analyzeState(spec, before) {
|
|
1233
|
-
const { tokens, finalState } = analyze(before, spec);
|
|
1234
|
-
const path = [spec];
|
|
1235
|
-
let current = spec;
|
|
1236
|
-
let pending = null;
|
|
1237
|
-
let pendingFilled = 0;
|
|
1238
|
-
let pendingMax = 0;
|
|
1239
|
-
let positionalIndex = 0;
|
|
1240
|
-
let forceArgs = false;
|
|
1241
|
-
let sawPositional = false;
|
|
1242
|
-
let usedNonPersistentOption = false;
|
|
851
|
+
function readLine(spec, before) {
|
|
852
|
+
const scanned = scan(before, spec, false);
|
|
853
|
+
const path = scanned.path;
|
|
1243
854
|
const presentNames = /* @__PURE__ */ new Set();
|
|
1244
855
|
const countByOption = /* @__PURE__ */ new Map();
|
|
1245
|
-
for (const
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
path.push(current);
|
|
1249
|
-
positionalIndex = 0;
|
|
1250
|
-
sawPositional = false;
|
|
1251
|
-
pending = null;
|
|
1252
|
-
break;
|
|
1253
|
-
case "option": {
|
|
1254
|
-
const option = token.option;
|
|
1255
|
-
for (const name of makeArray(option.name)) presentNames.add(name);
|
|
1256
|
-
countByOption.set(option, (countByOption.get(option) ?? 0) + 1);
|
|
1257
|
-
if (!option.isPersistent) usedNonPersistentOption = true;
|
|
1258
|
-
const max = getMaxArgs(option.args);
|
|
1259
|
-
if (max > 0 && !option.requiresSeparator) {
|
|
1260
|
-
pending = {
|
|
1261
|
-
option,
|
|
1262
|
-
argIndex: 0
|
|
1263
|
-
};
|
|
1264
|
-
pendingFilled = 0;
|
|
1265
|
-
pendingMax = max;
|
|
1266
|
-
} else pending = null;
|
|
1267
|
-
break;
|
|
1268
|
-
}
|
|
1269
|
-
case "option-arg":
|
|
1270
|
-
pending = null;
|
|
1271
|
-
break;
|
|
1272
|
-
case "arg":
|
|
1273
|
-
if (pending && pendingFilled < pendingMax) {
|
|
1274
|
-
pendingFilled += 1;
|
|
1275
|
-
pending = {
|
|
1276
|
-
option: pending.option,
|
|
1277
|
-
argIndex: pendingFilled
|
|
1278
|
-
};
|
|
1279
|
-
if (pendingFilled >= pendingMax) pending = null;
|
|
1280
|
-
} else {
|
|
1281
|
-
positionalIndex += 1;
|
|
1282
|
-
sawPositional = true;
|
|
1283
|
-
pending = null;
|
|
1284
|
-
}
|
|
1285
|
-
break;
|
|
1286
|
-
case "arg-separator":
|
|
1287
|
-
forceArgs = true;
|
|
1288
|
-
pending = null;
|
|
1289
|
-
break;
|
|
1290
|
-
case "unknown-option": pending = null;
|
|
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);
|
|
1291
859
|
}
|
|
860
|
+
const open = scanned.openOption;
|
|
861
|
+
const pending = open ? {
|
|
862
|
+
option: open.option,
|
|
863
|
+
argIndex: open.valueCount
|
|
864
|
+
} : null;
|
|
1292
865
|
const availableOptions = [];
|
|
1293
866
|
const seen = /* @__PURE__ */ new Set();
|
|
1294
|
-
for (const
|
|
1295
|
-
|
|
1296
|
-
|
|
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
|
+
}
|
|
1297
874
|
}
|
|
1298
875
|
return {
|
|
1299
876
|
path,
|
|
1300
|
-
current,
|
|
877
|
+
current: path[path.length - 1],
|
|
1301
878
|
availableOptions,
|
|
1302
879
|
pending,
|
|
1303
|
-
positionalIndex,
|
|
880
|
+
positionalIndex: scanned.args.length,
|
|
1304
881
|
presentNames,
|
|
1305
882
|
countByOption,
|
|
1306
|
-
forceArgs,
|
|
1307
|
-
sawPositional
|
|
1308
|
-
usedNonPersistentOption
|
|
883
|
+
forceArgs: scanned.argSeparatorIndex !== -1,
|
|
884
|
+
sawPositional: scanned.args.length > 0
|
|
1309
885
|
};
|
|
1310
886
|
}
|
|
1311
|
-
/**
|
|
887
|
+
/** A directive's value in effect (the deepest command that sets it). */
|
|
1312
888
|
function directiveOnPath(path, key) {
|
|
1313
|
-
|
|
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;
|
|
1314
894
|
}
|
|
1315
895
|
/** The option-arg separators in effect (the deepest command that sets them). */
|
|
1316
896
|
function separatorsOnPath(path) {
|
|
@@ -1361,21 +941,33 @@ async function argValues(arg, tokens, partial) {
|
|
|
1361
941
|
if (arg.template) candidates.push(...await enumeratePaths(partial, arg.template));
|
|
1362
942
|
return candidates;
|
|
1363
943
|
}
|
|
1364
|
-
/** Pick the argument
|
|
1365
|
-
function
|
|
1366
|
-
const args = asArray(command.args);
|
|
944
|
+
/** Pick the argument at an index, reusing a final variadic argument. */
|
|
945
|
+
function argAt(args, index) {
|
|
1367
946
|
if (args.length === 0) return void 0;
|
|
1368
947
|
if (index < args.length) return args[index];
|
|
1369
948
|
const last = args[args.length - 1];
|
|
1370
949
|
return last?.isVariadic ? last : void 0;
|
|
1371
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
|
+
/** Decide which kinds of candidate the word being completed can use. */
|
|
956
|
+
function wantedCandidates(state, req) {
|
|
957
|
+
const posixNoncompliant = directiveOnPath(state.path, "flagsArePosixNoncompliant");
|
|
958
|
+
const partialIsOption = !posixNoncompliant && (req.partial.startsWith("-") || req.partial.startsWith("+"));
|
|
959
|
+
return {
|
|
960
|
+
wantValues: !(req.gateOptions && partialIsOption),
|
|
961
|
+
wantOptions: !req.gateOptions || partialIsOption || posixNoncompliant
|
|
962
|
+
};
|
|
963
|
+
}
|
|
1372
964
|
/**
|
|
1373
|
-
* Resolve completion candidates for a partial command line,
|
|
1374
|
-
*
|
|
965
|
+
* Resolve completion candidates for a partial command line, reading it with the
|
|
966
|
+
* parser's `scan` for context. Dispatches between completing a option's value and
|
|
1375
967
|
* completing a fresh token (subcommand / positional / option).
|
|
1376
968
|
*/
|
|
1377
969
|
async function completeRuntime(req) {
|
|
1378
|
-
const state =
|
|
970
|
+
const state = readLine(req.spec, req.before);
|
|
1379
971
|
const separators = separatorsOnPath(state.path);
|
|
1380
972
|
const optionValue = await resolveOptionValue(state, req, separators);
|
|
1381
973
|
if (optionValue) return optionValue;
|
|
@@ -1400,8 +992,12 @@ async function resolveOptionValue(state, req, separators) {
|
|
|
1400
992
|
}));
|
|
1401
993
|
}
|
|
1402
994
|
if (state.pending) {
|
|
1403
|
-
const
|
|
1404
|
-
|
|
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) : []];
|
|
1405
1001
|
}
|
|
1406
1002
|
return null;
|
|
1407
1003
|
}
|
|
@@ -1409,15 +1005,12 @@ async function resolveOptionValue(state, req, separators) {
|
|
|
1409
1005
|
async function freshTokenCandidates(state, req, separators) {
|
|
1410
1006
|
const candidates = [];
|
|
1411
1007
|
const tokens = [...req.before];
|
|
1412
|
-
const
|
|
1413
|
-
const partialIsOption = !posixNoncompliant && (req.partial.startsWith("-") || req.partial.startsWith("+"));
|
|
1414
|
-
const wantValues = !(req.gateOptions && partialIsOption);
|
|
1415
|
-
const wantOptions = !req.gateOptions || partialIsOption || posixNoncompliant;
|
|
1008
|
+
const { wantValues, wantOptions } = wantedCandidates(state, req);
|
|
1416
1009
|
if (wantValues) {
|
|
1417
1010
|
const positional = positionalArg(state.current, state.positionalIndex);
|
|
1418
1011
|
if (positional) candidates.push(...await argValues(positional, tokens, req.partial));
|
|
1419
1012
|
if (state.forceArgs) return candidates;
|
|
1420
|
-
if (!state.sawPositional
|
|
1013
|
+
if (!state.sawPositional) for (const sub of state.current.subcommands ?? []) {
|
|
1421
1014
|
if (sub.hidden) continue;
|
|
1422
1015
|
const description = firstLine(sub.description);
|
|
1423
1016
|
for (const token of makeArray(sub.name)) candidates.push({
|
|
@@ -1426,10 +1019,13 @@ async function freshTokenCandidates(state, req, separators) {
|
|
|
1426
1019
|
});
|
|
1427
1020
|
}
|
|
1428
1021
|
} else if (state.forceArgs) return candidates;
|
|
1429
|
-
|
|
1430
|
-
if (wantOptions && !optionsBlocked) candidates.push(...optionCandidates(state, separators));
|
|
1022
|
+
if (wantOptions) candidates.push(...allowedOptionCandidates(state, separators));
|
|
1431
1023
|
return candidates;
|
|
1432
1024
|
}
|
|
1025
|
+
/** Option candidates, unless options must precede arguments and one was seen. */
|
|
1026
|
+
function allowedOptionCandidates(state, separators) {
|
|
1027
|
+
return state.sawPositional && directiveOnPath(state.path, "optionsMustPrecedeArguments") ? [] : optionCandidates(state, separators);
|
|
1028
|
+
}
|
|
1433
1029
|
/** Eligible option candidates after relationship filtering. */
|
|
1434
1030
|
function optionCandidates(state, separators) {
|
|
1435
1031
|
const required = [];
|
|
@@ -1504,49 +1100,8 @@ function renderRuntimeShim(spec, shell) {
|
|
|
1504
1100
|
].join("\n");
|
|
1505
1101
|
}
|
|
1506
1102
|
/** Generate a completion script for `shell` from a CLI spec. */
|
|
1507
|
-
function getCompletion(spec, shell
|
|
1508
|
-
|
|
1509
|
-
const nodes = walk(spec);
|
|
1510
|
-
switch (shell) {
|
|
1511
|
-
case "bash": return renderBash(spec, nodes);
|
|
1512
|
-
case "zsh": return renderZsh(spec, nodes);
|
|
1513
|
-
case "fish": return renderFish(spec, nodes);
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
/** Resolve the argument a {@link Location} points at, or null if it's stale. */
|
|
1517
|
-
function resolveArg(spec, location) {
|
|
1518
|
-
let command = spec;
|
|
1519
|
-
let ancestorPersistent = [];
|
|
1520
|
-
for (const segment of location.path) {
|
|
1521
|
-
ancestorPersistent = [...ancestorPersistent, ...(command.options ?? []).filter((option) => option.isPersistent)];
|
|
1522
|
-
const child = (command.subcommands ?? []).find((sub) => makeArray(sub.name).includes(segment));
|
|
1523
|
-
if (!child) return null;
|
|
1524
|
-
command = child;
|
|
1525
|
-
}
|
|
1526
|
-
let args;
|
|
1527
|
-
if (location.option !== void 0) {
|
|
1528
|
-
const option = [...ancestorPersistent, ...command.options ?? []].find((candidate) => makeArray(candidate.name).includes(location.option));
|
|
1529
|
-
if (!option) return null;
|
|
1530
|
-
args = asArray(option.args);
|
|
1531
|
-
} else args = asArray(command.args);
|
|
1532
|
-
return args[location.argIndex] ?? null;
|
|
1533
|
-
}
|
|
1534
|
-
/**
|
|
1535
|
-
* Run the generator a base64 location points at and return its suggestions as
|
|
1536
|
-
* insertable strings, or null if the location is invalid.
|
|
1537
|
-
*/
|
|
1538
|
-
async function generateSuggestions(spec, encoded, tokens) {
|
|
1539
|
-
let location;
|
|
1540
|
-
try {
|
|
1541
|
-
location = decodeLocation(encoded);
|
|
1542
|
-
} catch {
|
|
1543
|
-
return null;
|
|
1544
|
-
}
|
|
1545
|
-
const arg = resolveArg(spec, location);
|
|
1546
|
-
if (!arg) return null;
|
|
1547
|
-
const generator = makeArray(arg.generators)[location.genIndex];
|
|
1548
|
-
if (!generator?.custom) return null;
|
|
1549
|
-
return (await generator.custom(tokens)).map((suggestion) => typeof suggestion === "string" ? suggestion : makeArray(suggestion.name)[0]);
|
|
1103
|
+
function getCompletion(spec, shell) {
|
|
1104
|
+
return renderRuntimeShim(spec, shell);
|
|
1550
1105
|
}
|
|
1551
1106
|
/** A subcommand that prints a shell completion script for the whole CLI. */
|
|
1552
1107
|
const completion = {
|
|
@@ -1557,15 +1112,6 @@ const completion = {
|
|
|
1557
1112
|
isOptional: true,
|
|
1558
1113
|
suggestions: SHELLS
|
|
1559
1114
|
},
|
|
1560
|
-
options: [{
|
|
1561
|
-
name: "--runtime",
|
|
1562
|
-
description: "Resolve candidates by invoking the CLI (default)",
|
|
1563
|
-
exclusiveOn: ["--shell"]
|
|
1564
|
-
}, {
|
|
1565
|
-
name: "--shell",
|
|
1566
|
-
description: "Emit a self-contained script with no per-Tab CLI call",
|
|
1567
|
-
exclusiveOn: ["--runtime"]
|
|
1568
|
-
}],
|
|
1569
1115
|
subcommands: [{
|
|
1570
1116
|
name: "complete",
|
|
1571
1117
|
description: "Resolve completion candidates for a partial command line",
|
|
@@ -1591,31 +1137,8 @@ const completion = {
|
|
|
1591
1137
|
if (lines.length > 0) print(lines.join("\n"));
|
|
1592
1138
|
return 0;
|
|
1593
1139
|
}
|
|
1594
|
-
}, {
|
|
1595
|
-
name: "generate-suggestions",
|
|
1596
|
-
description: "Print dynamic suggestions for a completion generator",
|
|
1597
|
-
hidden: true,
|
|
1598
|
-
args: [{ name: "location" }, {
|
|
1599
|
-
name: "tokens",
|
|
1600
|
-
isVariadic: true,
|
|
1601
|
-
isOptional: true
|
|
1602
|
-
}],
|
|
1603
|
-
async action({ path, args: [encoded, ...tokens] }) {
|
|
1604
|
-
const spec = path[0];
|
|
1605
|
-
if (encoded === void 0) {
|
|
1606
|
-
printActionError("Missing generator location");
|
|
1607
|
-
return 1;
|
|
1608
|
-
}
|
|
1609
|
-
const suggestions = await generateSuggestions(spec, encoded, tokens);
|
|
1610
|
-
if (suggestions === null) {
|
|
1611
|
-
printActionError(`Unknown generator location '${encoded}'`);
|
|
1612
|
-
return 1;
|
|
1613
|
-
}
|
|
1614
|
-
if (suggestions.length > 0) print(suggestions.join("\n"));
|
|
1615
|
-
return 0;
|
|
1616
|
-
}
|
|
1617
1140
|
}],
|
|
1618
|
-
action({ path: [spec], args: [shell],
|
|
1141
|
+
action({ path: [spec], args: [shell], help }) {
|
|
1619
1142
|
if (shell === void 0) {
|
|
1620
1143
|
print(`Supported shells: ${SHELLS.join(", ")}`);
|
|
1621
1144
|
return 0;
|
|
@@ -1630,7 +1153,7 @@ const completion = {
|
|
|
1630
1153
|
})}`);
|
|
1631
1154
|
return 1;
|
|
1632
1155
|
}
|
|
1633
|
-
print(getCompletion(spec, shell
|
|
1156
|
+
print(getCompletion(spec, shell));
|
|
1634
1157
|
return 0;
|
|
1635
1158
|
}
|
|
1636
1159
|
};
|
|
@@ -1686,8 +1209,9 @@ async function run(spec, args = defaultArgs()) {
|
|
|
1686
1209
|
}
|
|
1687
1210
|
return Math.min(Math.max(exitCode ?? 0, 0), 255);
|
|
1688
1211
|
}
|
|
1212
|
+
/** Print `Error:` and the given values to stderr, separated by spaces. */
|
|
1689
1213
|
function printError(...strings) {
|
|
1690
|
-
|
|
1214
|
+
output.write(2, `${["Error:", ...strings].map(String).join(" ")}\n`);
|
|
1691
1215
|
}
|
|
1692
1216
|
function defaultArgs() {
|
|
1693
1217
|
return globalThis.process?.argv?.slice(2) ?? [];
|