@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/testing.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);
|
|
@@ -10,125 +8,64 @@ function makeArray(thing) {
|
|
|
10
8
|
if (isArray(thing)) return thing.slice();
|
|
11
9
|
return [thing];
|
|
12
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Like {@link makeArray}, but without copying when the input is already an
|
|
13
|
+
* array. Only safe when the caller treats the result as read-only — use it on
|
|
14
|
+
* the hot path to avoid slicing read-only spec metadata.
|
|
15
|
+
*/
|
|
16
|
+
function asArray(thing) {
|
|
17
|
+
if (thing === void 0) return [];
|
|
18
|
+
if (isArray(thing)) return thing;
|
|
19
|
+
return [thing];
|
|
20
|
+
}
|
|
13
21
|
function assert(expr, msg = "") {
|
|
14
22
|
if (!expr) throw new Error(msg);
|
|
15
23
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
var vlen_1 = Math.min(32, m) + start_1;
|
|
62
|
-
for (var k = start_1; k < vlen_1; k++) peq[b.charCodeAt(k)] |= 1 << k;
|
|
63
|
-
for (var i = 0; i < n; i++) {
|
|
64
|
-
var eq = peq[a.charCodeAt(i)];
|
|
65
|
-
var pb = phc[i / 32 | 0] >>> i & 1;
|
|
66
|
-
var mb = mhc[i / 32 | 0] >>> i & 1;
|
|
67
|
-
var xv = eq | mv_1;
|
|
68
|
-
var xh = ((eq | mb) & pv_1) + pv_1 ^ pv_1 | eq | mb;
|
|
69
|
-
var ph = mv_1 | ~(xh | pv_1);
|
|
70
|
-
var mh = pv_1 & xh;
|
|
71
|
-
if (ph >>> 31 ^ pb) phc[i / 32 | 0] ^= 1 << i;
|
|
72
|
-
if (mh >>> 31 ^ mb) mhc[i / 32 | 0] ^= 1 << i;
|
|
73
|
-
ph = ph << 1 | pb;
|
|
74
|
-
mh = mh << 1 | mb;
|
|
75
|
-
pv_1 = mh | ~(xv | ph);
|
|
76
|
-
mv_1 = ph & xv;
|
|
77
|
-
}
|
|
78
|
-
for (var k = start_1; k < vlen_1; k++) peq[b.charCodeAt(k)] = 0;
|
|
79
|
-
}
|
|
80
|
-
var mv = 0;
|
|
81
|
-
var pv = -1;
|
|
82
|
-
var start = j * 32;
|
|
83
|
-
var vlen = Math.min(32, m - start) + start;
|
|
84
|
-
for (var k = start; k < vlen; k++) peq[b.charCodeAt(k)] |= 1 << k;
|
|
85
|
-
var score = m;
|
|
86
|
-
for (var i = 0; i < n; i++) {
|
|
87
|
-
var eq = peq[a.charCodeAt(i)];
|
|
88
|
-
var pb = phc[i / 32 | 0] >>> i & 1;
|
|
89
|
-
var mb = mhc[i / 32 | 0] >>> i & 1;
|
|
90
|
-
var xv = eq | mv;
|
|
91
|
-
var xh = ((eq | mb) & pv) + pv ^ pv | eq | mb;
|
|
92
|
-
var ph = mv | ~(xh | pv);
|
|
93
|
-
var mh = pv & xh;
|
|
94
|
-
score += ph >>> m - 1 & 1;
|
|
95
|
-
score -= mh >>> m - 1 & 1;
|
|
96
|
-
if (ph >>> 31 ^ pb) phc[i / 32 | 0] ^= 1 << i;
|
|
97
|
-
if (mh >>> 31 ^ mb) mhc[i / 32 | 0] ^= 1 << i;
|
|
98
|
-
ph = ph << 1 | pb;
|
|
99
|
-
mh = mh << 1 | mb;
|
|
100
|
-
pv = mh | ~(xv | ph);
|
|
101
|
-
mv = ph & xv;
|
|
102
|
-
}
|
|
103
|
-
for (var k = start; k < vlen; k++) peq[b.charCodeAt(k)] = 0;
|
|
104
|
-
return score;
|
|
105
|
-
};
|
|
106
|
-
var distance = function(a, b) {
|
|
107
|
-
if (a.length < b.length) {
|
|
108
|
-
var tmp = b;
|
|
109
|
-
b = a;
|
|
110
|
-
a = tmp;
|
|
111
|
-
}
|
|
112
|
-
if (b.length === 0) return a.length;
|
|
113
|
-
if (a.length <= 32) return myers_32(a, b);
|
|
114
|
-
return myers_x(a, b);
|
|
115
|
-
};
|
|
116
|
-
exports.distance = distance;
|
|
117
|
-
var closest = function(str, arr) {
|
|
118
|
-
var min_distance = Infinity;
|
|
119
|
-
var min_index = 0;
|
|
120
|
-
for (var i = 0; i < arr.length; i++) {
|
|
121
|
-
var dist = distance(str, arr[i]);
|
|
122
|
-
if (dist < min_distance) {
|
|
123
|
-
min_distance = dist;
|
|
124
|
-
min_index = i;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return arr[min_index];
|
|
128
|
-
};
|
|
129
|
-
exports.closest = closest;
|
|
130
|
-
})))();
|
|
131
|
-
// src/parse.ts
|
|
24
|
+
const nodeProcess = globalThis.process;
|
|
25
|
+
/**
|
|
26
|
+
* Write text to stdout (1) or stderr (2) with a synchronous `write` on the file
|
|
27
|
+
* descriptor.
|
|
28
|
+
*
|
|
29
|
+
* The first `console.log` in a Node process builds the stdout stream, which
|
|
30
|
+
* costs 1-3 ms. That's more than parsing takes, so all of this library's output
|
|
31
|
+
* goes through here instead. Without `node:fs` it falls back to `console`.
|
|
32
|
+
*/
|
|
33
|
+
function writeFd(fd, text) {
|
|
34
|
+
const fs = nodeProcess?.getBuiltinModule?.("node:fs");
|
|
35
|
+
if (!fs) {
|
|
36
|
+
(fd === 1 ? console.log : console.error)(text.endsWith("\n") ? text.slice(0, -1) : text);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const bytes = new TextEncoder().encode(text);
|
|
40
|
+
let offset = 0;
|
|
41
|
+
try {
|
|
42
|
+
while (offset < bytes.length) offset += fs.writeSync(fd, bytes, offset, bytes.length - offset);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error.code === "EAGAIN") (fd === 1 ? nodeProcess?.stdout : nodeProcess?.stderr)?.write(bytes.subarray(offset));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Where this library's output goes. Tests replace `write` to capture output.
|
|
49
|
+
*
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
const output = { write: writeFd };
|
|
53
|
+
/** Print a message to stdout, trimming trailing whitespace. */
|
|
54
|
+
function print(text) {
|
|
55
|
+
output.write(1, `${text.trimEnd()}\n`);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Add the defaults of the arguments from index `from` on to `values`, stopping
|
|
59
|
+
* at the first argument without one. The parser and help both use this, so
|
|
60
|
+
* help only shows the defaults the parser uses.
|
|
61
|
+
*/
|
|
62
|
+
function addDefaults(values, args, from = 0) {
|
|
63
|
+
for (let i = from; i < args.length; i++) {
|
|
64
|
+
const value = args[i].default;
|
|
65
|
+
if (value === void 0) return;
|
|
66
|
+
values.push(value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
132
69
|
/**
|
|
133
70
|
* Returns the minimum number of arguments that satisfies the args.
|
|
134
71
|
*
|
|
@@ -152,6 +89,820 @@ function getMaxArgs(args) {
|
|
|
152
89
|
for (let i = 0; i < args.length; i++) if (args[args.length - 1 - i].isVariadic) return Infinity;
|
|
153
90
|
return args.length;
|
|
154
91
|
}
|
|
92
|
+
// src/directives.ts
|
|
93
|
+
/** The directives in effect when no command sets any. */
|
|
94
|
+
function defaultDirectives() {
|
|
95
|
+
return {
|
|
96
|
+
optionArgSeparators: ["="],
|
|
97
|
+
flagsArePosixNoncompliant: false,
|
|
98
|
+
optionsMustPrecedeArguments: false,
|
|
99
|
+
subcommandsMatchUniquePrefix: false
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Update `directives` for entering `command`. A subcommand inherits each
|
|
104
|
+
* directive until it sets its own.
|
|
105
|
+
*/
|
|
106
|
+
function applyDirectives(directives, command) {
|
|
107
|
+
const configured = command.parserDirectives;
|
|
108
|
+
if (!configured) return;
|
|
109
|
+
if (configured.optionArgSeparators) directives.optionArgSeparators = asArray(configured.optionArgSeparators);
|
|
110
|
+
if (typeof configured.subcommandsMatchUniquePrefix === "boolean") directives.subcommandsMatchUniquePrefix = configured.subcommandsMatchUniquePrefix;
|
|
111
|
+
if (typeof configured.optionsMustPrecedeArguments === "boolean") directives.optionsMustPrecedeArguments = configured.optionsMustPrecedeArguments;
|
|
112
|
+
if (typeof configured.flagsArePosixNoncompliant === "boolean") directives.flagsArePosixNoncompliant = configured.flagsArePosixNoncompliant;
|
|
113
|
+
}
|
|
114
|
+
// src/scope.ts
|
|
115
|
+
/**
|
|
116
|
+
* The options and directives in effect at one command on a path.
|
|
117
|
+
*
|
|
118
|
+
* Moving to a subcommand replaces the local options and adds its persistent
|
|
119
|
+
* options to the inherited ones. A name refers to one option: a local option
|
|
120
|
+
* wins over a persistent one, and a later option wins over an earlier one in
|
|
121
|
+
* the same list. The parser, help, completion and the spec checker all ask
|
|
122
|
+
* this module which option a name refers to, so they can't disagree. It also
|
|
123
|
+
* holds the persistent subcommands the current command inherits.
|
|
124
|
+
*/
|
|
125
|
+
var OptionScope = class OptionScope {
|
|
126
|
+
/** Non-persistent options of the current command, in definition order. */
|
|
127
|
+
local = [];
|
|
128
|
+
/** Persistent options from every command on the path, in definition order. */
|
|
129
|
+
persistent = [];
|
|
130
|
+
/**
|
|
131
|
+
* Persistent subcommands of the commands above the current one, nearest
|
|
132
|
+
* command first, so a nearer one wins a name.
|
|
133
|
+
*/
|
|
134
|
+
inheritedSubcommands = [];
|
|
135
|
+
/** The directives in effect. A subcommand inherits each until it sets its own. */
|
|
136
|
+
directives = defaultDirectives();
|
|
137
|
+
command = null;
|
|
138
|
+
/** The scope at the last command on `path`. */
|
|
139
|
+
static at(path) {
|
|
140
|
+
const scope = new OptionScope();
|
|
141
|
+
for (const command of path) scope.enter(command);
|
|
142
|
+
return scope;
|
|
143
|
+
}
|
|
144
|
+
/** Move to `command`, a subcommand of the current one. */
|
|
145
|
+
enter(command) {
|
|
146
|
+
const parentSubcommands = this.command?.subcommands;
|
|
147
|
+
if (parentSubcommands) {
|
|
148
|
+
const persistent = parentSubcommands.filter((subcommand) => subcommand.isPersistent);
|
|
149
|
+
if (persistent.length > 0) this.inheritedSubcommands.unshift(...persistent);
|
|
150
|
+
}
|
|
151
|
+
this.command = command;
|
|
152
|
+
this.local.length = 0;
|
|
153
|
+
if (command.options) for (const option of command.options) (option.isPersistent ? this.persistent : this.local).push(option);
|
|
154
|
+
applyDirectives(this.directives, command);
|
|
155
|
+
}
|
|
156
|
+
/** The option `name` refers to, if any. */
|
|
157
|
+
resolve(name) {
|
|
158
|
+
const { local, persistent } = this;
|
|
159
|
+
for (let i = local.length - 1; i >= 0; i--) if (hasName$1(local[i].name, name)) return local[i];
|
|
160
|
+
for (let i = persistent.length - 1; i >= 0; i--) if (hasName$1(persistent[i].name, name)) return persistent[i];
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The options that can be used here, in definition order, each with only
|
|
165
|
+
* its live names: the names that still refer to it. Options with every name
|
|
166
|
+
* shadowed are left out.
|
|
167
|
+
*/
|
|
168
|
+
live() {
|
|
169
|
+
const owner = /* @__PURE__ */ new Map();
|
|
170
|
+
for (const options of [this.persistent, this.local]) for (const option of options) for (const name of asArray(option.name)) owner.set(name, option);
|
|
171
|
+
const pick = (options) => {
|
|
172
|
+
const picked = [];
|
|
173
|
+
for (const option of options) {
|
|
174
|
+
const names = asArray(option.name).filter((name) => owner.get(name) === option);
|
|
175
|
+
if (names.length > 0) picked.push({
|
|
176
|
+
option,
|
|
177
|
+
names
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return picked;
|
|
181
|
+
};
|
|
182
|
+
return {
|
|
183
|
+
local: pick(this.local),
|
|
184
|
+
persistent: pick(this.persistent)
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/** The options a user can see here: the {@link live} ones that aren't hidden. */
|
|
188
|
+
visible() {
|
|
189
|
+
const { local, persistent } = this.live();
|
|
190
|
+
const shown = (visible) => !visible.option.hidden;
|
|
191
|
+
return {
|
|
192
|
+
local: local.filter(shown),
|
|
193
|
+
persistent: persistent.filter(shown)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/** The live names of each option a user could have meant, for "did you mean" suggestions. */
|
|
197
|
+
suggestionNames() {
|
|
198
|
+
const { local, persistent } = this.visible();
|
|
199
|
+
return [...local, ...persistent].map((visible) => visible.names);
|
|
200
|
+
}
|
|
201
|
+
/** The options that must be used, local first. */
|
|
202
|
+
required() {
|
|
203
|
+
return [...this.local, ...this.persistent].filter((option) => option.isRequired);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* How each of `names` is written with `option`'s value here. Help shows
|
|
207
|
+
* these, completion offers them, and the spec checker reads each one back
|
|
208
|
+
* through the parser.
|
|
209
|
+
*/
|
|
210
|
+
forms(option, names = asArray(option.name)) {
|
|
211
|
+
const join = (name) => {
|
|
212
|
+
if (getMaxArgs(option.args) === 0) return null;
|
|
213
|
+
if (option.requiresSeparator) return this.requiredSeparator(option);
|
|
214
|
+
return !this.directives.flagsArePosixNoncompliant && (name === "-" || name === "+") ? "" : null;
|
|
215
|
+
};
|
|
216
|
+
return names.map((name) => ({
|
|
217
|
+
name,
|
|
218
|
+
join: join(name)
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The separator that must come between `option` and its value, or null if
|
|
223
|
+
* it can take a value as a separate token.
|
|
224
|
+
*/
|
|
225
|
+
requiredSeparator(option) {
|
|
226
|
+
const required = option.requiresSeparator;
|
|
227
|
+
if (typeof required === "string") return required;
|
|
228
|
+
return required ? this.directives.optionArgSeparators[0] ?? "=" : null;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const optionKeys = /* @__PURE__ */ new WeakMap();
|
|
232
|
+
/**
|
|
233
|
+
* A key for the set of `option`'s names. Options with the same names, in any
|
|
234
|
+
* order, get the same key and are one option: the parser continues a
|
|
235
|
+
* repeatable option's uses across them, counts one as used when the other was,
|
|
236
|
+
* and stores their values under the same names, which is how they're read.
|
|
237
|
+
* Options that share only some names are different options, and each shared
|
|
238
|
+
* name refers to whichever one `resolve` picks.
|
|
239
|
+
*/
|
|
240
|
+
function optionKey(option) {
|
|
241
|
+
let key = optionKeys.get(option);
|
|
242
|
+
if (key === void 0) {
|
|
243
|
+
key = typeof option.name === "string" ? option.name : [...option.name].sort().join("\0");
|
|
244
|
+
optionKeys.set(option, key);
|
|
245
|
+
}
|
|
246
|
+
return key;
|
|
247
|
+
}
|
|
248
|
+
function hasName$1(names, name) {
|
|
249
|
+
return typeof names === "string" ? names === name : names.includes(name);
|
|
250
|
+
}
|
|
251
|
+
// src/subcommands.ts
|
|
252
|
+
/**
|
|
253
|
+
* Find the subcommand `token` names among `subcommands`, in a command where
|
|
254
|
+
* `directives` are in effect, or else among the persistent subcommands the
|
|
255
|
+
* command `inherited`. An exact name always wins. With
|
|
256
|
+
* `subcommandsMatchUniquePrefix`, a token that's a prefix of exactly one
|
|
257
|
+
* of the command's own names matches that command, unless `exactOnly`. A
|
|
258
|
+
* hidden or inherited command only matches its exact name, so it can't make a
|
|
259
|
+
* visible command's prefix ambiguous or take a prefix meant as an argument,
|
|
260
|
+
* and an empty token is no command's prefix.
|
|
261
|
+
*/
|
|
262
|
+
function matchSubcommand(subcommands, token, directives, exactOnly = false, inherited = []) {
|
|
263
|
+
for (const candidate of subcommands) if (hasName(candidate, token)) return { command: candidate };
|
|
264
|
+
for (const candidate of inherited) if (hasName(candidate, token)) return { command: candidate };
|
|
265
|
+
const matches = directives.subcommandsMatchUniquePrefix && !exactOnly ? prefixMatches(subcommands, token) : [];
|
|
266
|
+
if (matches.length === 1) return { command: matches[0] };
|
|
267
|
+
return new NoSubcommand(withInherited(subcommands, inherited), token, matches);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* A command's own `subcommands` followed by the `inherited` ones it can use,
|
|
271
|
+
* leaving out each inherited one that an earlier command shadows a name of.
|
|
272
|
+
*/
|
|
273
|
+
function withInherited(subcommands, inherited) {
|
|
274
|
+
if (inherited.length === 0) return subcommands;
|
|
275
|
+
const all = [...subcommands];
|
|
276
|
+
for (const candidate of inherited) if (!asArray(candidate.name).some((name) => all.some((earlier) => hasName(earlier, name)))) all.push(candidate);
|
|
277
|
+
return all;
|
|
278
|
+
}
|
|
279
|
+
function hasName(command, token) {
|
|
280
|
+
const names = command.name;
|
|
281
|
+
return typeof names === "string" ? names === token : names.includes(token);
|
|
282
|
+
}
|
|
283
|
+
/** The visible commands that `token` is a prefix of a name of, each listed once. */
|
|
284
|
+
function prefixMatches(subcommands, token) {
|
|
285
|
+
if (token === "") return [];
|
|
286
|
+
return subcommands.filter((candidate) => !candidate.hidden && asArray(candidate.name).some((name) => name.startsWith(token)));
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* A token that names no subcommand. The reason is only worked out when asked
|
|
290
|
+
* for, since most tokens the parser tries aren't subcommands.
|
|
291
|
+
*/
|
|
292
|
+
var NoSubcommand = class {
|
|
293
|
+
subcommands;
|
|
294
|
+
token;
|
|
295
|
+
prefixOf;
|
|
296
|
+
command = null;
|
|
297
|
+
/**
|
|
298
|
+
* `prefixOf` are the commands `token` is a prefix of a name of, where
|
|
299
|
+
* prefixes match. There is never exactly one of them.
|
|
300
|
+
*/
|
|
301
|
+
constructor(subcommands, token, prefixOf) {
|
|
302
|
+
this.subcommands = subcommands;
|
|
303
|
+
this.token = token;
|
|
304
|
+
this.prefixOf = prefixOf;
|
|
305
|
+
}
|
|
306
|
+
/** Why the token names none of the subcommands, as an error message and suggestions. */
|
|
307
|
+
reason() {
|
|
308
|
+
const token = this.token;
|
|
309
|
+
if (token === "") return { message: "Expected a command, but the value was empty" };
|
|
310
|
+
if (this.prefixOf.length > 0) return { message: `Ambiguous command '${token}', could be: ${this.prefixOf.flatMap((command) => asArray(command.name).filter((name) => name.startsWith(token))).join(", ")}` };
|
|
311
|
+
const choices = visibleSubcommands(this.subcommands).map((command) => asArray(command.name));
|
|
312
|
+
return {
|
|
313
|
+
message: `Unknown command '${token}'`,
|
|
314
|
+
didYouMean: {
|
|
315
|
+
input: token,
|
|
316
|
+
choices
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
/** The subcommands that help, completion and suggestions show. */
|
|
322
|
+
function visibleSubcommands(subcommands) {
|
|
323
|
+
return subcommands?.filter((subcommand) => !subcommand.hidden) ?? [];
|
|
324
|
+
}
|
|
325
|
+
/** The action of a command that has none of its own: print the command's help. */
|
|
326
|
+
const usage = ({ help }) => {
|
|
327
|
+
print(help());
|
|
328
|
+
return 0;
|
|
329
|
+
};
|
|
330
|
+
// src/help.ts
|
|
331
|
+
/**
|
|
332
|
+
* Show an option's names, each written with its arguments the way `forms`
|
|
333
|
+
* says. Names written the same way share one copy of the arguments.
|
|
334
|
+
*/
|
|
335
|
+
function optionToString(option, forms) {
|
|
336
|
+
const optionArgs = makeArray(option.args);
|
|
337
|
+
const sorted = [...forms].sort((a, b) => a.name.length - b.name.length);
|
|
338
|
+
return [...new Set(sorted.map((form) => form.join))].map((join) => {
|
|
339
|
+
return sorted.filter((form) => form.join === join).map((form) => form.name).join(", ") + argumentsAfterName(optionArgs, join);
|
|
340
|
+
}).join(", ");
|
|
341
|
+
}
|
|
342
|
+
/** The text after an option's name that shows its arguments, joined to the name by `join`. */
|
|
343
|
+
function argumentsAfterName(args, join) {
|
|
344
|
+
if (args.length === 0) return "";
|
|
345
|
+
if (join === null) return ` ${summarizeArguments(args)}`;
|
|
346
|
+
const [first, ...rest] = args;
|
|
347
|
+
const name = first.name || "argument";
|
|
348
|
+
const joined = (first.isOptional ? `[${join}${name}]` : `${join}<${name}>`) + (first.isVariadic ? "..." : "");
|
|
349
|
+
return rest.length > 0 ? `${joined} ${summarizeArguments(rest)}` : joined;
|
|
350
|
+
}
|
|
351
|
+
function summarizeArguments(args) {
|
|
352
|
+
return args.map((arg) => argToString(arg)).join(" ");
|
|
353
|
+
}
|
|
354
|
+
function argToString(arg) {
|
|
355
|
+
const name = arg.name || "argument";
|
|
356
|
+
let lhs = "<";
|
|
357
|
+
let rhs = ">";
|
|
358
|
+
let suffix = "";
|
|
359
|
+
if (arg.isOptional) {
|
|
360
|
+
lhs = "[";
|
|
361
|
+
rhs = "]";
|
|
362
|
+
}
|
|
363
|
+
if (arg.isVariadic) suffix = "...";
|
|
364
|
+
return `${lhs}${name}${rhs}${suffix}`;
|
|
365
|
+
}
|
|
366
|
+
// src/errors.ts
|
|
367
|
+
const repr$1 = JSON.stringify;
|
|
368
|
+
/**
|
|
369
|
+
* A command line the parser can't accept. `run` prints the message with the
|
|
370
|
+
* help for the command at `context.path`, and suggests the closest of
|
|
371
|
+
* `didYouMean.choices` if there is one.
|
|
372
|
+
*/
|
|
373
|
+
var ParseError = class extends Error {
|
|
374
|
+
context;
|
|
375
|
+
didYouMean;
|
|
376
|
+
constructor(context, message, didYouMean) {
|
|
377
|
+
super(message);
|
|
378
|
+
this.context = context;
|
|
379
|
+
this.didYouMean = didYouMean;
|
|
380
|
+
this.name = this.constructor.name;
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
var UnknownOption = class extends ParseError {
|
|
384
|
+
option;
|
|
385
|
+
constructor(option, validOptions, context) {
|
|
386
|
+
const msg = `${repr$1(option)} looks like a flag, but is unknown in this context`;
|
|
387
|
+
super(context, msg, {
|
|
388
|
+
input: option,
|
|
389
|
+
choices: validOptions
|
|
390
|
+
});
|
|
391
|
+
this.option = option;
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
var InvalidOptionArg = class extends ParseError {
|
|
395
|
+
option;
|
|
396
|
+
constructor(option, context) {
|
|
397
|
+
super(context, `Option ${option} doesn't take arguments`);
|
|
398
|
+
this.option = option;
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
var TooManyArguments = class extends ParseError {
|
|
402
|
+
constructor(value, context) {
|
|
403
|
+
super(context, `Too many arguments, ${repr$1(value)} was unexpected`);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
var TooFewOptionArguments = class extends ParseError {
|
|
407
|
+
constructor(option, min, max, context) {
|
|
408
|
+
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`;
|
|
409
|
+
super(context, msg);
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
var TooFewArguments = class extends ParseError {
|
|
413
|
+
constructor(min, max, context) {
|
|
414
|
+
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`;
|
|
415
|
+
super(context, msg);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
var MissingRequiredOption = class extends ParseError {
|
|
419
|
+
constructor(name, context) {
|
|
420
|
+
const msg = `Option ${name} is required, but wasn't found`;
|
|
421
|
+
super(context, msg);
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
// src/parse.ts
|
|
425
|
+
/** The first of `separators` that `text` starts with, or "" if none. */
|
|
426
|
+
function leadingSeparator(text, separators) {
|
|
427
|
+
for (const sep of separators) if (text.startsWith(sep)) return sep;
|
|
428
|
+
return "";
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Find the separator that appears first in `token`, so the value after it can
|
|
432
|
+
* contain the other separators. When two separators start at the same place,
|
|
433
|
+
* the longer one wins. Returns null if there's no separator, or if the one
|
|
434
|
+
* found is empty.
|
|
435
|
+
*/
|
|
436
|
+
function firstSeparator(token, separators) {
|
|
437
|
+
let sep = "";
|
|
438
|
+
let index = -1;
|
|
439
|
+
for (const candidate of separators) {
|
|
440
|
+
const at = token.indexOf(candidate);
|
|
441
|
+
if (at === -1) continue;
|
|
442
|
+
if (index === -1 || at < index || at === index && candidate.length > sep.length) {
|
|
443
|
+
sep = candidate;
|
|
444
|
+
index = at;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return sep ? {
|
|
448
|
+
index,
|
|
449
|
+
sep
|
|
450
|
+
} : null;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Throw if the options used break a `dependsOn` or `exclusiveOn` rule, or if
|
|
454
|
+
* any of `required` is missing.
|
|
455
|
+
*/
|
|
456
|
+
function checkOptionRules(options, seenOptions, required, context) {
|
|
457
|
+
for (const option of seenOptions) {
|
|
458
|
+
if (!option.dependsOn) continue;
|
|
459
|
+
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`);
|
|
460
|
+
}
|
|
461
|
+
for (const option of seenOptions) {
|
|
462
|
+
if (!option.exclusiveOn) continue;
|
|
463
|
+
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}`);
|
|
464
|
+
}
|
|
465
|
+
for (const option of required) for (const name of asArray(option.name)) if (!options.has(name)) throw new MissingRequiredOption(name, context);
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* The state of reading one command line in a single pass. This is the only
|
|
469
|
+
* code that decides what each input token is. When `strict`, it throws on the
|
|
470
|
+
* first problem, which is what {@link parse} needs. Otherwise it never throws:
|
|
471
|
+
* it skips whatever is invalid and keeps going, so {@link readNext} can still
|
|
472
|
+
* read a line that's wrong at the moment. Each method handles one kind of
|
|
473
|
+
* token.
|
|
474
|
+
*/
|
|
475
|
+
var Scanner = class {
|
|
476
|
+
strict;
|
|
477
|
+
path;
|
|
478
|
+
optionAction = void 0;
|
|
479
|
+
args = [];
|
|
480
|
+
options = /* @__PURE__ */ new Map();
|
|
481
|
+
seenOptions = [];
|
|
482
|
+
argSeparatorIndex = -1;
|
|
483
|
+
scope = new OptionScope();
|
|
484
|
+
subcommands = void 0;
|
|
485
|
+
commandArgsMin = 0;
|
|
486
|
+
commandArgsMax = 0;
|
|
487
|
+
directives = this.scope.directives;
|
|
488
|
+
hasFoundArg = false;
|
|
489
|
+
openOption = null;
|
|
490
|
+
openOptionName = "";
|
|
491
|
+
valueCount = 0;
|
|
492
|
+
valueMin = 0;
|
|
493
|
+
separateValueMax = 0;
|
|
494
|
+
values = null;
|
|
495
|
+
valuesStart = 0;
|
|
496
|
+
valuesMin = 0;
|
|
497
|
+
valuesMax = 0;
|
|
498
|
+
valueArgs = [];
|
|
499
|
+
requiredSeparator = false;
|
|
500
|
+
tooManyArgs = null;
|
|
501
|
+
repeatedUses = /* @__PURE__ */ new Map();
|
|
502
|
+
attached = null;
|
|
503
|
+
constructor(spec, strict) {
|
|
504
|
+
this.strict = strict;
|
|
505
|
+
this.path = [spec];
|
|
506
|
+
this.enterCommand(spec);
|
|
507
|
+
}
|
|
508
|
+
ctx() {
|
|
509
|
+
return { path: this.path };
|
|
510
|
+
}
|
|
511
|
+
/** Whether the open option can take the next token as a value. */
|
|
512
|
+
get hasRoomForValue() {
|
|
513
|
+
return this.valueCount < this.separateValueMax;
|
|
514
|
+
}
|
|
515
|
+
enterCommand(command) {
|
|
516
|
+
this.scope.enter(command);
|
|
517
|
+
this.subcommands = command.subcommands;
|
|
518
|
+
this.commandArgsMin = getMinArgs(command.args);
|
|
519
|
+
this.commandArgsMax = getMaxArgs(command.args);
|
|
520
|
+
}
|
|
521
|
+
findOption(name) {
|
|
522
|
+
return this.scope.resolve(name);
|
|
523
|
+
}
|
|
524
|
+
unknownOption(option) {
|
|
525
|
+
return new UnknownOption(option, this.scope.suggestionNames(), this.ctx());
|
|
526
|
+
}
|
|
527
|
+
endOption() {
|
|
528
|
+
const values = this.values;
|
|
529
|
+
if (this.strict && values) {
|
|
530
|
+
const given = values.length - this.valuesStart;
|
|
531
|
+
if (given < this.valuesMin) throw new TooFewOptionArguments(this.openOptionName, this.valuesMin, this.valuesMax, this.ctx());
|
|
532
|
+
addDefaults(values, this.valueArgs, given);
|
|
533
|
+
}
|
|
534
|
+
this.values = null;
|
|
535
|
+
}
|
|
536
|
+
/** End the open option, so no later token can be its value. */
|
|
537
|
+
closeOption() {
|
|
538
|
+
this.endOption();
|
|
539
|
+
this.openOption = null;
|
|
540
|
+
this.valueCount = this.valueMin = this.separateValueMax = 0;
|
|
541
|
+
}
|
|
542
|
+
useOption(option, name) {
|
|
543
|
+
this.endOption();
|
|
544
|
+
this.openOption = option;
|
|
545
|
+
this.openOptionName = name;
|
|
546
|
+
this.valueCount = 0;
|
|
547
|
+
this.valueMin = getMinArgs(option.args);
|
|
548
|
+
this.separateValueMax = option.requiresSeparator ? 0 : getMaxArgs(option.args);
|
|
549
|
+
this.seenOptions.push(option);
|
|
550
|
+
if (option.action) this.optionAction = option.action;
|
|
551
|
+
const names = asArray(option.name);
|
|
552
|
+
const options = this.options;
|
|
553
|
+
let optionValues;
|
|
554
|
+
if (option.isRepeatable) {
|
|
555
|
+
const maxRepeat = option.isRepeatable === true ? Infinity : option.isRepeatable;
|
|
556
|
+
const key = optionKey(option);
|
|
557
|
+
let uses = this.repeatedUses.get(key);
|
|
558
|
+
if (!uses) {
|
|
559
|
+
uses = {
|
|
560
|
+
values: [],
|
|
561
|
+
count: 0
|
|
562
|
+
};
|
|
563
|
+
this.repeatedUses.set(key, uses);
|
|
564
|
+
}
|
|
565
|
+
if (this.strict && uses.count >= maxRepeat) throw new ParseError(this.ctx(), "Too many repetitions");
|
|
566
|
+
uses.count += 1;
|
|
567
|
+
for (const other of names) options.set(other, uses.values);
|
|
568
|
+
if (!option.args) {
|
|
569
|
+
uses.values.push("");
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
optionValues = uses.values;
|
|
573
|
+
} else {
|
|
574
|
+
optionValues = [];
|
|
575
|
+
for (const other of names) options.set(other, optionValues);
|
|
576
|
+
}
|
|
577
|
+
this.valuesMin = this.valueMin;
|
|
578
|
+
this.valuesMax = getMaxArgs(option.args);
|
|
579
|
+
this.requiredSeparator = option.requiresSeparator ?? false;
|
|
580
|
+
if (this.valuesMax > 0) {
|
|
581
|
+
this.values = optionValues;
|
|
582
|
+
this.valuesStart = optionValues.length;
|
|
583
|
+
this.valueArgs = asArray(option.args);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Add a value to the open option. Returns whether it's a value the option
|
|
588
|
+
* accepts. A lenient read keeps going either way.
|
|
589
|
+
*/
|
|
590
|
+
addValue(value, separator) {
|
|
591
|
+
this.valueCount += 1;
|
|
592
|
+
if (!this.values) {
|
|
593
|
+
if (this.strict) throw new InvalidOptionArg(this.openOptionName, this.ctx());
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
const requiredSeparator = this.requiredSeparator;
|
|
597
|
+
let accepted = true;
|
|
598
|
+
if (typeof requiredSeparator === "string" && separator !== requiredSeparator) {
|
|
599
|
+
if (this.strict) throw new ParseError(this.ctx(), `Incorrect separator, use '${requiredSeparator}' instead of '${separator}'`);
|
|
600
|
+
accepted = false;
|
|
601
|
+
}
|
|
602
|
+
if (requiredSeparator === true && separator === "") {
|
|
603
|
+
if (this.strict) {
|
|
604
|
+
const suggested = this.scope.requiredSeparator(this.openOption);
|
|
605
|
+
throw new ParseError(this.ctx(), `Missing separator, use '${suggested}' between ${this.openOptionName} and its value`);
|
|
606
|
+
}
|
|
607
|
+
accepted = false;
|
|
608
|
+
}
|
|
609
|
+
this.values.push(value);
|
|
610
|
+
return accepted;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Add a value attached to the option's own token, the token at `index`,
|
|
614
|
+
* starting at `start`. It's the option's only value, so no later token can be
|
|
615
|
+
* one.
|
|
616
|
+
*/
|
|
617
|
+
addAttachedValue(index, start, value, separator) {
|
|
618
|
+
if (this.addValue(value, separator)) this.attached = {
|
|
619
|
+
option: this.openOption,
|
|
620
|
+
index,
|
|
621
|
+
start
|
|
622
|
+
};
|
|
623
|
+
this.valueMin = this.separateValueMax = 0;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Whether the command already has all the arguments it takes. A command that
|
|
627
|
+
* requires a subcommand takes none.
|
|
628
|
+
*/
|
|
629
|
+
get argsAreFull() {
|
|
630
|
+
const path = this.path;
|
|
631
|
+
return path[path.length - 1].requiresSubcommand === true || this.args.length >= this.commandArgsMax;
|
|
632
|
+
}
|
|
633
|
+
addArg(value) {
|
|
634
|
+
this.endOption();
|
|
635
|
+
if (this.strict && this.argsAreFull) this.tooManyArgs ??= new TooManyArguments(value, this.ctx());
|
|
636
|
+
this.args.push(value);
|
|
637
|
+
}
|
|
638
|
+
/** Read `--` and every token after it as arguments. */
|
|
639
|
+
argSeparator(input, index) {
|
|
640
|
+
this.closeOption();
|
|
641
|
+
const command = this.path[this.path.length - 1];
|
|
642
|
+
if (this.strict && this.argsAreFull && !command.requiresSubcommand) this.tooManyArgs ??= new ParseError(this.ctx(), "Unexpected argument '--', did you mean to use an option instead?");
|
|
643
|
+
this.argSeparatorIndex = this.args.length;
|
|
644
|
+
for (index += 1; index < input.length; index++) this.addArg(input[index]);
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Whether the next token can name a subcommand, its own or an inherited
|
|
648
|
+
* one. It can't once an argument is found, or while the open option needs
|
|
649
|
+
* the next token as its value. A required or variadic option value takes
|
|
650
|
+
* priority over a subcommand.
|
|
651
|
+
*/
|
|
652
|
+
get canMatchSubcommand() {
|
|
653
|
+
if (this.hasFoundArg) return false;
|
|
654
|
+
if (!this.subcommands && this.scope.inheritedSubcommands.length === 0) return false;
|
|
655
|
+
return !(this.valueCount < this.valueMin || this.hasRoomForValue && this.separateValueMax === Infinity);
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* The subcommand the next token enters if it's `token`, or why it enters
|
|
659
|
+
* none. Null if the next token can't be a subcommand at all.
|
|
660
|
+
*/
|
|
661
|
+
subcommandFor(token) {
|
|
662
|
+
if (!this.canMatchSubcommand) return null;
|
|
663
|
+
return matchSubcommand(this.subcommands ?? [], token, this.directives, this.hasRoomForValue, this.scope.inheritedSubcommands);
|
|
664
|
+
}
|
|
665
|
+
enterSubcommand(command) {
|
|
666
|
+
this.closeOption();
|
|
667
|
+
this.path.push(command);
|
|
668
|
+
this.enterCommand(command);
|
|
669
|
+
}
|
|
670
|
+
/** Read a token like `-abc`, `+x` or `-ovalue`. */
|
|
671
|
+
shortOptions(token, index) {
|
|
672
|
+
const leadingChar = token[0];
|
|
673
|
+
const dashOption = this.findOption(leadingChar);
|
|
674
|
+
if (dashOption?.args && !this.findOption(token.slice(0, 2))) {
|
|
675
|
+
this.useOption(dashOption, leadingChar);
|
|
676
|
+
this.addAttachedValue(index, 1, token.slice(1), "");
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
for (let char = 1; char < token.length; char++) {
|
|
680
|
+
const optionName = leadingChar + token[char];
|
|
681
|
+
const option = this.findOption(optionName);
|
|
682
|
+
if (!option) {
|
|
683
|
+
if (char === 1 && this.hasRoomForValue) {
|
|
684
|
+
this.addValue(token, "");
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
if (this.strict) throw this.unknownOption(optionName);
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
this.useOption(option, optionName);
|
|
691
|
+
if (char === token.length - 1) return;
|
|
692
|
+
const sep = leadingSeparator(token.slice(char + 1), this.directives.optionArgSeparators);
|
|
693
|
+
if (option.args || sep && !this.findOption(leadingChar + token[char + 1])) {
|
|
694
|
+
const argStart = char + sep.length + 1;
|
|
695
|
+
this.addAttachedValue(index, argStart, token.slice(argStart), sep);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Read a token like `--name` or `--name=value`, or any token when options
|
|
702
|
+
* aren't POSIX compliant. Returns false if the token should be an argument.
|
|
703
|
+
*/
|
|
704
|
+
longOption(token, index) {
|
|
705
|
+
const found = firstSeparator(token, this.directives.optionArgSeparators);
|
|
706
|
+
const optionName = found ? token.slice(0, found.index) : token;
|
|
707
|
+
const option = this.findOption(optionName);
|
|
708
|
+
if (option) {
|
|
709
|
+
this.useOption(option, optionName);
|
|
710
|
+
if (found) {
|
|
711
|
+
const valueStart = found.index + found.sep.length;
|
|
712
|
+
this.addAttachedValue(index, valueStart, token.slice(valueStart), found.sep);
|
|
713
|
+
}
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
716
|
+
if (this.directives.flagsArePosixNoncompliant) return false;
|
|
717
|
+
if (this.hasRoomForValue) {
|
|
718
|
+
this.addValue(token, "");
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
if (this.strict) throw this.unknownOption(optionName);
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Read a token that isn't `--`, a subcommand or an option. It's a value of
|
|
726
|
+
* the open option if that has room, and otherwise an argument, which stops
|
|
727
|
+
* later tokens from being subcommands. `unmatched` is why the token isn't a
|
|
728
|
+
* subcommand, if it could have been one.
|
|
729
|
+
*/
|
|
730
|
+
valueOrArg(token, unmatched) {
|
|
731
|
+
if (this.hasRoomForValue) {
|
|
732
|
+
this.addValue(token, "");
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
if (this.strict && unmatched && this.argsAreFull) {
|
|
736
|
+
const command = this.path[this.path.length - 1];
|
|
737
|
+
if (command.subcommands?.length || command.requiresSubcommand) {
|
|
738
|
+
const { message, didYouMean } = unmatched.reason();
|
|
739
|
+
this.tooManyArgs ??= new ParseError(this.ctx(), message, didYouMean);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
this.hasFoundArg = true;
|
|
743
|
+
this.addArg(token);
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Throw if the finished command line is missing anything. Returns the action
|
|
747
|
+
* to run.
|
|
748
|
+
*/
|
|
749
|
+
finish() {
|
|
750
|
+
const command = this.path[this.path.length - 1];
|
|
751
|
+
const showsUsage = command.requiresSubcommand && !command.action && this.args.length === 0;
|
|
752
|
+
const skipsChecks = this.optionAction !== void 0 || showsUsage;
|
|
753
|
+
if (!skipsChecks && this.tooManyArgs) throw this.tooManyArgs;
|
|
754
|
+
if (!skipsChecks && this.args.length < this.commandArgsMin) throw new TooFewArguments(this.commandArgsMin, this.commandArgsMax, this.ctx());
|
|
755
|
+
this.endOption();
|
|
756
|
+
if (!skipsChecks) checkOptionRules(this.options, this.seenOptions, this.scope.required(), this.ctx());
|
|
757
|
+
if (!command.requiresSubcommand && this.args.length >= this.commandArgsMin) addDefaults(this.args, asArray(command.args), this.args.length);
|
|
758
|
+
return this.optionAction ?? command.action ?? usage;
|
|
759
|
+
}
|
|
760
|
+
/** See {@link ParseResult.defaults}. */
|
|
761
|
+
defaults(name) {
|
|
762
|
+
if (this.options.has(name)) return;
|
|
763
|
+
const args = asArray(this.scope.resolve(name)?.args);
|
|
764
|
+
if (args.length === 0 || args[0].isOptional) return;
|
|
765
|
+
const values = [];
|
|
766
|
+
addDefaults(values, args);
|
|
767
|
+
return values.length > 0 ? values : void 0;
|
|
768
|
+
}
|
|
769
|
+
/** Whether using `option` next would go over its repeat limit or break an `exclusiveOn` rule. */
|
|
770
|
+
conflicts(option) {
|
|
771
|
+
const names = asArray(option.name);
|
|
772
|
+
const options = this.options;
|
|
773
|
+
if (typeof option.isRepeatable === "number") {
|
|
774
|
+
if ((this.repeatedUses.get(optionKey(option))?.count ?? 0) >= option.isRepeatable) return true;
|
|
775
|
+
}
|
|
776
|
+
if (option.exclusiveOn?.some((name) => options.has(name))) return true;
|
|
777
|
+
return this.seenOptions.some((seen) => seen.exclusiveOn?.some((name) => names.includes(name)));
|
|
778
|
+
}
|
|
779
|
+
/** The options the next token can use. See {@link NextToken.options}. */
|
|
780
|
+
usableOptions() {
|
|
781
|
+
const used = new Set(this.seenOptions.map(optionKey));
|
|
782
|
+
const { local, persistent } = this.scope.live();
|
|
783
|
+
const usable = [];
|
|
784
|
+
for (const { option, names } of [...local, ...persistent]) {
|
|
785
|
+
if (this.conflicts(option)) continue;
|
|
786
|
+
const typeable = names.filter((name) => !this.subcommandFor(name)?.command);
|
|
787
|
+
if (typeable.length === 0) continue;
|
|
788
|
+
usable.push({
|
|
789
|
+
option,
|
|
790
|
+
forms: this.scope.forms(option, typeable),
|
|
791
|
+
used: used.has(optionKey(option))
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
return usable;
|
|
795
|
+
}
|
|
796
|
+
/** What the next token can be. See {@link readNext}. */
|
|
797
|
+
next() {
|
|
798
|
+
const open = this.openOption && this.hasRoomForValue ? this.openOption : null;
|
|
799
|
+
const command = this.path[this.path.length - 1];
|
|
800
|
+
const afterSeparator = this.argSeparatorIndex !== -1;
|
|
801
|
+
const optionsAllowed = !afterSeparator && this.optionsAllowed;
|
|
802
|
+
return {
|
|
803
|
+
path: this.path,
|
|
804
|
+
directives: this.directives,
|
|
805
|
+
optionsAllowed,
|
|
806
|
+
subcommands: !afterSeparator && this.canMatchSubcommand ? withInherited(this.subcommands ?? [], this.scope.inheritedSubcommands) : [],
|
|
807
|
+
options: optionsAllowed ? this.usableOptions() : [],
|
|
808
|
+
value: open && {
|
|
809
|
+
option: open,
|
|
810
|
+
arg: argAt(asArray(open.args), this.valueCount),
|
|
811
|
+
required: this.valueCount < this.valueMin
|
|
812
|
+
},
|
|
813
|
+
arg: open ? void 0 : argAt(asArray(command.args), this.args.length)
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
/** Read the token at `index`, the next one, and say what it was read as. */
|
|
817
|
+
readAndClassify(input, index) {
|
|
818
|
+
if (this.argSeparatorIndex !== -1) return { kind: "argument" };
|
|
819
|
+
const depth = this.path.length;
|
|
820
|
+
const seen = this.seenOptions.length;
|
|
821
|
+
const args = this.args.length;
|
|
822
|
+
const values = this.valueCount;
|
|
823
|
+
if (this.readAt(input, index)) return { kind: "separator" };
|
|
824
|
+
if (this.path.length > depth) return {
|
|
825
|
+
kind: "subcommand",
|
|
826
|
+
command: this.path[depth]
|
|
827
|
+
};
|
|
828
|
+
if (this.seenOptions.length > seen) {
|
|
829
|
+
const found = this.attachedAt(index);
|
|
830
|
+
const attached = found && {
|
|
831
|
+
option: found.option,
|
|
832
|
+
start: found.start
|
|
833
|
+
};
|
|
834
|
+
return {
|
|
835
|
+
kind: "options",
|
|
836
|
+
options: this.seenOptions.slice(seen),
|
|
837
|
+
attached
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
if (this.args.length > args) return { kind: "argument" };
|
|
841
|
+
if (this.valueCount > values) return {
|
|
842
|
+
kind: "value",
|
|
843
|
+
option: this.openOption
|
|
844
|
+
};
|
|
845
|
+
return { kind: "unknown" };
|
|
846
|
+
}
|
|
847
|
+
/** The value attached to the token at `index`, if it has one the option accepts. */
|
|
848
|
+
attachedAt(index) {
|
|
849
|
+
return this.attached?.index === index ? this.attached : null;
|
|
850
|
+
}
|
|
851
|
+
/** Whether the next token can be an option. Once options must stop, every token is an argument. */
|
|
852
|
+
get optionsAllowed() {
|
|
853
|
+
return !(this.directives.optionsMustPrecedeArguments && this.hasFoundArg);
|
|
854
|
+
}
|
|
855
|
+
/** Read every token of `input`. */
|
|
856
|
+
read(input) {
|
|
857
|
+
for (let index = 0; index < input.length; index++) if (this.readAt(input, index)) return;
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Read the token at `index`. Returns true if it was `--`, which reads every
|
|
861
|
+
* token after it too.
|
|
862
|
+
*/
|
|
863
|
+
readAt(input, index) {
|
|
864
|
+
const token = input[index];
|
|
865
|
+
const optionsAllowed = this.optionsAllowed;
|
|
866
|
+
if (token === "--" && optionsAllowed) {
|
|
867
|
+
this.argSeparator(input, index);
|
|
868
|
+
return true;
|
|
869
|
+
}
|
|
870
|
+
const subcommand = this.subcommandFor(token);
|
|
871
|
+
if (subcommand?.command) {
|
|
872
|
+
this.enterSubcommand(subcommand.command);
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
if (optionsAllowed) {
|
|
876
|
+
const flagsArePosixNoncompliant = this.directives.flagsArePosixNoncompliant;
|
|
877
|
+
const first = token.charCodeAt(0);
|
|
878
|
+
const isDoubleDash = first === 45 && token.charCodeAt(1) === 45;
|
|
879
|
+
if (!flagsArePosixNoncompliant && token.length > 1 && (first === 45 || first === 43) && !isDoubleDash) {
|
|
880
|
+
this.shortOptions(token, index);
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
if (flagsArePosixNoncompliant || token.length > 2 && isDoubleDash) {
|
|
884
|
+
if (this.longOption(token, index)) return false;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
this.valueOrArg(token, subcommand);
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
/** The argument at `index`, reusing a final variadic argument. */
|
|
892
|
+
function argAt(args, index) {
|
|
893
|
+
if (index < args.length) return args[index];
|
|
894
|
+
const last = args[args.length - 1];
|
|
895
|
+
return last?.isVariadic ? last : void 0;
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Say what the parser reads `token` as when it comes after `before`. Reads
|
|
899
|
+
* leniently, like {@link readNext}.
|
|
900
|
+
*/
|
|
901
|
+
function readToken(spec, before, token) {
|
|
902
|
+
const s = new Scanner(spec, false);
|
|
903
|
+
s.read(before);
|
|
904
|
+
return s.readAndClassify([...before, token], before.length);
|
|
905
|
+
}
|
|
155
906
|
// src/testing.ts
|
|
156
907
|
const repr = JSON.stringify;
|
|
157
908
|
/**
|
|
@@ -165,12 +916,6 @@ function forEachArgArray(spec, fn, path = []) {
|
|
|
165
916
|
for (const option of makeArray(spec.options)) fn(makeArray(option.args), path);
|
|
166
917
|
for (const subcommand of makeArray(spec.subcommands)) forEachArgArray(subcommand, fn, path);
|
|
167
918
|
}
|
|
168
|
-
/** Run a function once for each `options` property in the spec (recursive) */
|
|
169
|
-
function forEachOptionArray(spec, fn, path = []) {
|
|
170
|
-
path = path.concat(spec);
|
|
171
|
-
fn(makeArray(spec.options), path);
|
|
172
|
-
for (const subcommand of makeArray(spec.subcommands)) forEachOptionArray(subcommand, fn, path);
|
|
173
|
-
}
|
|
174
919
|
/**
|
|
175
920
|
* Run a function once for each `subcommands` property in the spec (recursive)
|
|
176
921
|
*
|
|
@@ -184,9 +929,67 @@ function forEachSubcommand(spec, fn, path = []) {
|
|
|
184
929
|
function joinNames(names) {
|
|
185
930
|
return makeArray(names).sort((a, b) => a.length - b.length).join("|");
|
|
186
931
|
}
|
|
932
|
+
/** Compares spec values by shape. Any two functions count as the same shape. */
|
|
933
|
+
function isSameStructure(a, b) {
|
|
934
|
+
if (a === b) return true;
|
|
935
|
+
if (typeof a === "function" && typeof b === "function") return true;
|
|
936
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
937
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
938
|
+
const left = a;
|
|
939
|
+
const right = b;
|
|
940
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)])].every((key) => isSameStructure(left[key], right[key]));
|
|
941
|
+
}
|
|
187
942
|
function namedArrayToString(...parts) {
|
|
188
943
|
return `\`${parts.map((named) => joinNames(named.name)).join(" ")}\``;
|
|
189
944
|
}
|
|
945
|
+
/** Name the last command on `path` in a message, with its index among its siblings. */
|
|
946
|
+
function describeCommand(path) {
|
|
947
|
+
const command = path[path.length - 1];
|
|
948
|
+
const parent = path[path.length - 2];
|
|
949
|
+
if (!parent) return `Command ${namedArrayToString(command)}`;
|
|
950
|
+
const index = makeArray(parent.subcommands).indexOf(command);
|
|
951
|
+
return `Subcommand ${namedArrayToString(...path)} (index ${index})`;
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Name the option at `index` on the last command on `definedIn` in a message,
|
|
955
|
+
* and the command it's used in when that isn't the one that defines it.
|
|
956
|
+
*/
|
|
957
|
+
function describeOption(definedIn, index, usedIn = definedIn) {
|
|
958
|
+
const option = makeArray(definedIn[definedIn.length - 1].options)[index];
|
|
959
|
+
const name = `${namedArrayToString(...definedIn, option)} (index ${index})`;
|
|
960
|
+
return usedIn === definedIn ? `Option ${name}` : `In ${namedArrayToString(...usedIn)}, option ${name}`;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Run a function once for each option at each command it can be used in: the
|
|
964
|
+
* command's own options, in definition order, then the persistent ones it
|
|
965
|
+
* inherits. A check that is about how the option is written skips the uses
|
|
966
|
+
* that aren't `isDefinedHere`. A check that depends on the scope sees the
|
|
967
|
+
* option the way the parser does at each command.
|
|
968
|
+
*/
|
|
969
|
+
function forEachOption(spec, fn) {
|
|
970
|
+
const visit = (path, inherited) => {
|
|
971
|
+
const command = path[path.length - 1];
|
|
972
|
+
const scope = OptionScope.at(path);
|
|
973
|
+
const own = makeArray(command.options).map((option, index) => ({
|
|
974
|
+
option,
|
|
975
|
+
definedIn: path,
|
|
976
|
+
index
|
|
977
|
+
}));
|
|
978
|
+
for (const { option, definedIn, index } of [...own, ...inherited]) {
|
|
979
|
+
const where = describeOption(definedIn, index, path);
|
|
980
|
+
fn({
|
|
981
|
+
option,
|
|
982
|
+
path,
|
|
983
|
+
scope,
|
|
984
|
+
isDefinedHere: definedIn === path,
|
|
985
|
+
where
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
const next = [...inherited, ...own.filter(({ option }) => option.isPersistent)];
|
|
989
|
+
for (const subcommand of makeArray(command.subcommands)) visit([...path, subcommand], next);
|
|
990
|
+
};
|
|
991
|
+
visit([spec], []);
|
|
992
|
+
}
|
|
190
993
|
/** Asserts that required arguments do not come after an optional argument */
|
|
191
994
|
function assertRequiredArgumentsDoNotFollowOptionalArguments(spec) {
|
|
192
995
|
forEachArgArray(spec, (args, path) => {
|
|
@@ -199,20 +1002,38 @@ function assertRequiredArgumentsDoNotFollowOptionalArguments(spec) {
|
|
|
199
1002
|
}
|
|
200
1003
|
});
|
|
201
1004
|
}
|
|
202
|
-
/**
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
1005
|
+
/**
|
|
1006
|
+
* Asserts that the parser can use every default. A positional argument with a
|
|
1007
|
+
* default must be optional. A required option argument's default is only used
|
|
1008
|
+
* when the option is left out, so every argument before it needs a default
|
|
1009
|
+
* too, and the option can't be required.
|
|
1010
|
+
*/
|
|
1011
|
+
function assertDefaultsCanBeUsed(spec) {
|
|
1012
|
+
forEachSubcommand(spec, (command, path) => {
|
|
1013
|
+
for (const [index, arg] of makeArray(command.args).entries()) assert(arg.default === void 0 || arg.isOptional, `In ${namedArrayToString(...path)}, argument ${arg.name || index} has a default, but it's required, so the default is never used`);
|
|
1014
|
+
});
|
|
1015
|
+
forEachOption(spec, ({ option, isDefinedHere, where }) => {
|
|
1016
|
+
if (!isDefinedHere) return;
|
|
1017
|
+
let firstWithout = null;
|
|
1018
|
+
for (const [index, arg] of makeArray(option.args).entries()) {
|
|
1019
|
+
if (arg.default === void 0) {
|
|
1020
|
+
firstWithout ??= index;
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
if (arg.isOptional) continue;
|
|
1024
|
+
const name = `${where}, argument ${arg.name || index} has a default, but`;
|
|
1025
|
+
assert(!option.isRequired, `${name} the option is required, so the default is never used`);
|
|
1026
|
+
assert(firstWithout === null, `${name} the argument at index ${firstWithout} has none, so the default is never used`);
|
|
1027
|
+
}
|
|
206
1028
|
});
|
|
207
1029
|
}
|
|
208
1030
|
/** Asserts that `isRepeatable` is only assigned a positive integer */
|
|
209
1031
|
function assertRepeatableOptionsArePositiveIntegers(spec) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
1032
|
+
forEachOption(spec, ({ option, isDefinedHere, where }) => {
|
|
1033
|
+
if (!isDefinedHere || typeof option.isRepeatable !== "number") return;
|
|
1034
|
+
assert(Number.isSafeInteger(option.isRepeatable), `${where} has its \`isRepeatable\` value set to a number that is not an integer`);
|
|
1035
|
+
assert(!(option.isRepeatable < 1), `${where} has its \`isRepeatable\` value set to a number below 1, which means it can't be used at all`);
|
|
1036
|
+
assert(option.isRepeatable !== 1, `${where} has its \`isRepeatable\` value set to 1, which is no different that omitting the property entirely`);
|
|
216
1037
|
});
|
|
217
1038
|
}
|
|
218
1039
|
/** Asserts that options have unique names in their scope */
|
|
@@ -220,16 +1041,57 @@ function assertOptionsHaveLocallyUniqueNames(spec) {
|
|
|
220
1041
|
forEachSubcommand(spec, (spec, path) => {
|
|
221
1042
|
const optionNames = /* @__PURE__ */ new Set();
|
|
222
1043
|
for (const [index, option] of makeArray(spec.options).entries()) for (const name of makeArray(option.name)) {
|
|
223
|
-
assert(!optionNames.has(name),
|
|
1044
|
+
assert(!optionNames.has(name), `${describeOption(path, index)} has a non-unique name, '${name}'. Option names must be unique.`);
|
|
224
1045
|
optionNames.add(name);
|
|
225
1046
|
}
|
|
226
1047
|
});
|
|
227
1048
|
}
|
|
228
1049
|
/** Asserts that options don't shadow a persistent option */
|
|
229
1050
|
function assertOptionsDoNotShadowPersistentOptions(spec) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
1051
|
+
forEachOption(spec, ({ option, path, isDefinedHere, where }) => {
|
|
1052
|
+
if (!isDefinedHere) return;
|
|
1053
|
+
const inherited = OptionScope.at(path.slice(0, -1)).persistent;
|
|
1054
|
+
for (const name of makeArray(option.name)) assert(!inherited.some((persistent) => makeArray(persistent.name).includes(name)), `${where} shadows the name of a persistent option, ${repr(name)}. Option names can't shadow persistent options.`);
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Asserts that when a subcommand's option and a different option on a parent
|
|
1059
|
+
* share a name, each of them has a name of its own.
|
|
1060
|
+
*
|
|
1061
|
+
* Parsed values are looked up by name, and an option sets every one of its
|
|
1062
|
+
* names when it's used. If every name of the parent's option is also the
|
|
1063
|
+
* subcommand's, using the subcommand's option replaces the parent's value, so
|
|
1064
|
+
* it can't be read. If every name of the subcommand's option is also the
|
|
1065
|
+
* parent's, its names hold the parent's value when it isn't used, so it can't
|
|
1066
|
+
* tell whether it was. Options with the same names are fine when they're
|
|
1067
|
+
* identical, such as an option kept in a constant, because the parser treats
|
|
1068
|
+
* them as one option.
|
|
1069
|
+
*
|
|
1070
|
+
* This skips persistent parent options, because
|
|
1071
|
+
* {@link assertOptionsDoNotShadowPersistentOptions} covers those and
|
|
1072
|
+
* `allowShadowingPersistentOptions` opts out of it.
|
|
1073
|
+
*/
|
|
1074
|
+
function assertOptionsHaveDifferentNamesFromParents(spec) {
|
|
1075
|
+
forEachOption(spec, ({ option, path, isDefinedHere, where }) => {
|
|
1076
|
+
if (!isDefinedHere) return;
|
|
1077
|
+
const names = makeArray(option.name);
|
|
1078
|
+
for (const parent of path.slice(0, -1)) for (const parentOption of makeArray(parent.options)) {
|
|
1079
|
+
if (parentOption.isPersistent) continue;
|
|
1080
|
+
const parentNames = makeArray(parentOption.name);
|
|
1081
|
+
const other = namedArrayToString(parent, parentOption);
|
|
1082
|
+
if (optionKey(parentOption) === optionKey(option)) {
|
|
1083
|
+
assert(isSameStructure({
|
|
1084
|
+
...parentOption,
|
|
1085
|
+
name: 0
|
|
1086
|
+
}, {
|
|
1087
|
+
...option,
|
|
1088
|
+
name: 0
|
|
1089
|
+
}), `${where} has the same names as a different option, ${other}, so the parent's value can't be read. Add a name that only one of them uses.`);
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
assert(!parentNames.every((name) => names.includes(name)), `${where} has every name of a different option, ${other}, so the parent's value can't be read. Add a name that only the parent's option uses.`);
|
|
1093
|
+
assert(!names.every((name) => parentNames.includes(name)), `${where} only has names of a different option, ${other}, so it can't tell whether it was used. Add a name that only the subcommand's option uses.`);
|
|
1094
|
+
}
|
|
233
1095
|
});
|
|
234
1096
|
}
|
|
235
1097
|
/** Asserts that subcommands don't share a name with a sibling subcommand */
|
|
@@ -243,68 +1105,33 @@ function assertSubcommandsHaveLocallyUniqueNames(spec) {
|
|
|
243
1105
|
});
|
|
244
1106
|
}
|
|
245
1107
|
/**
|
|
246
|
-
* Asserts that option names starting with a single dash aren't longer
|
|
247
|
-
* than two characters, unless parserDirectives.flagsArePosixNoncompliant is
|
|
248
|
-
* set on an ancestor command.
|
|
249
|
-
*/
|
|
250
|
-
function assertLongOptionNamesDoNotStartWithSingleDash(spec) {
|
|
251
|
-
forEachOptionArray(spec, (options, path) => {
|
|
252
|
-
if (path.map((command) => command.parserDirectives?.flagsArePosixNoncompliant).filter((value) => value !== void 0).at(-1) || false) return;
|
|
253
|
-
for (const [index, option] of makeArray(options).entries()) for (const name of makeArray(option.name)) assert(!((name.startsWith("-") || name.startsWith("+")) && !name.startsWith("--") && name.length > 2), `Option ${namedArrayToString(...path, option)} (index ${index}) has a name that the parser is unable to parse, as it starts with a single '${name[0]}'. Tokens starting with that character are interpreted as short options, so passing '${name}' will be interpreted as '${Array.from(name.slice(1), (letter) => name[0] + letter).join(" ")}'. \`parserDirectives.flagsArePosixNoncompliant\` must be \`true\` on an ancestor command.`);
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* Asserts that all option names start with dashes
|
|
258
|
-
*/
|
|
259
|
-
function assertOptionNamesStartWithDashes(spec) {
|
|
260
|
-
forEachOptionArray(spec, (options, path) => {
|
|
261
|
-
if (path.map((command) => command.parserDirectives?.flagsArePosixNoncompliant).filter((value) => value !== void 0).at(-1) || false) return;
|
|
262
|
-
for (const [index, option] of makeArray(options).entries()) for (const name of makeArray(option.name)) assert(name.startsWith("-") || name.startsWith("+"), `Option ${namedArrayToString(...path, option)} (index ${index}) doesn't start with a dash or plus, the parser will be unable to find matching options without \`parserDirectives.flagsAreNonPosixCompliant\` being set to 'true' on an ancestor command`);
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* Asserts that nothing is named exactly '--'
|
|
267
|
-
*/
|
|
268
|
-
function assertNothingIsNamedDashDash(spec) {
|
|
269
|
-
forEachOptionArray(spec, (options, path) => {
|
|
270
|
-
for (const [index, option] of makeArray(options).entries()) for (const name of makeArray(option.name)) assert(name !== "--", `Option ${namedArrayToString(...path, option)} (index ${index}) is named '--', which is a special instruction to the parser to treat all following tokens as arguments. This will never be matched`);
|
|
271
|
-
});
|
|
272
|
-
forEachSubcommand(spec, (subcommands, path) => {
|
|
273
|
-
for (const [index, subcommand] of makeArray(subcommands).entries()) for (const name of makeArray(subcommand.name)) assert(name !== "--", `Subcommand ${namedArrayToString(...path, subcommand)} (index ${index}) is named '--', which is a special instruction to the parser to treat all following tokens as arguments. This will never be matched`);
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
1108
|
* Asserts that no option arg separator is an empty string
|
|
278
1109
|
*/
|
|
279
1110
|
function assertOptionArgSeparatorsHaveCharacters(spec) {
|
|
280
|
-
forEachSubcommand(spec, (
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
for (const [sepIndex, separator] of separators.entries()) assert(separator !== "", `Subcommand ${namedArrayToString(...path, subcommand)} (index ${index}) has an empty string for an option arg separator at index ${sepIndex}. If you wanted to disable option arg separators, use an empty array.`);
|
|
284
|
-
}
|
|
1111
|
+
forEachSubcommand(spec, (command, path) => {
|
|
1112
|
+
const separators = makeArray(command.parserDirectives?.optionArgSeparators);
|
|
1113
|
+
for (const [sepIndex, separator] of separators.entries()) assert(separator !== "", `${describeCommand(path)} has an empty string for an option arg separator at index ${sepIndex}. If you wanted to disable option arg separators, use an empty array.`);
|
|
285
1114
|
});
|
|
286
1115
|
}
|
|
287
1116
|
/**
|
|
288
|
-
* Asserts that options named "+" or "-"
|
|
1117
|
+
* Asserts that the parser reads options named "+" or "-" the way they're
|
|
1118
|
+
* written, which needs them to take one argument.
|
|
1119
|
+
*
|
|
1120
|
+
* @deprecated {@link assertNamesReadBack} checks this for every option.
|
|
289
1121
|
*/
|
|
290
1122
|
function assertPlusMinusOptionsTakeOneArg(spec) {
|
|
291
|
-
|
|
292
|
-
if (path.map((command) => command.parserDirectives?.flagsArePosixNoncompliant).filter((value) => value !== void 0).at(-1) || false) return;
|
|
293
|
-
for (const [index, option] of makeArray(options).entries()) for (const name of makeArray(option.name)) {
|
|
294
|
-
if (name !== "-" && name !== "+") continue;
|
|
295
|
-
assert(option.args && !Array.isArray(option.args), `Option ${namedArrayToString(...path, option)} (index ${index}) must take exactly one argument`);
|
|
296
|
-
}
|
|
297
|
-
});
|
|
1123
|
+
assertOptionFormsReadBack(spec, (_, name) => name === "-" || name === "+");
|
|
298
1124
|
}
|
|
299
1125
|
/**
|
|
300
1126
|
* Asserts that names do not have leading or trailing whitespace
|
|
301
1127
|
*/
|
|
302
1128
|
function assertNamesHaveNoExtraWhitespace(spec) {
|
|
303
|
-
|
|
304
|
-
|
|
1129
|
+
forEachOption(spec, ({ option, isDefinedHere, where }) => {
|
|
1130
|
+
if (!isDefinedHere) return;
|
|
1131
|
+
for (const name of makeArray(option.name)) assert(name === name.trim(), `${where} has a name with extra whitespace`);
|
|
305
1132
|
});
|
|
306
|
-
forEachSubcommand(spec, (
|
|
307
|
-
for (const
|
|
1133
|
+
forEachSubcommand(spec, (command, path) => {
|
|
1134
|
+
for (const name of makeArray(command.name)) assert(name === name.trim(), `${describeCommand(path)} has a name with extra whitespace`);
|
|
308
1135
|
});
|
|
309
1136
|
forEachArgArray(spec, (args, path) => {
|
|
310
1137
|
for (const [index, arg] of makeArray(args).entries()) for (const name of makeArray(arg.name)) assert(name === name.trim(), `Arg in ${namedArrayToString(...path)} (at index ${index}) has a name with extra whitespace`);
|
|
@@ -315,11 +1142,11 @@ function assertNamesHaveNoExtraWhitespace(spec) {
|
|
|
315
1142
|
*/
|
|
316
1143
|
function assertEverythingHasDescription(spec) {
|
|
317
1144
|
assert(spec.description, `Spec has no description`);
|
|
318
|
-
|
|
319
|
-
|
|
1145
|
+
forEachOption(spec, ({ option, isDefinedHere, where }) => {
|
|
1146
|
+
if (isDefinedHere) assert(option.description, `${where} has no description`);
|
|
320
1147
|
});
|
|
321
|
-
forEachSubcommand(spec, (
|
|
322
|
-
|
|
1148
|
+
forEachSubcommand(spec, (command, path) => {
|
|
1149
|
+
assert(command.description, `${describeCommand(path)} has no description`);
|
|
323
1150
|
});
|
|
324
1151
|
}
|
|
325
1152
|
/**
|
|
@@ -327,34 +1154,137 @@ function assertEverythingHasDescription(spec) {
|
|
|
327
1154
|
*/
|
|
328
1155
|
function assertDescriptionLineLengthUnder69(spec) {
|
|
329
1156
|
if (spec.description) assert(spec.description.split("\n").every((line) => line.length <= 68), `Spec has a description line over 68 characters`);
|
|
330
|
-
|
|
331
|
-
|
|
1157
|
+
forEachOption(spec, ({ option, isDefinedHere, where }) => {
|
|
1158
|
+
if (!isDefinedHere || !option.description) return;
|
|
1159
|
+
assert(option.description.split("\n").every((line) => line.length <= 68), `${where} has a description line over 68 characters`);
|
|
332
1160
|
});
|
|
333
|
-
forEachSubcommand(spec, (
|
|
334
|
-
|
|
1161
|
+
forEachSubcommand(spec, (command, path) => {
|
|
1162
|
+
if (command.description) assert(command.description.split("\n").every((line) => line.length <= 68), `${describeCommand(path)} has a description line over 68 characters`);
|
|
1163
|
+
for (const [index, arg] of makeArray(command.args).entries()) assert(!arg.description || arg.description.split("\n").every((line) => line.length <= 68), `In ${namedArrayToString(...path)}, argument ${arg.name || index} has a description line over 68 characters`);
|
|
335
1164
|
});
|
|
336
1165
|
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Asserts that the parser reads options with `requiresSeparator` the way
|
|
1168
|
+
* they're written, which needs them to take at most one argument.
|
|
1169
|
+
*
|
|
1170
|
+
* @deprecated {@link assertNamesReadBack} checks this for every option.
|
|
1171
|
+
*/
|
|
337
1172
|
function assertRequiresSeparatorTakesOneArg(spec) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
1173
|
+
assertOptionFormsReadBack(spec, (option) => Boolean(option.requiresSeparator));
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* What the parser read a token as, for a message. `path` is the command the
|
|
1177
|
+
* token came after.
|
|
1178
|
+
*/
|
|
1179
|
+
function describeReading(reading, path) {
|
|
1180
|
+
switch (reading.kind) {
|
|
1181
|
+
case "separator": return "the end of options, '--'";
|
|
1182
|
+
case "subcommand": return `the subcommand ${namedArrayToString(...path, reading.command)}`;
|
|
1183
|
+
case "options": return reading.options.length === 1 ? `the option ${namedArrayToString(reading.options[0])}` : `the options ${reading.options.map((option) => namedArrayToString(option)).join(", ")}`;
|
|
1184
|
+
case "value": return `a value of the option ${namedArrayToString(reading.option)}`;
|
|
1185
|
+
case "argument": return "an argument";
|
|
1186
|
+
case "unknown": return "an option that doesn't exist";
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
/** Whether the parser read a token as `option` alone. */
|
|
1190
|
+
function readsAs(reading, option) {
|
|
1191
|
+
return reading.kind === "options" && reading.options.length === 1 && reading.options[0] === option;
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* The tokens that reach the last command on `path`, using each command's first
|
|
1195
|
+
* name. {@link assertNamesReadBack} checks subcommand names from the root down
|
|
1196
|
+
* before options, so each of these is a name the parser reads.
|
|
1197
|
+
*/
|
|
1198
|
+
function tokensTo(path) {
|
|
1199
|
+
return path.slice(1).map((command) => makeArray(command.name)[0]);
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* A value for writing options with at the last command on `path`. No name
|
|
1203
|
+
* there starts with it, or with it after '-' or '+', so the parser can only
|
|
1204
|
+
* read it as a value.
|
|
1205
|
+
*/
|
|
1206
|
+
function placeholderValue(path, scope) {
|
|
1207
|
+
const names = [...[...scope.local, ...scope.persistent].flatMap((option) => makeArray(option.name)), ...makeArray(path[path.length - 1].subcommands).flatMap((command) => makeArray(command.name))];
|
|
1208
|
+
for (const value of "0123456789abcdefghijklmnopqrstuvwxyz") {
|
|
1209
|
+
const starts = [
|
|
1210
|
+
value,
|
|
1211
|
+
`-${value}`,
|
|
1212
|
+
`+${value}`
|
|
1213
|
+
];
|
|
1214
|
+
if (!names.some((name) => starts.some((start) => name.startsWith(start)))) return value;
|
|
1215
|
+
}
|
|
1216
|
+
return "0";
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* The tokens that write `form` with every value `option` takes, or one more
|
|
1220
|
+
* than its minimum if it takes any number. The first value is joined to the
|
|
1221
|
+
* name if the form says so, and the rest are separate tokens.
|
|
1222
|
+
*/
|
|
1223
|
+
function writeForm(option, form, value) {
|
|
1224
|
+
const max = getMaxArgs(option.args);
|
|
1225
|
+
const count = Number.isFinite(max) ? max : getMinArgs(option.args) + 1;
|
|
1226
|
+
const values = Array.from({ length: count }, () => value);
|
|
1227
|
+
if (form.join === null) return [form.name, ...values];
|
|
1228
|
+
return [form.name + form.join + values[0], ...values.slice(1)];
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Asserts that the parser reads each name of the options that `include`
|
|
1232
|
+
* picks the way the name is written. See {@link assertNamesReadBack}.
|
|
1233
|
+
*/
|
|
1234
|
+
function assertOptionFormsReadBack(spec, include) {
|
|
1235
|
+
const root = spec;
|
|
1236
|
+
forEachOption(spec, ({ option, where, scope, path }) => {
|
|
1237
|
+
const before = tokensTo(path);
|
|
1238
|
+
const value = placeholderValue(path, scope);
|
|
1239
|
+
const names = makeArray(option.name).filter((name) => scope.resolve(name) === option);
|
|
1240
|
+
for (const form of scope.forms(option, names)) {
|
|
1241
|
+
if (!include(option, form.name)) continue;
|
|
1242
|
+
if (form.join !== "") {
|
|
1243
|
+
const reading = readToken(root, before, form.name);
|
|
1244
|
+
assert(readsAs(reading, option), `${where} has the name '${form.name}', but the parser reads it as ${describeReading(reading, path)}`);
|
|
1245
|
+
}
|
|
1246
|
+
const tokens = writeForm(option, form, value);
|
|
1247
|
+
const written = `${where} is written '${optionToString(option, [form])}'`;
|
|
1248
|
+
if (form.join !== null) {
|
|
1249
|
+
const first = readToken(root, before, tokens[0]);
|
|
1250
|
+
const joined = first.kind === "options" && first.attached?.option === option;
|
|
1251
|
+
assert(readsAs(first, option) && joined, `${written}, but the parser reads '${tokens[0]}' as ${describeReading(first, path)}${joined ? "" : " without a value"}`);
|
|
1252
|
+
}
|
|
1253
|
+
for (let index = 1; index < tokens.length; index++) {
|
|
1254
|
+
const reading = readToken(root, [...before, ...tokens.slice(0, index)], tokens[index]);
|
|
1255
|
+
assert(reading.kind === "value" && reading.option === option, `${written}, but in '${tokens.join(" ")}' the parser reads '${tokens[index]}' as ${describeReading(reading, path)}`);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Asserts that the parser reads every name the way it's written: each name of
|
|
1262
|
+
* a subcommand enters that subcommand, and each name of an option uses that
|
|
1263
|
+
* option, at every command the option can be used in. Each option is also
|
|
1264
|
+
* written the way help shows it, with every value it takes, and the parser has
|
|
1265
|
+
* to read all of those values as the option's.
|
|
1266
|
+
*/
|
|
1267
|
+
function assertNamesReadBack(spec) {
|
|
1268
|
+
const root = spec;
|
|
1269
|
+
forEachSubcommand(spec, (command, path) => {
|
|
1270
|
+
const before = tokensTo(path);
|
|
1271
|
+
for (const subcommand of makeArray(command.subcommands)) for (const name of makeArray(subcommand.name)) {
|
|
1272
|
+
const reading = readToken(root, before, name);
|
|
1273
|
+
if (reading.kind === "subcommand" && reading.command === subcommand) continue;
|
|
1274
|
+
assert(false, `${describeCommand([...path, subcommand])} has the name '${name}', but the parser reads it as ${describeReading(reading, path)}`);
|
|
343
1275
|
}
|
|
344
1276
|
});
|
|
1277
|
+
assertOptionFormsReadBack(spec, () => true);
|
|
345
1278
|
}
|
|
346
1279
|
function assertCommonOptionsArePersistent(spec) {
|
|
347
|
-
const isSameName = (a, b) => {
|
|
348
|
-
if (isArray(a) && isArray(b)) return a.every((item) => b.includes(item)) && b.every((item) => a.includes(item));
|
|
349
|
-
return a === b;
|
|
350
|
-
};
|
|
351
1280
|
const failures = [];
|
|
352
1281
|
forEachSubcommand(spec, (subcommand, path) => {
|
|
353
1282
|
if (!subcommand.subcommands) return;
|
|
354
1283
|
for (const option of makeArray(subcommand.options)) {
|
|
355
1284
|
const allChildSubcommands = [];
|
|
356
1285
|
forEachSubcommand(subcommand, (cmd) => allChildSubcommands.push(cmd));
|
|
357
|
-
|
|
1286
|
+
const key = optionKey(option);
|
|
1287
|
+
if (allChildSubcommands.every((cmd) => makeArray(cmd.options).some((o) => optionKey(o) === key)) && !failures.some(([failed]) => failed === key)) failures.push([key, namedArrayToString(...path, option)]);
|
|
358
1288
|
}
|
|
359
1289
|
});
|
|
360
1290
|
assert(failures.length === 0, `The following options were also defined by all subcommands. Instead, define the option once, and use \`isPersistent: true\` to persist it across all subcommands.
|
|
@@ -363,15 +1293,12 @@ ${failures.map(([, line]) => " * " + line).join("\n")}
|
|
|
363
1293
|
}
|
|
364
1294
|
/**
|
|
365
1295
|
* Asserts that references to other options by name actually refer to options
|
|
366
|
-
* that exist in scope
|
|
1296
|
+
* that exist in scope, at every command the option can be used in
|
|
367
1297
|
*/
|
|
368
1298
|
function assertOptionNameReferencesExist(spec) {
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
for (const
|
|
372
|
-
for (const name of makeArray(option.dependsOn)) assert(optionNames.has(name), `The option ${namedArrayToString(...path, option)} (index ${index}) depends on an option named ${repr(name)}, which doesn't exist in its scope`);
|
|
373
|
-
for (const name of makeArray(option.exclusiveOn)) assert(optionNames.has(name), `The option ${namedArrayToString(...path, option)} (index ${index}) is exclusive on an option named ${repr(name)}, which doesn't exist in its scope`);
|
|
374
|
-
}
|
|
1299
|
+
forEachOption(spec, ({ option, where, scope }) => {
|
|
1300
|
+
for (const name of makeArray(option.dependsOn)) assert(scope.resolve(name) !== null, `${where} depends on an option named ${repr(name)}, which doesn't exist in its scope`);
|
|
1301
|
+
for (const name of makeArray(option.exclusiveOn)) assert(scope.resolve(name) !== null, `${where} is exclusive on an option named ${repr(name)}, which doesn't exist in its scope`);
|
|
375
1302
|
});
|
|
376
1303
|
}
|
|
377
1304
|
/**
|
|
@@ -379,13 +1306,10 @@ function assertOptionNameReferencesExist(spec) {
|
|
|
379
1306
|
*/
|
|
380
1307
|
function assertPrefixMatchCommandsHaveNoArguments(spec) {
|
|
381
1308
|
forEachSubcommand(spec, (subcommand, path) => {
|
|
382
|
-
if (!
|
|
1309
|
+
if (!OptionScope.at(path).directives.subcommandsMatchUniquePrefix) return;
|
|
383
1310
|
assert(!(subcommand.subcommands && subcommand.args), `The command ${namedArrayToString(...path)} has at least one argument and subcommand, but matches subcommands based on unique prefixes. To fix this, use \`parserDirectives: { subcommandsMatchUniquePrefix: false }\``);
|
|
384
1311
|
});
|
|
385
1312
|
}
|
|
386
|
-
function getName(namingStyle, names) {
|
|
387
|
-
return names[namingStyle];
|
|
388
|
-
}
|
|
389
1313
|
async function runStep(context, name, fn) {
|
|
390
1314
|
if (context) {
|
|
391
1315
|
await context.step(name, fn);
|
|
@@ -402,118 +1326,106 @@ async function runStep(context, name, fn) {
|
|
|
402
1326
|
*/
|
|
403
1327
|
function test(spec, options = {}) {
|
|
404
1328
|
const { allowShadowingPersistentOptions = false, allowNoDescription = false, allowLongDescriptionLines = false, allowMatchingSubcommandPrefixAndArgs = false, namingStyle = "sentence" } = options;
|
|
1329
|
+
const checks = [
|
|
1330
|
+
[
|
|
1331
|
+
"Required arguments don't follow optional arguments",
|
|
1332
|
+
"assertRequiredArgumentsDoNotFollowOptionalArguments",
|
|
1333
|
+
assertRequiredArgumentsDoNotFollowOptionalArguments,
|
|
1334
|
+
true
|
|
1335
|
+
],
|
|
1336
|
+
[
|
|
1337
|
+
"The parser can use every default",
|
|
1338
|
+
"assertDefaultsCanBeUsed",
|
|
1339
|
+
assertDefaultsCanBeUsed,
|
|
1340
|
+
true
|
|
1341
|
+
],
|
|
1342
|
+
[
|
|
1343
|
+
"Repeatable options that are numbers are integers greater than 1",
|
|
1344
|
+
"assertRepeatableOptionsArePositiveIntegers",
|
|
1345
|
+
assertRepeatableOptionsArePositiveIntegers,
|
|
1346
|
+
true
|
|
1347
|
+
],
|
|
1348
|
+
[
|
|
1349
|
+
"Options have locally unique names",
|
|
1350
|
+
"assertOptionsHaveLocallyUniqueNames",
|
|
1351
|
+
assertOptionsHaveLocallyUniqueNames,
|
|
1352
|
+
true
|
|
1353
|
+
],
|
|
1354
|
+
[
|
|
1355
|
+
"Options don't shadow persistent options",
|
|
1356
|
+
"assertOptionsDoNotShadowPersistentOptions",
|
|
1357
|
+
assertOptionsDoNotShadowPersistentOptions,
|
|
1358
|
+
!allowShadowingPersistentOptions
|
|
1359
|
+
],
|
|
1360
|
+
[
|
|
1361
|
+
"Different options on a subcommand and its parents have different names",
|
|
1362
|
+
"assertOptionsHaveDifferentNamesFromParents",
|
|
1363
|
+
assertOptionsHaveDifferentNamesFromParents,
|
|
1364
|
+
true
|
|
1365
|
+
],
|
|
1366
|
+
[
|
|
1367
|
+
"Subcommands have locally unique names",
|
|
1368
|
+
"assertSubcommandsHaveLocallyUniqueNames",
|
|
1369
|
+
assertSubcommandsHaveLocallyUniqueNames,
|
|
1370
|
+
true
|
|
1371
|
+
],
|
|
1372
|
+
[
|
|
1373
|
+
"The parser reads every option and subcommand the way it's written",
|
|
1374
|
+
"assertNamesReadBack",
|
|
1375
|
+
assertNamesReadBack,
|
|
1376
|
+
true
|
|
1377
|
+
],
|
|
1378
|
+
[
|
|
1379
|
+
"Option arg separators are not an empty string",
|
|
1380
|
+
"assertOptionArgSeparatorsHaveCharacters",
|
|
1381
|
+
assertOptionArgSeparatorsHaveCharacters,
|
|
1382
|
+
true
|
|
1383
|
+
],
|
|
1384
|
+
[
|
|
1385
|
+
"Names have no extra leading or trailing whitespace",
|
|
1386
|
+
"assertNamesHaveNoExtraWhitespace",
|
|
1387
|
+
assertNamesHaveNoExtraWhitespace,
|
|
1388
|
+
true
|
|
1389
|
+
],
|
|
1390
|
+
[
|
|
1391
|
+
"Everything has a description",
|
|
1392
|
+
"assertEverythingHasDescription",
|
|
1393
|
+
assertEverythingHasDescription,
|
|
1394
|
+
!allowNoDescription
|
|
1395
|
+
],
|
|
1396
|
+
[
|
|
1397
|
+
"Each line in descriptions is less than or equal to 68 characters",
|
|
1398
|
+
"assertDescriptionLineLengthUnder69",
|
|
1399
|
+
assertDescriptionLineLengthUnder69,
|
|
1400
|
+
!allowLongDescriptionLines
|
|
1401
|
+
],
|
|
1402
|
+
[
|
|
1403
|
+
"Common options are not defined on every subcommand",
|
|
1404
|
+
"assertCommonOptionsArePersistent",
|
|
1405
|
+
assertCommonOptionsArePersistent,
|
|
1406
|
+
true
|
|
1407
|
+
],
|
|
1408
|
+
[
|
|
1409
|
+
"References to other options by name are valid",
|
|
1410
|
+
"assertOptionNameReferencesExist",
|
|
1411
|
+
assertOptionNameReferencesExist,
|
|
1412
|
+
true
|
|
1413
|
+
],
|
|
1414
|
+
[
|
|
1415
|
+
"Commands with `subcommandsMatchUniquePrefix` don't have arguments and subcommands",
|
|
1416
|
+
"assertPrefixMatchCommandsHaveNoArguments",
|
|
1417
|
+
assertPrefixMatchCommandsHaveNoArguments,
|
|
1418
|
+
!allowMatchingSubcommandPrefixAndArgs
|
|
1419
|
+
]
|
|
1420
|
+
];
|
|
405
1421
|
return async (t) => {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
function:
|
|
409
|
-
}
|
|
410
|
-
assertRequiredArgumentsDoNotFollowOptionalArguments(spec);
|
|
411
|
-
});
|
|
412
|
-
await runStep(t, getName(namingStyle, {
|
|
413
|
-
sentence: "Repeatable options don't have arguments",
|
|
414
|
-
function: "assertRepeatableOptionsHaveNoArguments"
|
|
415
|
-
}), () => {
|
|
416
|
-
assertRepeatableOptionsHaveNoArguments(spec);
|
|
417
|
-
});
|
|
418
|
-
await runStep(t, getName(namingStyle, {
|
|
419
|
-
sentence: "Repeatable options that are numbers are integers greater than 1",
|
|
420
|
-
function: "assertRepeatableOptionsArePositiveIntegers"
|
|
421
|
-
}), () => {
|
|
422
|
-
assertRepeatableOptionsArePositiveIntegers(spec);
|
|
423
|
-
});
|
|
424
|
-
await runStep(t, getName(namingStyle, {
|
|
425
|
-
sentence: "Long option names don't start with a single dash",
|
|
426
|
-
function: "assertLongOptionNamesDoNotStartWithSingleDash"
|
|
427
|
-
}), () => {
|
|
428
|
-
assertLongOptionNamesDoNotStartWithSingleDash(spec);
|
|
429
|
-
});
|
|
430
|
-
await runStep(t, getName(namingStyle, {
|
|
431
|
-
sentence: "Option names all start with dashes",
|
|
432
|
-
function: "assertOptionNamesStartWithDashes"
|
|
433
|
-
}), () => {
|
|
434
|
-
assertOptionNamesStartWithDashes(spec);
|
|
435
|
-
});
|
|
436
|
-
await runStep(t, getName(namingStyle, {
|
|
437
|
-
sentence: "Options have locally unique names",
|
|
438
|
-
function: "assertOptionsHaveLocallyUniqueNames"
|
|
439
|
-
}), () => {
|
|
440
|
-
assertOptionsHaveLocallyUniqueNames(spec);
|
|
441
|
-
});
|
|
442
|
-
if (!allowShadowingPersistentOptions) await runStep(t, getName(namingStyle, {
|
|
443
|
-
sentence: "Options don't shadow persistent options",
|
|
444
|
-
function: "assertOptionsDoNotShadowPersistentOptions"
|
|
445
|
-
}), () => {
|
|
446
|
-
assertOptionsDoNotShadowPersistentOptions(spec);
|
|
447
|
-
});
|
|
448
|
-
await runStep(t, getName(namingStyle, {
|
|
449
|
-
sentence: "Subcommands have locally unique names",
|
|
450
|
-
function: "assertSubcommandsHaveLocallyUniqueNames"
|
|
451
|
-
}), () => {
|
|
452
|
-
assertSubcommandsHaveLocallyUniqueNames(spec);
|
|
453
|
-
});
|
|
454
|
-
await runStep(t, getName(namingStyle, {
|
|
455
|
-
sentence: "Nothing is named '--'",
|
|
456
|
-
function: "assertNothingIsNamedDashDash"
|
|
457
|
-
}), () => {
|
|
458
|
-
assertNothingIsNamedDashDash(spec);
|
|
459
|
-
});
|
|
460
|
-
await runStep(t, getName(namingStyle, {
|
|
461
|
-
sentence: "Option arg separators are not an empty string",
|
|
462
|
-
function: "assertOptionArgSeparatorsHaveCharacters"
|
|
463
|
-
}), () => {
|
|
464
|
-
assertOptionArgSeparatorsHaveCharacters(spec);
|
|
465
|
-
});
|
|
466
|
-
await runStep(t, getName(namingStyle, {
|
|
467
|
-
sentence: "Options named '-' or '+' take exactly one argument",
|
|
468
|
-
function: "assertPlusMinusOptionsTakeOneArg"
|
|
469
|
-
}), () => {
|
|
470
|
-
assertPlusMinusOptionsTakeOneArg(spec);
|
|
471
|
-
});
|
|
472
|
-
await runStep(t, getName(namingStyle, {
|
|
473
|
-
sentence: "Names have no extra leading or trailing whitespace",
|
|
474
|
-
function: "assertNamesHaveNoExtraWhitespace"
|
|
475
|
-
}), () => {
|
|
476
|
-
assertNamesHaveNoExtraWhitespace(spec);
|
|
477
|
-
});
|
|
478
|
-
if (!allowNoDescription) await runStep(t, getName(namingStyle, {
|
|
479
|
-
sentence: "Everything has a description",
|
|
480
|
-
function: "assertEverythingHasDescription"
|
|
481
|
-
}), () => {
|
|
482
|
-
assertEverythingHasDescription(spec);
|
|
483
|
-
});
|
|
484
|
-
if (!allowLongDescriptionLines) await runStep(t, getName(namingStyle, {
|
|
485
|
-
sentence: "Each line in descriptions is less than or equal to 68 characters",
|
|
486
|
-
function: "assertDescriptionLineLengthUnder69"
|
|
487
|
-
}), () => {
|
|
488
|
-
assertDescriptionLineLengthUnder69(spec);
|
|
489
|
-
});
|
|
490
|
-
await runStep(t, getName(namingStyle, {
|
|
491
|
-
sentence: "Options with `requiresSeparator` take exactly one argument",
|
|
492
|
-
function: "assertRequiresSeparatorTakesOneArg"
|
|
493
|
-
}), () => {
|
|
494
|
-
assertRequiresSeparatorTakesOneArg(spec);
|
|
495
|
-
});
|
|
496
|
-
await runStep(t, getName(namingStyle, {
|
|
497
|
-
sentence: "Common options are not defined on every subcommand",
|
|
498
|
-
function: "assertCommonOptionsArePersistent"
|
|
499
|
-
}), () => {
|
|
500
|
-
assertCommonOptionsArePersistent(spec);
|
|
501
|
-
});
|
|
502
|
-
await runStep(t, getName(namingStyle, {
|
|
503
|
-
sentence: "References to other options by name are valid",
|
|
504
|
-
function: "assertOptionNameReferencesExist"
|
|
505
|
-
}), () => {
|
|
506
|
-
assertOptionNameReferencesExist(spec);
|
|
507
|
-
});
|
|
508
|
-
if (!allowMatchingSubcommandPrefixAndArgs) await runStep(t, getName(namingStyle, {
|
|
509
|
-
sentence: "Commands with `subcommandsMatchUniquePrefix` don't have arguments and subcommands",
|
|
510
|
-
function: "assertPrefixMatchCommandsHaveNoArguments"
|
|
511
|
-
}), () => {
|
|
512
|
-
assertPrefixMatchCommandsHaveNoArguments(spec);
|
|
513
|
-
});
|
|
1422
|
+
for (const [sentence, functionName, check, enabled] of checks) {
|
|
1423
|
+
if (!enabled) continue;
|
|
1424
|
+
await runStep(t, namingStyle === "function" ? functionName : sentence, () => check(spec));
|
|
1425
|
+
}
|
|
514
1426
|
};
|
|
515
1427
|
}
|
|
516
1428
|
const testSpec = test;
|
|
517
|
-
export { assertCommonOptionsArePersistent, assertDescriptionLineLengthUnder69, assertEverythingHasDescription,
|
|
1429
|
+
export { assertCommonOptionsArePersistent, assertDefaultsCanBeUsed, assertDescriptionLineLengthUnder69, assertEverythingHasDescription, assertNamesHaveNoExtraWhitespace, assertNamesReadBack, assertOptionArgSeparatorsHaveCharacters, assertOptionNameReferencesExist, assertOptionsDoNotShadowPersistentOptions, assertOptionsHaveDifferentNamesFromParents, assertOptionsHaveLocallyUniqueNames, assertPlusMinusOptionsTakeOneArg, assertPrefixMatchCommandsHaveNoArguments, assertRepeatableOptionsArePositiveIntegers, assertRequiredArgumentsDoNotFollowOptionalArguments, assertRequiresSeparatorTakesOneArg, assertSubcommandsHaveLocallyUniqueNames, test, testSpec };
|
|
518
1430
|
|
|
519
1431
|
//# sourceMappingURL=testing.js.map
|