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