@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/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, MissingValueError, quoted, RepeatedOptionError, ShortGroupError, UnexpectedValueError, UnknownOptionError, } from './errors.js';
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
- /** Every parsed value lands in one of these maps; `lists` holds the repeated string options. */
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 { booleans: new Map(), lists: new Map(), spellings: new Map(), strings: new Map() };
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
- * The part of one declaration that yields a spelling of the given role, which a spelling fault
12
- * marks: the declared name for the long form, `short` for the short alias, and the `polarity` that
13
- * generates a negative form.
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 spellingMark(site, role) {
16
- if (role === 'long') {
17
- return site.named;
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 (!name || name.startsWith('-') || /[\s=]/u.test(name)) {
85
+ if (!isDeclaredName(name)) {
32
86
  throw new DeclarationError(declaredName, {
33
- correction: 'Use a nonempty name without a leading hyphen, whitespace, or "=".',
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
- /** A Boolean option's polarity, which a string option does not declare. */
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 (config.type !== 'boolean') {
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 "boolean".',
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
- if (config.type === 'boolean' && config.multiple !== undefined) {
106
- throw factFault(booleanOptionMultiple, site, {
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.role))),
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. Every scope reads it here, so a plugin option and a validated
132
- * declaration answer the same rule.
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.type === 'string'
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
- if (config.type === 'string' || config.polarity !== 'negative') {
166
- addSpelling(claims, `--${name}`, { option: { ...positive, role: 'long' }, site });
167
- }
168
- if (config.type === 'boolean' &&
169
- (config.polarity === 'both' || config.polarity === 'negative')) {
170
- addSpelling(claims, `--no-${name}`, {
171
- option: { name, role: 'negative', type: 'boolean', value: false },
172
- site,
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) || values.lists.has(name) || values.booleans.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
- }