@clo4/cli 1.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 +956 -43
- package/dist/index.d.ts +77 -19
- package/dist/index.js +1221 -1288
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +105 -31
- package/dist/testing.js +1236 -324
- package/dist/testing.js.map +1 -1
- package/package.json +17 -3
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,443 +23,412 @@ 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
|
-
/** Print
|
|
41
|
-
function
|
|
42
|
-
|
|
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`);
|
|
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
|
+
* 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
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Returns the minimum number of arguments that satisfies the args.
|
|
98
77
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* don't match an option or subcommand are yielded as arguments.
|
|
78
|
+
* Accepts the raw spec value (a single arg, an array, or `undefined`) so the
|
|
79
|
+
* hot path doesn't have to copy it into an array first.
|
|
102
80
|
*/
|
|
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
|
-
};
|
|
81
|
+
function getMinArgs(args) {
|
|
82
|
+
if (args === void 0) return 0;
|
|
83
|
+
if (!isArray(args)) return args.isOptional ? 0 : 1;
|
|
84
|
+
let count = 0;
|
|
85
|
+
for (let i = 0; i < args.length; i++) {
|
|
86
|
+
if (args[i].isOptional) break;
|
|
87
|
+
count++;
|
|
126
88
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
let
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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;
|
|
89
|
+
return count;
|
|
90
|
+
}
|
|
91
|
+
/** Returns the maximum number of arguments that satisfies the args. */
|
|
92
|
+
function getMaxArgs(args) {
|
|
93
|
+
if (args === void 0) return 0;
|
|
94
|
+
if (!isArray(args)) return args.isVariadic ? Infinity : 1;
|
|
95
|
+
for (let i = 0; i < args.length; i++) if (args[args.length - 1 - i].isVariadic) return Infinity;
|
|
96
|
+
return args.length;
|
|
97
|
+
}
|
|
98
|
+
// src/suggest.ts
|
|
99
|
+
/**
|
|
100
|
+
* "Did you mean" suggestions for unknown commands and options.
|
|
101
|
+
*
|
|
102
|
+
* This only runs after a mistake, but it's loaded on every run, so it's kept
|
|
103
|
+
* small and does nothing until it's called.
|
|
104
|
+
*
|
|
105
|
+
* @module
|
|
106
|
+
*/
|
|
107
|
+
/** How many `-` or `+` characters `name` starts with. */
|
|
108
|
+
function dashCount(name) {
|
|
109
|
+
let i = 0;
|
|
110
|
+
while (i < name.length && (name[i] === "-" || name[i] === "+")) i++;
|
|
111
|
+
return i;
|
|
112
|
+
}
|
|
113
|
+
/** Whether every character of `short` appears in `long`, in order. */
|
|
114
|
+
function isAbbreviation(short, long) {
|
|
115
|
+
let i = 0;
|
|
116
|
+
for (let j = 0; j < long.length && i < short.length; j++) if (long[j] === short[i]) i++;
|
|
117
|
+
return i === short.length;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Levenshtein distance where swapping two adjacent characters also counts as
|
|
121
|
+
* one edit (optimal string alignment). Returns `limit + 1` as soon as the
|
|
122
|
+
* distance must be larger than `limit`.
|
|
123
|
+
*/
|
|
124
|
+
function editDistance(a, b, limit) {
|
|
125
|
+
let before = new Uint32Array(b.length + 1);
|
|
126
|
+
let previous = new Uint32Array(b.length + 1);
|
|
127
|
+
let current = new Uint32Array(b.length + 1);
|
|
128
|
+
for (let j = 0; j <= b.length; j++) previous[j] = j;
|
|
129
|
+
for (let i = 1; i <= a.length; i++) {
|
|
130
|
+
current[0] = i;
|
|
131
|
+
let rowMin = i;
|
|
132
|
+
for (let j = 1; j <= b.length; j++) {
|
|
133
|
+
const substitution = previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1);
|
|
134
|
+
let d = Math.min(previous[j] + 1, current[j - 1] + 1, substitution);
|
|
135
|
+
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);
|
|
136
|
+
current[j] = d;
|
|
137
|
+
if (d < rowMin) rowMin = d;
|
|
183
138
|
}
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
139
|
+
if (rowMin > limit) return limit + 1;
|
|
140
|
+
const recycled = before;
|
|
141
|
+
before = previous;
|
|
142
|
+
previous = current;
|
|
143
|
+
current = recycled;
|
|
144
|
+
}
|
|
145
|
+
return previous[b.length];
|
|
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
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The name the user most likely meant by `input`, or `undefined` if none is
|
|
160
|
+
* close enough to be worth suggesting. In order:
|
|
161
|
+
*
|
|
162
|
+
* 1. The only choice that starts with the input (`--verb` for `--verbose`).
|
|
163
|
+
* 2. The only choice that contains every character of the input in order, for
|
|
164
|
+
* inputs of 3 or more characters (`bld` for `build`).
|
|
165
|
+
* 3. The name with the fewest edits, allowing one edit per three characters
|
|
166
|
+
* of the input. Leading dashes don't count toward the length or the edits.
|
|
167
|
+
*/
|
|
168
|
+
function closestChoice(input, choices) {
|
|
169
|
+
const inputDashes = dashCount(input);
|
|
170
|
+
const bare = input.slice(inputDashes);
|
|
171
|
+
if (bare.length >= 2) {
|
|
172
|
+
const match = onlyMatch(choices, (name) => name.startsWith(input));
|
|
173
|
+
if (match !== void 0) return match;
|
|
174
|
+
}
|
|
175
|
+
if (bare.length >= 3) {
|
|
176
|
+
const match = onlyMatch(choices, (name) => isAbbreviation(bare, name.slice(dashCount(name))));
|
|
177
|
+
if (match !== void 0) return match;
|
|
178
|
+
}
|
|
179
|
+
let best;
|
|
180
|
+
let limit = Math.floor(bare.length / 3);
|
|
181
|
+
for (const name of choices.flat()) {
|
|
182
|
+
const other = name.slice(dashCount(name));
|
|
183
|
+
if (Math.abs(other.length - bare.length) > limit) continue;
|
|
184
|
+
const distance = editDistance(bare, other, limit);
|
|
185
|
+
if (distance <= limit && (best === void 0 || distance < limit)) {
|
|
186
|
+
best = name;
|
|
187
|
+
limit = distance;
|
|
188
|
+
if (distance === 0) break;
|
|
192
189
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
190
|
+
}
|
|
191
|
+
return best;
|
|
192
|
+
}
|
|
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
|
|
255
259
|
}
|
|
256
|
-
}
|
|
257
|
-
if (!hasFoundArg) hasFoundArg = true;
|
|
258
|
-
tokens.push($arg(index, 0, token.length, token));
|
|
260
|
+
};
|
|
259
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() {
|
|
260
275
|
return {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
persistentRequiredOptions
|
|
266
|
-
},
|
|
267
|
-
tokens
|
|
276
|
+
optionArgSeparators: ["="],
|
|
277
|
+
flagsArePosixNoncompliant: false,
|
|
278
|
+
optionsMustPrecedeArguments: false,
|
|
279
|
+
subcommandsMatchUniquePrefix: false
|
|
268
280
|
};
|
|
269
281
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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;
|
|
293
|
+
}
|
|
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;
|
|
278
323
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
this.
|
|
290
|
-
this.validOptions = token.validOptions;
|
|
291
|
-
this.index = token.index;
|
|
292
|
-
this.start = token.start;
|
|
293
|
-
this.end = token.end;
|
|
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);
|
|
294
335
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
option;
|
|
302
|
-
constructor(token, context) {
|
|
303
|
-
const msg = `Option ${token.option} doesn't take arguments`;
|
|
304
|
-
super(context, msg);
|
|
305
|
-
this.index = token.index;
|
|
306
|
-
this.start = token.start;
|
|
307
|
-
this.end = token.end;
|
|
308
|
-
this.option = token.option;
|
|
309
|
-
this.arg = token.literal;
|
|
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;
|
|
310
342
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
this.
|
|
319
|
-
|
|
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
|
+
};
|
|
320
366
|
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
|
|
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
|
+
};
|
|
326
375
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
super(context, msg);
|
|
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);
|
|
332
380
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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;
|
|
338
409
|
}
|
|
339
410
|
};
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
for (i = 0; i < m; i++) {
|
|
355
|
-
var eq = peq[b.charCodeAt(i)];
|
|
356
|
-
var xv = eq | mv;
|
|
357
|
-
eq |= (eq & pv) + pv ^ pv;
|
|
358
|
-
mv |= ~(eq | pv);
|
|
359
|
-
pv &= eq;
|
|
360
|
-
if (mv & lst) sc++;
|
|
361
|
-
if (pv & lst) sc--;
|
|
362
|
-
mv = mv << 1 | 1;
|
|
363
|
-
pv = pv << 1 | ~(xv | mv);
|
|
364
|
-
mv &= xv;
|
|
365
|
-
}
|
|
366
|
-
i = n;
|
|
367
|
-
while (i--) peq[a.charCodeAt(i)] = 0;
|
|
368
|
-
return sc;
|
|
369
|
-
};
|
|
370
|
-
var myers_x = function(b, a) {
|
|
371
|
-
var n = a.length;
|
|
372
|
-
var m = b.length;
|
|
373
|
-
var mhc = [];
|
|
374
|
-
var phc = [];
|
|
375
|
-
var hsize = Math.ceil(n / 32);
|
|
376
|
-
var vsize = Math.ceil(m / 32);
|
|
377
|
-
for (var i = 0; i < hsize; i++) {
|
|
378
|
-
phc[i] = -1;
|
|
379
|
-
mhc[i] = 0;
|
|
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;
|
|
404
|
-
}
|
|
405
|
-
var mv = 0;
|
|
406
|
-
var pv = -1;
|
|
407
|
-
var start = j * 32;
|
|
408
|
-
var vlen = Math.min(32, m - start) + start;
|
|
409
|
-
for (var k = start; k < vlen; k++) peq[b.charCodeAt(k)] |= 1 << k;
|
|
410
|
-
var score = m;
|
|
411
|
-
for (var i = 0; i < n; i++) {
|
|
412
|
-
var eq = peq[a.charCodeAt(i)];
|
|
413
|
-
var pb = phc[i / 32 | 0] >>> i & 1;
|
|
414
|
-
var mb = mhc[i / 32 | 0] >>> i & 1;
|
|
415
|
-
var xv = eq | mv;
|
|
416
|
-
var xh = ((eq | mb) & pv) + pv ^ pv | eq | mb;
|
|
417
|
-
var ph = mv | ~(xh | pv);
|
|
418
|
-
var mh = pv & xh;
|
|
419
|
-
score += ph >>> m - 1 & 1;
|
|
420
|
-
score -= mh >>> m - 1 & 1;
|
|
421
|
-
if (ph >>> 31 ^ pb) phc[i / 32 | 0] ^= 1 << i;
|
|
422
|
-
if (mh >>> 31 ^ mb) mhc[i / 32 | 0] ^= 1 << i;
|
|
423
|
-
ph = ph << 1 | pb;
|
|
424
|
-
mh = mh << 1 | mb;
|
|
425
|
-
pv = mh | ~(xv | ph);
|
|
426
|
-
mv = ph & xv;
|
|
427
|
-
}
|
|
428
|
-
for (var k = start; k < vlen; k++) peq[b.charCodeAt(k)] = 0;
|
|
429
|
-
return score;
|
|
430
|
-
};
|
|
431
|
-
var distance = function(a, b) {
|
|
432
|
-
if (a.length < b.length) {
|
|
433
|
-
var tmp = b;
|
|
434
|
-
b = a;
|
|
435
|
-
a = tmp;
|
|
436
|
-
}
|
|
437
|
-
if (b.length === 0) return a.length;
|
|
438
|
-
if (a.length <= 32) return myers_32(a, b);
|
|
439
|
-
return myers_x(a, b);
|
|
440
|
-
};
|
|
441
|
-
exports.distance = distance;
|
|
442
|
-
var closest = function(str, arr) {
|
|
443
|
-
var min_distance = Infinity;
|
|
444
|
-
var min_index = 0;
|
|
445
|
-
for (var i = 0; i < arr.length; i++) {
|
|
446
|
-
var dist = distance(str, arr[i]);
|
|
447
|
-
if (dist < min_distance) {
|
|
448
|
-
min_distance = dist;
|
|
449
|
-
min_index = i;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
return arr[min_index];
|
|
453
|
-
};
|
|
454
|
-
exports.closest = closest;
|
|
455
|
-
})))();
|
|
456
|
-
function getNamed(named, name) {
|
|
457
|
-
return named.find((obj) => makeArray(obj.name).includes(name)) ?? null;
|
|
458
|
-
}
|
|
459
|
-
function getParserDirective(path, key) {
|
|
460
|
-
for (let index = path.length - 1; index >= 0; index -= 1) {
|
|
461
|
-
const command = path[index];
|
|
462
|
-
if (command?.parserDirectives && key in command.parserDirectives) return command.parserDirectives[key];
|
|
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);
|
|
463
425
|
}
|
|
426
|
+
return key;
|
|
464
427
|
}
|
|
428
|
+
function hasName(names, name) {
|
|
429
|
+
return typeof names === "string" ? names === name : names.includes(name);
|
|
430
|
+
}
|
|
431
|
+
// src/help.ts
|
|
465
432
|
function dedent(str) {
|
|
466
433
|
if (str[0] !== "\n") return str;
|
|
467
434
|
const lines = str.split("\n").slice(1, -1);
|
|
@@ -472,86 +439,58 @@ function dedent(str) {
|
|
|
472
439
|
function getHelp(path, config = {}) {
|
|
473
440
|
const { description = true, usage = true, didYouMean } = config;
|
|
474
441
|
const sections = getSections(path);
|
|
475
|
-
if (didYouMean) sections.didYouMean = (
|
|
442
|
+
if (didYouMean) sections.didYouMean = closestChoice(didYouMean.input, didYouMean.choices);
|
|
476
443
|
if (!description) sections.description = void 0;
|
|
477
444
|
if (!usage) sections.usage = void 0;
|
|
478
445
|
return formatHelpSections(sections);
|
|
479
446
|
}
|
|
480
447
|
/**
|
|
481
|
-
*
|
|
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.
|
|
482
465
|
*/
|
|
483
|
-
const usage = ({ path, args: [command], help }) => {
|
|
484
|
-
if (command === void 0) {
|
|
485
|
-
print(help());
|
|
486
|
-
return 0;
|
|
487
|
-
}
|
|
488
|
-
if (command === "") {
|
|
489
|
-
printActionError(`Expected a command, but the value was empty\n\n${help({ description: false })}`);
|
|
490
|
-
return 1;
|
|
491
|
-
}
|
|
492
|
-
const subcommands = path.at(-1)?.subcommands?.filter((cmd) => !cmd.hidden).flatMap((cmd) => cmd.name);
|
|
493
|
-
if (subcommands && subcommands.length > 0) {
|
|
494
|
-
if (getParserDirective(path, "subcommandsMatchUniquePrefix")) {
|
|
495
|
-
const possible = subcommands.filter((name) => name.startsWith(command));
|
|
496
|
-
if (possible.length > 0) {
|
|
497
|
-
printActionError(`Ambiguous command '${command}', could be: ${possible.join(", ")}\n\n${help({ description: false })}`);
|
|
498
|
-
return 1;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
printActionError(`Unknown command '${command}'\n\n${help({
|
|
502
|
-
description: false,
|
|
503
|
-
didYouMean: {
|
|
504
|
-
input: command,
|
|
505
|
-
choices: subcommands
|
|
506
|
-
}
|
|
507
|
-
})}`);
|
|
508
|
-
return 1;
|
|
509
|
-
}
|
|
510
|
-
printActionError(`Unknown command '${command}'\n\n${help({ description: false })}`);
|
|
511
|
-
return 1;
|
|
512
|
-
};
|
|
513
|
-
/** A subcommand that prints help for its parent or a sibling command. */
|
|
514
466
|
const helpCommand = {
|
|
515
467
|
name: "help",
|
|
516
468
|
description: "Print a help message",
|
|
517
469
|
args: {
|
|
518
470
|
name: "command",
|
|
519
|
-
isOptional: true
|
|
471
|
+
isOptional: true,
|
|
472
|
+
isVariadic: true
|
|
520
473
|
},
|
|
521
|
-
action({ path, args
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
return 1;
|
|
534
|
-
}
|
|
535
|
-
const command = getNamed(parent.subcommands, commandName);
|
|
536
|
-
if (!command) {
|
|
537
|
-
const subcommands = parent.subcommands.filter((cmd) => !cmd.hidden).flatMap((cmd) => cmd.name);
|
|
538
|
-
if (subcommands.length > 0) {
|
|
539
|
-
printActionError(`There is no subcommand named '${commandName}'\n\n${help({
|
|
540
|
-
path: helpRoot,
|
|
541
|
-
didYouMean: {
|
|
542
|
-
input: commandName,
|
|
543
|
-
choices: subcommands
|
|
544
|
-
}
|
|
545
|
-
})}`);
|
|
546
|
-
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;
|
|
547
486
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
})}`);
|
|
487
|
+
if (!command.requiresSubcommand && getMaxArgs(command.args) > 0) break;
|
|
488
|
+
const { message, didYouMean } = match.reason();
|
|
489
|
+
printUsageError(helpPath, message, didYouMean);
|
|
552
490
|
return 1;
|
|
553
491
|
}
|
|
554
|
-
print(help({ path:
|
|
492
|
+
print(help({ path: helpPath }));
|
|
493
|
+
return 0;
|
|
555
494
|
}
|
|
556
495
|
};
|
|
557
496
|
/** A persistent help option that prints help for the selected command. */
|
|
@@ -567,46 +506,60 @@ const help = {
|
|
|
567
506
|
function getSubcommandPathString(commands) {
|
|
568
507
|
return commands.map((command) => getLongestString(makeArray1(command.name))).join(" ");
|
|
569
508
|
}
|
|
570
|
-
function nonNullable(value) {
|
|
571
|
-
return value !== null && value !== void 0;
|
|
572
|
-
}
|
|
573
509
|
function getSections(commands) {
|
|
574
510
|
const command = commands[commands.length - 1];
|
|
511
|
+
const scope = OptionScope.at(commands);
|
|
512
|
+
const show = ({ option, names }) => optionToString(option, scope.forms(option, names));
|
|
575
513
|
const description = command.description;
|
|
576
514
|
const defaultDesc = "No description";
|
|
577
|
-
const subcommands = command.subcommands
|
|
578
|
-
|
|
579
|
-
let optionObjects =
|
|
580
|
-
let
|
|
581
|
-
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;
|
|
582
519
|
if (commands.length === 1 && subcommands.length === 0) {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
persistentOptionObjects = commands.map((command) => command.options).filter(nonNullable).flat().filter((option) => option.isPersistent && !option.hidden);
|
|
587
|
-
persistentOptions = persistentOptionObjects.map((option) => [optionToString(option), option.description ?? defaultDesc]);
|
|
588
|
-
optionObjects = (command.options ?? []).filter((option) => !option.hidden && !option.isPersistent);
|
|
589
|
-
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 = [];
|
|
590
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);
|
|
591
543
|
return {
|
|
592
|
-
usage
|
|
593
|
-
const parts = [getSubcommandPathString(commands)];
|
|
594
|
-
const required = [...optionObjects.filter((option) => option.isRequired), ...persistentOptionObjects.filter((option) => option.isRequired)];
|
|
595
|
-
for (const option of required) parts.push(optionToString(option));
|
|
596
|
-
if (options.length || persistentOptions.length) parts.push("[flags]");
|
|
597
|
-
if (command.requiresSubcommand) parts.push("<command>");
|
|
598
|
-
else {
|
|
599
|
-
const args = makeArray(command.args);
|
|
600
|
-
if (args.length > 0) parts.push(summarizeArguments(args));
|
|
601
|
-
}
|
|
602
|
-
return parts.join(" ");
|
|
603
|
-
})(),
|
|
544
|
+
usage,
|
|
604
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], "")]) : [],
|
|
605
547
|
persistentOptions,
|
|
606
548
|
options,
|
|
607
549
|
subcommands
|
|
608
550
|
};
|
|
609
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
|
+
}
|
|
610
563
|
function formatHelpSections(sections) {
|
|
611
564
|
const parts = [];
|
|
612
565
|
const indent = " ";
|
|
@@ -615,17 +568,26 @@ function formatHelpSections(sections) {
|
|
|
615
568
|
if (sections.usage) parts.push(`Usage:\n${indent}${sections.usage}`);
|
|
616
569
|
const longestOptionName = Math.max(0, ...sections.options.map(([name]) => name.length), ...sections.persistentOptions.map(([name]) => name.length));
|
|
617
570
|
const minSpacingBetweenNameAndDesc = 2;
|
|
618
|
-
const
|
|
571
|
+
const formatRows = (rows, width) => {
|
|
619
572
|
const lines = [];
|
|
620
|
-
const newline = indent + " ".repeat(
|
|
621
|
-
for (const [name, desc] of
|
|
622
|
-
|
|
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);
|
|
623
580
|
const [head, ...tail] = desc.split("\n");
|
|
624
581
|
const description = [head, ...tail.map((line) => newline + line)].join("\n");
|
|
625
582
|
lines.push(`${indent}${name}${spaces}${description}`);
|
|
626
583
|
}
|
|
627
584
|
return lines;
|
|
628
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
|
+
}
|
|
629
591
|
if (sections.subcommands.length > 0) {
|
|
630
592
|
const lines = ["Commands:"];
|
|
631
593
|
const longestName = Math.max(...sections.subcommands.map(([name]) => name.length));
|
|
@@ -656,16 +618,25 @@ function getLongestString(strings) {
|
|
|
656
618
|
}
|
|
657
619
|
return longest;
|
|
658
620
|
}
|
|
659
|
-
|
|
660
|
-
|
|
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) {
|
|
661
626
|
const optionArgs = makeArray(option.args);
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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;
|
|
669
640
|
}
|
|
670
641
|
function summarizeArguments(args) {
|
|
671
642
|
return args.map((arg) => argToString(arg)).join(" ");
|
|
@@ -682,217 +653,602 @@ function argToString(arg) {
|
|
|
682
653
|
if (arg.isVariadic) suffix = "...";
|
|
683
654
|
return `${lhs}${name}${rhs}${suffix}`;
|
|
684
655
|
}
|
|
685
|
-
// src/
|
|
656
|
+
// src/errors.ts
|
|
657
|
+
const repr = JSON.stringify;
|
|
686
658
|
/**
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
* hot path doesn't have to copy it into an array first.
|
|
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.
|
|
691
662
|
*/
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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;
|
|
699
671
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
+
};
|
|
714
|
+
// src/parse.ts
|
|
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 "";
|
|
708
719
|
}
|
|
709
|
-
/**
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
720
|
+
/**
|
|
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.
|
|
725
|
+
*/
|
|
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;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return sep ? {
|
|
738
|
+
index,
|
|
739
|
+
sep
|
|
740
|
+
} : null;
|
|
713
741
|
}
|
|
714
|
-
/**
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
742
|
+
/**
|
|
743
|
+
* Throw if the options used break a `dependsOn` or `exclusiveOn` rule, or if
|
|
744
|
+
* any of `required` is missing.
|
|
745
|
+
*/
|
|
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);
|
|
718
756
|
}
|
|
719
757
|
/**
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
* This function is exposed to make it easier to build a custom runner.
|
|
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.
|
|
728
764
|
*/
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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);
|
|
823
|
+
}
|
|
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;
|
|
841
|
+
const names = asArray(option.name);
|
|
842
|
+
const options = this.options;
|
|
843
|
+
let optionValues;
|
|
753
844
|
if (option.isRepeatable) {
|
|
754
845
|
const maxRepeat = option.isRepeatable === true ? Infinity : option.isRepeatable;
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
846
|
+
const key = optionKey(option);
|
|
847
|
+
let uses = this.repeatedUses.get(key);
|
|
848
|
+
if (!uses) {
|
|
849
|
+
uses = {
|
|
850
|
+
values: [],
|
|
851
|
+
count: 0
|
|
852
|
+
};
|
|
853
|
+
this.repeatedUses.set(key, uses);
|
|
854
|
+
}
|
|
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);
|
|
858
|
+
if (!option.args) {
|
|
859
|
+
uses.values.push("");
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
optionValues = uses.values;
|
|
863
|
+
} else {
|
|
864
|
+
optionValues = [];
|
|
865
|
+
for (const other of names) options.set(other, optionValues);
|
|
866
|
+
}
|
|
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);
|
|
874
|
+
}
|
|
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;
|
|
885
|
+
}
|
|
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;
|
|
891
|
+
}
|
|
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`);
|
|
759
896
|
}
|
|
760
|
-
|
|
761
|
-
|
|
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), "");
|
|
762
967
|
return;
|
|
763
968
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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;
|
|
976
|
+
}
|
|
977
|
+
if (this.strict) throw this.unknownOption(optionName);
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
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;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
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);
|
|
1003
|
+
}
|
|
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, "");
|
|
771
1023
|
return;
|
|
772
1024
|
}
|
|
773
|
-
if (
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
const parseArg = (token) => {
|
|
779
|
-
if (!path[path.length - 1].requiresSubcommand && foundArgs.length >= commandArgsMax) throw new TooManyArguments(token, ctx());
|
|
780
|
-
foundArgs.push(token.literal);
|
|
781
|
-
};
|
|
782
|
-
const parseArgSeparator = () => {
|
|
783
|
-
if (foundArgs.length >= commandArgsMax) throw new ParseError(ctx(), "Unexpected argument '--', did you mean to use an option instead?");
|
|
784
|
-
argSeparatorIndex = foundArgs.length;
|
|
785
|
-
state = 0;
|
|
786
|
-
};
|
|
787
|
-
const parseSubcommand = (token) => {
|
|
788
|
-
path.push(token.subcommand);
|
|
789
|
-
commandArgsMin = getMinArgs(token.subcommand.args);
|
|
790
|
-
commandArgsMax = getMaxArgs(token.subcommand.args);
|
|
791
|
-
if (token.subcommand.action) actions.push(token.subcommand.action);
|
|
792
|
-
else if (token.subcommand.requiresSubcommand) actions.push(usage);
|
|
793
|
-
};
|
|
794
|
-
const ctx = () => ({ path });
|
|
795
|
-
const { finalState, tokens } = analyze(input, spec);
|
|
796
|
-
for (const token of tokens) switch (state) {
|
|
797
|
-
case 0:
|
|
798
|
-
switch (token.kind) {
|
|
799
|
-
case "subcommand":
|
|
800
|
-
parseSubcommand(token);
|
|
801
|
-
break;
|
|
802
|
-
case "arg":
|
|
803
|
-
parseArg(token);
|
|
804
|
-
break;
|
|
805
|
-
case "option":
|
|
806
|
-
parseOption(token);
|
|
807
|
-
break;
|
|
808
|
-
case "unknown-option": throw new UnknownOption(token, ctx());
|
|
809
|
-
case "option-arg": throw new InvalidOptionArg(token, ctx());
|
|
810
|
-
case "arg-separator":
|
|
811
|
-
parseArgSeparator();
|
|
812
|
-
break;
|
|
813
|
-
default: throw new ParseError(ctx(), "Unreachable");
|
|
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);
|
|
814
1030
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1031
|
+
}
|
|
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;
|
|
1065
|
+
}
|
|
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
|
+
});
|
|
1083
|
+
}
|
|
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
|
+
};
|
|
1129
|
+
}
|
|
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;
|
|
842
1172
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
assert(Array.isArray(optionArgs), "Invalid state, must have an array to store option arguments");
|
|
846
|
-
assert(requiredSeparator, "Invalid state, a separator must be set");
|
|
847
|
-
switch (token.kind) {
|
|
848
|
-
case "option-arg":
|
|
849
|
-
if (typeof requiredSeparator === "string" && token.separator !== requiredSeparator) throw new ParseError(ctx(), `Incorrect separator, use '${requiredSeparator}' instead of '${token.separator}'`);
|
|
850
|
-
optionArgs.push(token.literal);
|
|
851
|
-
state = 0;
|
|
852
|
-
break;
|
|
853
|
-
case "arg":
|
|
854
|
-
if (optionArgsMin > 0) throw new TooFewOptionArguments(optionArgsMin, optionArgsMax, ctx());
|
|
855
|
-
optionArgs = null;
|
|
856
|
-
state = 0;
|
|
857
|
-
parseArg(token);
|
|
858
|
-
break;
|
|
859
|
-
case "option":
|
|
860
|
-
if (optionArgsMin > 0) throw new TooFewOptionArguments(optionArgsMin, optionArgsMax, ctx());
|
|
861
|
-
parseOption(token);
|
|
862
|
-
break;
|
|
863
|
-
case "unknown-option": throw new UnknownOption(token, ctx());
|
|
864
|
-
case "arg-separator": throw new ParseError(ctx(), "Unexpected token");
|
|
865
|
-
case "subcommand":
|
|
866
|
-
if (optionArgsMin > 0) throw new TooFewOptionArguments(optionArgsMin, optionArgsMax, ctx());
|
|
867
|
-
parseSubcommand(token);
|
|
868
|
-
state = 0;
|
|
869
|
-
break;
|
|
870
|
-
default: throw new ParseError(ctx(), "Unreachable");
|
|
1173
|
+
if (flagsArePosixNoncompliant || token.length > 2 && isDoubleDash) {
|
|
1174
|
+
if (this.longOption(token, index)) return false;
|
|
871
1175
|
}
|
|
1176
|
+
}
|
|
1177
|
+
this.valueOrArg(token, subcommand);
|
|
1178
|
+
return false;
|
|
872
1179
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
+
};
|
|
881
1208
|
}
|
|
882
1209
|
return {
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1210
|
+
...next,
|
|
1211
|
+
attached
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
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
|
+
/**
|
|
1224
|
+
* Parse the input tokens using the given CLI spec.
|
|
1225
|
+
*
|
|
1226
|
+
* This returns a `ParseResult`, which contains the array of
|
|
1227
|
+
* subcommands used, the provided options, the arguments, and the action to
|
|
1228
|
+
* run.
|
|
1229
|
+
*
|
|
1230
|
+
* If parsing fails, this function throws.
|
|
1231
|
+
*/
|
|
1232
|
+
function parse(input, spec) {
|
|
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)
|
|
889
1243
|
};
|
|
890
1244
|
}
|
|
891
1245
|
// src/completion.ts
|
|
892
1246
|
const SHELLS = [
|
|
893
1247
|
"bash",
|
|
894
1248
|
"zsh",
|
|
895
|
-
"fish"
|
|
1249
|
+
"fish",
|
|
1250
|
+
"powershell",
|
|
1251
|
+
"nushell"
|
|
896
1252
|
];
|
|
897
1253
|
function isShell(value) {
|
|
898
1254
|
return SHELLS.includes(value);
|
|
@@ -900,97 +1256,6 @@ function isShell(value) {
|
|
|
900
1256
|
const firstLine = (description) => description ? description.split("\n", 1)[0].trim() : "";
|
|
901
1257
|
/** Turn an arbitrary name into a safe shell identifier fragment. */
|
|
902
1258
|
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
1259
|
/** Single-quote a string for bash/zsh. */
|
|
995
1260
|
function shQuote(value) {
|
|
996
1261
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
@@ -999,338 +1264,30 @@ function shQuote(value) {
|
|
|
999
1264
|
function fishQuote(value) {
|
|
1000
1265
|
return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
|
|
1001
1266
|
}
|
|
1002
|
-
/**
|
|
1003
|
-
function
|
|
1004
|
-
|
|
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;
|
|
1267
|
+
/** Single-quote a string for PowerShell (a quote is written twice). */
|
|
1268
|
+
function psQuote(value) {
|
|
1269
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
1013
1270
|
}
|
|
1014
1271
|
/**
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
1017
|
-
* The generator callback uses the typed command and is guarded for resilience.
|
|
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.
|
|
1018
1274
|
*/
|
|
1019
|
-
function
|
|
1020
|
-
|
|
1021
|
-
|
|
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;
|
|
1243
|
-
const presentNames = /* @__PURE__ */ new Set();
|
|
1244
|
-
const countByOption = /* @__PURE__ */ new Map();
|
|
1245
|
-
for (const token of tokens) switch (token.kind) {
|
|
1246
|
-
case "subcommand":
|
|
1247
|
-
current = token.subcommand;
|
|
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;
|
|
1291
|
-
}
|
|
1292
|
-
const availableOptions = [];
|
|
1293
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1294
|
-
for (const map of [finalState.localOptions, finalState.persistentOptions]) for (const option of map.values()) if (!seen.has(option)) {
|
|
1295
|
-
seen.add(option);
|
|
1296
|
-
availableOptions.push(option);
|
|
1297
|
-
}
|
|
1298
|
-
return {
|
|
1299
|
-
path,
|
|
1300
|
-
current,
|
|
1301
|
-
availableOptions,
|
|
1302
|
-
pending,
|
|
1303
|
-
positionalIndex,
|
|
1304
|
-
presentNames,
|
|
1305
|
-
countByOption,
|
|
1306
|
-
forceArgs,
|
|
1307
|
-
sawPositional,
|
|
1308
|
-
usedNonPersistentOption
|
|
1309
|
-
};
|
|
1310
|
-
}
|
|
1311
|
-
/** Does any command on the path enable a sticky parser directive? */
|
|
1312
|
-
function directiveOnPath(path, key) {
|
|
1313
|
-
return path.some((command) => command.parserDirectives?.[key]);
|
|
1314
|
-
}
|
|
1315
|
-
/** The option-arg separators in effect (the deepest command that sets them). */
|
|
1316
|
-
function separatorsOnPath(path) {
|
|
1317
|
-
let separators = ["="];
|
|
1318
|
-
for (const command of path) {
|
|
1319
|
-
const configured = command.parserDirectives?.optionArgSeparators;
|
|
1320
|
-
if (configured) separators = makeArray(configured);
|
|
1321
|
-
}
|
|
1322
|
-
return separators;
|
|
1275
|
+
function nuQuote(value) {
|
|
1276
|
+
if (!value.includes("'")) return `'${value}'`;
|
|
1277
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
|
|
1323
1278
|
}
|
|
1324
1279
|
/** Enumerate filesystem entries matching `partial` for a path template. */
|
|
1325
1280
|
async function enumeratePaths(partial, template) {
|
|
1326
1281
|
const { readdirSync } = await import("node:fs");
|
|
1327
|
-
const
|
|
1282
|
+
const isWindows = globalThis.process?.platform === "win32";
|
|
1283
|
+
const slash = Math.max(partial.lastIndexOf("/"), isWindows ? partial.lastIndexOf("\\") : -1);
|
|
1328
1284
|
const dir = slash >= 0 ? partial.slice(0, slash + 1) : "";
|
|
1329
1285
|
const base = slash >= 0 ? partial.slice(slash + 1) : partial;
|
|
1286
|
+
const separator = slash >= 0 ? partial[slash] : "/";
|
|
1330
1287
|
const foldersOnly = template === "folders";
|
|
1331
1288
|
let entries;
|
|
1332
1289
|
try {
|
|
1333
|
-
entries = readdirSync(dir || ".", { withFileTypes: true });
|
|
1290
|
+
entries = readdirSync((isWindows ? dir.replaceAll("\\", "/") : dir) || ".", { withFileTypes: true });
|
|
1334
1291
|
} catch {
|
|
1335
1292
|
return [];
|
|
1336
1293
|
}
|
|
@@ -1339,7 +1296,7 @@ async function enumeratePaths(partial, template) {
|
|
|
1339
1296
|
if (!entry.name.startsWith(base)) continue;
|
|
1340
1297
|
const isDir = entry.isDirectory();
|
|
1341
1298
|
if (foldersOnly && !isDir) continue;
|
|
1342
|
-
candidates.push({ value: `${dir}${entry.name}${isDir ?
|
|
1299
|
+
candidates.push({ value: `${dir}${entry.name}${isDir ? separator : ""}` });
|
|
1343
1300
|
}
|
|
1344
1301
|
return candidates;
|
|
1345
1302
|
}
|
|
@@ -1361,100 +1318,68 @@ async function argValues(arg, tokens, partial) {
|
|
|
1361
1318
|
if (arg.template) candidates.push(...await enumeratePaths(partial, arg.template));
|
|
1362
1319
|
return candidates;
|
|
1363
1320
|
}
|
|
1364
|
-
/**
|
|
1365
|
-
function
|
|
1366
|
-
const
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1321
|
+
/** Decide which kinds of candidate the word being completed can use. */
|
|
1322
|
+
function wantedCandidates(next, req) {
|
|
1323
|
+
const posixNoncompliant = next.directives.flagsArePosixNoncompliant;
|
|
1324
|
+
const partialIsOption = next.optionsAllowed && !posixNoncompliant && (req.partial.startsWith("-") || req.partial.startsWith("+"));
|
|
1325
|
+
return {
|
|
1326
|
+
wantValues: !(req.gateOptions && partialIsOption),
|
|
1327
|
+
wantOptions: !req.gateOptions || partialIsOption || posixNoncompliant
|
|
1328
|
+
};
|
|
1371
1329
|
}
|
|
1372
1330
|
/**
|
|
1373
|
-
* Resolve completion candidates for a partial command line
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
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.
|
|
1376
1334
|
*/
|
|
1377
1335
|
async function completeRuntime(req) {
|
|
1378
|
-
const
|
|
1379
|
-
const separators = separatorsOnPath(state.path);
|
|
1380
|
-
const optionValue = await resolveOptionValue(state, req, separators);
|
|
1381
|
-
if (optionValue) return optionValue;
|
|
1382
|
-
return freshTokenCandidates(state, req, separators);
|
|
1383
|
-
}
|
|
1384
|
-
/**
|
|
1385
|
-
* Candidates when the word being completed is a option's value — either attached
|
|
1386
|
-
* (`--option=value`) or the separate token after a option that expects one. Returns
|
|
1387
|
-
* null when no option value is being completed.
|
|
1388
|
-
*/
|
|
1389
|
-
async function resolveOptionValue(state, req, separators) {
|
|
1336
|
+
const next = readNext(req.spec, req.before, req.partial);
|
|
1390
1337
|
const tokens = [...req.before];
|
|
1391
|
-
|
|
1392
|
-
const
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
const option = state.availableOptions.find((candidate) => makeArray(candidate.name).includes(optionName));
|
|
1396
|
-
const arg = option && asArray(option.args)[0];
|
|
1397
|
-
if (option && arg) return (await argValues(arg, tokens, req.partial.slice(at + separator.length))).map((candidate) => ({
|
|
1398
|
-
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,
|
|
1399
1342
|
description: candidate.description
|
|
1400
1343
|
}));
|
|
1401
1344
|
}
|
|
1402
|
-
if (
|
|
1403
|
-
const arg = asArray(state.pending.option.args)[state.pending.argIndex];
|
|
1404
|
-
return arg ? argValues(arg, tokens, req.partial) : [];
|
|
1405
|
-
}
|
|
1406
|
-
return null;
|
|
1407
|
-
}
|
|
1408
|
-
/** Candidates when completing a fresh token: subcommand / positional / option. */
|
|
1409
|
-
async function freshTokenCandidates(state, req, separators) {
|
|
1345
|
+
if (next.value?.required) return next.value.arg ? argValues(next.value.arg, tokens, req.partial) : [];
|
|
1410
1346
|
const candidates = [];
|
|
1411
|
-
const
|
|
1412
|
-
const posixNoncompliant = directiveOnPath(state.path, "flagsArePosixNoncompliant");
|
|
1413
|
-
const partialIsOption = !posixNoncompliant && (req.partial.startsWith("-") || req.partial.startsWith("+"));
|
|
1414
|
-
const wantValues = !(req.gateOptions && partialIsOption);
|
|
1415
|
-
const wantOptions = !req.gateOptions || partialIsOption || posixNoncompliant;
|
|
1347
|
+
const { wantValues, wantOptions } = wantedCandidates(next, req);
|
|
1416
1348
|
if (wantValues) {
|
|
1417
|
-
const
|
|
1418
|
-
if (
|
|
1419
|
-
|
|
1420
|
-
if (!state.sawPositional && !state.usedNonPersistentOption) for (const sub of state.current.subcommands ?? []) {
|
|
1421
|
-
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)) {
|
|
1422
1352
|
const description = firstLine(sub.description);
|
|
1423
1353
|
for (const token of makeArray(sub.name)) candidates.push({
|
|
1424
1354
|
value: token,
|
|
1425
1355
|
description
|
|
1426
1356
|
});
|
|
1427
1357
|
}
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
if (wantOptions && !optionsBlocked) candidates.push(...optionCandidates(state, separators));
|
|
1358
|
+
}
|
|
1359
|
+
if (wantOptions) candidates.push(...optionCandidates(next));
|
|
1431
1360
|
return candidates;
|
|
1432
1361
|
}
|
|
1433
|
-
/**
|
|
1434
|
-
function optionCandidates(
|
|
1362
|
+
/** The options a user can use next, required ones first. */
|
|
1363
|
+
function optionCandidates(next) {
|
|
1435
1364
|
const required = [];
|
|
1436
1365
|
const optional = [];
|
|
1437
|
-
for (const option of
|
|
1366
|
+
for (const { option, forms, used } of next.options) {
|
|
1438
1367
|
if (option.hidden) continue;
|
|
1439
|
-
|
|
1440
|
-
if (count > 0) {
|
|
1441
|
-
if (!option.isRepeatable) continue;
|
|
1442
|
-
if (typeof option.isRepeatable === "number" && count >= option.isRepeatable) continue;
|
|
1443
|
-
}
|
|
1444
|
-
if (option.exclusiveOn?.some((name) => state.presentNames.has(name))) continue;
|
|
1445
|
-
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;
|
|
1446
1369
|
let description = firstLine(option.description);
|
|
1447
1370
|
if (option.dependsOn) {
|
|
1448
1371
|
const needs = option.dependsOn.join(", ");
|
|
1449
1372
|
description = description ? `${description} (requires ${needs})` : `requires ${needs}`;
|
|
1450
1373
|
}
|
|
1451
1374
|
if (option.isRequired) description = description ? `${description} [required]` : "[required]";
|
|
1452
|
-
const sep = option.requiresSeparator === true ? separators[0] : typeof option.requiresSeparator === "string" ? option.requiresSeparator : void 0;
|
|
1453
1375
|
const bucket = option.isRequired ? required : optional;
|
|
1454
|
-
for (const
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1376
|
+
for (const { name, join } of forms) {
|
|
1377
|
+
if (join === "") continue;
|
|
1378
|
+
bucket.push({
|
|
1379
|
+
value: name + (join ?? ""),
|
|
1380
|
+
description
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1458
1383
|
}
|
|
1459
1384
|
return [...required, ...optional];
|
|
1460
1385
|
}
|
|
@@ -1492,6 +1417,85 @@ function renderRuntimeShim(spec, shell) {
|
|
|
1492
1417
|
`}`,
|
|
1493
1418
|
`_${id}_complete "$@"`
|
|
1494
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
|
+
}
|
|
1495
1499
|
return [
|
|
1496
1500
|
`function __${id}_complete`,
|
|
1497
1501
|
` set -l toks (commandline -opc)`,
|
|
@@ -1504,49 +1508,8 @@ function renderRuntimeShim(spec, shell) {
|
|
|
1504
1508
|
].join("\n");
|
|
1505
1509
|
}
|
|
1506
1510
|
/** 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]);
|
|
1511
|
+
function getCompletion(spec, shell) {
|
|
1512
|
+
return renderRuntimeShim(spec, shell);
|
|
1550
1513
|
}
|
|
1551
1514
|
/** A subcommand that prints a shell completion script for the whole CLI. */
|
|
1552
1515
|
const completion = {
|
|
@@ -1555,17 +1518,9 @@ const completion = {
|
|
|
1555
1518
|
args: {
|
|
1556
1519
|
name: "shell",
|
|
1557
1520
|
isOptional: true,
|
|
1558
|
-
suggestions: SHELLS
|
|
1521
|
+
suggestions: SHELLS,
|
|
1522
|
+
description: `One of: ${SHELLS.join(", ")}`
|
|
1559
1523
|
},
|
|
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
1524
|
subcommands: [{
|
|
1570
1525
|
name: "complete",
|
|
1571
1526
|
description: "Resolve completion candidates for a partial command line",
|
|
@@ -1573,6 +1528,9 @@ const completion = {
|
|
|
1573
1528
|
options: [{
|
|
1574
1529
|
name: "--gate-options",
|
|
1575
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"
|
|
1576
1534
|
}],
|
|
1577
1535
|
args: {
|
|
1578
1536
|
name: "tokens",
|
|
@@ -1581,56 +1539,32 @@ const completion = {
|
|
|
1581
1539
|
},
|
|
1582
1540
|
async action({ path, args, options }) {
|
|
1583
1541
|
const spec = path[0];
|
|
1584
|
-
const
|
|
1542
|
+
const emptyWord = options.has("--empty-word");
|
|
1543
|
+
const partial = emptyWord || args.length === 0 ? "" : args[args.length - 1];
|
|
1585
1544
|
const lines = (await completeRuntime({
|
|
1586
1545
|
spec,
|
|
1587
|
-
before: args.slice(0, -1),
|
|
1546
|
+
before: emptyWord ? args : args.slice(0, -1),
|
|
1588
1547
|
partial,
|
|
1589
1548
|
gateOptions: options.has("--gate-options")
|
|
1590
1549
|
})).map((candidate) => candidate.description ? `${candidate.value}\t${candidate.description}` : candidate.value);
|
|
1591
1550
|
if (lines.length > 0) print(lines.join("\n"));
|
|
1592
1551
|
return 0;
|
|
1593
1552
|
}
|
|
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
1553
|
}],
|
|
1618
|
-
action({ path
|
|
1554
|
+
action({ path, args: [shell] }) {
|
|
1555
|
+
const spec = path[0];
|
|
1619
1556
|
if (shell === void 0) {
|
|
1620
1557
|
print(`Supported shells: ${SHELLS.join(", ")}`);
|
|
1621
1558
|
return 0;
|
|
1622
1559
|
}
|
|
1623
1560
|
if (!isShell(shell)) {
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
choices: SHELLS
|
|
1629
|
-
}
|
|
1630
|
-
})}`);
|
|
1561
|
+
printUsageError(path, `Unsupported shell '${shell}'`, {
|
|
1562
|
+
input: shell,
|
|
1563
|
+
choices: SHELLS
|
|
1564
|
+
});
|
|
1631
1565
|
return 1;
|
|
1632
1566
|
}
|
|
1633
|
-
print(getCompletion(spec, shell
|
|
1567
|
+
print(getCompletion(spec, shell));
|
|
1634
1568
|
return 0;
|
|
1635
1569
|
}
|
|
1636
1570
|
};
|
|
@@ -1638,31 +1572,21 @@ const completion = {
|
|
|
1638
1572
|
/**
|
|
1639
1573
|
* Parse argv and execute the selected action.
|
|
1640
1574
|
*
|
|
1641
|
-
* The
|
|
1642
|
-
* actions take precedence so helpers like `--help` can run without
|
|
1643
|
-
* 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.
|
|
1644
1579
|
*/
|
|
1645
1580
|
async function run(spec, args = defaultArgs()) {
|
|
1646
1581
|
let result;
|
|
1647
1582
|
try {
|
|
1648
1583
|
result = parse(args, spec);
|
|
1649
1584
|
} catch (error) {
|
|
1650
|
-
if (error instanceof
|
|
1651
|
-
const helpMessage = getHelp(error.context.path, {
|
|
1652
|
-
description: false,
|
|
1653
|
-
didYouMean: {
|
|
1654
|
-
input: error.option,
|
|
1655
|
-
choices: error.validOptions
|
|
1656
|
-
}
|
|
1657
|
-
});
|
|
1658
|
-
printError(`${error.message}\n\n${helpMessage}`);
|
|
1659
|
-
} 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);
|
|
1660
1586
|
else printError(`${error}`);
|
|
1661
1587
|
return 1;
|
|
1662
1588
|
}
|
|
1663
|
-
const action
|
|
1664
|
-
if (!action) return 1;
|
|
1665
|
-
const options = result.options;
|
|
1589
|
+
const { action, options } = result;
|
|
1666
1590
|
const actionInit = {
|
|
1667
1591
|
help: (helpOptions) => getHelp(helpOptions?.path ?? result.path, helpOptions),
|
|
1668
1592
|
args: result.args,
|
|
@@ -1671,8 +1595,8 @@ async function run(spec, args = defaultArgs()) {
|
|
|
1671
1595
|
options: {
|
|
1672
1596
|
options,
|
|
1673
1597
|
has: (name) => options.has(name),
|
|
1674
|
-
get: (name) => options.get(name)?.[0],
|
|
1675
|
-
all: (name) => options.get(name),
|
|
1598
|
+
get: (name) => (options.get(name) ?? result.defaults(name))?.[0],
|
|
1599
|
+
all: (name) => options.get(name) ?? result.defaults(name),
|
|
1676
1600
|
count: (name) => options.get(name)?.length ?? 0
|
|
1677
1601
|
}
|
|
1678
1602
|
};
|
|
@@ -1681,13 +1605,22 @@ async function run(spec, args = defaultArgs()) {
|
|
|
1681
1605
|
const actionResult = action(actionInit);
|
|
1682
1606
|
exitCode = typeof actionResult === "object" ? await actionResult : actionResult;
|
|
1683
1607
|
} catch (error) {
|
|
1684
|
-
printError(
|
|
1608
|
+
printError(describeThrown(error));
|
|
1685
1609
|
return 1;
|
|
1686
1610
|
}
|
|
1687
1611
|
return Math.min(Math.max(exitCode ?? 0, 0), 255);
|
|
1688
1612
|
}
|
|
1689
|
-
|
|
1690
|
-
|
|
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);
|
|
1691
1624
|
}
|
|
1692
1625
|
function defaultArgs() {
|
|
1693
1626
|
return globalThis.process?.argv?.slice(2) ?? [];
|