@clo4/cli 3.0.0 → 3.0.1

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/index.js CHANGED
@@ -192,42 +192,6 @@ function closestChoice(input, choices) {
192
192
  }
193
193
  // src/subcommands.ts
194
194
  /**
195
- * Find the subcommand `token` names among `subcommands`, in a command where
196
- * `directives` are in effect, or else among the persistent subcommands the
197
- * command `inherited`. An exact name always wins. With
198
- * `subcommandsMatchUniquePrefix`, a token that's a prefix of exactly one
199
- * of the command's own names matches that command, unless `exactOnly`. A
200
- * hidden or inherited command only matches its exact name, so it can't make a
201
- * visible command's prefix ambiguous or take a prefix meant as an argument,
202
- * and an empty token is no command's prefix.
203
- */
204
- function matchSubcommand(subcommands, token, directives, exactOnly = false, inherited = []) {
205
- for (const candidate of subcommands) if (hasName$1(candidate, token)) return { command: candidate };
206
- for (const candidate of inherited) if (hasName$1(candidate, token)) return { command: candidate };
207
- const matches = directives.subcommandsMatchUniquePrefix && !exactOnly ? prefixMatches(subcommands, token) : [];
208
- if (matches.length === 1) return { command: matches[0] };
209
- return new NoSubcommand(withInherited(subcommands, inherited), token, matches);
210
- }
211
- /**
212
- * A command's own `subcommands` followed by the `inherited` ones it can use,
213
- * leaving out each inherited one that an earlier command shadows a name of.
214
- */
215
- function withInherited(subcommands, inherited) {
216
- if (inherited.length === 0) return subcommands;
217
- const all = [...subcommands];
218
- for (const candidate of inherited) if (!asArray(candidate.name).some((name) => all.some((earlier) => hasName$1(earlier, name)))) all.push(candidate);
219
- return all;
220
- }
221
- function hasName$1(command, token) {
222
- const names = command.name;
223
- return typeof names === "string" ? names === token : names.includes(token);
224
- }
225
- /** The visible commands that `token` is a prefix of a name of, each listed once. */
226
- function prefixMatches(subcommands, token) {
227
- if (token === "") return [];
228
- return subcommands.filter((candidate) => !candidate.hidden && asArray(candidate.name).some((name) => name.startsWith(token)));
229
- }
230
- /**
231
195
  * A token that names no subcommand. The reason is only worked out when asked
232
196
  * for, since most tokens the parser tries aren't subcommands.
233
197
  */
@@ -293,16 +257,17 @@ function applyDirectives(directives, command) {
293
257
  }
294
258
  // src/scope.ts
295
259
  /**
296
- * The options and directives in effect at one command on a path.
260
+ * What a name means at one command on a path: the options, subcommands,
261
+ * directives and argument limits in effect there.
297
262
  *
298
263
  * Moving to a subcommand replaces the local options and adds its persistent
299
264
  * options to the inherited ones. A name refers to one option: a local option
300
265
  * wins over a persistent one, and a later option wins over an earlier one in
301
- * the same list. The parser, help, completion and the spec checker all ask
302
- * this module which option a name refers to, so they can't disagree. It also
303
- * holds the persistent subcommands the current command inherits.
266
+ * the same list. A subcommand wins over an option where the next token can
267
+ * name a subcommand. The parser, help, completion and the spec checker all
268
+ * ask this module what a name refers to, so they can't disagree.
304
269
  */
305
- var OptionScope = class OptionScope {
270
+ var CommandScope = class CommandScope {
306
271
  /** Non-persistent options of the current command, in definition order. */
307
272
  local = [];
308
273
  /** Persistent options from every command on the path, in definition order. */
@@ -314,25 +279,69 @@ var OptionScope = class OptionScope {
314
279
  inheritedSubcommands = [];
315
280
  /** The directives in effect. A subcommand inherits each until it sets its own. */
316
281
  directives = defaultDirectives();
317
- command = null;
282
+ current = null;
283
+ /** The fewest positional arguments the current command takes. */
284
+ argsMin = 0;
285
+ /** The most positional arguments the current command takes. */
286
+ argsMax = 0;
318
287
  /** The scope at the last command on `path`. */
319
288
  static at(path) {
320
- const scope = new OptionScope();
289
+ const scope = new CommandScope();
321
290
  for (const command of path) scope.enter(command);
322
291
  return scope;
323
292
  }
324
293
  /** Move to `command`, a subcommand of the current one. */
325
294
  enter(command) {
326
- const parentSubcommands = this.command?.subcommands;
295
+ const parentSubcommands = this.current?.subcommands;
327
296
  if (parentSubcommands) {
328
297
  const persistent = parentSubcommands.filter((subcommand) => subcommand.isPersistent);
329
298
  if (persistent.length > 0) this.inheritedSubcommands.unshift(...persistent);
330
299
  }
331
- this.command = command;
300
+ this.current = command;
301
+ this.argsMin = getMinArgs(command.args);
302
+ this.argsMax = getMaxArgs(command.args);
332
303
  this.local.length = 0;
333
304
  if (command.options) for (const option of command.options) (option.isPersistent ? this.persistent : this.local).push(option);
334
305
  applyDirectives(this.directives, command);
335
306
  }
307
+ /** The current command. */
308
+ get command() {
309
+ if (!this.current) throw new Error("The scope hasn't entered a command");
310
+ return this.current;
311
+ }
312
+ /** Whether the current command has subcommands of its own or inherits any. */
313
+ get hasSubcommands() {
314
+ return this.current?.subcommands !== void 0 || this.inheritedSubcommands.length > 0;
315
+ }
316
+ /**
317
+ * The subcommands the next token can name: the current command's own,
318
+ * followed by each inherited one that an earlier one doesn't shadow a name
319
+ * of. Hidden ones are included.
320
+ */
321
+ subcommands() {
322
+ const own = this.current?.subcommands ?? [];
323
+ if (this.inheritedSubcommands.length === 0) return own;
324
+ const all = [...own];
325
+ for (const candidate of this.inheritedSubcommands) if (!asArray(candidate.name).some((name) => all.some((earlier) => hasName(earlier.name, name)))) all.push(candidate);
326
+ return all;
327
+ }
328
+ /**
329
+ * Find the subcommand `token` names: one of the current command's own, or
330
+ * else an inherited persistent one. An exact name always wins. With
331
+ * `subcommandsMatchUniquePrefix`, a token that's a prefix of exactly one of
332
+ * the command's own names matches that command, unless `exactOnly`. A hidden
333
+ * or inherited command only matches its exact name, so it can't make a
334
+ * visible command's prefix ambiguous or take a prefix meant as an argument,
335
+ * and an empty token is no command's prefix.
336
+ */
337
+ matchSubcommand(token, exactOnly = false) {
338
+ const own = this.current?.subcommands ?? [];
339
+ for (const candidate of own) if (hasName(candidate.name, token)) return { command: candidate };
340
+ for (const candidate of this.inheritedSubcommands) if (hasName(candidate.name, token)) return { command: candidate };
341
+ const matches = this.directives.subcommandsMatchUniquePrefix && !exactOnly ? prefixMatches(own, token) : [];
342
+ if (matches.length === 1) return { command: matches[0] };
343
+ return new NoSubcommand(this.subcommands(), token, matches);
344
+ }
336
345
  /** The option `name` refers to, if any. */
337
346
  resolve(name) {
338
347
  const { local, persistent } = this;
@@ -343,15 +352,16 @@ var OptionScope = class OptionScope {
343
352
  /**
344
353
  * The options that can be used here, in definition order, each with only
345
354
  * its live names: the names that still refer to it. Options with every name
346
- * shadowed are left out.
355
+ * shadowed are left out. Where the next token can name a subcommand, as
356
+ * `subcommands` says, a name that one matches is that subcommand's.
347
357
  */
348
- live() {
358
+ live(subcommands = null) {
349
359
  const owner = /* @__PURE__ */ new Map();
350
360
  for (const options of [this.persistent, this.local]) for (const option of options) for (const name of asArray(option.name)) owner.set(name, option);
351
361
  const pick = (options) => {
352
362
  const picked = [];
353
363
  for (const option of options) {
354
- const names = asArray(option.name).filter((name) => owner.get(name) === option);
364
+ const names = asArray(option.name).filter((name) => owner.get(name) === option && !(subcommands && this.matchSubcommand(name, subcommands.exactOnly).command));
355
365
  if (names.length > 0) picked.push({
356
366
  option,
357
367
  names
@@ -364,9 +374,12 @@ var OptionScope = class OptionScope {
364
374
  persistent: pick(this.persistent)
365
375
  };
366
376
  }
367
- /** The options a user can see here: the {@link live} ones that aren't hidden. */
377
+ /**
378
+ * The options a user can see at the start of the current command: the
379
+ * {@link live} ones that aren't hidden, without the names a subcommand takes.
380
+ */
368
381
  visible() {
369
- const { local, persistent } = this.live();
382
+ const { local, persistent } = this.live(this.hasSubcommands ? { exactOnly: false } : null);
370
383
  const shown = (visible) => !visible.option.hidden;
371
384
  return {
372
385
  local: local.filter(shown),
@@ -375,8 +388,9 @@ var OptionScope = class OptionScope {
375
388
  }
376
389
  /** The live names of each option a user could have meant, for "did you mean" suggestions. */
377
390
  suggestionNames() {
378
- const { local, persistent } = this.visible();
379
- return [...local, ...persistent].map((visible) => visible.names);
391
+ const { local, persistent } = this.live();
392
+ const shown = (visible) => !visible.option.hidden;
393
+ return [...local, ...persistent].filter(shown).map((visible) => visible.names);
380
394
  }
381
395
  /** The options that must be used, local first. */
382
396
  required() {
@@ -407,6 +421,46 @@ var OptionScope = class OptionScope {
407
421
  if (typeof required === "string") return required;
408
422
  return required ? this.directives.optionArgSeparators[0] ?? "=" : null;
409
423
  }
424
+ /** Whether the parser splits an option from its value at `separator` here. */
425
+ readsSeparator(separator) {
426
+ return this.directives.optionArgSeparators.includes(separator);
427
+ }
428
+ /** The separator that `text` starts with, or "" if none. Used after a short option. */
429
+ leadingSeparator(text) {
430
+ for (const separator of this.directives.optionArgSeparators) if (text.startsWith(separator)) return separator;
431
+ return "";
432
+ }
433
+ /**
434
+ * The separator that appears first in `token`, so the value after it can
435
+ * contain the other separators. When two separators start at the same place,
436
+ * the longer one wins. Null if there's none, or if the one found is empty.
437
+ */
438
+ firstSeparator(token) {
439
+ let found = "";
440
+ let index = -1;
441
+ for (const candidate of this.directives.optionArgSeparators) {
442
+ const at = token.indexOf(candidate);
443
+ if (at === -1) continue;
444
+ if (index === -1 || at < index || at === index && candidate.length > found.length) {
445
+ found = candidate;
446
+ index = at;
447
+ }
448
+ }
449
+ return found ? {
450
+ index,
451
+ separator: found
452
+ } : null;
453
+ }
454
+ /**
455
+ * Why `option`, used as `name`, can't take a value given after `separator`,
456
+ * or null if it can. A separate token has the separator "".
457
+ */
458
+ separatorProblem(option, name, separator) {
459
+ const required = option.requiresSeparator;
460
+ if (typeof required === "string" && separator !== required) return `Incorrect separator, use '${required}' instead of '${separator}'`;
461
+ if (required === true && separator === "") return `Missing separator, use '${this.requiredSeparator(option)}' between ${name} and its value`;
462
+ return null;
463
+ }
410
464
  };
411
465
  const optionKeys = /* @__PURE__ */ new WeakMap();
412
466
  /**
@@ -428,6 +482,11 @@ function optionKey(option) {
428
482
  function hasName(names, name) {
429
483
  return typeof names === "string" ? names === name : names.includes(name);
430
484
  }
485
+ /** The visible commands that `token` is a prefix of a name of, each listed once. */
486
+ function prefixMatches(subcommands, token) {
487
+ if (token === "") return [];
488
+ return subcommands.filter((candidate) => !candidate.hidden && asArray(candidate.name).some((name) => name.startsWith(token)));
489
+ }
431
490
  // src/help.ts
432
491
  function dedent(str) {
433
492
  if (str[0] !== "\n") return str;
@@ -474,14 +533,15 @@ const helpCommand = {
474
533
  action({ path, args, help }) {
475
534
  const self = path[path.length - 1];
476
535
  const helpPath = path.slice(0, -1);
536
+ const scope = CommandScope.at(helpPath);
477
537
  for (const name of args) {
478
538
  const command = helpPath[helpPath.length - 1];
479
539
  if (!command.subcommands) break;
480
- const scope = OptionScope.at(helpPath);
481
- const match = matchSubcommand(command.subcommands, name, scope.directives, false, scope.inheritedSubcommands);
540
+ const match = scope.matchSubcommand(name);
482
541
  if (match.command === self && !command.subcommands.includes(self)) break;
483
542
  if (match.command) {
484
543
  helpPath.push(match.command);
544
+ scope.enter(match.command);
485
545
  continue;
486
546
  }
487
547
  if (!command.requiresSubcommand && getMaxArgs(command.args) > 0) break;
@@ -508,7 +568,7 @@ function getSubcommandPathString(commands) {
508
568
  }
509
569
  function getSections(commands) {
510
570
  const command = commands[commands.length - 1];
511
- const scope = OptionScope.at(commands);
571
+ const scope = CommandScope.at(commands);
512
572
  const show = ({ option, names }) => optionToString(option, scope.forms(option, names));
513
573
  const description = command.description;
514
574
  const defaultDesc = "No description";
@@ -712,32 +772,9 @@ var MissingRequiredOption = class extends ParseError {
712
772
  }
713
773
  };
714
774
  // src/parse.ts
715
- /** The first of `separators` that `text` starts with, or "" if none. */
716
- function leadingSeparator(text, separators) {
717
- for (const sep of separators) if (text.startsWith(sep)) return sep;
718
- return "";
719
- }
720
- /**
721
- * Find the separator that appears first in `token`, so the value after it can
722
- * contain the other separators. When two separators start at the same place,
723
- * the longer one wins. Returns null if there's no separator, or if the one
724
- * found is empty.
725
- */
726
- function firstSeparator(token, separators) {
727
- let sep = "";
728
- let index = -1;
729
- for (const candidate of separators) {
730
- const at = token.indexOf(candidate);
731
- if (at === -1) continue;
732
- if (index === -1 || at < index || at === index && candidate.length > sep.length) {
733
- sep = candidate;
734
- index = at;
735
- }
736
- }
737
- return sep ? {
738
- index,
739
- sep
740
- } : null;
775
+ /** How many times `option` can be used. */
776
+ function repeatLimit(option) {
777
+ return typeof option.isRepeatable === "number" ? option.isRepeatable : Infinity;
741
778
  }
742
779
  /**
743
780
  * Throw if the options used break a `dependsOn` or `exclusiveOn` rule, or if
@@ -755,25 +792,21 @@ function checkOptionRules(options, seenOptions, required, context) {
755
792
  for (const option of required) for (const name of asArray(option.name)) if (!options.has(name)) throw new MissingRequiredOption(name, context);
756
793
  }
757
794
  /**
758
- * The state of reading one command line in a single pass. This is the only
759
- * code that decides what each input token is. When `strict`, it throws on the
760
- * first problem, which is what {@link parse} needs. Otherwise it never throws:
761
- * it skips whatever is invalid and keeps going, so {@link readNext} can still
762
- * read a line that's wrong at the moment. Each method handles one kind of
763
- * token.
795
+ * The state of reading one command line, one token at a time. This is the
796
+ * only code that decides what each input token is. It never throws: it skips
797
+ * whatever is invalid and keeps going, so {@link readNext} can still read a
798
+ * line that's wrong at the moment, and it keeps the first problem it finds for
799
+ * {@link parse} to throw. Each method handles one kind of token, and says what
800
+ * the token was read as.
764
801
  */
765
802
  var Scanner = class {
766
- strict;
767
803
  path;
768
804
  optionAction = void 0;
769
805
  args = [];
770
806
  options = /* @__PURE__ */ new Map();
771
807
  seenOptions = [];
772
808
  argSeparatorIndex = -1;
773
- scope = new OptionScope();
774
- subcommands = void 0;
775
- commandArgsMin = 0;
776
- commandArgsMax = 0;
809
+ scope = new CommandScope();
777
810
  directives = this.scope.directives;
778
811
  hasFoundArg = false;
779
812
  openOption = null;
@@ -786,40 +819,60 @@ var Scanner = class {
786
819
  valuesMin = 0;
787
820
  valuesMax = 0;
788
821
  valueArgs = [];
789
- requiredSeparator = false;
822
+ /**
823
+ * Makes the error for the first token that can't be read the way it's
824
+ * written, like an unknown option or a value an option doesn't take.
825
+ * {@link parse} stops there.
826
+ */
827
+ problem = null;
790
828
  tooManyArgs = null;
791
829
  repeatedUses = /* @__PURE__ */ new Map();
792
- attached = null;
793
- constructor(spec, strict) {
794
- this.strict = strict;
830
+ constructor(spec) {
795
831
  this.path = [spec];
796
- this.enterCommand(spec);
832
+ this.scope.enter(spec);
797
833
  }
798
834
  ctx() {
799
- return { path: this.path };
835
+ return { path: [...this.path] };
800
836
  }
801
837
  /** Whether the open option can take the next token as a value. */
802
838
  get hasRoomForValue() {
803
839
  return this.valueCount < this.separateValueMax;
804
840
  }
805
- enterCommand(command) {
806
- this.scope.enter(command);
807
- this.subcommands = command.subcommands;
808
- this.commandArgsMin = getMinArgs(command.args);
809
- this.commandArgsMax = getMaxArgs(command.args);
810
- }
811
841
  findOption(name) {
812
842
  return this.scope.resolve(name);
813
843
  }
844
+ /**
845
+ * Keep the problem `make` describes if it's the first. The error is only
846
+ * made if {@link parse} throws it, since a lenient read never needs it and
847
+ * an error is slow to make. So `make` can't read anything later tokens change.
848
+ */
849
+ fail(make) {
850
+ if (!this.problem) {
851
+ const ctx = this.ctx();
852
+ this.problem = () => make(ctx);
853
+ }
854
+ }
855
+ /** Keep the first argument the command has no room for, like {@link fail}. */
856
+ failArgs(make) {
857
+ if (!this.tooManyArgs) {
858
+ const ctx = this.ctx();
859
+ this.tooManyArgs = () => make(ctx);
860
+ }
861
+ }
814
862
  unknownOption(option) {
815
- return new UnknownOption(option, this.scope.suggestionNames(), this.ctx());
863
+ if (!this.problem) {
864
+ const names = this.scope.suggestionNames();
865
+ this.fail((ctx) => new UnknownOption(option, names, ctx));
866
+ }
816
867
  }
817
868
  endOption() {
818
869
  const values = this.values;
819
- if (this.strict && values) {
870
+ if (values) {
820
871
  const given = values.length - this.valuesStart;
821
- if (given < this.valuesMin) throw new TooFewOptionArguments(this.openOptionName, this.valuesMin, this.valuesMax, this.ctx());
822
- addDefaults(values, this.valueArgs, given);
872
+ if (given < this.valuesMin) {
873
+ const { openOptionName, valuesMin, valuesMax } = this;
874
+ this.fail((ctx) => new TooFewOptionArguments(openOptionName, valuesMin, valuesMax, ctx));
875
+ } else addDefaults(values, this.valueArgs, given);
823
876
  }
824
877
  this.values = null;
825
878
  }
@@ -842,7 +895,6 @@ var Scanner = class {
842
895
  const options = this.options;
843
896
  let optionValues;
844
897
  if (option.isRepeatable) {
845
- const maxRepeat = option.isRepeatable === true ? Infinity : option.isRepeatable;
846
898
  const key = optionKey(option);
847
899
  let uses = this.repeatedUses.get(key);
848
900
  if (!uses) {
@@ -852,7 +904,7 @@ var Scanner = class {
852
904
  };
853
905
  this.repeatedUses.set(key, uses);
854
906
  }
855
- if (this.strict && uses.count >= maxRepeat) throw new ParseError(this.ctx(), "Too many repetitions");
907
+ if (uses.count >= repeatLimit(option)) this.fail((ctx) => new ParseError(ctx, "Too many repetitions"));
856
908
  uses.count += 1;
857
909
  for (const other of names) options.set(other, uses.values);
858
910
  if (!option.args) {
@@ -866,150 +918,162 @@ var Scanner = class {
866
918
  }
867
919
  this.valuesMin = this.valueMin;
868
920
  this.valuesMax = getMaxArgs(option.args);
869
- this.requiredSeparator = option.requiresSeparator ?? false;
870
921
  if (this.valuesMax > 0) {
871
922
  this.values = optionValues;
872
923
  this.valuesStart = optionValues.length;
873
924
  this.valueArgs = asArray(option.args);
874
925
  }
875
926
  }
876
- /**
877
- * Add a value to the open option. Returns whether it's a value the option
878
- * accepts. A lenient read keeps going either way.
879
- */
927
+ /** Add a value to the open option. Returns whether it's a value the option accepts. */
880
928
  addValue(value, separator) {
881
929
  this.valueCount += 1;
882
930
  if (!this.values) {
883
- if (this.strict) throw new InvalidOptionArg(this.openOptionName, this.ctx());
931
+ const name = this.openOptionName;
932
+ this.fail((ctx) => new InvalidOptionArg(name, ctx));
884
933
  return false;
885
934
  }
886
- const requiredSeparator = this.requiredSeparator;
887
- let accepted = true;
888
- if (typeof requiredSeparator === "string" && separator !== requiredSeparator) {
889
- if (this.strict) throw new ParseError(this.ctx(), `Incorrect separator, use '${requiredSeparator}' instead of '${separator}'`);
890
- accepted = false;
891
- }
892
- if (requiredSeparator === true && separator === "") {
893
- if (this.strict) {
894
- const suggested = this.scope.requiredSeparator(this.openOption);
895
- throw new ParseError(this.ctx(), `Missing separator, use '${suggested}' between ${this.openOptionName} and its value`);
896
- }
897
- accepted = false;
898
- }
935
+ const problem = this.scope.separatorProblem(this.openOption, this.openOptionName, separator);
936
+ if (problem) this.fail((ctx) => new ParseError(ctx, problem));
899
937
  this.values.push(value);
900
- return accepted;
938
+ return problem === null;
939
+ }
940
+ /** Add a separate token as a value of the open option. */
941
+ value(token) {
942
+ this.addValue(token, "");
943
+ return {
944
+ kind: "value",
945
+ option: this.openOption
946
+ };
901
947
  }
902
948
  /**
903
- * Add a value attached to the option's own token, the token at `index`,
904
- * starting at `start`. It's the option's only value, so no later token can be
905
- * one.
949
+ * Add a value attached to the option's own token, starting at `start`. It's
950
+ * the option's only value, so no later token can be one. Returns where the
951
+ * value starts, or null if the option doesn't accept it.
906
952
  */
907
- addAttachedValue(index, start, value, separator) {
908
- if (this.addValue(value, separator)) this.attached = {
909
- option: this.openOption,
910
- index,
911
- start
912
- };
953
+ addAttachedValue(start, value, separator) {
954
+ const option = this.openOption;
955
+ const accepted = this.addValue(value, separator);
913
956
  this.valueMin = this.separateValueMax = 0;
957
+ return accepted ? {
958
+ option,
959
+ start
960
+ } : null;
914
961
  }
915
962
  /**
916
963
  * Whether the command already has all the arguments it takes. A command that
917
964
  * requires a subcommand takes none.
918
965
  */
919
966
  get argsAreFull() {
920
- const path = this.path;
921
- return path[path.length - 1].requiresSubcommand === true || this.args.length >= this.commandArgsMax;
967
+ return this.scope.command.requiresSubcommand === true || this.args.length >= this.scope.argsMax;
922
968
  }
923
969
  addArg(value) {
924
970
  this.endOption();
925
- if (this.strict && this.argsAreFull) this.tooManyArgs ??= new TooManyArguments(value, this.ctx());
971
+ if (this.argsAreFull) this.failArgs((ctx) => new TooManyArguments(value, ctx));
926
972
  this.args.push(value);
973
+ return { kind: "argument" };
927
974
  }
928
- /** Read `--` and every token after it as arguments. */
929
- argSeparator(input, index) {
975
+ /** Read `--`. Every token after it is an argument. */
976
+ argSeparator() {
930
977
  this.closeOption();
931
- const command = this.path[this.path.length - 1];
932
- if (this.strict && this.argsAreFull && !command.requiresSubcommand) this.tooManyArgs ??= new ParseError(this.ctx(), "Unexpected argument '--', did you mean to use an option instead?");
978
+ const command = this.scope.command;
979
+ if (this.argsAreFull && !command.requiresSubcommand) this.failArgs((ctx) => new ParseError(ctx, "Unexpected argument '--', did you mean to use an option instead?"));
933
980
  this.argSeparatorIndex = this.args.length;
934
- for (index += 1; index < input.length; index++) this.addArg(input[index]);
981
+ return { kind: "separator" };
935
982
  }
936
983
  /**
937
- * Whether the next token can name a subcommand, its own or an inherited
938
- * one. It can't once an argument is found, or while the open option needs
939
- * the next token as its value. A required or variadic option value takes
940
- * priority over a subcommand.
984
+ * How the next token can name a subcommand, its own or an inherited one, or
985
+ * null if it can't. It can't once an argument is found, or while the open
986
+ * option needs the next token as its value. A required or variadic option
987
+ * value takes priority over a subcommand. A short optional value could be a
988
+ * prefix of a subcommand's name, so only an exact name can end an option
989
+ * that still has room.
941
990
  */
942
- get canMatchSubcommand() {
943
- if (this.hasFoundArg) return false;
944
- if (!this.subcommands && this.scope.inheritedSubcommands.length === 0) return false;
945
- return !(this.valueCount < this.valueMin || this.hasRoomForValue && this.separateValueMax === Infinity);
991
+ get subcommandMatching() {
992
+ if (this.hasFoundArg || !this.scope.hasSubcommands) return null;
993
+ if (this.valueCount < this.valueMin || this.hasRoomForValue && this.separateValueMax === Infinity) return null;
994
+ return { exactOnly: this.hasRoomForValue };
946
995
  }
947
996
  /**
948
997
  * The subcommand the next token enters if it's `token`, or why it enters
949
998
  * none. Null if the next token can't be a subcommand at all.
950
999
  */
951
1000
  subcommandFor(token) {
952
- if (!this.canMatchSubcommand) return null;
953
- return matchSubcommand(this.subcommands ?? [], token, this.directives, this.hasRoomForValue, this.scope.inheritedSubcommands);
1001
+ const matching = this.subcommandMatching;
1002
+ return matching && this.scope.matchSubcommand(token, matching.exactOnly);
954
1003
  }
955
1004
  enterSubcommand(command) {
956
1005
  this.closeOption();
957
1006
  this.path.push(command);
958
- this.enterCommand(command);
1007
+ this.scope.enter(command);
1008
+ return {
1009
+ kind: "subcommand",
1010
+ command
1011
+ };
959
1012
  }
960
1013
  /** Read a token like `-abc`, `+x` or `-ovalue`. */
961
- shortOptions(token, index) {
1014
+ shortOptions(token) {
962
1015
  const leadingChar = token[0];
963
1016
  const dashOption = this.findOption(leadingChar);
964
1017
  if (dashOption?.args && !this.findOption(token.slice(0, 2))) {
965
1018
  this.useOption(dashOption, leadingChar);
966
- this.addAttachedValue(index, 1, token.slice(1), "");
967
- return;
1019
+ const attached = this.addAttachedValue(1, token.slice(1), "");
1020
+ return {
1021
+ kind: "options",
1022
+ options: [dashOption],
1023
+ attached
1024
+ };
968
1025
  }
1026
+ const used = [];
1027
+ let attached = null;
969
1028
  for (let char = 1; char < token.length; char++) {
970
1029
  const optionName = leadingChar + token[char];
971
1030
  const option = this.findOption(optionName);
972
1031
  if (!option) {
973
- if (char === 1 && this.hasRoomForValue) {
974
- this.addValue(token, "");
975
- return;
976
- }
977
- if (this.strict) throw this.unknownOption(optionName);
1032
+ if (char === 1 && this.hasRoomForValue) return this.value(token);
1033
+ this.unknownOption(optionName);
978
1034
  continue;
979
1035
  }
980
1036
  this.useOption(option, optionName);
981
- if (char === token.length - 1) return;
982
- const sep = leadingSeparator(token.slice(char + 1), this.directives.optionArgSeparators);
1037
+ used.push(option);
1038
+ if (char === token.length - 1) break;
1039
+ const sep = this.scope.leadingSeparator(token.slice(char + 1));
983
1040
  if (option.args || sep && !this.findOption(leadingChar + token[char + 1])) {
984
1041
  const argStart = char + sep.length + 1;
985
- this.addAttachedValue(index, argStart, token.slice(argStart), sep);
986
- return;
1042
+ attached = this.addAttachedValue(argStart, token.slice(argStart), sep);
1043
+ break;
987
1044
  }
988
1045
  }
1046
+ return used.length > 0 ? {
1047
+ kind: "options",
1048
+ options: used,
1049
+ attached
1050
+ } : { kind: "unknown" };
989
1051
  }
990
1052
  /**
991
1053
  * Read a token like `--name` or `--name=value`, or any token when options
992
- * aren't POSIX compliant. Returns false if the token should be an argument.
1054
+ * aren't POSIX compliant. Returns null if the token should be an argument.
993
1055
  */
994
- longOption(token, index) {
995
- const found = firstSeparator(token, this.directives.optionArgSeparators);
1056
+ longOption(token) {
1057
+ const found = this.scope.firstSeparator(token);
996
1058
  const optionName = found ? token.slice(0, found.index) : token;
997
1059
  const option = this.findOption(optionName);
998
1060
  if (option) {
999
1061
  this.useOption(option, optionName);
1062
+ let attached = null;
1000
1063
  if (found) {
1001
- const valueStart = found.index + found.sep.length;
1002
- this.addAttachedValue(index, valueStart, token.slice(valueStart), found.sep);
1064
+ const valueStart = found.index + found.separator.length;
1065
+ attached = this.addAttachedValue(valueStart, token.slice(valueStart), found.separator);
1003
1066
  }
1004
- return true;
1005
- }
1006
- if (this.directives.flagsArePosixNoncompliant) return false;
1007
- if (this.hasRoomForValue) {
1008
- this.addValue(token, "");
1009
- return true;
1067
+ return {
1068
+ kind: "options",
1069
+ options: [option],
1070
+ attached
1071
+ };
1010
1072
  }
1011
- if (this.strict) throw this.unknownOption(optionName);
1012
- return true;
1073
+ if (this.directives.flagsArePosixNoncompliant) return null;
1074
+ if (this.hasRoomForValue) return this.value(token);
1075
+ this.unknownOption(optionName);
1076
+ return { kind: "unknown" };
1013
1077
  }
1014
1078
  /**
1015
1079
  * Read a token that isn't `--`, a subcommand or an option. It's a value of
@@ -1018,34 +1082,39 @@ var Scanner = class {
1018
1082
  * subcommand, if it could have been one.
1019
1083
  */
1020
1084
  valueOrArg(token, unmatched) {
1021
- if (this.hasRoomForValue) {
1022
- this.addValue(token, "");
1023
- return;
1024
- }
1025
- if (this.strict && unmatched && this.argsAreFull) {
1026
- const command = this.path[this.path.length - 1];
1027
- if (command.subcommands?.length || command.requiresSubcommand) {
1085
+ if (this.hasRoomForValue) return this.value(token);
1086
+ if (unmatched && this.argsAreFull) {
1087
+ const command = this.scope.command;
1088
+ if (command.subcommands?.length || command.requiresSubcommand) this.failArgs((ctx) => {
1028
1089
  const { message, didYouMean } = unmatched.reason();
1029
- this.tooManyArgs ??= new ParseError(this.ctx(), message, didYouMean);
1030
- }
1090
+ return new ParseError(ctx, message, didYouMean);
1091
+ });
1031
1092
  }
1032
1093
  this.hasFoundArg = true;
1033
- this.addArg(token);
1094
+ return this.addArg(token);
1034
1095
  }
1035
1096
  /**
1036
- * Throw if the finished command line is missing anything. Returns the action
1037
- * to run.
1097
+ * Throw if the finished command line is missing anything. Returns what it
1098
+ * was parsed as. Only call this when there's no {@link problem}.
1038
1099
  */
1039
1100
  finish() {
1040
- const command = this.path[this.path.length - 1];
1101
+ const command = this.scope.command;
1041
1102
  const showsUsage = command.requiresSubcommand && !command.action && this.args.length === 0;
1042
1103
  const skipsChecks = this.optionAction !== void 0 || showsUsage;
1043
- if (!skipsChecks && this.tooManyArgs) throw this.tooManyArgs;
1044
- if (!skipsChecks && this.args.length < this.commandArgsMin) throw new TooFewArguments(this.commandArgsMin, this.commandArgsMax, this.ctx());
1104
+ if (!skipsChecks && this.tooManyArgs) throw this.tooManyArgs();
1105
+ if (!skipsChecks && this.args.length < this.scope.argsMin) throw new TooFewArguments(this.scope.argsMin, this.scope.argsMax, this.ctx());
1045
1106
  this.endOption();
1107
+ if (this.problem) throw this.problem();
1046
1108
  if (!skipsChecks) checkOptionRules(this.options, this.seenOptions, this.scope.required(), this.ctx());
1047
- if (!command.requiresSubcommand && this.args.length >= this.commandArgsMin) addDefaults(this.args, asArray(command.args), this.args.length);
1048
- return this.optionAction ?? command.action ?? usage;
1109
+ if (!command.requiresSubcommand && this.args.length >= this.scope.argsMin) addDefaults(this.args, asArray(command.args), this.args.length);
1110
+ return {
1111
+ path: this.path,
1112
+ action: this.optionAction ?? command.action ?? usage,
1113
+ args: this.args,
1114
+ argSeparatorIndex: this.argSeparatorIndex,
1115
+ options: this.options,
1116
+ defaults: (name) => this.defaults(name)
1117
+ };
1049
1118
  }
1050
1119
  /** See {@link ParseResult.defaults}. */
1051
1120
  defaults(name) {
@@ -1058,42 +1127,41 @@ var Scanner = class {
1058
1127
  }
1059
1128
  /** Whether using `option` next would go over its repeat limit or break an `exclusiveOn` rule. */
1060
1129
  conflicts(option) {
1061
- const names = asArray(option.name);
1130
+ if ((this.repeatedUses.get(optionKey(option))?.count ?? 0) >= repeatLimit(option)) return true;
1062
1131
  const options = this.options;
1063
- if (typeof option.isRepeatable === "number") {
1064
- if ((this.repeatedUses.get(optionKey(option))?.count ?? 0) >= option.isRepeatable) return true;
1065
- }
1066
1132
  if (option.exclusiveOn?.some((name) => options.has(name))) return true;
1133
+ const names = asArray(option.name);
1067
1134
  return this.seenOptions.some((seen) => seen.exclusiveOn?.some((name) => names.includes(name)));
1068
1135
  }
1069
1136
  /** The options the next token can use. See {@link NextToken.options}. */
1070
1137
  usableOptions() {
1071
1138
  const used = new Set(this.seenOptions.map(optionKey));
1072
- const { local, persistent } = this.scope.live();
1139
+ const { local, persistent } = this.scope.live(this.subcommandMatching);
1073
1140
  const usable = [];
1074
1141
  for (const { option, names } of [...local, ...persistent]) {
1075
1142
  if (this.conflicts(option)) continue;
1076
- const typeable = names.filter((name) => !this.subcommandFor(name)?.command);
1077
- if (typeable.length === 0) continue;
1078
1143
  usable.push({
1079
1144
  option,
1080
- forms: this.scope.forms(option, typeable),
1145
+ forms: this.scope.forms(option, names),
1081
1146
  used: used.has(optionKey(option))
1082
1147
  });
1083
1148
  }
1084
1149
  return usable;
1085
1150
  }
1086
- /** What the next token can be. See {@link readNext}. */
1151
+ /**
1152
+ * What the next token can be. See {@link readNext}. Reading more tokens
1153
+ * doesn't change what this returns.
1154
+ */
1087
1155
  next() {
1088
1156
  const open = this.openOption && this.hasRoomForValue ? this.openOption : null;
1089
- const command = this.path[this.path.length - 1];
1157
+ const command = this.scope.command;
1090
1158
  const afterSeparator = this.argSeparatorIndex !== -1;
1091
1159
  const optionsAllowed = !afterSeparator && this.optionsAllowed;
1092
1160
  return {
1093
- path: this.path,
1094
- directives: this.directives,
1161
+ path: [...this.path],
1162
+ directives: { ...this.directives },
1095
1163
  optionsAllowed,
1096
- subcommands: !afterSeparator && this.canMatchSubcommand ? withInherited(this.subcommands ?? [], this.scope.inheritedSubcommands) : [],
1164
+ subcommands: !afterSeparator && this.subcommandMatching ? this.scope.subcommands() : [],
1097
1165
  options: optionsAllowed ? this.usableOptions() : [],
1098
1166
  value: open && {
1099
1167
  option: open,
@@ -1103,79 +1171,27 @@ var Scanner = class {
1103
1171
  arg: open ? void 0 : argAt(asArray(command.args), this.args.length)
1104
1172
  };
1105
1173
  }
1106
- /** Read the token at `index`, the next one, and say what it was read as. */
1107
- readAndClassify(input, index) {
1108
- if (this.argSeparatorIndex !== -1) return { kind: "argument" };
1109
- const depth = this.path.length;
1110
- const seen = this.seenOptions.length;
1111
- const args = this.args.length;
1112
- const values = this.valueCount;
1113
- if (this.readAt(input, index)) return { kind: "separator" };
1114
- if (this.path.length > depth) return {
1115
- kind: "subcommand",
1116
- command: this.path[depth]
1117
- };
1118
- if (this.seenOptions.length > seen) {
1119
- const found = this.attachedAt(index);
1120
- const attached = found && {
1121
- option: found.option,
1122
- start: found.start
1123
- };
1124
- return {
1125
- kind: "options",
1126
- options: this.seenOptions.slice(seen),
1127
- attached
1128
- };
1129
- }
1130
- if (this.args.length > args) return { kind: "argument" };
1131
- if (this.valueCount > values) return {
1132
- kind: "value",
1133
- option: this.openOption
1134
- };
1135
- return { kind: "unknown" };
1136
- }
1137
- /** The value attached to the token at `index`, if it has one the option accepts. */
1138
- attachedAt(index) {
1139
- return this.attached?.index === index ? this.attached : null;
1140
- }
1141
1174
  /** Whether the next token can be an option. Once options must stop, every token is an argument. */
1142
1175
  get optionsAllowed() {
1143
1176
  return !(this.directives.optionsMustPrecedeArguments && this.hasFoundArg);
1144
1177
  }
1145
- /** Read every token of `input`. */
1146
- read(input) {
1147
- for (let index = 0; index < input.length; index++) if (this.readAt(input, index)) return;
1148
- }
1149
- /**
1150
- * Read the token at `index`. Returns true if it was `--`, which reads every
1151
- * token after it too.
1152
- */
1153
- readAt(input, index) {
1154
- const token = input[index];
1178
+ read(token) {
1179
+ if (this.argSeparatorIndex !== -1) return this.addArg(token);
1155
1180
  const optionsAllowed = this.optionsAllowed;
1156
- if (token === "--" && optionsAllowed) {
1157
- this.argSeparator(input, index);
1158
- return true;
1159
- }
1181
+ if (token === "--" && optionsAllowed) return this.argSeparator();
1160
1182
  const subcommand = this.subcommandFor(token);
1161
- if (subcommand?.command) {
1162
- this.enterSubcommand(subcommand.command);
1163
- return false;
1164
- }
1183
+ if (subcommand?.command) return this.enterSubcommand(subcommand.command);
1165
1184
  if (optionsAllowed) {
1166
1185
  const flagsArePosixNoncompliant = this.directives.flagsArePosixNoncompliant;
1167
1186
  const first = token.charCodeAt(0);
1168
1187
  const isDoubleDash = first === 45 && token.charCodeAt(1) === 45;
1169
- if (!flagsArePosixNoncompliant && token.length > 1 && (first === 45 || first === 43) && !isDoubleDash) {
1170
- this.shortOptions(token, index);
1171
- return false;
1172
- }
1188
+ if (!flagsArePosixNoncompliant && token.length > 1 && (first === 45 || first === 43) && !isDoubleDash) return this.shortOptions(token);
1173
1189
  if (flagsArePosixNoncompliant || token.length > 2 && isDoubleDash) {
1174
- if (this.longOption(token, index)) return false;
1190
+ const reading = this.longOption(token);
1191
+ if (reading) return reading;
1175
1192
  }
1176
1193
  }
1177
- this.valueOrArg(token, subcommand);
1178
- return false;
1194
+ return this.valueOrArg(token, subcommand);
1179
1195
  }
1180
1196
  };
1181
1197
  /** The argument at `index`, reusing a final variadic argument. */
@@ -1186,17 +1202,19 @@ function argAt(args, index) {
1186
1202
  }
1187
1203
  /**
1188
1204
  * Work out what the token after `before` can be, and how the parser reads
1189
- * `partial` if it's an option with a value attached. Reads leniently, so a
1190
- * line that's wrong at the moment still has an answer. Completion is built on
1191
- * this.
1205
+ * `partial`: whether it's an option, and if so, whether it has a value
1206
+ * attached. Reads leniently, so a line that's wrong at the moment still has
1207
+ * an answer. Completion is built on this.
1192
1208
  */
1193
1209
  function readNext(spec, before, partial) {
1194
- const s = new Scanner(spec, false);
1195
- s.read(before);
1210
+ const s = new Scanner(spec);
1211
+ for (const token of before) s.read(token);
1196
1212
  const next = s.next();
1197
1213
  let attached = null;
1214
+ let partialIsOption = false;
1198
1215
  if (partial !== "" && next.optionsAllowed) {
1199
- const reading = readToken(spec, before, partial);
1216
+ const reading = s.read(partial);
1217
+ partialIsOption = reading.kind === "options" || reading.kind === "unknown" || reading.kind === "separator";
1200
1218
  const found = reading.kind === "options" ? reading.attached : null;
1201
1219
  const arg = found && asArray(found.option.args)[0];
1202
1220
  if (found && arg) attached = {
@@ -1208,19 +1226,11 @@ function readNext(spec, before, partial) {
1208
1226
  }
1209
1227
  return {
1210
1228
  ...next,
1211
- attached
1229
+ attached,
1230
+ partialIsOption
1212
1231
  };
1213
1232
  }
1214
1233
  /**
1215
- * Say what the parser reads `token` as when it comes after `before`. Reads
1216
- * leniently, like {@link readNext}.
1217
- */
1218
- function readToken(spec, before, token) {
1219
- const s = new Scanner(spec, false);
1220
- s.read(before);
1221
- return s.readAndClassify([...before, token], before.length);
1222
- }
1223
- /**
1224
1234
  * Parse the input tokens using the given CLI spec.
1225
1235
  *
1226
1236
  * This returns a `ParseResult`, which contains the array of
@@ -1230,17 +1240,12 @@ function readToken(spec, before, token) {
1230
1240
  * If parsing fails, this function throws.
1231
1241
  */
1232
1242
  function parse(input, spec) {
1233
- const s = new Scanner(spec, true);
1234
- s.read(input);
1235
- const action = s.finish();
1236
- return {
1237
- path: s.path,
1238
- action,
1239
- args: s.args,
1240
- argSeparatorIndex: s.argSeparatorIndex,
1241
- options: s.options,
1242
- defaults: (name) => s.defaults(name)
1243
- };
1243
+ const s = new Scanner(spec);
1244
+ for (const token of input) {
1245
+ s.read(token);
1246
+ if (s.problem) throw s.problem();
1247
+ }
1248
+ return s.finish();
1244
1249
  }
1245
1250
  // src/completion.ts
1246
1251
  const SHELLS = [
@@ -1320,11 +1325,18 @@ async function argValues(arg, tokens, partial) {
1320
1325
  }
1321
1326
  /** Decide which kinds of candidate the word being completed can use. */
1322
1327
  function wantedCandidates(next, req) {
1323
- const posixNoncompliant = next.directives.flagsArePosixNoncompliant;
1324
- const partialIsOption = next.optionsAllowed && !posixNoncompliant && (req.partial.startsWith("-") || req.partial.startsWith("+"));
1328
+ if (!req.gateOptions || next.directives.flagsArePosixNoncompliant) return {
1329
+ wantValues: true,
1330
+ wantOptions: true,
1331
+ wantSubcommand: () => true
1332
+ };
1333
+ const partial = req.partial;
1334
+ const wantValues = !next.partialIsOption;
1335
+ const starts = (name) => partial !== "" && name.startsWith(partial);
1325
1336
  return {
1326
- wantValues: !(req.gateOptions && partialIsOption),
1327
- wantOptions: !req.gateOptions || partialIsOption || posixNoncompliant
1337
+ wantValues,
1338
+ wantOptions: next.options.some(({ forms }) => forms.some(({ name }) => starts(name))),
1339
+ wantSubcommand: (name) => wantValues || starts(name)
1328
1340
  };
1329
1341
  }
1330
1342
  /**
@@ -1344,17 +1356,17 @@ async function completeRuntime(req) {
1344
1356
  }
1345
1357
  if (next.value?.required) return next.value.arg ? argValues(next.value.arg, tokens, req.partial) : [];
1346
1358
  const candidates = [];
1347
- const { wantValues, wantOptions } = wantedCandidates(next, req);
1359
+ const { wantValues, wantOptions, wantSubcommand } = wantedCandidates(next, req);
1348
1360
  if (wantValues) {
1349
1361
  const arg = next.value ? next.value.arg : next.arg;
1350
1362
  if (arg) candidates.push(...await argValues(arg, tokens, req.partial));
1351
- for (const sub of visibleSubcommands(next.subcommands)) {
1352
- const description = firstLine(sub.description);
1353
- for (const token of makeArray(sub.name)) candidates.push({
1354
- value: token,
1355
- description
1356
- });
1357
- }
1363
+ }
1364
+ for (const sub of visibleSubcommands(next.subcommands)) {
1365
+ const description = firstLine(sub.description);
1366
+ for (const token of makeArray(sub.name)) if (wantSubcommand(token)) candidates.push({
1367
+ value: token,
1368
+ description
1369
+ });
1358
1370
  }
1359
1371
  if (wantOptions) candidates.push(...optionCandidates(next));
1360
1372
  return candidates;