@loomcli/core 0.7.0 → 0.8.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/dist/application.d.ts +11 -6
- package/dist/application.js +11 -5
- package/dist/chain.d.ts +28 -17
- package/dist/chain.js +11 -10
- package/dist/command-rules.d.ts +1 -1
- package/dist/command-rules.js +2 -2
- package/dist/command.d.ts +31 -47
- package/dist/command.js +136 -170
- package/dist/errors.d.ts +22 -21
- package/dist/errors.js +38 -18
- package/dist/globals.d.ts +48 -22
- package/dist/globals.js +63 -22
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/input-rules.d.ts +16 -2
- package/dist/input-rules.js +40 -5
- package/dist/inspect.d.ts +35 -11
- package/dist/inspect.js +69 -66
- package/dist/locate.js +52 -60
- package/dist/options.d.ts +94 -83
- package/dist/options.js +298 -260
- package/dist/parse.d.ts +162 -0
- package/dist/parse.js +601 -0
- package/dist/plugin-rules.d.ts +2 -4
- package/dist/plugin-rules.js +3 -8
- package/dist/plugin.d.ts +26 -21
- package/dist/plugin.js +10 -45
- package/dist/sources.d.ts +18 -9
- package/dist/sources.js +46 -16
- package/dist/types.d.ts +68 -28
- package/dist/validation.d.ts +84 -31
- package/dist/validation.js +222 -110
- package/package.json +1 -1
package/dist/options.js
CHANGED
|
@@ -1,22 +1,76 @@
|
|
|
1
|
-
import { declaredName } from './command-rules.js';
|
|
1
|
+
import { declaredName, repeatedAlias } from './command-rules.js';
|
|
2
2
|
import { valueCode } from './diagnostic-text.js';
|
|
3
|
-
import { DeclarationError,
|
|
3
|
+
import { DeclarationError, quoted } from './errors.js';
|
|
4
4
|
import { factFault, flagFault, siteFinding } from './facts.js';
|
|
5
|
-
import { booleanOptionMultiple, optionDeclaredTwice, optionPolarity, optionType, polarityOnString, shortAlias, shortOnlyBothPolarities, shortOnlyWithoutShort, spellingTaken, } from './input-rules.js';
|
|
6
|
-
|
|
5
|
+
import { booleanOptionMultiple, countOptionMultiple, impliedNotAString, impliedOnBooleanOrCount, optionDeclaredTwice, optionPolarity, optionType, polarityOnCount, polarityOnString, shortAlias, shortOnlyBothPolarities, shortOnlyWithAliases, shortOnlyWithoutShort, spellingTaken, } from './input-rules.js';
|
|
6
|
+
import { notAList } from './plugin-rules.js';
|
|
7
|
+
/**
|
|
8
|
+
* Every parsed value lands in one of these maps; `lists` holds the repeated string options and
|
|
9
|
+
* `counts` the counted ones.
|
|
10
|
+
*/
|
|
7
11
|
export function emptyValues() {
|
|
8
|
-
return {
|
|
12
|
+
return {
|
|
13
|
+
booleans: new Map(),
|
|
14
|
+
counts: new Map(),
|
|
15
|
+
implied: new Map(),
|
|
16
|
+
lists: new Map(),
|
|
17
|
+
spellings: new Map(),
|
|
18
|
+
strings: new Map(),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** One scope's compiled spellings as table entries, each marked with whether the scope is global. */
|
|
22
|
+
export function tableEntries(spellings, global) {
|
|
23
|
+
return [...spellings].map(([spelling, option]) => [spelling, { ...option, global }]);
|
|
9
24
|
}
|
|
10
25
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
26
|
+
* Reads one option's spellings out of a compiled table, so a reader cannot report a form the parser
|
|
27
|
+
* does not accept. Each entry carries its own role, so the naming convention has one owner: the
|
|
28
|
+
* table that writes it.
|
|
14
29
|
*/
|
|
15
|
-
export function
|
|
16
|
-
|
|
17
|
-
|
|
30
|
+
export function spellingsOf(table, name) {
|
|
31
|
+
const spellings = { long: null, negative: null, short: null };
|
|
32
|
+
for (const [spelling, option] of table) {
|
|
33
|
+
// An alias is unadvertised, so the spellings are the ones the declared name derives.
|
|
34
|
+
if (option.name === name && option.role !== 'alias') {
|
|
35
|
+
spellings[option.role] = spelling;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return spellings;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The spelling every reported problem names an option by: its long form, a negative-only Boolean
|
|
42
|
+
* option's negative form, and otherwise its short form, which a short-only option alone publishes.
|
|
43
|
+
* The problem belongs to the option, so an alias or the spelling the operator typed is never it.
|
|
44
|
+
*/
|
|
45
|
+
export function reportedOf({ long, negative, short }, name) {
|
|
46
|
+
// Every compiled option publishes one of the three, because shortOnly requires a short.
|
|
47
|
+
// A node no table wrote falls back to the declared name's long form.
|
|
48
|
+
return long ?? negative ?? short ?? `--${name}`;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The part of one declaration that yields a spelling of the given origin, which a spelling fault
|
|
52
|
+
* marks: the declared name for the long form, `short` for the short alias, the `polarity` that
|
|
53
|
+
* generates a negative form, and the alias in `aliases` for either form of an alias.
|
|
54
|
+
*/
|
|
55
|
+
export function spellingMark(site, origin) {
|
|
56
|
+
switch (origin.role) {
|
|
57
|
+
case 'long': {
|
|
58
|
+
return site.named;
|
|
59
|
+
}
|
|
60
|
+
case 'short': {
|
|
61
|
+
return `${site.at}.short`;
|
|
62
|
+
}
|
|
63
|
+
case 'negative': {
|
|
64
|
+
return `${site.at}.polarity`;
|
|
65
|
+
}
|
|
66
|
+
case 'alias': {
|
|
67
|
+
return `${site.at}.aliases.${String(origin.index)}`;
|
|
68
|
+
}
|
|
69
|
+
default: {
|
|
70
|
+
const exhaustive = origin;
|
|
71
|
+
return exhaustive;
|
|
72
|
+
}
|
|
18
73
|
}
|
|
19
|
-
return `${site.at}.${role === 'short' ? 'short' : 'polarity'}`;
|
|
20
74
|
}
|
|
21
75
|
/** The declared name answers the declared-name rule an argument's name answers. */
|
|
22
76
|
export function checkOptionName(name, site) {
|
|
@@ -28,14 +82,36 @@ export function checkOptionName(name, site) {
|
|
|
28
82
|
sentence: `Option name ${valueCode(name)} is not a string.`,
|
|
29
83
|
});
|
|
30
84
|
}
|
|
31
|
-
if (!
|
|
85
|
+
if (!isDeclaredName(name)) {
|
|
32
86
|
throw new DeclarationError(declaredName, {
|
|
33
|
-
correction:
|
|
87
|
+
correction: declaredNameCorrection,
|
|
34
88
|
findings,
|
|
35
89
|
sentence: `Option name ${quoted(name)} is invalid.`,
|
|
36
90
|
});
|
|
37
91
|
}
|
|
38
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* The one name rule for an argument, option, alias of an option, or view name: a bare token the
|
|
95
|
+
* parser can read, nonempty, with no leading hyphen, whitespace, or `=`, which it reads apart.
|
|
96
|
+
*/
|
|
97
|
+
export function isDeclaredName(name) {
|
|
98
|
+
return typeof name === 'string' && name !== '' && !name.startsWith('-') && !/[\s=]/u.test(name);
|
|
99
|
+
}
|
|
100
|
+
/** The one correction every declared-name diagnostic for a string ends with. */
|
|
101
|
+
export const declaredNameCorrection = 'Use a nonempty name without a leading hyphen, whitespace, or "=".';
|
|
102
|
+
/** The sentence and correction of each repeated-alias fault, for the Command or option `subject` names. */
|
|
103
|
+
export function repeatedAliasText(subject, alias) {
|
|
104
|
+
return {
|
|
105
|
+
own: {
|
|
106
|
+
correction: 'Remove the alias.',
|
|
107
|
+
sentence: `${subject} declares alias ${quoted(alias)}, which is its own name.`,
|
|
108
|
+
},
|
|
109
|
+
twice: {
|
|
110
|
+
correction: 'Remove the repeated alias.',
|
|
111
|
+
sentence: `${subject} declares alias ${quoted(alias)} twice.`,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
39
115
|
/** Whether a declared short alias is one ASCII letter, the rule every short spelling answers. */
|
|
40
116
|
export function isShortAlias(short) {
|
|
41
117
|
return typeof short === 'string' && /^[A-Za-z]$/u.test(short);
|
|
@@ -63,12 +139,79 @@ function checkShortForms(config, site, subject) {
|
|
|
63
139
|
});
|
|
64
140
|
}
|
|
65
141
|
}
|
|
66
|
-
/**
|
|
142
|
+
/**
|
|
143
|
+
* The aliases, each a name the declared-name rule accepts, none repeating the option's name or
|
|
144
|
+
* another alias. `shortOnly` removes every long spelling, so it declares none.
|
|
145
|
+
*/
|
|
146
|
+
function checkAliases(config, site, name) {
|
|
147
|
+
const subject = `Option ${quoted(name)}`;
|
|
148
|
+
// The types reject the pair, so this reads `shortOnly` before `aliases` narrows it away.
|
|
149
|
+
if (config.shortOnly === true && config.aliases !== undefined) {
|
|
150
|
+
throw factFault(shortOnlyWithAliases, site, {
|
|
151
|
+
correction: 'Remove aliases or shortOnly.',
|
|
152
|
+
fact: 'aliases',
|
|
153
|
+
sentence: `${subject} declares aliases and shortOnly, which removes every long spelling.`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const { aliases } = config;
|
|
157
|
+
if (aliases === undefined) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (!Array.isArray(aliases)) {
|
|
161
|
+
throw factFault(notAList, site, {
|
|
162
|
+
correction: 'Supply a list of alias names.',
|
|
163
|
+
fact: 'aliases',
|
|
164
|
+
sentence: `${subject} declares aliases that are not an array.`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const seen = new Set();
|
|
168
|
+
for (const [index, alias] of aliases.entries()) {
|
|
169
|
+
const at = `${site.at}.aliases.${String(index)}`;
|
|
170
|
+
if (typeof alias !== 'string') {
|
|
171
|
+
throw new DeclarationError(declaredName, {
|
|
172
|
+
correction: 'Supply a string name.',
|
|
173
|
+
findings: [siteFinding(site, at)],
|
|
174
|
+
sentence: `${subject} declares an alias named ${valueCode(alias)}.`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (!isDeclaredName(alias)) {
|
|
178
|
+
throw new DeclarationError(declaredName, {
|
|
179
|
+
correction: declaredNameCorrection,
|
|
180
|
+
findings: [siteFinding(site, at)],
|
|
181
|
+
sentence: `${subject} declares an alias named ${quoted(alias)}.`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
const repeated = repeatedAliasText(subject, alias);
|
|
185
|
+
if (alias === name) {
|
|
186
|
+
throw new DeclarationError(repeatedAlias, {
|
|
187
|
+
...repeated.own,
|
|
188
|
+
findings: [siteFinding(site, at, 'its own name')],
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (seen.has(alias)) {
|
|
192
|
+
throw new DeclarationError(repeatedAlias, {
|
|
193
|
+
...repeated.twice,
|
|
194
|
+
findings: [siteFinding(site, at, 'already an alias')],
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
seen.add(alias);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/** A Boolean option's polarity, which a string option and a counted option do not declare. */
|
|
67
201
|
function checkPolarity(config, site, subject) {
|
|
202
|
+
// The type is read before the check below narrows the config to the kind the types allow.
|
|
203
|
+
const { type } = config;
|
|
68
204
|
if (config.polarity === undefined) {
|
|
69
205
|
return;
|
|
70
206
|
}
|
|
71
|
-
if (
|
|
207
|
+
if (type === 'count') {
|
|
208
|
+
throw factFault(polarityOnCount, site, {
|
|
209
|
+
correction: 'Remove polarity or use type "boolean".',
|
|
210
|
+
fact: 'polarity',
|
|
211
|
+
sentence: `${subject} declares polarity but is a counted option.`,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
if (type !== 'boolean') {
|
|
72
215
|
throw factFault(polarityOnString, site, {
|
|
73
216
|
correction: 'Remove polarity or use type "boolean".',
|
|
74
217
|
fact: 'polarity',
|
|
@@ -90,50 +233,156 @@ function checkPolarity(config, site, subject) {
|
|
|
90
233
|
});
|
|
91
234
|
}
|
|
92
235
|
}
|
|
236
|
+
/** `multiple` belongs to a string option: a Boolean option has one value and a count counts. */
|
|
237
|
+
function checkMultiple(config, site, subject) {
|
|
238
|
+
// The type is read before the check below narrows the config to the kind the types allow.
|
|
239
|
+
const { type } = config;
|
|
240
|
+
if (config.multiple === undefined) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (type === 'boolean') {
|
|
244
|
+
throw factFault(booleanOptionMultiple, site, {
|
|
245
|
+
correction: 'Remove multiple or declare a string option.',
|
|
246
|
+
fact: 'multiple',
|
|
247
|
+
sentence: `${subject} is a boolean option and declares multiple.`,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (type === 'count') {
|
|
251
|
+
throw factFault(countOptionMultiple, site, {
|
|
252
|
+
correction: 'Remove multiple; a counted option already counts every occurrence.',
|
|
253
|
+
fact: 'multiple',
|
|
254
|
+
sentence: `${subject} is a counted option and declares multiple.`,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
if (typeof config.multiple !== 'boolean') {
|
|
258
|
+
throw flagFault(site, 'multiple');
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** The kind each option type reads as in a sentence about a value it cannot take. */
|
|
262
|
+
const valuelessKinds = { boolean: 'a Boolean option', count: 'a counted option' };
|
|
263
|
+
/**
|
|
264
|
+
* A string option's implied value, the string a bare spelling supplies. A Boolean option and a
|
|
265
|
+
* counted option take no value, so neither declares one. Presence is the key, so a declared
|
|
266
|
+
* `undefined` is a declaration the rule reads.
|
|
267
|
+
*/
|
|
268
|
+
function checkImplied(config, site, subject) {
|
|
269
|
+
if (!('implied' in config)) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (config.type !== 'string') {
|
|
273
|
+
throw factFault(impliedOnBooleanOrCount, site, {
|
|
274
|
+
correction: 'Remove implied or declare a string option.',
|
|
275
|
+
fact: 'implied',
|
|
276
|
+
sentence: `${subject} is ${valuelessKinds[config.type]} and declares implied.`,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (typeof config.implied !== 'string') {
|
|
280
|
+
throw factFault(impliedNotAString, site, {
|
|
281
|
+
correction: 'Supply a string, the value a bare spelling supplies.',
|
|
282
|
+
fact: 'implied',
|
|
283
|
+
sentence: `${subject} declares implied that is not a string.`,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
93
287
|
/** Every rule one option declaration answers alone, before the table meets it. */
|
|
94
288
|
function validateDeclaration({ name, config }, site) {
|
|
95
289
|
checkOptionName(name, site);
|
|
96
290
|
const subject = `Option ${quoted(name)}`;
|
|
97
|
-
if (!['string', 'boolean'].includes(config.type)) {
|
|
291
|
+
if (!['string', 'boolean', 'count'].includes(config.type)) {
|
|
98
292
|
throw factFault(optionType, site, {
|
|
99
|
-
correction: 'Use "string" or "
|
|
293
|
+
correction: 'Use "string", "boolean", or "count".',
|
|
100
294
|
fact: 'type',
|
|
101
295
|
sentence: `${subject} has an invalid type.`,
|
|
102
296
|
});
|
|
103
297
|
}
|
|
104
298
|
checkShortForms(config, site, subject);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
correction: 'Remove multiple or declare a string option.',
|
|
108
|
-
fact: 'multiple',
|
|
109
|
-
sentence: `${subject} is a boolean option and declares multiple.`,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
if (config.multiple !== undefined && typeof config.multiple !== 'boolean') {
|
|
113
|
-
throw flagFault(site, 'multiple');
|
|
114
|
-
}
|
|
299
|
+
checkAliases(config, site, name);
|
|
300
|
+
checkMultiple(config, site, subject);
|
|
115
301
|
checkPolarity(config, site, subject);
|
|
302
|
+
checkImplied(config, site, subject);
|
|
116
303
|
}
|
|
117
304
|
function addSpelling(claims, spelling, claim) {
|
|
118
305
|
const existing = claims.get(spelling);
|
|
119
306
|
if (existing) {
|
|
120
307
|
throw new DeclarationError(spellingTaken, {
|
|
121
308
|
correction: 'Change one declaration.',
|
|
122
|
-
findings: [existing, claim].map(({ option, site }) => siteFinding(site, spellingMark(site, option
|
|
309
|
+
findings: [existing, claim].map(({ option, site }) => siteFinding(site, spellingMark(site, option))),
|
|
123
310
|
sentence: `Option spelling ${quoted(spelling)} is used by both ${quoted(existing.option.name)} and ${quoted(claim.option.name)}.`,
|
|
124
311
|
});
|
|
125
312
|
}
|
|
126
313
|
claims.set(spelling, claim);
|
|
127
314
|
}
|
|
315
|
+
/**
|
|
316
|
+
* The long forms one name yields for an option, the declared name or an alias: the positive form,
|
|
317
|
+
* unless the polarity is negative, and the negative form for `both` and `negative` polarity.
|
|
318
|
+
*/
|
|
319
|
+
function addLongForms(claims, option, long) {
|
|
320
|
+
const { config, positive, site } = option;
|
|
321
|
+
const forms = [];
|
|
322
|
+
if (config.type !== 'boolean' || config.polarity !== 'negative') {
|
|
323
|
+
forms.push([`--${long.name}`, { ...positive, ...long.positive }]);
|
|
324
|
+
}
|
|
325
|
+
if (config.type === 'boolean' && (config.polarity === 'both' || config.polarity === 'negative')) {
|
|
326
|
+
forms.push([
|
|
327
|
+
`--no-${long.name}`,
|
|
328
|
+
{
|
|
329
|
+
name: positive.name,
|
|
330
|
+
type: 'boolean',
|
|
331
|
+
value: false,
|
|
332
|
+
valueClass: 'boolean',
|
|
333
|
+
...long.negative,
|
|
334
|
+
},
|
|
335
|
+
]);
|
|
336
|
+
}
|
|
337
|
+
for (const [spelling, form] of forms) {
|
|
338
|
+
// The name's forms claim first, so a spelling the option already holds repeats it by an alias.
|
|
339
|
+
if (form.role === 'alias' && claims.get(spelling)?.option.name === positive.name) {
|
|
340
|
+
throw new DeclarationError(repeatedAlias, {
|
|
341
|
+
correction: 'Remove the alias.',
|
|
342
|
+
findings: [
|
|
343
|
+
siteFinding(site, spellingMark(site, form), 'a spelling the option already accepts'),
|
|
344
|
+
],
|
|
345
|
+
sentence: `Option ${quoted(positive.name)} declares alias ${quoted(long.name)}, whose spelling ${quoted(spelling)} the option already accepts.`,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
addSpelling(claims, spelling, { option: form, site });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
128
351
|
/**
|
|
129
352
|
* One Boolean option's value for one invocation: the value the parser consumed, or the value its
|
|
130
353
|
* declared polarity gives an absent option. A negative-only option is absent as `true`, because its
|
|
131
|
-
* one spelling turns the value off.
|
|
132
|
-
*
|
|
354
|
+
* one spelling turns the value off. Validation reads every Boolean option's value here, whichever
|
|
355
|
+
* scope declared it.
|
|
133
356
|
*/
|
|
134
357
|
export function booleanValue(values, name, config) {
|
|
135
358
|
return values.booleans.get(name) ?? config.polarity === 'negative';
|
|
136
359
|
}
|
|
360
|
+
/** The form an option's positive spellings read words by, its long form and short alias alike. */
|
|
361
|
+
function positiveForm(name, config) {
|
|
362
|
+
switch (config.type) {
|
|
363
|
+
case 'string': {
|
|
364
|
+
const multiple = config.multiple === true;
|
|
365
|
+
return config.implied === undefined
|
|
366
|
+
? { multiple, name, type: 'string', valueClass: 'separate' }
|
|
367
|
+
: { implied: config.implied, multiple, name, type: 'string', valueClass: 'implied' };
|
|
368
|
+
}
|
|
369
|
+
case 'boolean': {
|
|
370
|
+
return {
|
|
371
|
+
name,
|
|
372
|
+
type: 'boolean',
|
|
373
|
+
value: config.polarity !== 'negative',
|
|
374
|
+
valueClass: 'boolean',
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
case 'count': {
|
|
378
|
+
return { name, type: 'count', valueClass: 'count' };
|
|
379
|
+
}
|
|
380
|
+
default: {
|
|
381
|
+
const exhaustive = config;
|
|
382
|
+
return exhaustive;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
137
386
|
/**
|
|
138
387
|
* One scope's options compiled into the spelling table the parser reads, with every rule one
|
|
139
388
|
* declaration answers alone and every rule two of them answer together.
|
|
@@ -158,19 +407,17 @@ export function compileOptions(declarations, scope) {
|
|
|
158
407
|
});
|
|
159
408
|
}
|
|
160
409
|
names.set(name, declaration);
|
|
161
|
-
const positive = config
|
|
162
|
-
? { multiple: config.multiple === true, name, type: 'string' }
|
|
163
|
-
: { name, type: 'boolean', value: config.polarity !== 'negative' };
|
|
410
|
+
const positive = positiveForm(name, config);
|
|
164
411
|
if (!config.shortOnly) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
});
|
|
412
|
+
const option = { config, positive, site };
|
|
413
|
+
addLongForms(claims, option, {
|
|
414
|
+
name,
|
|
415
|
+
negative: { role: 'negative' },
|
|
416
|
+
positive: { role: 'long' },
|
|
417
|
+
});
|
|
418
|
+
for (const [index, alias] of (config.aliases ?? []).entries()) {
|
|
419
|
+
const origin = { index, role: 'alias' };
|
|
420
|
+
addLongForms(claims, option, { name: alias, negative: origin, positive: origin });
|
|
174
421
|
}
|
|
175
422
|
}
|
|
176
423
|
if (config.short !== undefined) {
|
|
@@ -179,196 +426,22 @@ export function compileOptions(declarations, scope) {
|
|
|
179
426
|
}
|
|
180
427
|
return new Map([...claims].map(([spelling, { option }]) => [spelling, option]));
|
|
181
428
|
}
|
|
182
|
-
/**
|
|
183
|
-
* Whether a token reads as an option: it starts with a hyphen. Routing stops at one, and a
|
|
184
|
-
* separate value is never one. The parser and `locate` read each token through this rule.
|
|
185
|
-
*/
|
|
186
|
-
export function isOptionToken(token) {
|
|
187
|
-
return token.startsWith('-');
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* A token that starts with `--` split at its first `=` into the spelling and the inline value, or
|
|
191
|
-
* `undefined` for any other token. The parser and `locate` split long tokens through this rule.
|
|
192
|
-
*/
|
|
193
|
-
export function longToken(token) {
|
|
194
|
-
if (!token.startsWith('--')) {
|
|
195
|
-
return undefined;
|
|
196
|
-
}
|
|
197
|
-
const equals = token.indexOf('=');
|
|
198
|
-
return equals === -1
|
|
199
|
-
? { inline: undefined, spelling: token }
|
|
200
|
-
: { inline: token.slice(equals + 1), spelling: token.slice(0, equals) };
|
|
201
|
-
}
|
|
202
|
-
function lookup(spellings, spelling) {
|
|
203
|
-
const option = spellings.get(spelling);
|
|
204
|
-
if (!option) {
|
|
205
|
-
throw new UnknownOptionError(spelling);
|
|
206
|
-
}
|
|
207
|
-
return option;
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* The name of the string option one long spelling names in a table, which takes its value after
|
|
211
|
-
* `=`, or `undefined` for a Boolean, negative, short, or unknown spelling.
|
|
212
|
-
*/
|
|
213
|
-
export function longStringOption(spellings, spelling) {
|
|
214
|
-
const option = spellings.get(spelling);
|
|
215
|
-
return option?.type === 'string' && option.role === 'long' ? option.name : undefined;
|
|
216
|
-
}
|
|
217
|
-
function acceptValue({ option, spelling, values, next, inline, }) {
|
|
218
|
-
const repeatable = option.type === 'string' && option.multiple;
|
|
219
|
-
if (!repeatable && (values.strings.has(option.name) || values.booleans.has(option.name))) {
|
|
220
|
-
throw new RepeatedOptionError(spelling);
|
|
221
|
-
}
|
|
222
|
-
// A repeatable option records its last occurrence, because each one overwrites the entry.
|
|
223
|
-
values.spellings.set(option.name, spelling);
|
|
224
|
-
if (option.type === 'boolean') {
|
|
225
|
-
if (inline !== undefined) {
|
|
226
|
-
throw new UnexpectedValueError(spelling, inline);
|
|
227
|
-
}
|
|
228
|
-
values.booleans.set(option.name, option.value);
|
|
229
|
-
return 'alone';
|
|
230
|
-
}
|
|
231
|
-
const value = inline ?? next;
|
|
232
|
-
if (value === undefined) {
|
|
233
|
-
return { name: option.name, spelling };
|
|
234
|
-
}
|
|
235
|
-
if (inline === undefined && isOptionToken(value)) {
|
|
236
|
-
throw new MissingValueError(spelling);
|
|
237
|
-
}
|
|
238
|
-
if (repeatable) {
|
|
239
|
-
const collected = values.lists.get(option.name) ?? [];
|
|
240
|
-
collected.push(value);
|
|
241
|
-
values.lists.set(option.name, collected);
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
values.strings.set(option.name, value);
|
|
245
|
-
}
|
|
246
|
-
return inline === undefined ? 'next' : 'alone';
|
|
247
|
-
}
|
|
248
|
-
function parseOption(spellings, input, values) {
|
|
249
|
-
const { token, next } = input;
|
|
250
|
-
const long = longToken(token);
|
|
251
|
-
if (long) {
|
|
252
|
-
const { inline, spelling } = long;
|
|
253
|
-
return acceptValue({ inline, next, option: lookup(spellings, spelling), spelling, values });
|
|
254
|
-
}
|
|
255
|
-
if (token === '-') {
|
|
256
|
-
lookup(spellings, token);
|
|
257
|
-
}
|
|
258
|
-
for (let index = 1; index < token.length; index += 1) {
|
|
259
|
-
const spelling = `-${token[index]}`;
|
|
260
|
-
const option = lookup(spellings, spelling);
|
|
261
|
-
const suffix = token.slice(index + 1);
|
|
262
|
-
if (option.type === 'string' && suffix !== '') {
|
|
263
|
-
throw new ShortGroupError({ reason: 'value-position', token: spelling });
|
|
264
|
-
}
|
|
265
|
-
const inline = suffix.startsWith('=') ? suffix.slice(1) : undefined;
|
|
266
|
-
const reading = acceptValue({ inline, next, option, spelling, values });
|
|
267
|
-
if (reading !== 'alone') {
|
|
268
|
-
return reading;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
return 'alone';
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Reads one option token into the scan and answers whether it took the next token too. The names
|
|
275
|
-
* it newly supplied join `supplied` in the order the values map first recorded them, so a repeated
|
|
276
|
-
* option keeps its first position.
|
|
277
|
-
*/
|
|
278
|
-
function readOption(spellings, input, state) {
|
|
279
|
-
const known = state.values.spellings.size;
|
|
280
|
-
const reading = parseOption(spellings, input, state.values);
|
|
281
|
-
for (const name of [...state.values.spellings.keys()].slice(known)) {
|
|
282
|
-
state.supplied.push({ name, token: input.index });
|
|
283
|
-
}
|
|
284
|
-
if (typeof reading === 'object') {
|
|
285
|
-
state.awaiting = reading;
|
|
286
|
-
}
|
|
287
|
-
return reading === 'next';
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* A hyphen token belongs to the globals when its long spelling or every short letter does. The
|
|
291
|
-
* pre-scan reads the globals alone, so a letter it does not own is only "not a global option".
|
|
292
|
-
* The scan stops at a global value option, because the letters after it may be the value the
|
|
293
|
-
* operator meant to pass: the token then belongs to the globals, and parsing reports the
|
|
294
|
-
* value-position fault that names that option alone.
|
|
295
|
-
*/
|
|
296
|
-
function isGlobalToken(spellings, token) {
|
|
297
|
-
const long = longToken(token);
|
|
298
|
-
if (long) {
|
|
299
|
-
return spellings.has(long.spelling);
|
|
300
|
-
}
|
|
301
|
-
const group = token.slice(1).split('=')[0] ?? '';
|
|
302
|
-
let global = '';
|
|
303
|
-
let other = '';
|
|
304
|
-
for (let index = 0; index < group.length; index += 1) {
|
|
305
|
-
const letter = group.charAt(index);
|
|
306
|
-
const option = spellings.get(`-${letter}`);
|
|
307
|
-
if (option) {
|
|
308
|
-
global = global === '' ? letter : global;
|
|
309
|
-
if (option.type === 'string') {
|
|
310
|
-
break;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
other = other === '' ? letter : other;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
if (global === '') {
|
|
318
|
-
return false;
|
|
319
|
-
}
|
|
320
|
-
if (other !== '') {
|
|
321
|
-
throw new ShortGroupError({ global, other, reason: 'mixed-scope', token });
|
|
322
|
-
}
|
|
323
|
-
return true;
|
|
324
|
-
}
|
|
325
|
-
/** Consumes global options anywhere before the passthrough delimiter and leaves the rest routable. */
|
|
326
|
-
export function scanGlobals(spellings, tokens) {
|
|
327
|
-
const state = { awaiting: undefined, supplied: [], values: emptyValues() };
|
|
328
|
-
const rest = [];
|
|
329
|
-
const positions = [];
|
|
330
|
-
for (let index = 0; index < tokens.length; index += 1) {
|
|
331
|
-
const token = tokens[index];
|
|
332
|
-
if (token === undefined) {
|
|
333
|
-
break;
|
|
334
|
-
}
|
|
335
|
-
if (token === '--') {
|
|
336
|
-
// One push per token, because a spread call would overflow the stack on a long list.
|
|
337
|
-
for (const [offset, tail] of tokens.slice(index).entries()) {
|
|
338
|
-
rest.push(tail);
|
|
339
|
-
positions.push(index + offset);
|
|
340
|
-
}
|
|
341
|
-
break;
|
|
342
|
-
}
|
|
343
|
-
if (!isOptionToken(token) || !isGlobalToken(spellings, token)) {
|
|
344
|
-
rest.push(token);
|
|
345
|
-
positions.push(index);
|
|
346
|
-
}
|
|
347
|
-
else if (readOption(spellings, { index, next: tokens[index + 1], token }, state)) {
|
|
348
|
-
index += 1;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
return { ...state, positions, rest };
|
|
352
|
-
}
|
|
353
|
-
/** The pre-scan of a complete invocation, where a global still waiting for its value is a fault. */
|
|
354
|
-
export function extractGlobals(spellings, tokens) {
|
|
355
|
-
const { awaiting, rest, values } = scanGlobals(spellings, tokens);
|
|
356
|
-
if (awaiting) {
|
|
357
|
-
throw new MissingValueError(awaiting.spelling);
|
|
358
|
-
}
|
|
359
|
-
return { rest, values };
|
|
360
|
-
}
|
|
361
429
|
/**
|
|
362
430
|
* Whether one option holds a value a tier supplied: a token in any spelling it accepts, or a fill
|
|
363
431
|
* from an input source. A declared default is never in these maps, so it never counts.
|
|
364
432
|
*/
|
|
365
433
|
export function isSupplied(values, name) {
|
|
366
|
-
return values.strings.has(name) ||
|
|
434
|
+
return (values.strings.has(name) ||
|
|
435
|
+
values.lists.has(name) ||
|
|
436
|
+
values.booleans.has(name) ||
|
|
437
|
+
values.counts.has(name));
|
|
367
438
|
}
|
|
368
439
|
/** One run's own copy of parsed values, which the input-source stage fills without touching argv's. */
|
|
369
440
|
export function copyValues(values) {
|
|
370
441
|
return {
|
|
371
442
|
booleans: new Map(values.booleans),
|
|
443
|
+
counts: new Map(values.counts),
|
|
444
|
+
implied: new Map([...values.implied].map(([name, positions]) => [name, [...positions]])),
|
|
372
445
|
lists: new Map([...values.lists].map(([name, list]) => [name, [...list]])),
|
|
373
446
|
spellings: new Map(values.spellings),
|
|
374
447
|
strings: new Map(values.strings),
|
|
@@ -378,45 +451,10 @@ export function copyValues(values) {
|
|
|
378
451
|
export function mergeValues(globals, locals) {
|
|
379
452
|
return {
|
|
380
453
|
booleans: new Map([...globals.booleans, ...locals.booleans]),
|
|
454
|
+
counts: new Map([...globals.counts, ...locals.counts]),
|
|
455
|
+
implied: new Map([...globals.implied, ...locals.implied]),
|
|
381
456
|
lists: new Map([...globals.lists, ...locals.lists]),
|
|
382
457
|
spellings: new Map([...globals.spellings, ...locals.spellings]),
|
|
383
458
|
strings: new Map([...globals.strings, ...locals.strings]),
|
|
384
459
|
};
|
|
385
460
|
}
|
|
386
|
-
/** Reads one Command's tokens into options, positionals, and the passthrough tail. */
|
|
387
|
-
export function scanInputs(spellings, tokens) {
|
|
388
|
-
const state = { awaiting: undefined, supplied: [], values: emptyValues() };
|
|
389
|
-
const positionals = [];
|
|
390
|
-
const read = (passthrough, delimited) => ({
|
|
391
|
-
awaiting: state.awaiting,
|
|
392
|
-
delimited,
|
|
393
|
-
options: state.values,
|
|
394
|
-
passthrough,
|
|
395
|
-
positionals,
|
|
396
|
-
supplied: state.supplied,
|
|
397
|
-
});
|
|
398
|
-
for (let index = 0; index < tokens.length; index += 1) {
|
|
399
|
-
const token = tokens[index];
|
|
400
|
-
if (token === undefined) {
|
|
401
|
-
break;
|
|
402
|
-
}
|
|
403
|
-
if (token === '--') {
|
|
404
|
-
return read(tokens.slice(index + 1), true);
|
|
405
|
-
}
|
|
406
|
-
if (!isOptionToken(token)) {
|
|
407
|
-
positionals.push(token);
|
|
408
|
-
}
|
|
409
|
-
else if (readOption(spellings, { index, next: tokens[index + 1], token }, state)) {
|
|
410
|
-
index += 1;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
return read([], false);
|
|
414
|
-
}
|
|
415
|
-
/** Parses a complete invocation's local tokens, where a waiting option is a missing value. */
|
|
416
|
-
export function parseInputs(spellings, tokens) {
|
|
417
|
-
const { awaiting, options, passthrough, positionals } = scanInputs(spellings, tokens);
|
|
418
|
-
if (awaiting) {
|
|
419
|
-
throw new MissingValueError(awaiting.spelling);
|
|
420
|
-
}
|
|
421
|
-
return { options, passthrough, positionals };
|
|
422
|
-
}
|