@guanghechen/commander 4.7.1 → 4.7.3

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/lib/esm/node.mjs CHANGED
@@ -195,6 +195,140 @@ function kebabToCamelCase(str) {
195
195
  function camelToKebabCase$1(str) {
196
196
  return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
197
197
  }
198
+ const ANSI_ESCAPE_REGEX = new RegExp(String.raw `\\x1B\\[[0-?]*[ -/]*[@-~]`, 'g');
199
+ const DECIMAL_INTEGER_REGEX = /^\d(?:_?\d)*$/;
200
+ const DECIMAL_FRACTION_REGEX = /^\d(?:_?\d)*$/;
201
+ const DECIMAL_EXPONENT_REGEX = /^[eE][+-]?\d(?:_?\d)*$/;
202
+ const BINARY_LITERAL_REGEX = /^0[bB][01](?:_?[01])*$/;
203
+ const OCTAL_LITERAL_REGEX = /^0[oO][0-7](?:_?[0-7])*$/;
204
+ const HEX_LITERAL_REGEX = /^0[xX][0-9a-fA-F](?:_?[0-9a-fA-F])*$/;
205
+ function stripAnsi(value) {
206
+ return value.replace(ANSI_ESCAPE_REGEX, '');
207
+ }
208
+ function isCombiningMark(codePoint) {
209
+ return ((codePoint >= 0x0300 && codePoint <= 0x036f) ||
210
+ (codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
211
+ (codePoint >= 0x1dc0 && codePoint <= 0x1dff) ||
212
+ (codePoint >= 0x20d0 && codePoint <= 0x20ff) ||
213
+ (codePoint >= 0xfe20 && codePoint <= 0xfe2f));
214
+ }
215
+ function isWideCodePoint(codePoint) {
216
+ if (codePoint < 0x1100) {
217
+ return false;
218
+ }
219
+ return (codePoint <= 0x115f ||
220
+ codePoint === 0x2329 ||
221
+ codePoint === 0x232a ||
222
+ (codePoint >= 0x2e80 && codePoint <= 0x3247 && codePoint !== 0x303f) ||
223
+ (codePoint >= 0x3250 && codePoint <= 0x4dbf) ||
224
+ (codePoint >= 0x4e00 && codePoint <= 0xa4c6) ||
225
+ (codePoint >= 0xa960 && codePoint <= 0xa97c) ||
226
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
227
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
228
+ (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
229
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6b) ||
230
+ (codePoint >= 0xff01 && codePoint <= 0xff60) ||
231
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
232
+ (codePoint >= 0x1b000 && codePoint <= 0x1b001) ||
233
+ (codePoint >= 0x1f200 && codePoint <= 0x1f251) ||
234
+ (codePoint >= 0x20000 && codePoint <= 0x3fffd));
235
+ }
236
+ function getDisplayWidth(value) {
237
+ const normalized = stripAnsi(value).normalize('NFC');
238
+ let width = 0;
239
+ for (const char of normalized) {
240
+ const codePoint = char.codePointAt(0);
241
+ if (codePoint === undefined || isCombiningMark(codePoint)) {
242
+ continue;
243
+ }
244
+ width += isWideCodePoint(codePoint) ? 2 : 1;
245
+ }
246
+ return width;
247
+ }
248
+ function padDisplayEnd(value, targetWidth) {
249
+ const width = getDisplayWidth(value);
250
+ if (width >= targetWidth) {
251
+ return value;
252
+ }
253
+ return value + ' '.repeat(targetWidth - width);
254
+ }
255
+ function isValidPrimitiveNumberLiteral(rawValue) {
256
+ if (rawValue.trim() !== rawValue || rawValue.length === 0) {
257
+ return false;
258
+ }
259
+ if (rawValue === 'NaN' || rawValue === 'Infinity' || rawValue === '-Infinity') {
260
+ return false;
261
+ }
262
+ if (BINARY_LITERAL_REGEX.test(rawValue)) {
263
+ return true;
264
+ }
265
+ if (OCTAL_LITERAL_REGEX.test(rawValue)) {
266
+ return true;
267
+ }
268
+ if (HEX_LITERAL_REGEX.test(rawValue)) {
269
+ return true;
270
+ }
271
+ const sign = rawValue[0] === '+' || rawValue[0] === '-' ? rawValue[0] : '';
272
+ const body = sign ? rawValue.slice(1) : rawValue;
273
+ if (body.length === 0) {
274
+ return false;
275
+ }
276
+ const expIndex = body.search(/[eE]/);
277
+ const basePart = expIndex === -1 ? body : body.slice(0, expIndex);
278
+ const expPart = expIndex === -1 ? '' : body.slice(expIndex);
279
+ if (expPart && !DECIMAL_EXPONENT_REGEX.test(expPart)) {
280
+ return false;
281
+ }
282
+ if (basePart.includes('.')) {
283
+ const decimalParts = basePart.split('.');
284
+ if (decimalParts.length !== 2) {
285
+ return false;
286
+ }
287
+ const [intPart, fracPart] = decimalParts;
288
+ const intOk = intPart.length === 0 || DECIMAL_INTEGER_REGEX.test(intPart);
289
+ const fracOk = fracPart.length === 0 || DECIMAL_FRACTION_REGEX.test(fracPart);
290
+ if (!intOk || !fracOk) {
291
+ return false;
292
+ }
293
+ return intPart.length > 0 || fracPart.length > 0;
294
+ }
295
+ return DECIMAL_INTEGER_REGEX.test(basePart);
296
+ }
297
+ function parsePrimitiveNumber(rawValue) {
298
+ if (!isValidPrimitiveNumberLiteral(rawValue)) {
299
+ return undefined;
300
+ }
301
+ const normalized = rawValue.replaceAll('_', '');
302
+ const value = Number(normalized);
303
+ if (!Number.isFinite(value)) {
304
+ return undefined;
305
+ }
306
+ return value;
307
+ }
308
+ function normalizeSubcommandNameForDistance(name) {
309
+ return camelToKebabCase$1(name).toLowerCase();
310
+ }
311
+ function levenshteinDistance(left, right) {
312
+ if (left === right) {
313
+ return 0;
314
+ }
315
+ if (left.length === 0) {
316
+ return right.length;
317
+ }
318
+ if (right.length === 0) {
319
+ return left.length;
320
+ }
321
+ let prev = Array.from({ length: right.length + 1 }, (_, i) => i);
322
+ for (let i = 0; i < left.length; i += 1) {
323
+ const current = [i + 1];
324
+ for (let j = 0; j < right.length; j += 1) {
325
+ const substitutionCost = left[i] === right[j] ? 0 : 1;
326
+ current[j + 1] = Math.min(current[j] + 1, prev[j + 1] + 1, prev[j] + substitutionCost);
327
+ }
328
+ prev = current;
329
+ }
330
+ return prev[right.length];
331
+ }
198
332
  function tokenizeLongOption(arg, commandPath) {
199
333
  const eqIdx = arg.indexOf('=');
200
334
  const namePart = eqIdx !== -1 ? arg.slice(0, eqIdx) : arg;
@@ -431,9 +565,13 @@ class Command {
431
565
  if (cmd.#parent && cmd.#parent !== this) {
432
566
  throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
433
567
  }
568
+ const occupied = this.#subcommandsMap.get(name);
569
+ if (occupied && occupied !== cmd) {
570
+ throw new CommanderError('ConfigurationError', `subcommand name/alias "${name}" conflicts with an existing command`, this.#getCommandPath());
571
+ }
434
572
  const existing = this.#subcommandsList.find(e => e.command === cmd);
435
573
  if (existing) {
436
- if (existing.aliases.includes(name)) {
574
+ if (existing.name === name || existing.aliases.includes(name)) {
437
575
  return this;
438
576
  }
439
577
  existing.aliases.push(name);
@@ -562,16 +700,41 @@ class Command {
562
700
  else if (arg.kind === 'optional') {
563
701
  usage += ` [${arg.name}]`;
564
702
  }
703
+ else if (arg.kind === 'some') {
704
+ usage += ` <${arg.name}...>`;
705
+ }
565
706
  else {
566
707
  usage += ` [${arg.name}...]`;
567
708
  }
568
709
  }
710
+ const argumentsLines = [];
711
+ for (const arg of this.#arguments) {
712
+ const sig = arg.kind === 'required'
713
+ ? `<${arg.name}>`
714
+ : arg.kind === 'optional'
715
+ ? `[${arg.name}]`
716
+ : arg.kind === 'some'
717
+ ? `<${arg.name}...>`
718
+ : `[${arg.name}...]`;
719
+ const metadata = [`[type: ${arg.type}]`];
720
+ if (arg.kind === 'optional' && arg.default !== undefined) {
721
+ metadata.push(`[default: ${JSON.stringify(arg.default)}]`);
722
+ }
723
+ if (arg.choices && arg.choices.length > 0) {
724
+ metadata.push(`[choices: ${arg.choices.map(choice => JSON.stringify(choice)).join(', ')}]`);
725
+ }
726
+ const desc = metadata.length > 0 ? `${arg.desc} ${metadata.join(' ')}` : arg.desc;
727
+ argumentsLines.push({ sig, desc });
728
+ }
569
729
  const options = [];
570
730
  for (const opt of allOptions) {
571
731
  const kebabLong = camelToKebabCase$1(opt.long);
572
732
  let sig = opt.short ? `-${opt.short}, ` : ' ';
573
733
  sig += `--${kebabLong}`;
574
- if (opt.args !== 'none') {
734
+ if (opt.args === 'optional') {
735
+ sig += ' [value]';
736
+ }
737
+ else if (opt.args !== 'none') {
575
738
  sig += ' <value>';
576
739
  }
577
740
  let desc = opt.desc;
@@ -579,7 +742,7 @@ class Command {
579
742
  desc += ` (default: ${JSON.stringify(opt.default)})`;
580
743
  }
581
744
  if (opt.choices) {
582
- desc += ` [choices: ${opt.choices.join(', ')}]`;
745
+ desc += ` [choices: ${opt.choices.map(choice => JSON.stringify(choice)).join(', ')}]`;
583
746
  }
584
747
  options.push({ sig, desc });
585
748
  if (opt.type === 'boolean' &&
@@ -611,6 +774,7 @@ class Command {
611
774
  return {
612
775
  desc: this.#desc,
613
776
  usage,
777
+ arguments: argumentsLines,
614
778
  options,
615
779
  commands,
616
780
  examples,
@@ -618,25 +782,29 @@ class Command {
618
782
  }
619
783
  #renderHelpPlain(helpData) {
620
784
  const lines = [];
785
+ const labelWidth = this.#getHelpLabelWidth(helpData);
621
786
  lines.push(helpData.desc);
622
787
  lines.push('');
623
788
  lines.push(helpData.usage);
624
789
  lines.push('');
790
+ if (helpData.arguments.length > 0) {
791
+ lines.push('Arguments:');
792
+ for (const { sig, desc } of helpData.arguments) {
793
+ lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth));
794
+ }
795
+ lines.push('');
796
+ }
625
797
  if (helpData.options.length > 0) {
626
798
  lines.push('Options:');
627
- const maxSigLen = Math.max(...helpData.options.map(line => line.sig.length));
628
799
  for (const { sig, desc } of helpData.options) {
629
- const padding = ' '.repeat(maxSigLen - sig.length + 2);
630
- lines.push(` ${sig}${padding}${desc}`);
800
+ lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth));
631
801
  }
632
802
  lines.push('');
633
803
  }
634
804
  if (helpData.commands.length > 0) {
635
805
  lines.push('Commands:');
636
- const maxNameLen = Math.max(...helpData.commands.map(line => line.name.length));
637
806
  for (const { name, desc } of helpData.commands) {
638
- const padding = ' '.repeat(maxNameLen - name.length + 2);
639
- lines.push(` ${name}${padding}${desc}`);
807
+ lines.push(this.#renderAlignedHelpLine(name, desc, labelWidth));
640
808
  }
641
809
  lines.push('');
642
810
  }
@@ -653,25 +821,29 @@ class Command {
653
821
  }
654
822
  #renderHelpTerminal(helpData) {
655
823
  const lines = [];
824
+ const labelWidth = this.#getHelpLabelWidth(helpData);
656
825
  lines.push(helpData.desc);
657
826
  lines.push('');
658
827
  lines.push(styleText(helpData.usage, TERMINAL_STYLE.bold));
659
828
  lines.push('');
829
+ if (helpData.arguments.length > 0) {
830
+ lines.push(styleText('Arguments:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
831
+ for (const { sig, desc } of helpData.arguments) {
832
+ lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth, value => styleText(value, TERMINAL_STYLE.cyan)));
833
+ }
834
+ lines.push('');
835
+ }
660
836
  if (helpData.options.length > 0) {
661
837
  lines.push(styleText('Options:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
662
- const maxSigLen = Math.max(...helpData.options.map(line => line.sig.length));
663
838
  for (const { sig, desc } of helpData.options) {
664
- const padding = ' '.repeat(maxSigLen - sig.length + 2);
665
- lines.push(` ${styleText(sig, TERMINAL_STYLE.cyan)}${padding}${desc}`);
839
+ lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth, value => styleText(value, TERMINAL_STYLE.cyan)));
666
840
  }
667
841
  lines.push('');
668
842
  }
669
843
  if (helpData.commands.length > 0) {
670
844
  lines.push(styleText('Commands:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
671
- const maxNameLen = Math.max(...helpData.commands.map(line => line.name.length));
672
845
  for (const { name, desc } of helpData.commands) {
673
- const padding = ' '.repeat(maxNameLen - name.length + 2);
674
- lines.push(` ${styleText(name, TERMINAL_STYLE.cyan)}${padding}${desc}`);
846
+ lines.push(this.#renderAlignedHelpLine(name, desc, labelWidth, value => styleText(value, TERMINAL_STYLE.cyan)));
675
847
  }
676
848
  lines.push('');
677
849
  }
@@ -686,16 +858,41 @@ class Command {
686
858
  }
687
859
  return lines.join('\n');
688
860
  }
861
+ #getHelpLabelWidth(helpData) {
862
+ const labels = [
863
+ ...helpData.arguments.map(line => line.sig),
864
+ ...helpData.options.map(line => line.sig),
865
+ ...helpData.commands.map(line => line.name),
866
+ ];
867
+ if (labels.length === 0) {
868
+ return 0;
869
+ }
870
+ return Math.max(...labels.map(getDisplayWidth));
871
+ }
872
+ #renderAlignedHelpLine(label, desc, labelWidth, styleLabel) {
873
+ const paddedLabel = padDisplayEnd(label, labelWidth);
874
+ const outputLabel = styleLabel ? styleLabel(paddedLabel) : paddedLabel;
875
+ return ` ${outputLabel} ${desc}`;
876
+ }
689
877
  getCompletionMeta() {
690
878
  const allOptions = this.#resolveOptionPolicy().mergedOptions;
691
879
  const options = [];
880
+ const argumentsMeta = [];
692
881
  for (const opt of allOptions) {
693
882
  options.push({
694
883
  long: opt.long,
695
884
  short: opt.short,
696
885
  desc: opt.desc,
697
886
  takesValue: opt.args !== 'none',
698
- choices: opt.choices,
887
+ choices: opt.choices?.map(choice => String(choice)),
888
+ });
889
+ }
890
+ for (const arg of this.#arguments) {
891
+ argumentsMeta.push({
892
+ name: arg.name,
893
+ kind: arg.kind,
894
+ type: arg.type,
895
+ choices: arg.type === 'choice' ? arg.choices?.map(choice => String(choice)) : undefined,
699
896
  });
700
897
  }
701
898
  return {
@@ -703,6 +900,7 @@ class Command {
703
900
  desc: this.#desc,
704
901
  aliases: [],
705
902
  options,
903
+ arguments: argumentsMeta,
706
904
  subcommands: this.#subcommandsList.map(entry => {
707
905
  const subMeta = entry.command.getCompletionMeta();
708
906
  return {
@@ -1155,6 +1353,14 @@ class Command {
1155
1353
  consumed.push(tokens[i]);
1156
1354
  }
1157
1355
  }
1356
+ else if (opt.args === 'optional') {
1357
+ if (!token.resolved.includes('=') &&
1358
+ i + 1 < tokens.length &&
1359
+ tokens[i + 1].type === 'none') {
1360
+ i += 1;
1361
+ consumed.push(tokens[i]);
1362
+ }
1363
+ }
1158
1364
  else if (opt.args === 'variadic') {
1159
1365
  if (!token.resolved.includes('=')) {
1160
1366
  while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
@@ -1180,6 +1386,12 @@ class Command {
1180
1386
  consumed.push(tokens[i]);
1181
1387
  }
1182
1388
  }
1389
+ else if (opt.args === 'optional') {
1390
+ if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
1391
+ i += 1;
1392
+ consumed.push(tokens[i]);
1393
+ }
1394
+ }
1183
1395
  else if (opt.args === 'variadic') {
1184
1396
  while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
1185
1397
  i += 1;
@@ -1221,6 +1433,7 @@ class Command {
1221
1433
  leafLocalOpts[opt.long] = leafParsedOpts[opt.long];
1222
1434
  }
1223
1435
  }
1436
+ leafCommand.#assertUnknownSubcommand(ctx.sources.user.argv);
1224
1437
  const rawArgStrings = [...argTokens.map(t => t.original), ...restArgs];
1225
1438
  const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
1226
1439
  const parseCtx = {
@@ -1303,6 +1516,23 @@ class Command {
1303
1516
  i += 1;
1304
1517
  continue;
1305
1518
  }
1519
+ if (opt.args === 'optional') {
1520
+ const eqIdx = token.resolved.indexOf('=');
1521
+ if (eqIdx !== -1) {
1522
+ opts[opt.long] = this.#convertValue(opt, token.resolved.slice(eqIdx + 1));
1523
+ i += 1;
1524
+ continue;
1525
+ }
1526
+ if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
1527
+ opts[opt.long] = this.#convertValue(opt, tokens[i + 1].original);
1528
+ i += 1;
1529
+ }
1530
+ else {
1531
+ opts[opt.long] = undefined;
1532
+ }
1533
+ i += 1;
1534
+ continue;
1535
+ }
1306
1536
  if (opt.args === 'variadic') {
1307
1537
  const values = Array.isArray(opts[opt.long]) ? opts[opt.long] : [];
1308
1538
  const eqIdx = token.resolved.indexOf('=');
@@ -1322,7 +1552,7 @@ class Command {
1322
1552
  i += 1;
1323
1553
  }
1324
1554
  for (const opt of allOptions) {
1325
- if (opt.required && opts[opt.long] === undefined) {
1555
+ if (opt.required && !Object.prototype.hasOwnProperty.call(opts, opt.long)) {
1326
1556
  throw new CommanderError('MissingRequired', `missing required option "--${camelToKebabCase$1(opt.long)}"`, this.#getCommandPath());
1327
1557
  }
1328
1558
  }
@@ -1348,8 +1578,8 @@ class Command {
1348
1578
  return opt.coerce(rawValue);
1349
1579
  }
1350
1580
  if (opt.type === 'number') {
1351
- const num = Number(rawValue);
1352
- if (Number.isNaN(num)) {
1581
+ const num = parsePrimitiveNumber(rawValue);
1582
+ if (num === undefined) {
1353
1583
  throw new CommanderError('InvalidType', `invalid number "${rawValue}" for option "--${camelToKebabCase$1(opt.long)}"`, this.#getCommandPath());
1354
1584
  }
1355
1585
  return num;
@@ -1359,12 +1589,37 @@ class Command {
1359
1589
  #parseArguments(rawArgs) {
1360
1590
  const argumentDefs = this.#arguments;
1361
1591
  const args = {};
1362
- const requiredCount = argumentDefs.filter(a => a.kind === 'required').length;
1363
- if (rawArgs.length < requiredCount) {
1364
- const missing = argumentDefs
1365
- .filter(a => a.kind === 'required')
1366
- .slice(rawArgs.length)
1367
- .map(a => a.name);
1592
+ if (argumentDefs.length === 0 && rawArgs.length > 0) {
1593
+ throw new CommanderError('UnexpectedArgument', `unexpected argument "${rawArgs[0]}"`, this.#getCommandPath());
1594
+ }
1595
+ const missing = [];
1596
+ let remaining = rawArgs.length;
1597
+ for (const def of argumentDefs) {
1598
+ if (def.kind === 'required') {
1599
+ if (remaining === 0) {
1600
+ missing.push(def.name);
1601
+ }
1602
+ else {
1603
+ remaining -= 1;
1604
+ }
1605
+ continue;
1606
+ }
1607
+ if (def.kind === 'optional') {
1608
+ if (remaining > 0) {
1609
+ remaining -= 1;
1610
+ }
1611
+ continue;
1612
+ }
1613
+ if (def.kind === 'some') {
1614
+ if (remaining === 0) {
1615
+ missing.push(def.name);
1616
+ }
1617
+ remaining = 0;
1618
+ continue;
1619
+ }
1620
+ remaining = 0;
1621
+ }
1622
+ if (missing.length > 0) {
1368
1623
  throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
1369
1624
  }
1370
1625
  let index = 0;
@@ -1375,41 +1630,101 @@ class Command {
1375
1630
  index = rawArgs.length;
1376
1631
  break;
1377
1632
  }
1378
- const raw = rawArgs[index];
1379
- if (raw === undefined) {
1380
- if (def.kind === 'optional') {
1633
+ if (def.kind === 'some') {
1634
+ const rest = rawArgs.slice(index);
1635
+ args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
1636
+ index = rawArgs.length;
1637
+ break;
1638
+ }
1639
+ if (def.kind === 'optional') {
1640
+ const raw = rawArgs[index];
1641
+ if (raw === undefined) {
1381
1642
  args[def.name] = def.default ?? undefined;
1382
1643
  continue;
1383
1644
  }
1384
- }
1385
- else {
1386
1645
  args[def.name] = this.#convertArgument(def, raw);
1387
1646
  index += 1;
1647
+ continue;
1388
1648
  }
1649
+ const raw = rawArgs[index];
1650
+ args[def.name] = this.#convertArgument(def, raw);
1651
+ index += 1;
1389
1652
  }
1390
- const hasVariadic = argumentDefs.some(a => a.kind === 'variadic');
1391
- if (!hasVariadic && index < rawArgs.length) {
1653
+ const hasRestArgument = argumentDefs.some(a => a.kind === 'variadic' || a.kind === 'some');
1654
+ if (!hasRestArgument && index < rawArgs.length) {
1392
1655
  throw new CommanderError('TooManyArguments', `too many arguments: expected ${argumentDefs.length}, got ${rawArgs.length}`, this.#getCommandPath());
1393
1656
  }
1394
1657
  return { args, rawArgs };
1395
1658
  }
1396
1659
  #convertArgument(def, raw) {
1660
+ let value;
1397
1661
  if (def.coerce) {
1398
1662
  try {
1399
- return def.coerce(raw);
1663
+ value = def.coerce(raw);
1400
1664
  }
1401
1665
  catch {
1402
1666
  throw new CommanderError('InvalidType', `invalid value "${raw}" for argument "${def.name}"`, this.#getCommandPath());
1403
1667
  }
1404
1668
  }
1405
- if (def.type === 'number') {
1406
- const n = Number(raw);
1407
- if (Number.isNaN(n)) {
1408
- throw new CommanderError('InvalidType', `invalid number "${raw}" for argument "${def.name}"`, this.#getCommandPath());
1669
+ else {
1670
+ value = raw;
1671
+ }
1672
+ if (typeof value !== 'string') {
1673
+ throw new CommanderError('InvalidType', `invalid value for argument "${def.name}": expected ${def.type}`, this.#getCommandPath());
1674
+ }
1675
+ if (def.type === 'choice') {
1676
+ const choices = def.choices ?? [];
1677
+ if (!choices.includes(value)) {
1678
+ throw new CommanderError('InvalidChoice', `invalid value "${value}" for argument "${def.name}". Allowed: ${choices
1679
+ .map(choice => JSON.stringify(choice))
1680
+ .join(', ')}`, this.#getCommandPath());
1409
1681
  }
1410
- return n;
1411
1682
  }
1412
- return raw;
1683
+ return value;
1684
+ }
1685
+ #assertUnknownSubcommand(userTailArgv) {
1686
+ if (this.#subcommandsList.length === 0) {
1687
+ return;
1688
+ }
1689
+ const token = userTailArgv[0];
1690
+ if (token === undefined || token.startsWith('-') || token === 'help') {
1691
+ return;
1692
+ }
1693
+ if (this.#findSubcommandEntry(token) !== undefined) {
1694
+ return;
1695
+ }
1696
+ const hints = [];
1697
+ if (this.#arguments.length === 0) {
1698
+ hints.push(`Hint: command "${this.#getCommandPath()}" does not accept positional arguments.`);
1699
+ }
1700
+ const candidate = this.#resolveDidYouMeanSubcommandName(token);
1701
+ if (candidate !== undefined) {
1702
+ hints.push(`Hint: did you mean "${candidate}"?`);
1703
+ }
1704
+ const details = hints.length > 0 ? `\n${hints.join('\n')}` : '';
1705
+ throw new CommanderError('UnknownSubcommand', `unknown subcommand "${token}" for command "${this.#getCommandPath()}"${details}`, this.#getCommandPath());
1706
+ }
1707
+ #resolveDidYouMeanSubcommandName(token) {
1708
+ const source = normalizeSubcommandNameForDistance(token);
1709
+ let minDistance = Number.POSITIVE_INFINITY;
1710
+ let bestName;
1711
+ let isUniqueBest = false;
1712
+ for (const entry of this.#subcommandsList) {
1713
+ const target = normalizeSubcommandNameForDistance(entry.name);
1714
+ const distance = levenshteinDistance(source, target);
1715
+ if (distance < minDistance) {
1716
+ minDistance = distance;
1717
+ bestName = entry.name;
1718
+ isUniqueBest = true;
1719
+ }
1720
+ else if (distance === minDistance) {
1721
+ isUniqueBest = false;
1722
+ }
1723
+ }
1724
+ if (minDistance <= 2 && isUniqueBest) {
1725
+ return bestName;
1726
+ }
1727
+ return undefined;
1413
1728
  }
1414
1729
  #hasUserOption(long) {
1415
1730
  return this.#options.some(option => option.long === long);
@@ -1454,11 +1769,9 @@ class Command {
1454
1769
  return optionPolicyMap;
1455
1770
  }
1456
1771
  #mustGetOptionPolicy(optionPolicyMap, cmd) {
1457
- const policy = optionPolicyMap.get(cmd);
1458
- if (policy !== undefined) {
1459
- return policy;
1460
- }
1461
- throw new CommanderError('ConfigurationError', `missing option policy for command "${cmd.#getCommandPath()}"`, this.#getCommandPath());
1772
+ const policy = optionPolicyMap.get(cmd) ?? cmd.#resolveOptionPolicy();
1773
+ optionPolicyMap.set(cmd, policy);
1774
+ return policy;
1462
1775
  }
1463
1776
  #validateMergedShortOptions(chain, optionPolicyMap) {
1464
1777
  const mergedByLong = new Map();
@@ -1487,7 +1800,10 @@ class Command {
1487
1800
  throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" must have args: 'none'`, this.#getCommandPath());
1488
1801
  }
1489
1802
  if ((opt.type === 'string' || opt.type === 'number') && opt.args === 'none') {
1490
- throw new CommanderError('ConfigurationError', `${opt.type} option "--${opt.long}" must have args: 'required' or 'variadic'`, this.#getCommandPath());
1803
+ throw new CommanderError('ConfigurationError', `${opt.type} option "--${opt.long}" must have args: 'required', 'optional', or 'variadic'`, this.#getCommandPath());
1804
+ }
1805
+ if (opt.type === 'number' && opt.args === 'optional') {
1806
+ throw new CommanderError('ConfigurationError', `number option "--${opt.long}" does not support args: 'optional'`, this.#getCommandPath());
1491
1807
  }
1492
1808
  if (opt.long.startsWith('no')) {
1493
1809
  throw new CommanderError('ConfigurationError', `option long name cannot start with "no": "${opt.long}"`, this.#getCommandPath());
@@ -1495,12 +1811,18 @@ class Command {
1495
1811
  if (!/^[a-z][a-zA-Z0-9]*$/.test(opt.long)) {
1496
1812
  throw new CommanderError('ConfigurationError', `option long name must be camelCase: "${opt.long}"`, this.#getCommandPath());
1497
1813
  }
1814
+ if (opt.short !== undefined && opt.short.length !== 1) {
1815
+ throw new CommanderError('ConfigurationError', `option short name must be a single character: "${opt.short}"`, this.#getCommandPath());
1816
+ }
1498
1817
  if (opt.required && opt.default !== undefined) {
1499
1818
  throw new CommanderError('ConfigurationError', `option "--${opt.long}" cannot be both required and have a default value`, this.#getCommandPath());
1500
1819
  }
1501
1820
  if (opt.type === 'boolean' && opt.required) {
1502
1821
  throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" cannot be required`, this.#getCommandPath());
1503
1822
  }
1823
+ if (opt.required && opt.args !== 'required') {
1824
+ throw new CommanderError('ConfigurationError', `required option "--${opt.long}" must use args: 'required'`, this.#getCommandPath());
1825
+ }
1504
1826
  }
1505
1827
  #checkOptionUniqueness(opt) {
1506
1828
  if (this.#options.some(o => o.long === opt.long)) {
@@ -1511,24 +1833,58 @@ class Command {
1511
1833
  }
1512
1834
  }
1513
1835
  #validateArgumentConfig(arg) {
1514
- if (arg.kind === 'required' && arg.default !== undefined) {
1515
- throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot have a default value`, this.#getCommandPath());
1836
+ if (arg.kind !== 'required' &&
1837
+ arg.kind !== 'optional' &&
1838
+ arg.kind !== 'variadic' &&
1839
+ arg.kind !== 'some') {
1840
+ throw new CommanderError('ConfigurationError', `argument "${arg.name}" must specify a valid kind`, this.#getCommandPath());
1841
+ }
1842
+ if (arg.type !== 'string' && arg.type !== 'choice') {
1843
+ throw new CommanderError('ConfigurationError', `argument "${arg.name}" must specify a valid type`, this.#getCommandPath());
1516
1844
  }
1517
- if (arg.kind === 'variadic') {
1518
- if (this.#arguments.some(a => a.kind === 'variadic')) {
1519
- throw new CommanderError('ConfigurationError', 'only one variadic argument is allowed', this.#getCommandPath());
1845
+ if (arg.default !== undefined && arg.kind !== 'optional') {
1846
+ throw new CommanderError('ConfigurationError', `only optional argument "${arg.name}" can have a default value`, this.#getCommandPath());
1847
+ }
1848
+ if (arg.type === 'string' && arg.choices !== undefined) {
1849
+ throw new CommanderError('ConfigurationError', `argument "${arg.name}" of type "string" cannot declare choices`, this.#getCommandPath());
1850
+ }
1851
+ if (arg.type === 'choice') {
1852
+ if (!Array.isArray(arg.choices) || arg.choices.length === 0) {
1853
+ throw new CommanderError('ConfigurationError', `argument "${arg.name}" of type "choice" must declare a non-empty choices array`, this.#getCommandPath());
1854
+ }
1855
+ if (arg.choices.some(choice => typeof choice !== 'string')) {
1856
+ throw new CommanderError('ConfigurationError', `argument "${arg.name}" choices must be string[]`, this.#getCommandPath());
1857
+ }
1858
+ }
1859
+ if (arg.default !== undefined) {
1860
+ this.#validateArgumentDefaultValue(arg);
1861
+ }
1862
+ if (arg.kind === 'variadic' || arg.kind === 'some') {
1863
+ if (this.#arguments.some(a => a.kind === 'variadic' || a.kind === 'some')) {
1864
+ throw new CommanderError('ConfigurationError', 'only one variadic/some argument is allowed', this.#getCommandPath());
1520
1865
  }
1521
1866
  }
1522
1867
  if (this.#arguments.length > 0) {
1523
1868
  const last = this.#arguments[this.#arguments.length - 1];
1524
- if (last.kind === 'variadic') {
1525
- throw new CommanderError('ConfigurationError', 'variadic argument must be the last argument', this.#getCommandPath());
1869
+ if (last.kind === 'variadic' || last.kind === 'some') {
1870
+ throw new CommanderError('ConfigurationError', 'variadic/some argument must be the last argument', this.#getCommandPath());
1526
1871
  }
1527
1872
  }
1528
1873
  if (arg.kind === 'required') {
1529
- const hasOptional = this.#arguments.some(a => a.kind === 'optional' || a.kind === 'variadic');
1874
+ const hasOptional = this.#arguments.some(a => a.kind === 'optional' || a.kind === 'variadic' || a.kind === 'some');
1530
1875
  if (hasOptional) {
1531
- throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot come after optional/variadic arguments`, this.#getCommandPath());
1876
+ throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot come after optional/variadic/some arguments`, this.#getCommandPath());
1877
+ }
1878
+ }
1879
+ }
1880
+ #validateArgumentDefaultValue(arg) {
1881
+ if (typeof arg.default !== 'string') {
1882
+ throw new CommanderError('ConfigurationError', `default value for argument "${arg.name}" must match type "${arg.type}"`, this.#getCommandPath());
1883
+ }
1884
+ if (arg.type === 'choice') {
1885
+ const choices = arg.choices ?? [];
1886
+ if (!choices.includes(arg.default)) {
1887
+ throw new CommanderError('ConfigurationError', `default value for argument "${arg.name}" must be one of declared choices`, this.#getCommandPath());
1532
1888
  }
1533
1889
  }
1534
1890
  }
@@ -1775,6 +2131,35 @@ class Coerce {
1775
2131
  function camelToKebabCase(str) {
1776
2132
  return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
1777
2133
  }
2134
+ const COMPLETION_SHELL_STATE = Symbol('completion-shell-state');
2135
+ function getCommandPath(ctx) {
2136
+ const names = ctx.chain
2137
+ .map(command => command.name)
2138
+ .filter((name) => Boolean(name));
2139
+ if (names.length > 0) {
2140
+ return names.join(' ');
2141
+ }
2142
+ return ctx.cmd.name ?? 'command';
2143
+ }
2144
+ function getCompletionShellState(ctx) {
2145
+ const host = ctx;
2146
+ host[COMPLETION_SHELL_STATE] ??= {};
2147
+ return host[COMPLETION_SHELL_STATE];
2148
+ }
2149
+ function registerCompletionShell(ctx, shell) {
2150
+ const state = getCompletionShellState(ctx);
2151
+ if (state.shell !== undefined && state.shell !== shell) {
2152
+ throw new CommanderError('OptionConflict', 'options "--bash", "--fish", and "--pwsh" are mutually exclusive', getCommandPath(ctx));
2153
+ }
2154
+ state.shell = shell;
2155
+ }
2156
+ function mustGetCompletionShell(ctx) {
2157
+ const state = getCompletionShellState(ctx);
2158
+ if (state.shell === undefined) {
2159
+ throw new CommanderError('MissingRequired', 'missing required option: one of "--bash", "--fish", or "--pwsh"', getCommandPath(ctx));
2160
+ }
2161
+ return state.shell;
2162
+ }
1778
2163
  class CompletionCommand extends Command {
1779
2164
  constructor(root, config = {}) {
1780
2165
  const programName = config.programName ?? root.name ?? 'program';
@@ -1788,45 +2173,45 @@ class CompletionCommand extends Command {
1788
2173
  type: 'boolean',
1789
2174
  args: 'none',
1790
2175
  desc: 'Generate Bash completion script',
2176
+ apply: (value, ctx) => {
2177
+ if (value === true) {
2178
+ registerCompletionShell(ctx, 'bash');
2179
+ }
2180
+ },
1791
2181
  })
1792
2182
  .option({
1793
2183
  long: 'fish',
1794
2184
  type: 'boolean',
1795
2185
  args: 'none',
1796
2186
  desc: 'Generate Fish completion script',
2187
+ apply: (value, ctx) => {
2188
+ if (value === true) {
2189
+ registerCompletionShell(ctx, 'fish');
2190
+ }
2191
+ },
1797
2192
  })
1798
2193
  .option({
1799
2194
  long: 'pwsh',
1800
2195
  type: 'boolean',
1801
2196
  args: 'none',
1802
2197
  desc: 'Generate PowerShell completion script',
2198
+ apply: (value, ctx) => {
2199
+ if (value === true) {
2200
+ registerCompletionShell(ctx, 'pwsh');
2201
+ }
2202
+ mustGetCompletionShell(ctx);
2203
+ },
1803
2204
  })
1804
2205
  .option({
1805
2206
  long: 'write',
1806
2207
  short: 'w',
1807
2208
  type: 'string',
1808
- args: 'required',
1809
- desc: 'Write to file (use shell default path if empty)',
1810
- default: undefined,
2209
+ args: 'optional',
2210
+ desc: 'Write to file (use shell default path when value is omitted or empty)',
1811
2211
  })
1812
- .action(({ opts }) => {
2212
+ .action(({ opts, ctx }) => {
1813
2213
  const meta = root.getCompletionMeta();
1814
- const selectedShells = [
1815
- opts['bash'] && 'bash',
1816
- opts['fish'] && 'fish',
1817
- opts['pwsh'] && 'pwsh',
1818
- ].filter(Boolean);
1819
- if (selectedShells.length === 0) {
1820
- console.error('Please specify a shell: --bash, --fish, or --pwsh');
1821
- process.exit(1);
1822
- return;
1823
- }
1824
- if (selectedShells.length > 1) {
1825
- console.error('Please specify only one shell option');
1826
- process.exit(1);
1827
- return;
1828
- }
1829
- const shell = selectedShells[0];
2214
+ const shell = mustGetCompletionShell(ctx);
1830
2215
  let script;
1831
2216
  switch (shell) {
1832
2217
  case 'bash':
@@ -1839,8 +2224,9 @@ class CompletionCommand extends Command {
1839
2224
  script = new PwshCompletion(meta, programName).generate();
1840
2225
  break;
1841
2226
  }
1842
- const writeOpt = opts['write'];
1843
- if (writeOpt !== undefined) {
2227
+ const hasWrite = Object.prototype.hasOwnProperty.call(opts, 'write');
2228
+ if (hasWrite) {
2229
+ const writeOpt = opts['write'];
1844
2230
  const filePath = typeof writeOpt === 'string' && writeOpt !== '' ? writeOpt : paths[shell];
1845
2231
  const expandedPath = expandHome(filePath);
1846
2232
  const dir = path.dirname(expandedPath);
@@ -1885,6 +2271,7 @@ class BashCompletion {
1885
2271
  '',
1886
2272
  `${funcName}() {`,
1887
2273
  ' local cur prev words cword',
2274
+ ' local opts arg_choices prefer_value_choices',
1888
2275
  ' _init_completion || return',
1889
2276
  '',
1890
2277
  ...this.#generateCommandCase(this.#meta, 1),
@@ -1904,13 +2291,15 @@ class BashCompletion {
1904
2291
  for (const opt of cmd.options) {
1905
2292
  const kebabLong = camelToKebabCase(opt.long);
1906
2293
  if (opt.short)
1907
- optParts.push(`-${opt.short}`);
1908
- optParts.push(`--${kebabLong}`);
2294
+ optParts.push(this.#escapeWord(`-${opt.short}`));
2295
+ optParts.push(this.#escapeWord(`--${kebabLong}`));
1909
2296
  if (!opt.takesValue) {
1910
- optParts.push(`--no-${kebabLong}`);
2297
+ optParts.push(this.#escapeWord(`--no-${kebabLong}`));
1911
2298
  }
1912
2299
  }
1913
- const subParts = cmd.subcommands.flatMap(sub => [sub.name, ...sub.aliases]);
2300
+ const subParts = cmd.subcommands
2301
+ .flatMap(sub => [sub.name, ...sub.aliases])
2302
+ .map(value => this.#escapeWord(value));
1914
2303
  const allOpts = [...optParts, ...subParts].join(' ');
1915
2304
  if (cmd.subcommands.length > 0) {
1916
2305
  lines.push(`${indent}case "\${words[${depth}]}" in`);
@@ -1922,14 +2311,105 @@ class BashCompletion {
1922
2311
  }
1923
2312
  lines.push(`${indent} *)`);
1924
2313
  lines.push(`${indent} opts="${allOpts}"`);
2314
+ this.#appendChoiceLogicForCommand(lines, `${indent} `, cmd, depth);
1925
2315
  lines.push(`${indent} ;;`);
1926
2316
  lines.push(`${indent}esac`);
1927
2317
  }
1928
2318
  else {
1929
2319
  lines.push(`${indent}opts="${allOpts}"`);
2320
+ this.#appendChoiceLogicForCommand(lines, indent, cmd, depth);
1930
2321
  }
1931
2322
  return lines;
1932
2323
  }
2324
+ #serializeWordList(words) {
2325
+ return words.map(choice => this.#escapeWord(choice)).join(' ');
2326
+ }
2327
+ #appendChoiceLogicForCommand(lines, indent, cmd, depth) {
2328
+ const valueOptions = cmd.options.filter(opt => opt.takesValue);
2329
+ const valueOptionsWithChoices = valueOptions.filter(opt => opt.choices && opt.choices.length > 0);
2330
+ const valueLongPatterns = valueOptions.map(opt => `--${camelToKebabCase(opt.long)}`);
2331
+ const valueShortPatterns = valueOptions
2332
+ .map(opt => opt.short)
2333
+ .filter((short) => typeof short === 'string');
2334
+ lines.push(`${indent}prefer_value_choices=0`);
2335
+ if (valueOptionsWithChoices.length > 0) {
2336
+ lines.push(`${indent}if [[ "$cur" != -* ]]; then`);
2337
+ lines.push(`${indent} case "$prev" in`);
2338
+ for (const opt of valueOptionsWithChoices) {
2339
+ const patterns = [`--${camelToKebabCase(opt.long)}`];
2340
+ if (opt.short) {
2341
+ patterns.push(`-${opt.short}`);
2342
+ }
2343
+ lines.push(`${indent} ${patterns.join('|')})`);
2344
+ lines.push(`${indent} opts="${this.#serializeWordList(opt.choices ?? [])}"`);
2345
+ lines.push(`${indent} prefer_value_choices=1`);
2346
+ lines.push(`${indent} ;;`);
2347
+ }
2348
+ lines.push(`${indent} esac`);
2349
+ lines.push(`${indent}fi`);
2350
+ }
2351
+ lines.push(`${indent}if [[ $prefer_value_choices -eq 0 ]]; then`);
2352
+ lines.push(`${indent} positional_count=0`);
2353
+ lines.push(`${indent} expect_value=0`);
2354
+ lines.push(`${indent} for ((idx=${depth}; idx<cword; idx++)); do`);
2355
+ lines.push(`${indent} token="\${words[idx]}"`);
2356
+ lines.push(`${indent} if [[ $expect_value -eq 1 ]]; then`);
2357
+ lines.push(`${indent} expect_value=0`);
2358
+ lines.push(`${indent} continue`);
2359
+ lines.push(`${indent} fi`);
2360
+ lines.push(`${indent} if [[ "$token" == --* ]]; then`);
2361
+ lines.push(`${indent} if [[ "$token" == *=* ]]; then`);
2362
+ lines.push(`${indent} continue`);
2363
+ lines.push(`${indent} fi`);
2364
+ if (valueLongPatterns.length > 0) {
2365
+ lines.push(`${indent} case "$token" in`);
2366
+ lines.push(`${indent} ${valueLongPatterns.join('|')}) expect_value=1 ;;`);
2367
+ lines.push(`${indent} esac`);
2368
+ }
2369
+ lines.push(`${indent} continue`);
2370
+ lines.push(`${indent} fi`);
2371
+ lines.push(`${indent} if [[ "$token" == -* && "$token" != "-" ]]; then`);
2372
+ lines.push(`${indent} if [[ \${#token} -eq 2 ]]; then`);
2373
+ if (valueShortPatterns.length > 0) {
2374
+ lines.push(`${indent} case "\${token:1:1}" in`);
2375
+ lines.push(`${indent} ${valueShortPatterns.join('|')}) expect_value=1 ;;`);
2376
+ lines.push(`${indent} esac`);
2377
+ }
2378
+ lines.push(`${indent} fi`);
2379
+ lines.push(`${indent} continue`);
2380
+ lines.push(`${indent} fi`);
2381
+ lines.push(`${indent} positional_count=$((positional_count + 1))`);
2382
+ lines.push(`${indent} done`);
2383
+ lines.push(`${indent} if [[ $expect_value -eq 1 ]]; then`);
2384
+ lines.push(`${indent} opts=""`);
2385
+ lines.push(`${indent} prefer_value_choices=1`);
2386
+ lines.push(`${indent} elif [[ "$cur" != -* ]]; then`);
2387
+ lines.push(`${indent} arg_slot=-1`);
2388
+ lines.push(`${indent} arg_count=${cmd.arguments.length}`);
2389
+ const hasRestArgument = cmd.arguments.length > 0 &&
2390
+ (cmd.arguments[cmd.arguments.length - 1].kind === 'variadic' ||
2391
+ cmd.arguments[cmd.arguments.length - 1].kind === 'some');
2392
+ lines.push(`${indent} has_rest=${hasRestArgument ? 1 : 0}`);
2393
+ lines.push(`${indent} if [[ $has_rest -eq 1 && $positional_count -ge $((arg_count - 1)) ]]; then`);
2394
+ lines.push(`${indent} arg_slot=$((arg_count - 1))`);
2395
+ lines.push(`${indent} elif [[ $positional_count -lt $arg_count ]]; then`);
2396
+ lines.push(`${indent} arg_slot=$positional_count`);
2397
+ lines.push(`${indent} fi`);
2398
+ lines.push(`${indent} case "$arg_slot" in`);
2399
+ for (let index = 0; index < cmd.arguments.length; index += 1) {
2400
+ const arg = cmd.arguments[index];
2401
+ if (arg.type !== 'choice' || !arg.choices || arg.choices.length === 0) {
2402
+ continue;
2403
+ }
2404
+ lines.push(`${indent} ${index}) opts="${this.#serializeWordList(arg.choices)}" ;;`);
2405
+ }
2406
+ lines.push(`${indent} esac`);
2407
+ lines.push(`${indent} fi`);
2408
+ lines.push(`${indent}fi`);
2409
+ }
2410
+ #escapeWord(word) {
2411
+ return word.replace(/([\\\s'"`$!])/g, '\\$1');
2412
+ }
1933
2413
  #sanitizeName(name) {
1934
2414
  return name.replace(/[^a-zA-Z0-9]/g, '_');
1935
2415
  }
@@ -1937,15 +2417,19 @@ class BashCompletion {
1937
2417
  class FishCompletion {
1938
2418
  #meta;
1939
2419
  #programName;
2420
+ #slotMatcherName;
1940
2421
  constructor(meta, programName) {
1941
2422
  this.#meta = meta;
1942
2423
  this.#programName = programName;
2424
+ this.#slotMatcherName = `__${this.#sanitizeName(programName)}_match_arg_slot`;
1943
2425
  }
1944
2426
  generate() {
1945
2427
  const lines = [
1946
2428
  `# Fish completion for ${this.#programName}`,
1947
2429
  '# Generated by @guanghechen/commander',
1948
2430
  '',
2431
+ ...this.#generateSlotMatcherFunction(),
2432
+ '',
1949
2433
  ...this.#generateCommandCompletions(this.#meta, []),
1950
2434
  '',
1951
2435
  ];
@@ -1965,7 +2449,7 @@ class FishCompletion {
1965
2449
  line += ` -l ${kebabLong}`;
1966
2450
  line += ` -d '${this.#escape(opt.desc)}'`;
1967
2451
  if (opt.choices && opt.choices.length > 0) {
1968
- line += ` -xa '${opt.choices.join(' ')}'`;
2452
+ line += ` -xa '${opt.choices.map(choice => this.#escapeChoice(choice)).join(' ')}'`;
1969
2453
  }
1970
2454
  lines.push(line);
1971
2455
  if (!opt.takesValue) {
@@ -1977,6 +2461,36 @@ class FishCompletion {
1977
2461
  lines.push(noLine);
1978
2462
  }
1979
2463
  }
2464
+ const valueOptionLongs = cmd.options
2465
+ .filter(opt => opt.takesValue)
2466
+ .map(opt => camelToKebabCase(opt.long))
2467
+ .join(',');
2468
+ const valueOptionShorts = cmd.options
2469
+ .filter(opt => opt.takesValue && opt.short)
2470
+ .map(opt => opt.short)
2471
+ .join(',');
2472
+ const argCount = cmd.arguments.length;
2473
+ const hasRestArgument = argCount > 0 &&
2474
+ (cmd.arguments[argCount - 1].kind === 'variadic' ||
2475
+ cmd.arguments[argCount - 1].kind === 'some');
2476
+ for (let index = 0; index < cmd.arguments.length; index += 1) {
2477
+ const arg = cmd.arguments[index];
2478
+ if (arg.type !== 'choice' || !arg.choices || arg.choices.length === 0) {
2479
+ continue;
2480
+ }
2481
+ let line = `complete -c ${this.#programName}`;
2482
+ const slotCondition = `${this.#slotMatcherName} ${parentPath.length} ${argCount} ${hasRestArgument ? 1 : 0} ${index} '${valueOptionLongs}' '${valueOptionShorts}'`;
2483
+ if (condition) {
2484
+ line += ` -n '${condition}; and ${slotCondition}'`;
2485
+ }
2486
+ else {
2487
+ line += ` -n '${slotCondition}'`;
2488
+ }
2489
+ line += ` -f`;
2490
+ line += ` -a '${arg.choices.map(choice => this.#escapeChoice(choice)).join(' ')}'`;
2491
+ line += ` -d '${this.#escape(`Argument: ${arg.name}`)}'`;
2492
+ lines.push(line);
2493
+ }
1980
2494
  for (const sub of cmd.subcommands) {
1981
2495
  let line = `complete -c ${this.#programName}`;
1982
2496
  if (isRoot) {
@@ -2000,7 +2514,7 @@ class FishCompletion {
2000
2514
  aliasLine += ` -d 'Alias for ${sub.name}'`;
2001
2515
  lines.push(aliasLine);
2002
2516
  }
2003
- const newPath = [...parentPath, sub.name];
2517
+ const newPath = [...parentPath, [sub.name, ...sub.aliases]];
2004
2518
  lines.push(...this.#generateCommandCompletions(sub, newPath));
2005
2519
  }
2006
2520
  return lines;
@@ -2008,7 +2522,7 @@ class FishCompletion {
2008
2522
  #buildCondition(path) {
2009
2523
  if (path.length === 0)
2010
2524
  return '';
2011
- return `__fish_seen_subcommand_from ${path[path.length - 1]}`;
2525
+ return path.map(level => `__fish_seen_subcommand_from ${level.join(' ')}`).join('; and ');
2012
2526
  }
2013
2527
  #getSubcommandNames(cmd) {
2014
2528
  return cmd.subcommands.flatMap(sub => [sub.name, ...sub.aliases]);
@@ -2016,6 +2530,68 @@ class FishCompletion {
2016
2530
  #escape(s) {
2017
2531
  return s.replace(/'/g, "\\'");
2018
2532
  }
2533
+ #escapeChoice(s) {
2534
+ return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\s/g, '\\ ');
2535
+ }
2536
+ #sanitizeName(name) {
2537
+ return name.replace(/[^a-zA-Z0-9]/g, '_');
2538
+ }
2539
+ #generateSlotMatcherFunction() {
2540
+ return [
2541
+ `function ${this.#slotMatcherName} --argument-names depth arg_count has_rest target_index long_opts short_opts`,
2542
+ ' set -l tokens (commandline -opc)',
2543
+ ' set -l start (math $depth + 2)',
2544
+ ' set -l positional 0',
2545
+ ' set -l expect_value 0',
2546
+ ' set -l i $start',
2547
+ ' set -l token_count (count $tokens)',
2548
+ ' set -l long_list (string split "," -- $long_opts)',
2549
+ ' set -l short_list (string split "," -- $short_opts)',
2550
+ ' while test $i -le $token_count',
2551
+ ' set -l token $tokens[$i]',
2552
+ ' if test $expect_value -eq 1',
2553
+ ' set expect_value 0',
2554
+ ' set i (math $i + 1)',
2555
+ ' continue',
2556
+ ' end',
2557
+ ' if string match -q -- "--*" $token',
2558
+ ' if string match -q -- "*=*" $token',
2559
+ ' set i (math $i + 1)',
2560
+ ' continue',
2561
+ ' end',
2562
+ ' set -l opt_name (string replace -r "^--" "" -- $token)',
2563
+ ' if contains -- $opt_name $long_list',
2564
+ ' set expect_value 1',
2565
+ ' end',
2566
+ ' set i (math $i + 1)',
2567
+ ' continue',
2568
+ ' end',
2569
+ ' if test "$token" != "-"; and string match -q -- "-*" $token',
2570
+ ' set -l raw_short (string replace -r "^-" "" -- $token)',
2571
+ ' if test (string length -- $raw_short) -eq 1',
2572
+ ' if contains -- $raw_short $short_list',
2573
+ ' set expect_value 1',
2574
+ ' end',
2575
+ ' end',
2576
+ ' set i (math $i + 1)',
2577
+ ' continue',
2578
+ ' end',
2579
+ ' set positional (math $positional + 1)',
2580
+ ' set i (math $i + 1)',
2581
+ ' end',
2582
+ ' if test $expect_value -eq 1',
2583
+ ' return 1',
2584
+ ' end',
2585
+ ' set -l slot -1',
2586
+ ' if test $has_rest -eq 1; and test $positional -ge (math $arg_count - 1)',
2587
+ ' set slot (math $arg_count - 1)',
2588
+ ' else if test $positional -lt $arg_count',
2589
+ ' set slot $positional',
2590
+ ' end',
2591
+ ' test $slot -eq $target_index',
2592
+ 'end',
2593
+ ];
2594
+ }
2019
2595
  }
2020
2596
  class PwshCompletion {
2021
2597
  #meta;
@@ -2041,16 +2617,105 @@ class PwshCompletion {
2041
2617
  '',
2042
2618
  ' # Find current command context',
2043
2619
  ' $cmd = $commands',
2620
+ ' $commandDepth = 1',
2044
2621
  ' foreach ($word in $words[1..($words.Count - 1)]) {',
2045
2622
  ' if ($word.StartsWith("-")) { continue }',
2046
2623
  ' if ($cmd.subcommands -and $cmd.subcommands.ContainsKey($word)) {',
2047
2624
  ' $cmd = $cmd.subcommands[$word]',
2625
+ ' $commandDepth += 1',
2048
2626
  ' }',
2049
2627
  ' }',
2050
2628
  '',
2051
2629
  ' # Generate completions',
2052
2630
  ' $completions = @()',
2053
2631
  '',
2632
+ ' # Option value slot (always higher priority than arguments)',
2633
+ ' $previous = if ($words.Count -ge 2) { $words[$words.Count - 2] } else { $null }',
2634
+ ' if ($previous) {',
2635
+ ' foreach ($opt in $cmd.options) {',
2636
+ ' $isLong = $previous -eq "--$($opt.long)"',
2637
+ ' $isShort = $opt.short -and $previous -eq "-$($opt.short)"',
2638
+ ' if ($isLong -or $isShort) {',
2639
+ ' if ($opt.choices) {',
2640
+ ' foreach ($choice in $opt.choices) {',
2641
+ ' if ($choice -like "$current*") {',
2642
+ ' $completions += [System.Management.Automation.CompletionResult]::new(',
2643
+ ' $choice,',
2644
+ ' $choice,',
2645
+ ' "ParameterValue",',
2646
+ ' $choice',
2647
+ ' )',
2648
+ ' }',
2649
+ ' }',
2650
+ ' }',
2651
+ ' return $completions',
2652
+ ' }',
2653
+ ' }',
2654
+ ' }',
2655
+ '',
2656
+ ' # Determine argument slot',
2657
+ ' $positionalCount = 0',
2658
+ ' $expectValue = $false',
2659
+ ' for ($i = $commandDepth; $i -lt ($words.Count - 1); $i += 1) {',
2660
+ ' $token = $words[$i]',
2661
+ ' if ($expectValue) {',
2662
+ ' $expectValue = $false',
2663
+ ' continue',
2664
+ ' }',
2665
+ ' if ($token.StartsWith("--")) {',
2666
+ ' if ($token.Contains("=")) { continue }',
2667
+ ' foreach ($opt in $cmd.options) {',
2668
+ ' if ($token -eq "--$($opt.long)" -and $opt.takesValue) {',
2669
+ ' $expectValue = $true',
2670
+ ' break',
2671
+ ' }',
2672
+ ' }',
2673
+ ' continue',
2674
+ ' }',
2675
+ ' if ($token.StartsWith("-") -and $token -ne "-") {',
2676
+ ' if ($token.Length -eq 2) {',
2677
+ ' foreach ($opt in $cmd.options) {',
2678
+ ' if ($opt.short -and $token -eq "-$($opt.short)" -and $opt.takesValue) {',
2679
+ ' $expectValue = $true',
2680
+ ' break',
2681
+ ' }',
2682
+ ' }',
2683
+ ' }',
2684
+ ' continue',
2685
+ ' }',
2686
+ ' $positionalCount += 1',
2687
+ ' }',
2688
+ ' if ($expectValue) {',
2689
+ ' return $completions',
2690
+ ' }',
2691
+ ' if (-not $current.StartsWith("-") -and $cmd.arguments -and $cmd.arguments.Count -gt 0) {',
2692
+ ' $argSlot = -1',
2693
+ ' $argCount = $cmd.arguments.Count',
2694
+ ' $lastArg = $cmd.arguments[$argCount - 1]',
2695
+ ' $hasRest = $lastArg.kind -eq "variadic" -or $lastArg.kind -eq "some"',
2696
+ ' if ($hasRest -and $positionalCount -ge ($argCount - 1)) {',
2697
+ ' $argSlot = $argCount - 1',
2698
+ ' } elseif ($positionalCount -lt $argCount) {',
2699
+ ' $argSlot = $positionalCount',
2700
+ ' }',
2701
+ ' if ($argSlot -ge 0) {',
2702
+ ' $argMeta = $cmd.arguments[$argSlot]',
2703
+ ' if ($argMeta.choices) {',
2704
+ ' foreach ($choice in $argMeta.choices) {',
2705
+ ' if ($choice -like "$current*") {',
2706
+ ' $completions += [System.Management.Automation.CompletionResult]::new(',
2707
+ ' $choice,',
2708
+ ' $choice,',
2709
+ ' "ParameterValue",',
2710
+ ' $choice',
2711
+ ' )',
2712
+ ' }',
2713
+ ' }',
2714
+ ' return $completions',
2715
+ ' }',
2716
+ ' }',
2717
+ ' }',
2718
+ '',
2054
2719
  ' # Options',
2055
2720
  ' if ($current.StartsWith("-")) {',
2056
2721
  ' foreach ($opt in $cmd.options) {',
@@ -2059,7 +2724,7 @@ class PwshCompletion {
2059
2724
  ' "--$($opt.long)",',
2060
2725
  ' $opt.long,',
2061
2726
  ' "ParameterName",',
2062
- ' $opt.desc',
2727
+ ' $opt.description',
2063
2728
  ' )',
2064
2729
  ' }',
2065
2730
  ' if ($opt.isBoolean -and "--no-$($opt.long)" -like "$current*") {',
@@ -2067,7 +2732,7 @@ class PwshCompletion {
2067
2732
  ' "--no-$($opt.long)",',
2068
2733
  ' "no-$($opt.long)",',
2069
2734
  ' "ParameterName",',
2070
- ' $opt.desc',
2735
+ ' $opt.description',
2071
2736
  ' )',
2072
2737
  ' }',
2073
2738
  ' if ($opt.short -and "-$($opt.short)" -like "$current*") {',
@@ -2075,7 +2740,7 @@ class PwshCompletion {
2075
2740
  ' "-$($opt.short)",',
2076
2741
  ' $opt.short,',
2077
2742
  ' "ParameterName",',
2078
- ' $opt.desc',
2743
+ ' $opt.description',
2079
2744
  ' )',
2080
2745
  ' }',
2081
2746
  ' }',
@@ -2089,7 +2754,7 @@ class PwshCompletion {
2089
2754
  ' $sub,',
2090
2755
  ' $sub,',
2091
2756
  ' "Command",',
2092
- ' $cmd.subcommands[$sub].desc',
2757
+ ' $cmd.subcommands[$sub].description',
2093
2758
  ' )',
2094
2759
  ' }',
2095
2760
  ' }',
@@ -2113,8 +2778,25 @@ class PwshCompletion {
2113
2778
  lines.push(`${indent} long = '${kebabLong}'`);
2114
2779
  lines.push(`${indent} description = '${this.#escape(opt.desc)}'`);
2115
2780
  lines.push(`${indent} isBoolean = $${!opt.takesValue}`);
2781
+ lines.push(`${indent} takesValue = $${opt.takesValue}`);
2116
2782
  if (opt.choices) {
2117
- lines.push(`${indent} choices = @('${opt.choices.join("', '")}')`);
2783
+ lines.push(`${indent} choices = @('${opt.choices
2784
+ .map(choice => this.#escape(choice))
2785
+ .join("', '")}')`);
2786
+ }
2787
+ lines.push(`${indent} }`);
2788
+ }
2789
+ lines.push(`${indent})`);
2790
+ lines.push(`${indent}arguments = @(`);
2791
+ for (const arg of cmd.arguments) {
2792
+ lines.push(`${indent} @{`);
2793
+ lines.push(`${indent} name = '${this.#escape(arg.name)}'`);
2794
+ lines.push(`${indent} kind = '${arg.kind}'`);
2795
+ lines.push(`${indent} type = '${arg.type}'`);
2796
+ if (arg.choices && arg.choices.length > 0) {
2797
+ lines.push(`${indent} choices = @('${arg.choices
2798
+ .map(choice => this.#escape(choice))
2799
+ .join("', '")}')`);
2118
2800
  }
2119
2801
  lines.push(`${indent} }`);
2120
2802
  }