@clo4/cli 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -56,9 +56,21 @@ const output = { write: writeFd };
56
56
  function print(text) {
57
57
  output.write(1, `${text.trimEnd()}\n`);
58
58
  }
59
- /** Print an error message to stderr with the shared `Error:` prefix. */
60
- function printActionError(message) {
61
- output.write(2, `Error: ${message}\n`);
59
+ /** Print `Error:` and the given values to stderr, separated by spaces. */
60
+ function printError(...strings) {
61
+ output.write(2, `${["Error:", ...strings].map(String).join(" ")}\n`);
62
+ }
63
+ /**
64
+ * Add the defaults of the arguments from index `from` on to `values`, stopping
65
+ * at the first argument without one. The parser and help both use this, so
66
+ * help only shows the defaults the parser uses.
67
+ */
68
+ function addDefaults(values, args, from = 0) {
69
+ for (let i = from; i < args.length; i++) {
70
+ const value = args[i].default;
71
+ if (value === void 0) return;
72
+ values.push(value);
73
+ }
62
74
  }
63
75
  /**
64
76
  * Returns the minimum number of arguments that satisfies the args.
@@ -83,76 +95,6 @@ function getMaxArgs(args) {
83
95
  for (let i = 0; i < args.length; i++) if (args[args.length - 1 - i].isVariadic) return Infinity;
84
96
  return args.length;
85
97
  }
86
- // src/errors.ts
87
- const repr = JSON.stringify;
88
- var ParseError = class extends Error {
89
- context;
90
- constructor(context, ...args) {
91
- super(...args);
92
- this.context = context;
93
- this.name = this.constructor.name;
94
- }
95
- };
96
- var UnknownOption = class extends ParseError {
97
- index;
98
- start;
99
- end;
100
- option;
101
- validOptions;
102
- constructor(found, context) {
103
- const msg = `${repr(found.option)} looks like a flag, but is unknown in this context`;
104
- super(context, msg);
105
- this.option = found.option;
106
- this.validOptions = found.validOptions;
107
- this.index = found.index;
108
- this.start = found.start;
109
- this.end = found.end;
110
- }
111
- };
112
- var InvalidOptionArg = class extends ParseError {
113
- index;
114
- start;
115
- end;
116
- arg;
117
- option;
118
- constructor(found, context) {
119
- const msg = `Option ${found.option} doesn't take arguments`;
120
- super(context, msg);
121
- this.index = found.index;
122
- this.start = found.start;
123
- this.end = found.end;
124
- this.option = found.option;
125
- this.arg = found.arg;
126
- }
127
- };
128
- var TooManyArguments = class extends ParseError {
129
- index;
130
- value;
131
- constructor(found, context) {
132
- const msg = `Too many arguments, ${repr(found.value)} was unexpected`;
133
- super(context, msg);
134
- this.index = found.index;
135
- this.value = found.value;
136
- }
137
- };
138
- var TooFewOptionArguments = class extends ParseError {
139
- constructor(min, max, context) {
140
- const msg = min === max ? `Option needs ${min} argument${min === 1 ? "" : "s"}` : !Number.isFinite(max) ? `Option needs at least ${min} argument${min === 1 ? "" : "s"}` : `Option needs between ${min} and ${max} arguments`;
141
- super(context, msg);
142
- }
143
- };
144
- var TooFewArguments = class extends ParseError {
145
- constructor(min, max, context) {
146
- const msg = min === max ? `Expected ${min} argument${min === 1 ? "" : "s"}` : !Number.isFinite(max) ? `Expected at least ${min} argument${min === 1 ? "" : "s"}` : `Expected between ${min} and ${max} arguments`;
147
- super(context, msg);
148
- }
149
- };
150
- var MissingRequiredOption = class extends ParseError {
151
- constructor(name, context) {
152
- const msg = `Option ${name} is required, but wasn't found`;
153
- super(context, msg);
154
- }
155
- };
156
98
  // src/suggest.ts
157
99
  /**
158
100
  * "Did you mean" suggestions for unknown commands and options.
@@ -202,61 +144,291 @@ function editDistance(a, b, limit) {
202
144
  }
203
145
  return previous[b.length];
204
146
  }
147
+ /** The first name of the only choice with a name that `matches`, if exactly one has one. */
148
+ function onlyMatch(choices, matches) {
149
+ let match;
150
+ for (const choice of choices) {
151
+ const name = (typeof choice === "string" ? [choice] : choice).find(matches);
152
+ if (name === void 0) continue;
153
+ if (match !== void 0) return;
154
+ match = name;
155
+ }
156
+ return match;
157
+ }
205
158
  /**
206
- * The choice the user most likely meant by `input`, or `undefined` if none is
159
+ * The name the user most likely meant by `input`, or `undefined` if none is
207
160
  * close enough to be worth suggesting. In order:
208
161
  *
209
162
  * 1. The only choice that starts with the input (`--verb` for `--verbose`).
210
163
  * 2. The only choice that contains every character of the input in order, for
211
164
  * inputs of 3 or more characters (`bld` for `build`).
212
- * 3. The choice with the fewest edits, allowing one edit per three characters
165
+ * 3. The name with the fewest edits, allowing one edit per three characters
213
166
  * of the input. Leading dashes don't count toward the length or the edits.
214
167
  */
215
168
  function closestChoice(input, choices) {
216
169
  const inputDashes = dashCount(input);
217
170
  const bare = input.slice(inputDashes);
218
171
  if (bare.length >= 2) {
219
- let match;
220
- let count = 0;
221
- for (const choice of choices) if (choice.startsWith(input)) {
222
- match = choice;
223
- count++;
224
- }
225
- if (count === 1) return match;
172
+ const match = onlyMatch(choices, (name) => name.startsWith(input));
173
+ if (match !== void 0) return match;
226
174
  }
227
175
  if (bare.length >= 3) {
228
- let match;
229
- let count = 0;
230
- for (const choice of choices) if (isAbbreviation(bare, choice.slice(dashCount(choice)))) {
231
- match = choice;
232
- count++;
233
- }
234
- if (count === 1) return match;
176
+ const match = onlyMatch(choices, (name) => isAbbreviation(bare, name.slice(dashCount(name))));
177
+ if (match !== void 0) return match;
235
178
  }
236
179
  let best;
237
180
  let limit = Math.floor(bare.length / 3);
238
- for (const choice of choices) {
239
- const other = choice.slice(dashCount(choice));
181
+ for (const name of choices.flat()) {
182
+ const other = name.slice(dashCount(name));
240
183
  if (Math.abs(other.length - bare.length) > limit) continue;
241
184
  const distance = editDistance(bare, other, limit);
242
185
  if (distance <= limit && (best === void 0 || distance < limit)) {
243
- best = choice;
186
+ best = name;
244
187
  limit = distance;
245
188
  if (distance === 0) break;
246
189
  }
247
190
  }
248
191
  return best;
249
192
  }
250
- // src/help.ts
251
- function getNamed(named, name) {
252
- return named.find((obj) => makeArray(obj.name).includes(name)) ?? null;
193
+ // src/subcommands.ts
194
+ /**
195
+ * Find the subcommand `token` names among `subcommands`, in a command where
196
+ * `directives` are in effect, or else among the persistent subcommands the
197
+ * command `inherited`. An exact name always wins. With
198
+ * `subcommandsMatchUniquePrefix`, a token that's a prefix of exactly one
199
+ * of the command's own names matches that command, unless `exactOnly`. A
200
+ * hidden or inherited command only matches its exact name, so it can't make a
201
+ * visible command's prefix ambiguous or take a prefix meant as an argument,
202
+ * and an empty token is no command's prefix.
203
+ */
204
+ function matchSubcommand(subcommands, token, directives, exactOnly = false, inherited = []) {
205
+ for (const candidate of subcommands) if (hasName$1(candidate, token)) return { command: candidate };
206
+ for (const candidate of inherited) if (hasName$1(candidate, token)) return { command: candidate };
207
+ const matches = directives.subcommandsMatchUniquePrefix && !exactOnly ? prefixMatches(subcommands, token) : [];
208
+ if (matches.length === 1) return { command: matches[0] };
209
+ return new NoSubcommand(withInherited(subcommands, inherited), token, matches);
210
+ }
211
+ /**
212
+ * A command's own `subcommands` followed by the `inherited` ones it can use,
213
+ * leaving out each inherited one that an earlier command shadows a name of.
214
+ */
215
+ function withInherited(subcommands, inherited) {
216
+ if (inherited.length === 0) return subcommands;
217
+ const all = [...subcommands];
218
+ for (const candidate of inherited) if (!asArray(candidate.name).some((name) => all.some((earlier) => hasName$1(earlier, name)))) all.push(candidate);
219
+ return all;
220
+ }
221
+ function hasName$1(command, token) {
222
+ const names = command.name;
223
+ return typeof names === "string" ? names === token : names.includes(token);
224
+ }
225
+ /** The visible commands that `token` is a prefix of a name of, each listed once. */
226
+ function prefixMatches(subcommands, token) {
227
+ if (token === "") return [];
228
+ return subcommands.filter((candidate) => !candidate.hidden && asArray(candidate.name).some((name) => name.startsWith(token)));
229
+ }
230
+ /**
231
+ * A token that names no subcommand. The reason is only worked out when asked
232
+ * for, since most tokens the parser tries aren't subcommands.
233
+ */
234
+ var NoSubcommand = class {
235
+ subcommands;
236
+ token;
237
+ prefixOf;
238
+ command = null;
239
+ /**
240
+ * `prefixOf` are the commands `token` is a prefix of a name of, where
241
+ * prefixes match. There is never exactly one of them.
242
+ */
243
+ constructor(subcommands, token, prefixOf) {
244
+ this.subcommands = subcommands;
245
+ this.token = token;
246
+ this.prefixOf = prefixOf;
247
+ }
248
+ /** Why the token names none of the subcommands, as an error message and suggestions. */
249
+ reason() {
250
+ const token = this.token;
251
+ if (token === "") return { message: "Expected a command, but the value was empty" };
252
+ if (this.prefixOf.length > 0) return { message: `Ambiguous command '${token}', could be: ${this.prefixOf.flatMap((command) => asArray(command.name).filter((name) => name.startsWith(token))).join(", ")}` };
253
+ const choices = visibleSubcommands(this.subcommands).map((command) => asArray(command.name));
254
+ return {
255
+ message: `Unknown command '${token}'`,
256
+ didYouMean: {
257
+ input: token,
258
+ choices
259
+ }
260
+ };
261
+ }
262
+ };
263
+ /** The subcommands that help, completion and suggestions show. */
264
+ function visibleSubcommands(subcommands) {
265
+ return subcommands?.filter((subcommand) => !subcommand.hidden) ?? [];
266
+ }
267
+ /** The action of a command that has none of its own: print the command's help. */
268
+ const usage = ({ help }) => {
269
+ print(help());
270
+ return 0;
271
+ };
272
+ // src/directives.ts
273
+ /** The directives in effect when no command sets any. */
274
+ function defaultDirectives() {
275
+ return {
276
+ optionArgSeparators: ["="],
277
+ flagsArePosixNoncompliant: false,
278
+ optionsMustPrecedeArguments: false,
279
+ subcommandsMatchUniquePrefix: false
280
+ };
281
+ }
282
+ /**
283
+ * Update `directives` for entering `command`. A subcommand inherits each
284
+ * directive until it sets its own.
285
+ */
286
+ function applyDirectives(directives, command) {
287
+ const configured = command.parserDirectives;
288
+ if (!configured) return;
289
+ if (configured.optionArgSeparators) directives.optionArgSeparators = asArray(configured.optionArgSeparators);
290
+ if (typeof configured.subcommandsMatchUniquePrefix === "boolean") directives.subcommandsMatchUniquePrefix = configured.subcommandsMatchUniquePrefix;
291
+ if (typeof configured.optionsMustPrecedeArguments === "boolean") directives.optionsMustPrecedeArguments = configured.optionsMustPrecedeArguments;
292
+ if (typeof configured.flagsArePosixNoncompliant === "boolean") directives.flagsArePosixNoncompliant = configured.flagsArePosixNoncompliant;
253
293
  }
254
- function getParserDirective(path, key) {
255
- for (let index = path.length - 1; index >= 0; index -= 1) {
256
- const command = path[index];
257
- if (command?.parserDirectives && key in command.parserDirectives) return command.parserDirectives[key];
294
+ // src/scope.ts
295
+ /**
296
+ * The options and directives in effect at one command on a path.
297
+ *
298
+ * Moving to a subcommand replaces the local options and adds its persistent
299
+ * options to the inherited ones. A name refers to one option: a local option
300
+ * wins over a persistent one, and a later option wins over an earlier one in
301
+ * the same list. The parser, help, completion and the spec checker all ask
302
+ * this module which option a name refers to, so they can't disagree. It also
303
+ * holds the persistent subcommands the current command inherits.
304
+ */
305
+ var OptionScope = class OptionScope {
306
+ /** Non-persistent options of the current command, in definition order. */
307
+ local = [];
308
+ /** Persistent options from every command on the path, in definition order. */
309
+ persistent = [];
310
+ /**
311
+ * Persistent subcommands of the commands above the current one, nearest
312
+ * command first, so a nearer one wins a name.
313
+ */
314
+ inheritedSubcommands = [];
315
+ /** The directives in effect. A subcommand inherits each until it sets its own. */
316
+ directives = defaultDirectives();
317
+ command = null;
318
+ /** The scope at the last command on `path`. */
319
+ static at(path) {
320
+ const scope = new OptionScope();
321
+ for (const command of path) scope.enter(command);
322
+ return scope;
323
+ }
324
+ /** Move to `command`, a subcommand of the current one. */
325
+ enter(command) {
326
+ const parentSubcommands = this.command?.subcommands;
327
+ if (parentSubcommands) {
328
+ const persistent = parentSubcommands.filter((subcommand) => subcommand.isPersistent);
329
+ if (persistent.length > 0) this.inheritedSubcommands.unshift(...persistent);
330
+ }
331
+ this.command = command;
332
+ this.local.length = 0;
333
+ if (command.options) for (const option of command.options) (option.isPersistent ? this.persistent : this.local).push(option);
334
+ applyDirectives(this.directives, command);
335
+ }
336
+ /** The option `name` refers to, if any. */
337
+ resolve(name) {
338
+ const { local, persistent } = this;
339
+ for (let i = local.length - 1; i >= 0; i--) if (hasName(local[i].name, name)) return local[i];
340
+ for (let i = persistent.length - 1; i >= 0; i--) if (hasName(persistent[i].name, name)) return persistent[i];
341
+ return null;
342
+ }
343
+ /**
344
+ * The options that can be used here, in definition order, each with only
345
+ * its live names: the names that still refer to it. Options with every name
346
+ * shadowed are left out.
347
+ */
348
+ live() {
349
+ const owner = /* @__PURE__ */ new Map();
350
+ for (const options of [this.persistent, this.local]) for (const option of options) for (const name of asArray(option.name)) owner.set(name, option);
351
+ const pick = (options) => {
352
+ const picked = [];
353
+ for (const option of options) {
354
+ const names = asArray(option.name).filter((name) => owner.get(name) === option);
355
+ if (names.length > 0) picked.push({
356
+ option,
357
+ names
358
+ });
359
+ }
360
+ return picked;
361
+ };
362
+ return {
363
+ local: pick(this.local),
364
+ persistent: pick(this.persistent)
365
+ };
366
+ }
367
+ /** The options a user can see here: the {@link live} ones that aren't hidden. */
368
+ visible() {
369
+ const { local, persistent } = this.live();
370
+ const shown = (visible) => !visible.option.hidden;
371
+ return {
372
+ local: local.filter(shown),
373
+ persistent: persistent.filter(shown)
374
+ };
375
+ }
376
+ /** The live names of each option a user could have meant, for "did you mean" suggestions. */
377
+ suggestionNames() {
378
+ const { local, persistent } = this.visible();
379
+ return [...local, ...persistent].map((visible) => visible.names);
380
+ }
381
+ /** The options that must be used, local first. */
382
+ required() {
383
+ return [...this.local, ...this.persistent].filter((option) => option.isRequired);
384
+ }
385
+ /**
386
+ * How each of `names` is written with `option`'s value here. Help shows
387
+ * these, completion offers them, and the spec checker reads each one back
388
+ * through the parser.
389
+ */
390
+ forms(option, names = asArray(option.name)) {
391
+ const join = (name) => {
392
+ if (getMaxArgs(option.args) === 0) return null;
393
+ if (option.requiresSeparator) return this.requiredSeparator(option);
394
+ return !this.directives.flagsArePosixNoncompliant && (name === "-" || name === "+") ? "" : null;
395
+ };
396
+ return names.map((name) => ({
397
+ name,
398
+ join: join(name)
399
+ }));
400
+ }
401
+ /**
402
+ * The separator that must come between `option` and its value, or null if
403
+ * it can take a value as a separate token.
404
+ */
405
+ requiredSeparator(option) {
406
+ const required = option.requiresSeparator;
407
+ if (typeof required === "string") return required;
408
+ return required ? this.directives.optionArgSeparators[0] ?? "=" : null;
409
+ }
410
+ };
411
+ const optionKeys = /* @__PURE__ */ new WeakMap();
412
+ /**
413
+ * A key for the set of `option`'s names. Options with the same names, in any
414
+ * order, get the same key and are one option: the parser continues a
415
+ * repeatable option's uses across them, counts one as used when the other was,
416
+ * and stores their values under the same names, which is how they're read.
417
+ * Options that share only some names are different options, and each shared
418
+ * name refers to whichever one `resolve` picks.
419
+ */
420
+ function optionKey(option) {
421
+ let key = optionKeys.get(option);
422
+ if (key === void 0) {
423
+ key = typeof option.name === "string" ? option.name : [...option.name].sort().join("\0");
424
+ optionKeys.set(option, key);
258
425
  }
426
+ return key;
259
427
  }
428
+ function hasName(names, name) {
429
+ return typeof names === "string" ? names === name : names.includes(name);
430
+ }
431
+ // src/help.ts
260
432
  function dedent(str) {
261
433
  if (str[0] !== "\n") return str;
262
434
  const lines = str.split("\n").slice(1, -1);
@@ -273,80 +445,52 @@ function getHelp(path, config = {}) {
273
445
  return formatHelpSections(sections);
274
446
  }
275
447
  /**
276
- * Action that prints usage for commands where a subcommand must be provided.
448
+ * Print `message` as an error, then the usage and flags of the last command on
449
+ * `path`, suggesting the closest of `didYouMean.choices` if one is close.
450
+ */
451
+ function printUsageError(path, message, didYouMean) {
452
+ printError(`${message}\n\n${getHelp(path, {
453
+ description: false,
454
+ didYouMean
455
+ })}`);
456
+ }
457
+ /**
458
+ * A subcommand that prints help for its parent, or for the command the names
459
+ * after it lead to from there, so `cli help a b` prints the help of `cli a b`.
460
+ * It reads names until one isn't a subcommand. A token where the command takes
461
+ * arguments, or where it has no subcommands, ends the names, so
462
+ * `cli help completion zsh` prints the help of `completion`. Any other token
463
+ * is an unknown command. Add `{ ...helpCommand, isPersistent: true }` instead
464
+ * to make `cli a b help` work too.
277
465
  */
278
- const usage = ({ path, args: [command], help }) => {
279
- if (command === void 0) {
280
- print(help());
281
- return 0;
282
- }
283
- if (command === "") {
284
- printActionError(`Expected a command, but the value was empty\n\n${help({ description: false })}`);
285
- return 1;
286
- }
287
- const subcommands = path.at(-1)?.subcommands?.filter((cmd) => !cmd.hidden).flatMap((cmd) => cmd.name);
288
- if (subcommands && subcommands.length > 0) {
289
- if (getParserDirective(path, "subcommandsMatchUniquePrefix")) {
290
- const possible = subcommands.filter((name) => name.startsWith(command));
291
- if (possible.length > 0) {
292
- printActionError(`Ambiguous command '${command}', could be: ${possible.join(", ")}\n\n${help({ description: false })}`);
293
- return 1;
294
- }
295
- }
296
- printActionError(`Unknown command '${command}'\n\n${help({
297
- description: false,
298
- didYouMean: {
299
- input: command,
300
- choices: subcommands
301
- }
302
- })}`);
303
- return 1;
304
- }
305
- printActionError(`Unknown command '${command}'\n\n${help({ description: false })}`);
306
- return 1;
307
- };
308
- /** A subcommand that prints help for its parent or a sibling command. */
309
466
  const helpCommand = {
310
467
  name: "help",
311
468
  description: "Print a help message",
312
469
  args: {
313
470
  name: "command",
314
- isOptional: true
471
+ isOptional: true,
472
+ isVariadic: true
315
473
  },
316
- action({ path, args: [commandName], help }) {
317
- const helpRoot = path.slice(0, -1);
318
- if (!commandName) {
319
- print(help({ path: helpRoot }));
320
- return 0;
321
- }
322
- const parent = path[path.length - 2];
323
- if (!parent.subcommands) {
324
- printActionError(`No subcommands, try using 'help' with nothing after it\n\n${help({
325
- description: false,
326
- path: helpRoot
327
- })}`);
328
- return 1;
329
- }
330
- const command = getNamed(parent.subcommands, commandName);
331
- if (!command) {
332
- const subcommands = parent.subcommands.filter((cmd) => !cmd.hidden).flatMap((cmd) => cmd.name);
333
- if (subcommands.length > 0) {
334
- printActionError(`There is no subcommand named '${commandName}'\n\n${help({
335
- path: helpRoot,
336
- didYouMean: {
337
- input: commandName,
338
- choices: subcommands
339
- }
340
- })}`);
341
- return 1;
474
+ action({ path, args, help }) {
475
+ const self = path[path.length - 1];
476
+ const helpPath = path.slice(0, -1);
477
+ for (const name of args) {
478
+ const command = helpPath[helpPath.length - 1];
479
+ if (!command.subcommands) break;
480
+ const scope = OptionScope.at(helpPath);
481
+ const match = matchSubcommand(command.subcommands, name, scope.directives, false, scope.inheritedSubcommands);
482
+ if (match.command === self && !command.subcommands.includes(self)) break;
483
+ if (match.command) {
484
+ helpPath.push(match.command);
485
+ continue;
342
486
  }
343
- printActionError(`There is no subcommand named '${commandName}'\n\n${help({
344
- description: false,
345
- path: helpRoot
346
- })}`);
487
+ if (!command.requiresSubcommand && getMaxArgs(command.args) > 0) break;
488
+ const { message, didYouMean } = match.reason();
489
+ printUsageError(helpPath, message, didYouMean);
347
490
  return 1;
348
491
  }
349
- print(help({ path: [...helpRoot, command] }));
492
+ print(help({ path: helpPath }));
493
+ return 0;
350
494
  }
351
495
  };
352
496
  /** A persistent help option that prints help for the selected command. */
@@ -362,46 +506,60 @@ const help = {
362
506
  function getSubcommandPathString(commands) {
363
507
  return commands.map((command) => getLongestString(makeArray1(command.name))).join(" ");
364
508
  }
365
- function nonNullable(value) {
366
- return value !== null && value !== void 0;
367
- }
368
509
  function getSections(commands) {
369
510
  const command = commands[commands.length - 1];
511
+ const scope = OptionScope.at(commands);
512
+ const show = ({ option, names }) => optionToString(option, scope.forms(option, names));
370
513
  const description = command.description;
371
514
  const defaultDesc = "No description";
372
- const subcommands = command.subcommands ? command.subcommands.filter((subcommand) => !subcommand.hidden).map((subcommand) => [makeArray1(subcommand.name).sort((a, b) => a.length - b.length).join(", "), subcommand.description ?? defaultDesc]) : [];
373
- let persistentOptionObjects = [];
374
- let optionObjects = [];
375
- let persistentOptions = [];
376
- let options = [];
515
+ const subcommands = visibleSubcommands(command.subcommands).map((subcommand) => [makeArray1(subcommand.name).sort((a, b) => a.length - b.length).join(", "), subcommand.description ?? defaultDesc]);
516
+ const visible = scope.visible();
517
+ let optionObjects = visible.local;
518
+ let persistentOptionObjects = visible.persistent;
377
519
  if (commands.length === 1 && subcommands.length === 0) {
378
- optionObjects = command.options ? command.options.filter((option) => !option.hidden) : [];
379
- options = optionObjects.map((option) => [optionToString(option), option.description ?? defaultDesc]);
380
- } else {
381
- persistentOptionObjects = commands.map((command) => command.options).filter(nonNullable).flat().filter((option) => option.isPersistent && !option.hidden);
382
- persistentOptions = persistentOptionObjects.map((option) => [optionToString(option), option.description ?? defaultDesc]);
383
- optionObjects = (command.options ?? []).filter((option) => !option.hidden && !option.isPersistent);
384
- options = optionObjects.map((option) => [optionToString(option), option.description ?? defaultDesc]);
520
+ const byOption = new Map([...optionObjects, ...persistentOptionObjects].map((v) => [v.option, v]));
521
+ optionObjects = (command.options ?? []).flatMap((option) => byOption.get(option) ?? []);
522
+ persistentOptionObjects = [];
385
523
  }
524
+ const row = (visible) => {
525
+ const defaults = [];
526
+ addDefaults(defaults, asArray(visible.option.args));
527
+ return [show(visible), withDefaults(visible.option.description, defaults, defaultDesc)];
528
+ };
529
+ const options = optionObjects.map(row);
530
+ const persistentOptions = persistentOptionObjects.map(row);
531
+ const usage = (() => {
532
+ const parts = [getSubcommandPathString(commands)];
533
+ for (const visible of [...optionObjects, ...persistentOptionObjects]) if (visible.option.isRequired) parts.push(show(visible));
534
+ if (options.length || persistentOptions.length) parts.push("[flags]");
535
+ if (command.requiresSubcommand) parts.push("<command>");
536
+ else {
537
+ const args = makeArray(command.args);
538
+ if (args.length > 0) parts.push(summarizeArguments(args));
539
+ }
540
+ return parts.join(" ");
541
+ })();
542
+ const positionals = command.requiresSubcommand ? [] : asArray(command.args);
386
543
  return {
387
- usage: (() => {
388
- const parts = [getSubcommandPathString(commands)];
389
- const required = [...optionObjects.filter((option) => option.isRequired), ...persistentOptionObjects.filter((option) => option.isRequired)];
390
- for (const option of required) parts.push(optionToString(option));
391
- if (options.length || persistentOptions.length) parts.push("[flags]");
392
- if (command.requiresSubcommand) parts.push("<command>");
393
- else {
394
- const args = makeArray(command.args);
395
- if (args.length > 0) parts.push(summarizeArguments(args));
396
- }
397
- return parts.join(" ");
398
- })(),
544
+ usage,
399
545
  description,
546
+ args: positionals.some((arg) => arg.description !== void 0 || arg.default !== void 0) ? positionals.map((arg) => [(arg.name || "argument") + (arg.isVariadic ? "..." : ""), withDefaults(arg.description, arg.default === void 0 ? [] : [arg.default], "")]) : [],
400
547
  persistentOptions,
401
548
  options,
402
549
  subcommands
403
550
  };
404
551
  }
552
+ /**
553
+ * A description ending with the defaults the parser uses. They go on a line of
554
+ * their own when the last line would be too long for the 68 characters the
555
+ * spec checks allow.
556
+ */
557
+ function withDefaults(description, defaults, defaultDesc) {
558
+ if (defaults.length === 0) return description ?? defaultDesc;
559
+ const shown = `(default: ${defaults.join(" ")})`;
560
+ if (description === void 0) return shown;
561
+ return `${description}${description.slice(description.lastIndexOf("\n") + 1).length + 1 + shown.length <= 68 ? " " : "\n"}${shown}`;
562
+ }
405
563
  function formatHelpSections(sections) {
406
564
  const parts = [];
407
565
  const indent = " ";
@@ -410,17 +568,26 @@ function formatHelpSections(sections) {
410
568
  if (sections.usage) parts.push(`Usage:\n${indent}${sections.usage}`);
411
569
  const longestOptionName = Math.max(0, ...sections.options.map(([name]) => name.length), ...sections.persistentOptions.map(([name]) => name.length));
412
570
  const minSpacingBetweenNameAndDesc = 2;
413
- const formatOptions = (options) => {
571
+ const formatRows = (rows, width) => {
414
572
  const lines = [];
415
- const newline = indent + " ".repeat(longestOptionName + minSpacingBetweenNameAndDesc);
416
- for (const [name, desc] of options) {
417
- const spaces = " ".repeat(longestOptionName - name.length + minSpacingBetweenNameAndDesc);
573
+ const newline = indent + " ".repeat(width + minSpacingBetweenNameAndDesc);
574
+ for (const [name, desc] of rows) {
575
+ if (desc === "") {
576
+ lines.push(`${indent}${name}`);
577
+ continue;
578
+ }
579
+ const spaces = " ".repeat(width - name.length + minSpacingBetweenNameAndDesc);
418
580
  const [head, ...tail] = desc.split("\n");
419
581
  const description = [head, ...tail.map((line) => newline + line)].join("\n");
420
582
  lines.push(`${indent}${name}${spaces}${description}`);
421
583
  }
422
584
  return lines;
423
585
  };
586
+ const formatOptions = (options) => formatRows(options, longestOptionName);
587
+ if (sections.args.length > 0) {
588
+ const longestArgName = Math.max(...sections.args.map(([name]) => name.length));
589
+ parts.push(["Arguments:", ...formatRows(sections.args, longestArgName)].join("\n"));
590
+ }
424
591
  if (sections.subcommands.length > 0) {
425
592
  const lines = ["Commands:"];
426
593
  const longestName = Math.max(...sections.subcommands.map(([name]) => name.length));
@@ -451,16 +618,25 @@ function getLongestString(strings) {
451
618
  }
452
619
  return longest;
453
620
  }
454
- function optionToString(option) {
455
- const name = makeArray1(option.name).sort((a, b) => a.length - b.length).join(", ");
621
+ /**
622
+ * Show an option's names, each written with its arguments the way `forms`
623
+ * says. Names written the same way share one copy of the arguments.
624
+ */
625
+ function optionToString(option, forms) {
456
626
  const optionArgs = makeArray(option.args);
457
- if (optionArgs.length === 0) return name;
458
- if (option.requiresSeparator) {
459
- const separator = option.requiresSeparator === true ? "=" : option.requiresSeparator;
460
- if (optionArgs[0].isOptional) return `${name}[${separator}${optionArgs[0].name || "argument"}]`;
461
- return `${name}${separator}<${optionArgs[0].name || "argument"}>`;
462
- }
463
- return `${name} ${summarizeArguments(optionArgs)}`;
627
+ const sorted = [...forms].sort((a, b) => a.name.length - b.name.length);
628
+ return [...new Set(sorted.map((form) => form.join))].map((join) => {
629
+ return sorted.filter((form) => form.join === join).map((form) => form.name).join(", ") + argumentsAfterName(optionArgs, join);
630
+ }).join(", ");
631
+ }
632
+ /** The text after an option's name that shows its arguments, joined to the name by `join`. */
633
+ function argumentsAfterName(args, join) {
634
+ if (args.length === 0) return "";
635
+ if (join === null) return ` ${summarizeArguments(args)}`;
636
+ const [first, ...rest] = args;
637
+ const name = first.name || "argument";
638
+ const joined = (first.isOptional ? `[${join}${name}]` : `${join}<${name}>`) + (first.isVariadic ? "..." : "");
639
+ return rest.length > 0 ? `${joined} ${summarizeArguments(rest)}` : joined;
464
640
  }
465
641
  function summarizeArguments(args) {
466
642
  return args.map((arg) => argToString(arg)).join(" ");
@@ -477,358 +653,602 @@ function argToString(arg) {
477
653
  if (arg.isVariadic) suffix = "...";
478
654
  return `${lhs}${name}${rhs}${suffix}`;
479
655
  }
656
+ // src/errors.ts
657
+ const repr = JSON.stringify;
658
+ /**
659
+ * A command line the parser can't accept. `run` prints the message with the
660
+ * help for the command at `context.path`, and suggests the closest of
661
+ * `didYouMean.choices` if there is one.
662
+ */
663
+ var ParseError = class extends Error {
664
+ context;
665
+ didYouMean;
666
+ constructor(context, message, didYouMean) {
667
+ super(message);
668
+ this.context = context;
669
+ this.didYouMean = didYouMean;
670
+ this.name = this.constructor.name;
671
+ }
672
+ };
673
+ var UnknownOption = class extends ParseError {
674
+ option;
675
+ constructor(option, validOptions, context) {
676
+ const msg = `${repr(option)} looks like a flag, but is unknown in this context`;
677
+ super(context, msg, {
678
+ input: option,
679
+ choices: validOptions
680
+ });
681
+ this.option = option;
682
+ }
683
+ };
684
+ var InvalidOptionArg = class extends ParseError {
685
+ option;
686
+ constructor(option, context) {
687
+ super(context, `Option ${option} doesn't take arguments`);
688
+ this.option = option;
689
+ }
690
+ };
691
+ var TooManyArguments = class extends ParseError {
692
+ constructor(value, context) {
693
+ super(context, `Too many arguments, ${repr(value)} was unexpected`);
694
+ }
695
+ };
696
+ var TooFewOptionArguments = class extends ParseError {
697
+ constructor(option, min, max, context) {
698
+ const msg = min === max ? `Option ${option} needs ${min} argument${min === 1 ? "" : "s"}` : !Number.isFinite(max) ? `Option ${option} needs at least ${min} argument${min === 1 ? "" : "s"}` : `Option ${option} needs between ${min} and ${max} arguments`;
699
+ super(context, msg);
700
+ }
701
+ };
702
+ var TooFewArguments = class extends ParseError {
703
+ constructor(min, max, context) {
704
+ const msg = min === max ? `Expected ${min} argument${min === 1 ? "" : "s"}` : !Number.isFinite(max) ? `Expected at least ${min} argument${min === 1 ? "" : "s"}` : `Expected between ${min} and ${max} arguments`;
705
+ super(context, msg);
706
+ }
707
+ };
708
+ var MissingRequiredOption = class extends ParseError {
709
+ constructor(name, context) {
710
+ const msg = `Option ${name} is required, but wasn't found`;
711
+ super(context, msg);
712
+ }
713
+ };
480
714
  // src/parse.ts
481
- function hasName(names, name) {
482
- return typeof names === "string" ? names === name : names.includes(name);
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 "";
483
719
  }
484
720
  /**
485
- * Find the option `name` refers to. Local options win over persistent ones, and
486
- * within each list a later definition wins over an earlier one.
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.
487
725
  */
488
- function findOption(local, persistent, name) {
489
- for (let i = local.length - 1; i >= 0; i--) if (hasName(local[i].name, name)) return local[i];
490
- for (let i = persistent.length - 1; i >= 0; i--) if (hasName(persistent[i].name, name)) return persistent[i];
491
- return null;
492
- }
493
- /** Every name that's valid as an option here, for "did you mean" suggestions. */
494
- function optionNames(local, persistent) {
495
- const names = [];
496
- for (const options of [local, persistent]) {
497
- const seen = /* @__PURE__ */ new Set();
498
- for (const option of options) for (const name of asArray(option.name)) if (!seen.has(name)) {
499
- seen.add(name);
500
- names.push(name);
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;
501
735
  }
502
736
  }
503
- return names;
737
+ return sep ? {
738
+ index,
739
+ sep
740
+ } : null;
504
741
  }
505
742
  /**
506
- * Read a command line against a spec in one pass.
507
- *
508
- * This is the only code that decides what each input token is. With `strict`,
509
- * it throws on the first problem, which is what {@link parse} needs. Without
510
- * it, it never throws: it skips whatever is invalid and keeps going, so
511
- * completion can still read a line that's wrong at the moment.
512
- *
513
- * This runs once per process, so it's written for a cold start: one loop, few
514
- * helper functions and plain arrays, because every extra function is compiled
515
- * the first time it runs.
743
+ * Throw if the options used break a `dependsOn` or `exclusiveOn` rule, or if
744
+ * any of `required` is missing.
516
745
  */
517
- function scan(input, spec, strict) {
518
- const path = [spec];
519
- const actions = [];
520
- const optionActions = [];
521
- const args = [];
522
- const options = /* @__PURE__ */ new Map();
523
- const seenOptions = [];
524
- let argSeparatorIndex = -1;
525
- let local = [];
526
- const persistent = [];
527
- const localRequired = [];
528
- const persistentRequired = [];
529
- let subcommands;
530
- let commandArgsMin = 0;
531
- let commandArgsMax = 0;
532
- let separators = ["="];
533
- let posixCompliantOptions = true;
534
- let subcommandsMatchUniquePrefix = false;
535
- let optionsMustPrecedeArguments = false;
536
- let hasFoundArg = false;
537
- let openOption = null;
538
- let openOptionName = "";
539
- let valueCount = 0;
540
- let valueMin = 0;
541
- let separateValueMax = 0;
542
- let values = null;
543
- let valuesStart = 0;
544
- let valuesMin = 0;
545
- let valuesMax = 0;
546
- let requiredSeparator = false;
547
- const repeatCounts = /* @__PURE__ */ new Map();
548
- const ctx = () => ({ path });
549
- const enterCommand = (command) => {
550
- local = [];
551
- localRequired.length = 0;
552
- if (command.options) for (const option of command.options) if (option.isPersistent) {
553
- persistent.push(option);
554
- if (option.isRequired) persistentRequired.push(option);
555
- } else {
556
- local.push(option);
557
- if (option.isRequired) localRequired.push(option);
558
- }
559
- subcommands = command.subcommands;
560
- commandArgsMin = getMinArgs(command.args);
561
- commandArgsMax = getMaxArgs(command.args);
562
- if (command.action) actions.push(command.action);
563
- else if (command.requiresSubcommand) actions.push(usage);
564
- const directives = command.parserDirectives;
565
- if (directives) {
566
- if (directives.optionArgSeparators) separators = asArray(directives.optionArgSeparators);
567
- if (typeof directives.subcommandsMatchUniquePrefix === "boolean") subcommandsMatchUniquePrefix = directives.subcommandsMatchUniquePrefix;
568
- if (typeof directives.optionsMustPrecedeArguments === "boolean") optionsMustPrecedeArguments = directives.optionsMustPrecedeArguments;
569
- if (typeof directives.flagsArePosixNoncompliant === "boolean") posixCompliantOptions = !directives.flagsArePosixNoncompliant;
746
+ function checkOptionRules(options, seenOptions, required, context) {
747
+ for (const option of seenOptions) {
748
+ if (!option.dependsOn) continue;
749
+ for (const name of option.dependsOn) if (!options.has(name)) throw new ParseError(context, `${asArray(option.name).join("/")} requires ${name}, add it to fix this error`);
750
+ }
751
+ for (const option of seenOptions) {
752
+ if (!option.exclusiveOn) continue;
753
+ for (const name of option.exclusiveOn) if (options.has(name)) throw new ParseError(context, `${asArray(option.name).join("/")} can't be used together with ${name}`);
754
+ }
755
+ for (const option of required) for (const name of asArray(option.name)) if (!options.has(name)) throw new MissingRequiredOption(name, context);
756
+ }
757
+ /**
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.
764
+ */
765
+ var Scanner = class {
766
+ strict;
767
+ path;
768
+ optionAction = void 0;
769
+ args = [];
770
+ options = /* @__PURE__ */ new Map();
771
+ seenOptions = [];
772
+ argSeparatorIndex = -1;
773
+ scope = new OptionScope();
774
+ subcommands = void 0;
775
+ commandArgsMin = 0;
776
+ commandArgsMax = 0;
777
+ directives = this.scope.directives;
778
+ hasFoundArg = false;
779
+ openOption = null;
780
+ openOptionName = "";
781
+ valueCount = 0;
782
+ valueMin = 0;
783
+ separateValueMax = 0;
784
+ values = null;
785
+ valuesStart = 0;
786
+ valuesMin = 0;
787
+ valuesMax = 0;
788
+ valueArgs = [];
789
+ requiredSeparator = false;
790
+ tooManyArgs = null;
791
+ repeatedUses = /* @__PURE__ */ new Map();
792
+ attached = null;
793
+ constructor(spec, strict) {
794
+ this.strict = strict;
795
+ this.path = [spec];
796
+ this.enterCommand(spec);
797
+ }
798
+ ctx() {
799
+ return { path: this.path };
800
+ }
801
+ /** Whether the open option can take the next token as a value. */
802
+ get hasRoomForValue() {
803
+ return this.valueCount < this.separateValueMax;
804
+ }
805
+ enterCommand(command) {
806
+ this.scope.enter(command);
807
+ this.subcommands = command.subcommands;
808
+ this.commandArgsMin = getMinArgs(command.args);
809
+ this.commandArgsMax = getMaxArgs(command.args);
810
+ }
811
+ findOption(name) {
812
+ return this.scope.resolve(name);
813
+ }
814
+ unknownOption(option) {
815
+ return new UnknownOption(option, this.scope.suggestionNames(), this.ctx());
816
+ }
817
+ endOption() {
818
+ const values = this.values;
819
+ if (this.strict && values) {
820
+ const given = values.length - this.valuesStart;
821
+ if (given < this.valuesMin) throw new TooFewOptionArguments(this.openOptionName, this.valuesMin, this.valuesMax, this.ctx());
822
+ addDefaults(values, this.valueArgs, given);
570
823
  }
571
- };
572
- const endOption = () => {
573
- if (strict && values && values.length - valuesStart < valuesMin) throw new TooFewOptionArguments(valuesMin, valuesMax, ctx());
574
- values = null;
575
- };
576
- const useOption = (option, name) => {
577
- endOption();
578
- openOption = option;
579
- openOptionName = name;
580
- valueCount = 0;
581
- valueMin = getMinArgs(option.args);
582
- separateValueMax = option.requiresSeparator ? 0 : getMaxArgs(option.args);
583
- seenOptions.push(option);
584
- if (option.action) optionActions.push(option.action);
824
+ this.values = null;
825
+ }
826
+ /** End the open option, so no later token can be its value. */
827
+ closeOption() {
828
+ this.endOption();
829
+ this.openOption = null;
830
+ this.valueCount = this.valueMin = this.separateValueMax = 0;
831
+ }
832
+ useOption(option, name) {
833
+ this.endOption();
834
+ this.openOption = option;
835
+ this.openOptionName = name;
836
+ this.valueCount = 0;
837
+ this.valueMin = getMinArgs(option.args);
838
+ this.separateValueMax = option.requiresSeparator ? 0 : getMaxArgs(option.args);
839
+ this.seenOptions.push(option);
840
+ if (option.action) this.optionAction = option.action;
585
841
  const names = asArray(option.name);
842
+ const options = this.options;
586
843
  let optionValues;
587
844
  if (option.isRepeatable) {
588
845
  const maxRepeat = option.isRepeatable === true ? Infinity : option.isRepeatable;
589
- const existing = options.get(names[0]);
590
- let isSharedCount = true;
591
- for (const other of names) if (options.get(other) !== existing) isSharedCount = false;
592
- let uses = isSharedCount ? existing : void 0;
846
+ const key = optionKey(option);
847
+ let uses = this.repeatedUses.get(key);
593
848
  if (!uses) {
594
- uses = [];
595
- for (const other of names) options.set(other, uses);
849
+ uses = {
850
+ values: [],
851
+ count: 0
852
+ };
853
+ this.repeatedUses.set(key, uses);
596
854
  }
597
- const count = repeatCounts.get(uses) ?? uses.length;
598
- if (strict && count >= maxRepeat) throw new ParseError(ctx(), "Too many repetitions");
599
- repeatCounts.set(uses, count + 1);
855
+ if (this.strict && uses.count >= maxRepeat) throw new ParseError(this.ctx(), "Too many repetitions");
856
+ uses.count += 1;
857
+ for (const other of names) options.set(other, uses.values);
600
858
  if (!option.args) {
601
- uses.push("");
859
+ uses.values.push("");
602
860
  return;
603
861
  }
604
- optionValues = uses;
862
+ optionValues = uses.values;
605
863
  } else {
606
864
  optionValues = [];
607
865
  for (const other of names) options.set(other, optionValues);
608
866
  }
609
- valuesMin = valueMin;
610
- valuesMax = getMaxArgs(option.args);
611
- requiredSeparator = option.requiresSeparator ?? false;
612
- if (valuesMax > 0) {
613
- values = optionValues;
614
- valuesStart = optionValues.length;
867
+ this.valuesMin = this.valueMin;
868
+ this.valuesMax = getMaxArgs(option.args);
869
+ this.requiredSeparator = option.requiresSeparator ?? false;
870
+ if (this.valuesMax > 0) {
871
+ this.values = optionValues;
872
+ this.valuesStart = optionValues.length;
873
+ this.valueArgs = asArray(option.args);
615
874
  }
616
- };
617
- const addValue = (index, start, end, value, separator) => {
618
- valueCount += 1;
619
- if (!values) {
620
- if (strict) throw new InvalidOptionArg({
621
- option: openOptionName,
622
- arg: value,
623
- index,
624
- start,
625
- end
626
- }, ctx());
627
- return;
875
+ }
876
+ /**
877
+ * Add a value to the open option. Returns whether it's a value the option
878
+ * accepts. A lenient read keeps going either way.
879
+ */
880
+ addValue(value, separator) {
881
+ this.valueCount += 1;
882
+ if (!this.values) {
883
+ if (this.strict) throw new InvalidOptionArg(this.openOptionName, this.ctx());
884
+ return false;
628
885
  }
629
- if (strict && typeof requiredSeparator === "string" && separator !== requiredSeparator) throw new ParseError(ctx(), `Incorrect separator, use '${requiredSeparator}' instead of '${separator}'`);
630
- values.push(value);
631
- };
632
- const addArg = (index, value) => {
633
- endOption();
634
- if (strict && !path[path.length - 1].requiresSubcommand && args.length >= commandArgsMax) throw new TooManyArguments({
635
- value,
636
- index
637
- }, ctx());
638
- args.push(value);
639
- };
640
- enterCommand(spec);
641
- tokens: for (let index = 0; index < input.length; index++) {
642
- const token = input[index];
643
- if (token === "--") {
644
- endOption();
645
- if (strict && args.length >= commandArgsMax) throw new ParseError(ctx(), "Unexpected argument '--', did you mean to use an option instead?");
646
- argSeparatorIndex = args.length;
647
- openOption = null;
648
- valueCount = valueMin = separateValueMax = 0;
649
- for (index += 1; index < input.length; index++) addArg(index, input[index]);
650
- break tokens;
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;
651
891
  }
652
- const hasRoomForValue = valueCount < separateValueMax;
653
- if (!hasFoundArg && !(valueCount < valueMin || hasRoomForValue && separateValueMax === Infinity) && subcommands) {
654
- const allowPrefix = subcommandsMatchUniquePrefix && !hasRoomForValue && token !== "";
655
- let command = null;
656
- let prefixCount = 0;
657
- commands: for (const candidate of subcommands) {
658
- const names = candidate.name;
659
- if (typeof names === "string") {
660
- if (names === token) {
661
- command = candidate;
662
- prefixCount = -1;
663
- break commands;
664
- }
665
- if (allowPrefix && names.startsWith(token)) {
666
- command = candidate;
667
- prefixCount += 1;
668
- }
669
- continue commands;
670
- }
671
- let prefixMatched = false;
672
- for (const name of names) {
673
- if (name === token) {
674
- command = candidate;
675
- prefixCount = -1;
676
- break commands;
677
- }
678
- if (allowPrefix && !prefixMatched && name.startsWith(token)) {
679
- prefixMatched = true;
680
- command = candidate;
681
- prefixCount += 1;
682
- }
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
+ }
899
+ this.values.push(value);
900
+ return accepted;
901
+ }
902
+ /**
903
+ * Add a value attached to the option's own token, the token at `index`,
904
+ * starting at `start`. It's the option's only value, so no later token can be
905
+ * one.
906
+ */
907
+ addAttachedValue(index, start, value, separator) {
908
+ if (this.addValue(value, separator)) this.attached = {
909
+ option: this.openOption,
910
+ index,
911
+ start
912
+ };
913
+ this.valueMin = this.separateValueMax = 0;
914
+ }
915
+ /**
916
+ * Whether the command already has all the arguments it takes. A command that
917
+ * requires a subcommand takes none.
918
+ */
919
+ get argsAreFull() {
920
+ const path = this.path;
921
+ return path[path.length - 1].requiresSubcommand === true || this.args.length >= this.commandArgsMax;
922
+ }
923
+ addArg(value) {
924
+ this.endOption();
925
+ if (this.strict && this.argsAreFull) this.tooManyArgs ??= new TooManyArguments(value, this.ctx());
926
+ this.args.push(value);
927
+ }
928
+ /** Read `--` and every token after it as arguments. */
929
+ argSeparator(input, index) {
930
+ this.closeOption();
931
+ const command = this.path[this.path.length - 1];
932
+ if (this.strict && this.argsAreFull && !command.requiresSubcommand) this.tooManyArgs ??= new ParseError(this.ctx(), "Unexpected argument '--', did you mean to use an option instead?");
933
+ this.argSeparatorIndex = this.args.length;
934
+ for (index += 1; index < input.length; index++) this.addArg(input[index]);
935
+ }
936
+ /**
937
+ * Whether the next token can name a subcommand, its own or an inherited
938
+ * one. It can't once an argument is found, or while the open option needs
939
+ * the next token as its value. A required or variadic option value takes
940
+ * priority over a subcommand.
941
+ */
942
+ get canMatchSubcommand() {
943
+ if (this.hasFoundArg) return false;
944
+ if (!this.subcommands && this.scope.inheritedSubcommands.length === 0) return false;
945
+ return !(this.valueCount < this.valueMin || this.hasRoomForValue && this.separateValueMax === Infinity);
946
+ }
947
+ /**
948
+ * The subcommand the next token enters if it's `token`, or why it enters
949
+ * none. Null if the next token can't be a subcommand at all.
950
+ */
951
+ subcommandFor(token) {
952
+ if (!this.canMatchSubcommand) return null;
953
+ return matchSubcommand(this.subcommands ?? [], token, this.directives, this.hasRoomForValue, this.scope.inheritedSubcommands);
954
+ }
955
+ enterSubcommand(command) {
956
+ this.closeOption();
957
+ this.path.push(command);
958
+ this.enterCommand(command);
959
+ }
960
+ /** Read a token like `-abc`, `+x` or `-ovalue`. */
961
+ shortOptions(token, index) {
962
+ const leadingChar = token[0];
963
+ const dashOption = this.findOption(leadingChar);
964
+ if (dashOption?.args && !this.findOption(token.slice(0, 2))) {
965
+ this.useOption(dashOption, leadingChar);
966
+ this.addAttachedValue(index, 1, token.slice(1), "");
967
+ return;
968
+ }
969
+ for (let char = 1; char < token.length; char++) {
970
+ const optionName = leadingChar + token[char];
971
+ const option = this.findOption(optionName);
972
+ if (!option) {
973
+ if (char === 1 && this.hasRoomForValue) {
974
+ this.addValue(token, "");
975
+ return;
683
976
  }
977
+ if (this.strict) throw this.unknownOption(optionName);
978
+ continue;
684
979
  }
685
- if (command && (prefixCount === -1 || prefixCount === 1)) {
686
- endOption();
687
- openOption = null;
688
- valueCount = valueMin = separateValueMax = 0;
689
- path.push(command);
690
- enterCommand(command);
691
- continue tokens;
980
+ this.useOption(option, optionName);
981
+ if (char === token.length - 1) return;
982
+ const sep = leadingSeparator(token.slice(char + 1), this.directives.optionArgSeparators);
983
+ if (option.args || sep && !this.findOption(leadingChar + token[char + 1])) {
984
+ const argStart = char + sep.length + 1;
985
+ this.addAttachedValue(index, argStart, token.slice(argStart), sep);
986
+ return;
692
987
  }
693
988
  }
694
- if (!(optionsMustPrecedeArguments && hasFoundArg)) {
695
- const first = token.charCodeAt(0);
696
- const isDoubleDash = first === 45 && token.charCodeAt(1) === 45;
697
- if (posixCompliantOptions && token.length > 1 && (first === 45 || first === 43) && !isDoubleDash) {
698
- const leadingChar = token[0];
699
- const dashOption = findOption(local, persistent, leadingChar);
700
- if (dashOption?.args && !findOption(local, persistent, token.slice(0, 2))) {
701
- useOption(dashOption, leadingChar);
702
- addValue(index, 1, token.length, token.slice(1), "");
703
- continue tokens;
704
- }
705
- for (let char = 1; char < token.length; char++) {
706
- const optionName = leadingChar + token[char];
707
- const option = findOption(local, persistent, optionName);
708
- if (!option) {
709
- if (char === 1 && valueCount < separateValueMax) {
710
- addValue(index, 0, token.length, token, "");
711
- continue tokens;
712
- }
713
- if (strict) throw new UnknownOption({
714
- option: optionName,
715
- validOptions: optionNames(local, persistent),
716
- index,
717
- start: char,
718
- end: char + 1
719
- }, ctx());
720
- continue;
721
- }
722
- useOption(option, optionName);
723
- if (option.args && char < token.length - 1) {
724
- const remainingChars = token.slice(char + 1);
725
- let sep = "";
726
- for (const candidate of separators) if (remainingChars.startsWith(candidate)) {
727
- sep = candidate;
728
- break;
729
- }
730
- const argStart = char + sep.length + 1;
731
- addValue(index, argStart, token.length, token.slice(argStart), sep);
732
- break;
733
- }
734
- }
735
- continue tokens;
989
+ }
990
+ /**
991
+ * Read a token like `--name` or `--name=value`, or any token when options
992
+ * aren't POSIX compliant. Returns false if the token should be an argument.
993
+ */
994
+ longOption(token, index) {
995
+ const found = firstSeparator(token, this.directives.optionArgSeparators);
996
+ const optionName = found ? token.slice(0, found.index) : token;
997
+ const option = this.findOption(optionName);
998
+ if (option) {
999
+ this.useOption(option, optionName);
1000
+ if (found) {
1001
+ const valueStart = found.index + found.sep.length;
1002
+ this.addAttachedValue(index, valueStart, token.slice(valueStart), found.sep);
736
1003
  }
737
- if (!posixCompliantOptions || token.length > 2 && isDoubleDash) {
738
- let foundSep = "";
739
- let foundSepIndex = -1;
740
- for (const sep of separators) {
741
- const at = token.indexOf(sep);
742
- if (at === -1) continue;
743
- if (foundSepIndex === -1 || at < foundSepIndex || at === foundSepIndex && sep.length > foundSep.length) {
744
- foundSep = sep;
745
- foundSepIndex = at;
746
- }
747
- }
748
- const optionName = foundSep ? token.slice(0, foundSepIndex) : token;
749
- const option = findOption(local, persistent, optionName);
750
- if (option) {
751
- useOption(option, optionName);
752
- if (foundSep) {
753
- const valueStart = foundSepIndex + foundSep.length;
754
- addValue(index, valueStart, token.length, token.slice(valueStart), foundSep);
755
- }
756
- continue tokens;
757
- }
758
- if (posixCompliantOptions) {
759
- if (valueCount < separateValueMax) {
760
- addValue(index, 0, token.length, token, "");
761
- continue tokens;
762
- }
763
- if (strict) throw new UnknownOption({
764
- option: optionName,
765
- validOptions: optionNames(local, persistent),
766
- index,
767
- start: 0,
768
- end: optionName.length
769
- }, ctx());
770
- continue tokens;
771
- }
1004
+ return true;
1005
+ }
1006
+ if (this.directives.flagsArePosixNoncompliant) return false;
1007
+ if (this.hasRoomForValue) {
1008
+ this.addValue(token, "");
1009
+ return true;
1010
+ }
1011
+ if (this.strict) throw this.unknownOption(optionName);
1012
+ return true;
1013
+ }
1014
+ /**
1015
+ * Read a token that isn't `--`, a subcommand or an option. It's a value of
1016
+ * the open option if that has room, and otherwise an argument, which stops
1017
+ * later tokens from being subcommands. `unmatched` is why the token isn't a
1018
+ * subcommand, if it could have been one.
1019
+ */
1020
+ valueOrArg(token, unmatched) {
1021
+ if (this.hasRoomForValue) {
1022
+ this.addValue(token, "");
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) {
1028
+ const { message, didYouMean } = unmatched.reason();
1029
+ this.tooManyArgs ??= new ParseError(this.ctx(), message, didYouMean);
772
1030
  }
773
1031
  }
774
- if (valueCount < separateValueMax) {
775
- addValue(index, 0, token.length, token, "");
776
- continue tokens;
1032
+ this.hasFoundArg = true;
1033
+ this.addArg(token);
1034
+ }
1035
+ /**
1036
+ * Throw if the finished command line is missing anything. Returns the action
1037
+ * to run.
1038
+ */
1039
+ finish() {
1040
+ const command = this.path[this.path.length - 1];
1041
+ const showsUsage = command.requiresSubcommand && !command.action && this.args.length === 0;
1042
+ const skipsChecks = this.optionAction !== void 0 || showsUsage;
1043
+ if (!skipsChecks && this.tooManyArgs) throw this.tooManyArgs;
1044
+ if (!skipsChecks && this.args.length < this.commandArgsMin) throw new TooFewArguments(this.commandArgsMin, this.commandArgsMax, this.ctx());
1045
+ this.endOption();
1046
+ if (!skipsChecks) checkOptionRules(this.options, this.seenOptions, this.scope.required(), this.ctx());
1047
+ if (!command.requiresSubcommand && this.args.length >= this.commandArgsMin) addDefaults(this.args, asArray(command.args), this.args.length);
1048
+ return this.optionAction ?? command.action ?? usage;
1049
+ }
1050
+ /** See {@link ParseResult.defaults}. */
1051
+ defaults(name) {
1052
+ if (this.options.has(name)) return;
1053
+ const args = asArray(this.scope.resolve(name)?.args);
1054
+ if (args.length === 0 || args[0].isOptional) return;
1055
+ const values = [];
1056
+ addDefaults(values, args);
1057
+ return values.length > 0 ? values : void 0;
1058
+ }
1059
+ /** Whether using `option` next would go over its repeat limit or break an `exclusiveOn` rule. */
1060
+ conflicts(option) {
1061
+ const names = asArray(option.name);
1062
+ const options = this.options;
1063
+ if (typeof option.isRepeatable === "number") {
1064
+ if ((this.repeatedUses.get(optionKey(option))?.count ?? 0) >= option.isRepeatable) return true;
777
1065
  }
778
- hasFoundArg = true;
779
- addArg(index, token);
780
- }
781
- if (strict) {
782
- const hasOptionAction = optionActions.length > 0;
783
- if (!hasOptionAction && args.length < commandArgsMin) throw new TooFewArguments(commandArgsMin, commandArgsMax, ctx());
784
- endOption();
785
- for (const option of seenOptions) {
786
- if (!option.dependsOn) continue;
787
- for (const name of option.dependsOn) if (!options.has(name)) throw new ParseError(ctx(), `${asArray(option.name).join("/")} requires ${name}, add it to fix this error`);
1066
+ if (option.exclusiveOn?.some((name) => options.has(name))) return true;
1067
+ return this.seenOptions.some((seen) => seen.exclusiveOn?.some((name) => names.includes(name)));
1068
+ }
1069
+ /** The options the next token can use. See {@link NextToken.options}. */
1070
+ usableOptions() {
1071
+ const used = new Set(this.seenOptions.map(optionKey));
1072
+ const { local, persistent } = this.scope.live();
1073
+ const usable = [];
1074
+ for (const { option, names } of [...local, ...persistent]) {
1075
+ if (this.conflicts(option)) continue;
1076
+ const typeable = names.filter((name) => !this.subcommandFor(name)?.command);
1077
+ if (typeable.length === 0) continue;
1078
+ usable.push({
1079
+ option,
1080
+ forms: this.scope.forms(option, typeable),
1081
+ used: used.has(optionKey(option))
1082
+ });
788
1083
  }
789
- for (const option of seenOptions) {
790
- if (!option.exclusiveOn) continue;
791
- for (const name of option.exclusiveOn) if (options.has(name)) throw new ParseError(ctx(), `${asArray(option.name).join("/")} can't be used together with ${name}`);
1084
+ return usable;
1085
+ }
1086
+ /** What the next token can be. See {@link readNext}. */
1087
+ next() {
1088
+ const open = this.openOption && this.hasRoomForValue ? this.openOption : null;
1089
+ const command = this.path[this.path.length - 1];
1090
+ const afterSeparator = this.argSeparatorIndex !== -1;
1091
+ const optionsAllowed = !afterSeparator && this.optionsAllowed;
1092
+ return {
1093
+ path: this.path,
1094
+ directives: this.directives,
1095
+ optionsAllowed,
1096
+ subcommands: !afterSeparator && this.canMatchSubcommand ? withInherited(this.subcommands ?? [], this.scope.inheritedSubcommands) : [],
1097
+ options: optionsAllowed ? this.usableOptions() : [],
1098
+ value: open && {
1099
+ option: open,
1100
+ arg: argAt(asArray(open.args), this.valueCount),
1101
+ required: this.valueCount < this.valueMin
1102
+ },
1103
+ arg: open ? void 0 : argAt(asArray(command.args), this.args.length)
1104
+ };
1105
+ }
1106
+ /** Read the token at `index`, the next one, and say what it was read as. */
1107
+ readAndClassify(input, index) {
1108
+ if (this.argSeparatorIndex !== -1) return { kind: "argument" };
1109
+ const depth = this.path.length;
1110
+ const seen = this.seenOptions.length;
1111
+ const args = this.args.length;
1112
+ const values = this.valueCount;
1113
+ if (this.readAt(input, index)) return { kind: "separator" };
1114
+ if (this.path.length > depth) return {
1115
+ kind: "subcommand",
1116
+ command: this.path[depth]
1117
+ };
1118
+ if (this.seenOptions.length > seen) {
1119
+ const found = this.attachedAt(index);
1120
+ const attached = found && {
1121
+ option: found.option,
1122
+ start: found.start
1123
+ };
1124
+ return {
1125
+ kind: "options",
1126
+ options: this.seenOptions.slice(seen),
1127
+ attached
1128
+ };
792
1129
  }
793
- if (!hasOptionAction) {
794
- for (const required of [localRequired, persistentRequired]) for (const option of required) for (const name of asArray(option.name)) if (!options.has(name)) throw new MissingRequiredOption(name, ctx());
1130
+ if (this.args.length > args) return { kind: "argument" };
1131
+ if (this.valueCount > values) return {
1132
+ kind: "value",
1133
+ option: this.openOption
1134
+ };
1135
+ return { kind: "unknown" };
1136
+ }
1137
+ /** The value attached to the token at `index`, if it has one the option accepts. */
1138
+ attachedAt(index) {
1139
+ return this.attached?.index === index ? this.attached : null;
1140
+ }
1141
+ /** Whether the next token can be an option. Once options must stop, every token is an argument. */
1142
+ get optionsAllowed() {
1143
+ return !(this.directives.optionsMustPrecedeArguments && this.hasFoundArg);
1144
+ }
1145
+ /** Read every token of `input`. */
1146
+ read(input) {
1147
+ for (let index = 0; index < input.length; index++) if (this.readAt(input, index)) return;
1148
+ }
1149
+ /**
1150
+ * Read the token at `index`. Returns true if it was `--`, which reads every
1151
+ * token after it too.
1152
+ */
1153
+ readAt(input, index) {
1154
+ const token = input[index];
1155
+ const optionsAllowed = this.optionsAllowed;
1156
+ if (token === "--" && optionsAllowed) {
1157
+ this.argSeparator(input, index);
1158
+ return true;
1159
+ }
1160
+ const subcommand = this.subcommandFor(token);
1161
+ if (subcommand?.command) {
1162
+ this.enterSubcommand(subcommand.command);
1163
+ return false;
1164
+ }
1165
+ if (optionsAllowed) {
1166
+ const flagsArePosixNoncompliant = this.directives.flagsArePosixNoncompliant;
1167
+ const first = token.charCodeAt(0);
1168
+ const isDoubleDash = first === 45 && token.charCodeAt(1) === 45;
1169
+ if (!flagsArePosixNoncompliant && token.length > 1 && (first === 45 || first === 43) && !isDoubleDash) {
1170
+ this.shortOptions(token, index);
1171
+ return false;
1172
+ }
1173
+ if (flagsArePosixNoncompliant || token.length > 2 && isDoubleDash) {
1174
+ if (this.longOption(token, index)) return false;
1175
+ }
795
1176
  }
1177
+ this.valueOrArg(token, subcommand);
1178
+ return false;
1179
+ }
1180
+ };
1181
+ /** The argument at `index`, reusing a final variadic argument. */
1182
+ function argAt(args, index) {
1183
+ if (index < args.length) return args[index];
1184
+ const last = args[args.length - 1];
1185
+ return last?.isVariadic ? last : void 0;
1186
+ }
1187
+ /**
1188
+ * Work out what the token after `before` can be, and how the parser reads
1189
+ * `partial` if it's an option with a value attached. Reads leniently, so a
1190
+ * line that's wrong at the moment still has an answer. Completion is built on
1191
+ * this.
1192
+ */
1193
+ function readNext(spec, before, partial) {
1194
+ const s = new Scanner(spec, false);
1195
+ s.read(before);
1196
+ const next = s.next();
1197
+ let attached = null;
1198
+ if (partial !== "" && next.optionsAllowed) {
1199
+ const reading = readToken(spec, before, partial);
1200
+ const found = reading.kind === "options" ? reading.attached : null;
1201
+ const arg = found && asArray(found.option.args)[0];
1202
+ if (found && arg) attached = {
1203
+ option: found.option,
1204
+ arg,
1205
+ prefix: partial.slice(0, found.start),
1206
+ value: partial.slice(found.start)
1207
+ };
796
1208
  }
797
- const open = openOption;
798
1209
  return {
799
- path,
800
- actions,
801
- optionActions,
802
- args,
803
- argSeparatorIndex,
804
- options,
805
- seenOptions,
806
- localOptions: local,
807
- persistentOptions: persistent,
808
- openOption: open && valueCount < separateValueMax ? {
809
- option: open,
810
- valueCount
811
- } : null
1210
+ ...next,
1211
+ attached
812
1212
  };
813
1213
  }
814
1214
  /**
1215
+ * Say what the parser reads `token` as when it comes after `before`. Reads
1216
+ * leniently, like {@link readNext}.
1217
+ */
1218
+ function readToken(spec, before, token) {
1219
+ const s = new Scanner(spec, false);
1220
+ s.read(before);
1221
+ return s.readAndClassify([...before, token], before.length);
1222
+ }
1223
+ /**
815
1224
  * Parse the input tokens using the given CLI spec.
816
1225
  *
817
1226
  * This returns a `ParseResult`, which contains the array of
818
- * subcommands used, the provided options, and the arguments.
1227
+ * subcommands used, the provided options, the arguments, and the action to
1228
+ * run.
819
1229
  *
820
1230
  * If parsing fails, this function throws.
821
- *
822
- * This function is exposed to make it easier to build a custom runner.
823
1231
  */
824
1232
  function parse(input, spec) {
825
- return scan(input, spec, true);
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
+ };
826
1244
  }
827
1245
  // src/completion.ts
828
1246
  const SHELLS = [
829
1247
  "bash",
830
1248
  "zsh",
831
- "fish"
1249
+ "fish",
1250
+ "powershell",
1251
+ "nushell"
832
1252
  ];
833
1253
  function isShell(value) {
834
1254
  return SHELLS.includes(value);
@@ -844,73 +1264,30 @@ function shQuote(value) {
844
1264
  function fishQuote(value) {
845
1265
  return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
846
1266
  }
1267
+ /** Single-quote a string for PowerShell (a quote is written twice). */
1268
+ function psQuote(value) {
1269
+ return `'${value.replaceAll("'", "''")}'`;
1270
+ }
847
1271
  /**
848
- * Read the line typed so far with the parser in lenient mode, which skips
849
- * anything invalid instead of throwing.
1272
+ * Quote a string for Nushell. A single-quoted string has no escapes, so a name
1273
+ * with a quote in it is written as a double-quoted string instead.
850
1274
  */
851
- function readLine(spec, before) {
852
- const scanned = scan(before, spec, false);
853
- const path = scanned.path;
854
- const presentNames = /* @__PURE__ */ new Set();
855
- const countByOption = /* @__PURE__ */ new Map();
856
- for (const option of scanned.seenOptions) {
857
- for (const name of makeArray(option.name)) presentNames.add(name);
858
- countByOption.set(option, (countByOption.get(option) ?? 0) + 1);
859
- }
860
- const open = scanned.openOption;
861
- const pending = open ? {
862
- option: open.option,
863
- argIndex: open.valueCount
864
- } : null;
865
- const availableOptions = [];
866
- const seen = /* @__PURE__ */ new Set();
867
- for (const list of [scanned.localOptions, scanned.persistentOptions]) {
868
- const byName = /* @__PURE__ */ new Map();
869
- for (const option of list) for (const name of makeArray(option.name)) byName.set(name, option);
870
- for (const option of byName.values()) if (!seen.has(option)) {
871
- seen.add(option);
872
- availableOptions.push(option);
873
- }
874
- }
875
- return {
876
- path,
877
- current: path[path.length - 1],
878
- availableOptions,
879
- pending,
880
- positionalIndex: scanned.args.length,
881
- presentNames,
882
- countByOption,
883
- forceArgs: scanned.argSeparatorIndex !== -1,
884
- sawPositional: scanned.args.length > 0
885
- };
886
- }
887
- /** A directive's value in effect (the deepest command that sets it). */
888
- function directiveOnPath(path, key) {
889
- for (let i = path.length - 1; i >= 0; i--) {
890
- const value = path[i].parserDirectives?.[key];
891
- if (typeof value === "boolean") return value;
892
- }
893
- return false;
894
- }
895
- /** The option-arg separators in effect (the deepest command that sets them). */
896
- function separatorsOnPath(path) {
897
- let separators = ["="];
898
- for (const command of path) {
899
- const configured = command.parserDirectives?.optionArgSeparators;
900
- if (configured) separators = makeArray(configured);
901
- }
902
- return separators;
1275
+ function nuQuote(value) {
1276
+ if (!value.includes("'")) return `'${value}'`;
1277
+ return `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
903
1278
  }
904
1279
  /** Enumerate filesystem entries matching `partial` for a path template. */
905
1280
  async function enumeratePaths(partial, template) {
906
1281
  const { readdirSync } = await import("node:fs");
907
- const slash = partial.lastIndexOf("/");
1282
+ const isWindows = globalThis.process?.platform === "win32";
1283
+ const slash = Math.max(partial.lastIndexOf("/"), isWindows ? partial.lastIndexOf("\\") : -1);
908
1284
  const dir = slash >= 0 ? partial.slice(0, slash + 1) : "";
909
1285
  const base = slash >= 0 ? partial.slice(slash + 1) : partial;
1286
+ const separator = slash >= 0 ? partial[slash] : "/";
910
1287
  const foldersOnly = template === "folders";
911
1288
  let entries;
912
1289
  try {
913
- entries = readdirSync(dir || ".", { withFileTypes: true });
1290
+ entries = readdirSync((isWindows ? dir.replaceAll("\\", "/") : dir) || ".", { withFileTypes: true });
914
1291
  } catch {
915
1292
  return [];
916
1293
  }
@@ -919,7 +1296,7 @@ async function enumeratePaths(partial, template) {
919
1296
  if (!entry.name.startsWith(base)) continue;
920
1297
  const isDir = entry.isDirectory();
921
1298
  if (foldersOnly && !isDir) continue;
922
- candidates.push({ value: `${dir}${entry.name}${isDir ? "/" : ""}` });
1299
+ candidates.push({ value: `${dir}${entry.name}${isDir ? separator : ""}` });
923
1300
  }
924
1301
  return candidates;
925
1302
  }
@@ -941,116 +1318,68 @@ async function argValues(arg, tokens, partial) {
941
1318
  if (arg.template) candidates.push(...await enumeratePaths(partial, arg.template));
942
1319
  return candidates;
943
1320
  }
944
- /** Pick the argument at an index, reusing a final variadic argument. */
945
- function argAt(args, index) {
946
- if (args.length === 0) return void 0;
947
- if (index < args.length) return args[index];
948
- const last = args[args.length - 1];
949
- return last?.isVariadic ? last : void 0;
950
- }
951
- /** Pick the argument being completed at a positional index (honors variadic). */
952
- function positionalArg(command, index) {
953
- return argAt(asArray(command.args), index);
954
- }
955
1321
  /** Decide which kinds of candidate the word being completed can use. */
956
- function wantedCandidates(state, req) {
957
- const posixNoncompliant = directiveOnPath(state.path, "flagsArePosixNoncompliant");
958
- const partialIsOption = !posixNoncompliant && (req.partial.startsWith("-") || req.partial.startsWith("+"));
1322
+ function wantedCandidates(next, req) {
1323
+ const posixNoncompliant = next.directives.flagsArePosixNoncompliant;
1324
+ const partialIsOption = next.optionsAllowed && !posixNoncompliant && (req.partial.startsWith("-") || req.partial.startsWith("+"));
959
1325
  return {
960
1326
  wantValues: !(req.gateOptions && partialIsOption),
961
1327
  wantOptions: !req.gateOptions || partialIsOption || posixNoncompliant
962
1328
  };
963
1329
  }
964
1330
  /**
965
- * Resolve completion candidates for a partial command line, reading it with the
966
- * parser's `scan` for context. Dispatches between completing a option's value and
967
- * completing a fresh token (subcommand / positional / option).
1331
+ * Resolve completion candidates for a partial command line. The parser works
1332
+ * out what the word can be with {@link readNext}, and this offers candidates
1333
+ * for each of those.
968
1334
  */
969
1335
  async function completeRuntime(req) {
970
- const state = readLine(req.spec, req.before);
971
- const separators = separatorsOnPath(state.path);
972
- const optionValue = await resolveOptionValue(state, req, separators);
973
- if (optionValue) return optionValue;
974
- return freshTokenCandidates(state, req, separators);
975
- }
976
- /**
977
- * Candidates when the word being completed is a option's value — either attached
978
- * (`--option=value`) or the separate token after a option that expects one. Returns
979
- * null when no option value is being completed.
980
- */
981
- async function resolveOptionValue(state, req, separators) {
1336
+ const next = readNext(req.spec, req.before, req.partial);
982
1337
  const tokens = [...req.before];
983
- for (const separator of separators) {
984
- const at = req.partial.indexOf(separator);
985
- if (at <= 0) continue;
986
- const optionName = req.partial.slice(0, at);
987
- const option = state.availableOptions.find((candidate) => makeArray(candidate.name).includes(optionName));
988
- const arg = option && asArray(option.args)[0];
989
- if (option && arg) return (await argValues(arg, tokens, req.partial.slice(at + separator.length))).map((candidate) => ({
990
- value: `${optionName}${separator}${candidate.value}`,
1338
+ if (next.attached) {
1339
+ const { prefix, arg, value } = next.attached;
1340
+ return (await argValues(arg, tokens, value)).map((candidate) => ({
1341
+ value: prefix + candidate.value,
991
1342
  description: candidate.description
992
1343
  }));
993
1344
  }
994
- if (state.pending) {
995
- const { option, argIndex } = state.pending;
996
- const args = asArray(option.args);
997
- const arg = argAt(args, argIndex);
998
- if (argIndex < args.length) return arg ? argValues(arg, tokens, req.partial) : [];
999
- const { wantValues, wantOptions } = wantedCandidates(state, req);
1000
- return [...arg && wantValues ? await argValues(arg, tokens, req.partial) : [], ...wantOptions ? allowedOptionCandidates(state, separators) : []];
1001
- }
1002
- return null;
1003
- }
1004
- /** Candidates when completing a fresh token: subcommand / positional / option. */
1005
- async function freshTokenCandidates(state, req, separators) {
1345
+ if (next.value?.required) return next.value.arg ? argValues(next.value.arg, tokens, req.partial) : [];
1006
1346
  const candidates = [];
1007
- const tokens = [...req.before];
1008
- const { wantValues, wantOptions } = wantedCandidates(state, req);
1347
+ const { wantValues, wantOptions } = wantedCandidates(next, req);
1009
1348
  if (wantValues) {
1010
- const positional = positionalArg(state.current, state.positionalIndex);
1011
- if (positional) candidates.push(...await argValues(positional, tokens, req.partial));
1012
- if (state.forceArgs) return candidates;
1013
- if (!state.sawPositional) for (const sub of state.current.subcommands ?? []) {
1014
- if (sub.hidden) continue;
1349
+ const arg = next.value ? next.value.arg : next.arg;
1350
+ if (arg) candidates.push(...await argValues(arg, tokens, req.partial));
1351
+ for (const sub of visibleSubcommands(next.subcommands)) {
1015
1352
  const description = firstLine(sub.description);
1016
1353
  for (const token of makeArray(sub.name)) candidates.push({
1017
1354
  value: token,
1018
1355
  description
1019
1356
  });
1020
1357
  }
1021
- } else if (state.forceArgs) return candidates;
1022
- if (wantOptions) candidates.push(...allowedOptionCandidates(state, separators));
1358
+ }
1359
+ if (wantOptions) candidates.push(...optionCandidates(next));
1023
1360
  return candidates;
1024
1361
  }
1025
- /** Option candidates, unless options must precede arguments and one was seen. */
1026
- function allowedOptionCandidates(state, separators) {
1027
- return state.sawPositional && directiveOnPath(state.path, "optionsMustPrecedeArguments") ? [] : optionCandidates(state, separators);
1028
- }
1029
- /** Eligible option candidates after relationship filtering. */
1030
- function optionCandidates(state, separators) {
1362
+ /** The options a user can use next, required ones first. */
1363
+ function optionCandidates(next) {
1031
1364
  const required = [];
1032
1365
  const optional = [];
1033
- for (const option of state.availableOptions) {
1366
+ for (const { option, forms, used } of next.options) {
1034
1367
  if (option.hidden) continue;
1035
- const count = state.countByOption.get(option) ?? 0;
1036
- if (count > 0) {
1037
- if (!option.isRepeatable) continue;
1038
- if (typeof option.isRepeatable === "number" && count >= option.isRepeatable) continue;
1039
- }
1040
- if (option.exclusiveOn?.some((name) => state.presentNames.has(name))) continue;
1041
- if (state.availableOptions.some((other) => other !== option && makeArray(other.name).some((name) => state.presentNames.has(name)) && other.exclusiveOn?.some((name) => makeArray(option.name).includes(name)))) continue;
1368
+ if (used && !option.isRepeatable) continue;
1042
1369
  let description = firstLine(option.description);
1043
1370
  if (option.dependsOn) {
1044
1371
  const needs = option.dependsOn.join(", ");
1045
1372
  description = description ? `${description} (requires ${needs})` : `requires ${needs}`;
1046
1373
  }
1047
1374
  if (option.isRequired) description = description ? `${description} [required]` : "[required]";
1048
- const sep = option.requiresSeparator === true ? separators[0] : typeof option.requiresSeparator === "string" ? option.requiresSeparator : void 0;
1049
1375
  const bucket = option.isRequired ? required : optional;
1050
- for (const token of makeArray(option.name)) bucket.push({
1051
- value: sep ? `${token}${sep}` : token,
1052
- description
1053
- });
1376
+ for (const { name, join } of forms) {
1377
+ if (join === "") continue;
1378
+ bucket.push({
1379
+ value: name + (join ?? ""),
1380
+ description
1381
+ });
1382
+ }
1054
1383
  }
1055
1384
  return [...required, ...optional];
1056
1385
  }
@@ -1088,6 +1417,85 @@ function renderRuntimeShim(spec, shell) {
1088
1417
  `}`,
1089
1418
  `_${id}_complete "$@"`
1090
1419
  ].join("\n");
1420
+ if (shell === "powershell") {
1421
+ const stringAst = "[System.Management.Automation.Language.StringConstantExpressionAst]";
1422
+ return [
1423
+ `Register-ArgumentCompleter -Native -CommandName ${psQuote(name)} -ScriptBlock {`,
1424
+ ` param($wordToComplete, $commandAst, $cursorPosition)`,
1425
+ ` $elements = $commandAst.CommandElements`,
1426
+ ` $first = $elements[0]`,
1427
+ ` $bin = if ($first -is ${stringAst}) { $first.Value } else { $first.Extent.Text }`,
1428
+ ` if (-not (Get-Command $bin -ErrorAction SilentlyContinue)) { return }`,
1429
+ ` $partial = $wordToComplete`,
1430
+ ` if ($partial.StartsWith("'")) { $partial = $partial.Substring(1).TrimEnd("'").Replace("''", "'") }`,
1431
+ ` elseif ($partial.StartsWith('"')) { $partial = $partial.Substring(1).TrimEnd('"') }`,
1432
+ ` $arguments = @('completion', 'complete', '--gate-options')`,
1433
+ ` if ($partial -eq '') { $arguments += '--empty-word' }`,
1434
+ ` $arguments += '--'`,
1435
+ ` for ($i = 1; $i -lt $elements.Count; $i++) {`,
1436
+ ` $element = $elements[$i]`,
1437
+ ` if ($element.Extent.EndOffset -ge $cursorPosition) { break }`,
1438
+ ` if ($element -is ${stringAst}) {`,
1439
+ ` $arguments += $element.Value`,
1440
+ ` } else {`,
1441
+ ` $arguments += $element.Extent.Text`,
1442
+ ` }`,
1443
+ ` }`,
1444
+ ` if ($partial -ne '') { $arguments += $partial }`,
1445
+ ` & $bin @arguments 2>$null | ForEach-Object {`,
1446
+ " $value, $description = $_ -split \"`t\", 2",
1447
+ ` if (-not $value.StartsWith($partial, [System.StringComparison]::OrdinalIgnoreCase)) { return }`,
1448
+ " $text = if ($value -match '[\\s''\"`$&|;,(){}<>@#]') { \"'$($value.Replace(\"'\", \"''\"))'\" } else { $value }",
1449
+ ` if (-not $description) { $description = $value }`,
1450
+ ` [System.Management.Automation.CompletionResult]::new($text, $value, 'ParameterValue', $description)`,
1451
+ ` }`,
1452
+ `}`
1453
+ ].join("\n");
1454
+ }
1455
+ if (shell === "nushell") {
1456
+ const previous = `$__${id}_previous_completer`;
1457
+ const names = [
1458
+ "",
1459
+ ".exe",
1460
+ ".cmd",
1461
+ ".bat"
1462
+ ].map((extension) => nuQuote(name + extension)).join(" ");
1463
+ return [
1464
+ `let __${id}_previous_completer = $env.config.completions.external.completer?`,
1465
+ `$env.config.completions.external.completer = {|spans|`,
1466
+ ` let words = $spans | each {|span|`,
1467
+ ` let quote = $span | str substring 0..0`,
1468
+ ` if $quote in ["'", '"', '\`'] { $span | str substring 1.. | str trim --right --char $quote } else { $span }`,
1469
+ ` }`,
1470
+ ` let command = $words.0`,
1471
+ ` if ($command | path basename) not-in [${names}] {`,
1472
+ ` return (if ${previous} != null { do ${previous} $spans })`,
1473
+ ` }`,
1474
+ ` if (which $command | is-empty) { return }`,
1475
+ ` let args = $words | skip 1`,
1476
+ ` let partial = $args | last | default ""`,
1477
+ ` ^$command completion complete --gate-options -- ...$args`,
1478
+ ` | complete`,
1479
+ ` | get stdout`,
1480
+ ` | lines`,
1481
+ ` | each {|line|`,
1482
+ ` let parts = $line | split row "\\t"`,
1483
+ ` { value: $parts.0, description: ($parts | get 1? | default "") }`,
1484
+ ` }`,
1485
+ ` | where { $in.value | str starts-with --ignore-case $partial }`,
1486
+ " | update value {|candidate|",
1487
+ " let value = $candidate.value",
1488
+ " if ($value !~ \"[\\\\s'\\\"`$|;#()\\\\[\\\\]{}]\") {",
1489
+ " $value",
1490
+ " } else if ($value | str contains \"'\") {",
1491
+ " $value | to json",
1492
+ " } else {",
1493
+ " $\"'($value)'\"",
1494
+ " }",
1495
+ " }",
1496
+ `}`
1497
+ ].join("\n");
1498
+ }
1091
1499
  return [
1092
1500
  `function __${id}_complete`,
1093
1501
  ` set -l toks (commandline -opc)`,
@@ -1110,7 +1518,8 @@ const completion = {
1110
1518
  args: {
1111
1519
  name: "shell",
1112
1520
  isOptional: true,
1113
- suggestions: SHELLS
1521
+ suggestions: SHELLS,
1522
+ description: `One of: ${SHELLS.join(", ")}`
1114
1523
  },
1115
1524
  subcommands: [{
1116
1525
  name: "complete",
@@ -1119,6 +1528,9 @@ const completion = {
1119
1528
  options: [{
1120
1529
  name: "--gate-options",
1121
1530
  description: "Only suggest options after a leading dash"
1531
+ }, {
1532
+ name: "--empty-word",
1533
+ description: "The word being completed is empty, so every token is before it"
1122
1534
  }],
1123
1535
  args: {
1124
1536
  name: "tokens",
@@ -1127,10 +1539,11 @@ const completion = {
1127
1539
  },
1128
1540
  async action({ path, args, options }) {
1129
1541
  const spec = path[0];
1130
- const partial = args.length > 0 ? args[args.length - 1] : "";
1542
+ const emptyWord = options.has("--empty-word");
1543
+ const partial = emptyWord || args.length === 0 ? "" : args[args.length - 1];
1131
1544
  const lines = (await completeRuntime({
1132
1545
  spec,
1133
- before: args.slice(0, -1),
1546
+ before: emptyWord ? args : args.slice(0, -1),
1134
1547
  partial,
1135
1548
  gateOptions: options.has("--gate-options")
1136
1549
  })).map((candidate) => candidate.description ? `${candidate.value}\t${candidate.description}` : candidate.value);
@@ -1138,19 +1551,17 @@ const completion = {
1138
1551
  return 0;
1139
1552
  }
1140
1553
  }],
1141
- action({ path: [spec], args: [shell], help }) {
1554
+ action({ path, args: [shell] }) {
1555
+ const spec = path[0];
1142
1556
  if (shell === void 0) {
1143
1557
  print(`Supported shells: ${SHELLS.join(", ")}`);
1144
1558
  return 0;
1145
1559
  }
1146
1560
  if (!isShell(shell)) {
1147
- printActionError(`Unsupported shell '${shell}'\n\n${help({
1148
- description: false,
1149
- didYouMean: {
1150
- input: shell,
1151
- choices: SHELLS
1152
- }
1153
- })}`);
1561
+ printUsageError(path, `Unsupported shell '${shell}'`, {
1562
+ input: shell,
1563
+ choices: SHELLS
1564
+ });
1154
1565
  return 1;
1155
1566
  }
1156
1567
  print(getCompletion(spec, shell));
@@ -1161,31 +1572,21 @@ const completion = {
1161
1572
  /**
1162
1573
  * Parse argv and execute the selected action.
1163
1574
  *
1164
- * The final command action runs unless a parsed option has an action. Option
1165
- * actions take precedence so helpers like `--help` can run without satisfying
1166
- * the selected command's required arguments.
1575
+ * The selected command's action runs unless a parsed option has an action.
1576
+ * Option actions take precedence so helpers like `--help` can run without
1577
+ * satisfying the selected command's required arguments. A command without an
1578
+ * action prints its help.
1167
1579
  */
1168
1580
  async function run(spec, args = defaultArgs()) {
1169
1581
  let result;
1170
1582
  try {
1171
1583
  result = parse(args, spec);
1172
1584
  } catch (error) {
1173
- if (error instanceof UnknownOption) {
1174
- const helpMessage = getHelp(error.context.path, {
1175
- description: false,
1176
- didYouMean: {
1177
- input: error.option,
1178
- choices: error.validOptions
1179
- }
1180
- });
1181
- printError(`${error.message}\n\n${helpMessage}`);
1182
- } else if (error instanceof ParseError) printError(`${error.message}\n\n${getHelp(error.context.path, { description: false })}`);
1585
+ if (error instanceof ParseError) printUsageError(error.context.path, error.message, error.didYouMean);
1183
1586
  else printError(`${error}`);
1184
1587
  return 1;
1185
1588
  }
1186
- const action = result.optionActions.at(-1) ?? result.actions.at(-1);
1187
- if (!action) return 1;
1188
- const options = result.options;
1589
+ const { action, options } = result;
1189
1590
  const actionInit = {
1190
1591
  help: (helpOptions) => getHelp(helpOptions?.path ?? result.path, helpOptions),
1191
1592
  args: result.args,
@@ -1194,8 +1595,8 @@ async function run(spec, args = defaultArgs()) {
1194
1595
  options: {
1195
1596
  options,
1196
1597
  has: (name) => options.has(name),
1197
- get: (name) => options.get(name)?.[0],
1198
- all: (name) => options.get(name),
1598
+ get: (name) => (options.get(name) ?? result.defaults(name))?.[0],
1599
+ all: (name) => options.get(name) ?? result.defaults(name),
1199
1600
  count: (name) => options.get(name)?.length ?? 0
1200
1601
  }
1201
1602
  };
@@ -1204,14 +1605,22 @@ async function run(spec, args = defaultArgs()) {
1204
1605
  const actionResult = action(actionInit);
1205
1606
  exitCode = typeof actionResult === "object" ? await actionResult : actionResult;
1206
1607
  } catch (error) {
1207
- printError(error instanceof Error ? error.message : String(error));
1608
+ printError(describeThrown(error));
1208
1609
  return 1;
1209
1610
  }
1210
1611
  return Math.min(Math.max(exitCode ?? 0, 0), 255);
1211
1612
  }
1212
- /** Print `Error:` and the given values to stderr, separated by spaces. */
1213
- function printError(...strings) {
1214
- output.write(2, `${["Error:", ...strings].map(String).join(" ")}\n`);
1613
+ /**
1614
+ * The message for a value an action threw. A value that isn't an Error is shown
1615
+ * as JSON where it can be, since `String` turns an object into
1616
+ * "[object Object]".
1617
+ */
1618
+ function describeThrown(value) {
1619
+ if (value instanceof Error) return value.message;
1620
+ if (typeof value === "object" && value !== null) try {
1621
+ return JSON.stringify(value);
1622
+ } catch {}
1623
+ return String(value);
1215
1624
  }
1216
1625
  function defaultArgs() {
1217
1626
  return globalThis.process?.argv?.slice(2) ?? [];