@guanghechen/commander 4.7.0 → 4.7.2
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/CHANGELOG.md +14 -0
- package/lib/cjs/browser.cjs +290 -42
- package/lib/cjs/index.cjs +2089 -0
- package/lib/cjs/node.cjs +598 -54
- package/lib/esm/browser.mjs +290 -42
- package/lib/esm/index.mjs +2054 -0
- package/lib/esm/node.mjs +598 -54
- package/lib/types/browser.d.ts +27 -6
- package/lib/types/index.d.ts +560 -0
- package/lib/types/node.d.ts +27 -6
- package/package.json +3 -3
package/lib/cjs/node.cjs
CHANGED
|
@@ -197,6 +197,116 @@ 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
|
+
}
|
|
200
310
|
function tokenizeLongOption(arg, commandPath) {
|
|
201
311
|
const eqIdx = arg.indexOf('=');
|
|
202
312
|
const namePart = eqIdx !== -1 ? arg.slice(0, eqIdx) : arg;
|
|
@@ -433,9 +543,13 @@ class Command {
|
|
|
433
543
|
if (cmd.#parent && cmd.#parent !== this) {
|
|
434
544
|
throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
|
|
435
545
|
}
|
|
546
|
+
const occupied = this.#subcommandsMap.get(name);
|
|
547
|
+
if (occupied && occupied !== cmd) {
|
|
548
|
+
throw new CommanderError('ConfigurationError', `subcommand name/alias "${name}" conflicts with an existing command`, this.#getCommandPath());
|
|
549
|
+
}
|
|
436
550
|
const existing = this.#subcommandsList.find(e => e.command === cmd);
|
|
437
551
|
if (existing) {
|
|
438
|
-
if (existing.aliases.includes(name)) {
|
|
552
|
+
if (existing.name === name || existing.aliases.includes(name)) {
|
|
439
553
|
return this;
|
|
440
554
|
}
|
|
441
555
|
existing.aliases.push(name);
|
|
@@ -564,10 +678,32 @@ class Command {
|
|
|
564
678
|
else if (arg.kind === 'optional') {
|
|
565
679
|
usage += ` [${arg.name}]`;
|
|
566
680
|
}
|
|
681
|
+
else if (arg.kind === 'some') {
|
|
682
|
+
usage += ` <${arg.name}...>`;
|
|
683
|
+
}
|
|
567
684
|
else {
|
|
568
685
|
usage += ` [${arg.name}...]`;
|
|
569
686
|
}
|
|
570
687
|
}
|
|
688
|
+
const argumentsLines = [];
|
|
689
|
+
for (const arg of this.#arguments) {
|
|
690
|
+
const sig = arg.kind === 'required'
|
|
691
|
+
? `<${arg.name}>`
|
|
692
|
+
: arg.kind === 'optional'
|
|
693
|
+
? `[${arg.name}]`
|
|
694
|
+
: arg.kind === 'some'
|
|
695
|
+
? `<${arg.name}...>`
|
|
696
|
+
: `[${arg.name}...]`;
|
|
697
|
+
const metadata = [`[type: ${arg.type}]`];
|
|
698
|
+
if (arg.kind === 'optional' && arg.default !== undefined) {
|
|
699
|
+
metadata.push(`[default: ${JSON.stringify(arg.default)}]`);
|
|
700
|
+
}
|
|
701
|
+
if (arg.choices && arg.choices.length > 0) {
|
|
702
|
+
metadata.push(`[choices: ${arg.choices.map(choice => JSON.stringify(choice)).join(', ')}]`);
|
|
703
|
+
}
|
|
704
|
+
const desc = metadata.length > 0 ? `${arg.desc} ${metadata.join(' ')}` : arg.desc;
|
|
705
|
+
argumentsLines.push({ sig, desc });
|
|
706
|
+
}
|
|
571
707
|
const options = [];
|
|
572
708
|
for (const opt of allOptions) {
|
|
573
709
|
const kebabLong = camelToKebabCase$1(opt.long);
|
|
@@ -581,7 +717,7 @@ class Command {
|
|
|
581
717
|
desc += ` (default: ${JSON.stringify(opt.default)})`;
|
|
582
718
|
}
|
|
583
719
|
if (opt.choices) {
|
|
584
|
-
desc += ` [choices: ${opt.choices.join(', ')}]`;
|
|
720
|
+
desc += ` [choices: ${opt.choices.map(choice => JSON.stringify(choice)).join(', ')}]`;
|
|
585
721
|
}
|
|
586
722
|
options.push({ sig, desc });
|
|
587
723
|
if (opt.type === 'boolean' &&
|
|
@@ -613,6 +749,7 @@ class Command {
|
|
|
613
749
|
return {
|
|
614
750
|
desc: this.#desc,
|
|
615
751
|
usage,
|
|
752
|
+
arguments: argumentsLines,
|
|
616
753
|
options,
|
|
617
754
|
commands,
|
|
618
755
|
examples,
|
|
@@ -620,25 +757,29 @@ class Command {
|
|
|
620
757
|
}
|
|
621
758
|
#renderHelpPlain(helpData) {
|
|
622
759
|
const lines = [];
|
|
760
|
+
const labelWidth = this.#getHelpLabelWidth(helpData);
|
|
623
761
|
lines.push(helpData.desc);
|
|
624
762
|
lines.push('');
|
|
625
763
|
lines.push(helpData.usage);
|
|
626
764
|
lines.push('');
|
|
765
|
+
if (helpData.arguments.length > 0) {
|
|
766
|
+
lines.push('Arguments:');
|
|
767
|
+
for (const { sig, desc } of helpData.arguments) {
|
|
768
|
+
lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth));
|
|
769
|
+
}
|
|
770
|
+
lines.push('');
|
|
771
|
+
}
|
|
627
772
|
if (helpData.options.length > 0) {
|
|
628
773
|
lines.push('Options:');
|
|
629
|
-
const maxSigLen = Math.max(...helpData.options.map(line => line.sig.length));
|
|
630
774
|
for (const { sig, desc } of helpData.options) {
|
|
631
|
-
|
|
632
|
-
lines.push(` ${sig}${padding}${desc}`);
|
|
775
|
+
lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth));
|
|
633
776
|
}
|
|
634
777
|
lines.push('');
|
|
635
778
|
}
|
|
636
779
|
if (helpData.commands.length > 0) {
|
|
637
780
|
lines.push('Commands:');
|
|
638
|
-
const maxNameLen = Math.max(...helpData.commands.map(line => line.name.length));
|
|
639
781
|
for (const { name, desc } of helpData.commands) {
|
|
640
|
-
|
|
641
|
-
lines.push(` ${name}${padding}${desc}`);
|
|
782
|
+
lines.push(this.#renderAlignedHelpLine(name, desc, labelWidth));
|
|
642
783
|
}
|
|
643
784
|
lines.push('');
|
|
644
785
|
}
|
|
@@ -655,25 +796,29 @@ class Command {
|
|
|
655
796
|
}
|
|
656
797
|
#renderHelpTerminal(helpData) {
|
|
657
798
|
const lines = [];
|
|
799
|
+
const labelWidth = this.#getHelpLabelWidth(helpData);
|
|
658
800
|
lines.push(helpData.desc);
|
|
659
801
|
lines.push('');
|
|
660
802
|
lines.push(styleText(helpData.usage, TERMINAL_STYLE.bold));
|
|
661
803
|
lines.push('');
|
|
804
|
+
if (helpData.arguments.length > 0) {
|
|
805
|
+
lines.push(styleText('Arguments:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
|
|
806
|
+
for (const { sig, desc } of helpData.arguments) {
|
|
807
|
+
lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth, value => styleText(value, TERMINAL_STYLE.cyan)));
|
|
808
|
+
}
|
|
809
|
+
lines.push('');
|
|
810
|
+
}
|
|
662
811
|
if (helpData.options.length > 0) {
|
|
663
812
|
lines.push(styleText('Options:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
|
|
664
|
-
const maxSigLen = Math.max(...helpData.options.map(line => line.sig.length));
|
|
665
813
|
for (const { sig, desc } of helpData.options) {
|
|
666
|
-
|
|
667
|
-
lines.push(` ${styleText(sig, TERMINAL_STYLE.cyan)}${padding}${desc}`);
|
|
814
|
+
lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth, value => styleText(value, TERMINAL_STYLE.cyan)));
|
|
668
815
|
}
|
|
669
816
|
lines.push('');
|
|
670
817
|
}
|
|
671
818
|
if (helpData.commands.length > 0) {
|
|
672
819
|
lines.push(styleText('Commands:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
|
|
673
|
-
const maxNameLen = Math.max(...helpData.commands.map(line => line.name.length));
|
|
674
820
|
for (const { name, desc } of helpData.commands) {
|
|
675
|
-
|
|
676
|
-
lines.push(` ${styleText(name, TERMINAL_STYLE.cyan)}${padding}${desc}`);
|
|
821
|
+
lines.push(this.#renderAlignedHelpLine(name, desc, labelWidth, value => styleText(value, TERMINAL_STYLE.cyan)));
|
|
677
822
|
}
|
|
678
823
|
lines.push('');
|
|
679
824
|
}
|
|
@@ -688,16 +833,41 @@ class Command {
|
|
|
688
833
|
}
|
|
689
834
|
return lines.join('\n');
|
|
690
835
|
}
|
|
836
|
+
#getHelpLabelWidth(helpData) {
|
|
837
|
+
const labels = [
|
|
838
|
+
...helpData.arguments.map(line => line.sig),
|
|
839
|
+
...helpData.options.map(line => line.sig),
|
|
840
|
+
...helpData.commands.map(line => line.name),
|
|
841
|
+
];
|
|
842
|
+
if (labels.length === 0) {
|
|
843
|
+
return 0;
|
|
844
|
+
}
|
|
845
|
+
return Math.max(...labels.map(getDisplayWidth));
|
|
846
|
+
}
|
|
847
|
+
#renderAlignedHelpLine(label, desc, labelWidth, styleLabel) {
|
|
848
|
+
const paddedLabel = padDisplayEnd(label, labelWidth);
|
|
849
|
+
const outputLabel = styleLabel ? styleLabel(paddedLabel) : paddedLabel;
|
|
850
|
+
return ` ${outputLabel} ${desc}`;
|
|
851
|
+
}
|
|
691
852
|
getCompletionMeta() {
|
|
692
853
|
const allOptions = this.#resolveOptionPolicy().mergedOptions;
|
|
693
854
|
const options = [];
|
|
855
|
+
const argumentsMeta = [];
|
|
694
856
|
for (const opt of allOptions) {
|
|
695
857
|
options.push({
|
|
696
858
|
long: opt.long,
|
|
697
859
|
short: opt.short,
|
|
698
860
|
desc: opt.desc,
|
|
699
861
|
takesValue: opt.args !== 'none',
|
|
700
|
-
choices: opt.choices,
|
|
862
|
+
choices: opt.choices?.map(choice => String(choice)),
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
for (const arg of this.#arguments) {
|
|
866
|
+
argumentsMeta.push({
|
|
867
|
+
name: arg.name,
|
|
868
|
+
kind: arg.kind,
|
|
869
|
+
type: arg.type,
|
|
870
|
+
choices: arg.type === 'choice' ? arg.choices?.map(choice => String(choice)) : undefined,
|
|
701
871
|
});
|
|
702
872
|
}
|
|
703
873
|
return {
|
|
@@ -705,6 +875,7 @@ class Command {
|
|
|
705
875
|
desc: this.#desc,
|
|
706
876
|
aliases: [],
|
|
707
877
|
options,
|
|
878
|
+
arguments: argumentsMeta,
|
|
708
879
|
subcommands: this.#subcommandsList.map(entry => {
|
|
709
880
|
const subMeta = entry.command.getCompletionMeta();
|
|
710
881
|
return {
|
|
@@ -1350,8 +1521,8 @@ class Command {
|
|
|
1350
1521
|
return opt.coerce(rawValue);
|
|
1351
1522
|
}
|
|
1352
1523
|
if (opt.type === 'number') {
|
|
1353
|
-
const num =
|
|
1354
|
-
if (
|
|
1524
|
+
const num = parsePrimitiveNumber(rawValue);
|
|
1525
|
+
if (num === undefined) {
|
|
1355
1526
|
throw new CommanderError('InvalidType', `invalid number "${rawValue}" for option "--${camelToKebabCase$1(opt.long)}"`, this.#getCommandPath());
|
|
1356
1527
|
}
|
|
1357
1528
|
return num;
|
|
@@ -1361,12 +1532,34 @@ class Command {
|
|
|
1361
1532
|
#parseArguments(rawArgs) {
|
|
1362
1533
|
const argumentDefs = this.#arguments;
|
|
1363
1534
|
const args = {};
|
|
1364
|
-
const
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1535
|
+
const missing = [];
|
|
1536
|
+
let remaining = rawArgs.length;
|
|
1537
|
+
for (const def of argumentDefs) {
|
|
1538
|
+
if (def.kind === 'required') {
|
|
1539
|
+
if (remaining === 0) {
|
|
1540
|
+
missing.push(def.name);
|
|
1541
|
+
}
|
|
1542
|
+
else {
|
|
1543
|
+
remaining -= 1;
|
|
1544
|
+
}
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
if (def.kind === 'optional') {
|
|
1548
|
+
if (remaining > 0) {
|
|
1549
|
+
remaining -= 1;
|
|
1550
|
+
}
|
|
1551
|
+
continue;
|
|
1552
|
+
}
|
|
1553
|
+
if (def.kind === 'some') {
|
|
1554
|
+
if (remaining === 0) {
|
|
1555
|
+
missing.push(def.name);
|
|
1556
|
+
}
|
|
1557
|
+
remaining = 0;
|
|
1558
|
+
continue;
|
|
1559
|
+
}
|
|
1560
|
+
remaining = 0;
|
|
1561
|
+
}
|
|
1562
|
+
if (missing.length > 0) {
|
|
1370
1563
|
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
|
|
1371
1564
|
}
|
|
1372
1565
|
let index = 0;
|
|
@@ -1377,47 +1570,65 @@ class Command {
|
|
|
1377
1570
|
index = rawArgs.length;
|
|
1378
1571
|
break;
|
|
1379
1572
|
}
|
|
1573
|
+
if (def.kind === 'some') {
|
|
1574
|
+
const rest = rawArgs.slice(index);
|
|
1575
|
+
if (rest.length === 0) {
|
|
1576
|
+
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${def.name}`, this.#getCommandPath());
|
|
1577
|
+
}
|
|
1578
|
+
args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
|
|
1579
|
+
index = rawArgs.length;
|
|
1580
|
+
break;
|
|
1581
|
+
}
|
|
1380
1582
|
const raw = rawArgs[index];
|
|
1381
1583
|
if (raw === undefined) {
|
|
1382
1584
|
if (def.kind === 'optional') {
|
|
1383
1585
|
args[def.name] = def.default ?? undefined;
|
|
1384
1586
|
continue;
|
|
1385
1587
|
}
|
|
1588
|
+
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${def.name}`, this.#getCommandPath());
|
|
1386
1589
|
}
|
|
1387
1590
|
else {
|
|
1388
1591
|
args[def.name] = this.#convertArgument(def, raw);
|
|
1389
1592
|
index += 1;
|
|
1390
1593
|
}
|
|
1391
1594
|
}
|
|
1392
|
-
const
|
|
1393
|
-
if (!
|
|
1595
|
+
const hasRestArgument = argumentDefs.some(a => a.kind === 'variadic' || a.kind === 'some');
|
|
1596
|
+
if (!hasRestArgument && index < rawArgs.length) {
|
|
1394
1597
|
throw new CommanderError('TooManyArguments', `too many arguments: expected ${argumentDefs.length}, got ${rawArgs.length}`, this.#getCommandPath());
|
|
1395
1598
|
}
|
|
1396
1599
|
return { args, rawArgs };
|
|
1397
1600
|
}
|
|
1398
1601
|
#convertArgument(def, raw) {
|
|
1602
|
+
let value;
|
|
1399
1603
|
if (def.coerce) {
|
|
1400
1604
|
try {
|
|
1401
|
-
|
|
1605
|
+
value = def.coerce(raw);
|
|
1402
1606
|
}
|
|
1403
1607
|
catch {
|
|
1404
1608
|
throw new CommanderError('InvalidType', `invalid value "${raw}" for argument "${def.name}"`, this.#getCommandPath());
|
|
1405
1609
|
}
|
|
1406
1610
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1611
|
+
else {
|
|
1612
|
+
value = raw;
|
|
1613
|
+
}
|
|
1614
|
+
if (typeof value !== 'string') {
|
|
1615
|
+
throw new CommanderError('InvalidType', `invalid value for argument "${def.name}": expected ${def.type}`, this.#getCommandPath());
|
|
1616
|
+
}
|
|
1617
|
+
if (def.type === 'choice') {
|
|
1618
|
+
const choices = def.choices ?? [];
|
|
1619
|
+
if (!choices.includes(value)) {
|
|
1620
|
+
throw new CommanderError('InvalidChoice', `invalid value "${value}" for argument "${def.name}". Allowed: ${choices
|
|
1621
|
+
.map(choice => JSON.stringify(choice))
|
|
1622
|
+
.join(', ')}`, this.#getCommandPath());
|
|
1411
1623
|
}
|
|
1412
|
-
return n;
|
|
1413
1624
|
}
|
|
1414
|
-
return
|
|
1625
|
+
return value;
|
|
1415
1626
|
}
|
|
1416
1627
|
#hasUserOption(long) {
|
|
1417
1628
|
return this.#options.some(option => option.long === long);
|
|
1418
1629
|
}
|
|
1419
1630
|
#supportsBuiltinVersion() {
|
|
1420
|
-
return this.#
|
|
1631
|
+
return this.#version !== undefined && this.#builtin.option.version;
|
|
1421
1632
|
}
|
|
1422
1633
|
#resolveOptionPolicy() {
|
|
1423
1634
|
const optionMap = new Map();
|
|
@@ -1497,6 +1708,9 @@ class Command {
|
|
|
1497
1708
|
if (!/^[a-z][a-zA-Z0-9]*$/.test(opt.long)) {
|
|
1498
1709
|
throw new CommanderError('ConfigurationError', `option long name must be camelCase: "${opt.long}"`, this.#getCommandPath());
|
|
1499
1710
|
}
|
|
1711
|
+
if (opt.short !== undefined && opt.short.length !== 1) {
|
|
1712
|
+
throw new CommanderError('ConfigurationError', `option short name must be a single character: "${opt.short}"`, this.#getCommandPath());
|
|
1713
|
+
}
|
|
1500
1714
|
if (opt.required && opt.default !== undefined) {
|
|
1501
1715
|
throw new CommanderError('ConfigurationError', `option "--${opt.long}" cannot be both required and have a default value`, this.#getCommandPath());
|
|
1502
1716
|
}
|
|
@@ -1513,24 +1727,58 @@ class Command {
|
|
|
1513
1727
|
}
|
|
1514
1728
|
}
|
|
1515
1729
|
#validateArgumentConfig(arg) {
|
|
1516
|
-
if (arg.kind
|
|
1517
|
-
|
|
1730
|
+
if (arg.kind !== 'required' &&
|
|
1731
|
+
arg.kind !== 'optional' &&
|
|
1732
|
+
arg.kind !== 'variadic' &&
|
|
1733
|
+
arg.kind !== 'some') {
|
|
1734
|
+
throw new CommanderError('ConfigurationError', `argument "${arg.name}" must specify a valid kind`, this.#getCommandPath());
|
|
1735
|
+
}
|
|
1736
|
+
if (arg.type !== 'string' && arg.type !== 'choice') {
|
|
1737
|
+
throw new CommanderError('ConfigurationError', `argument "${arg.name}" must specify a valid type`, this.#getCommandPath());
|
|
1738
|
+
}
|
|
1739
|
+
if (arg.default !== undefined && arg.kind !== 'optional') {
|
|
1740
|
+
throw new CommanderError('ConfigurationError', `only optional argument "${arg.name}" can have a default value`, this.#getCommandPath());
|
|
1741
|
+
}
|
|
1742
|
+
if (arg.type === 'string' && arg.choices !== undefined) {
|
|
1743
|
+
throw new CommanderError('ConfigurationError', `argument "${arg.name}" of type "string" cannot declare choices`, this.#getCommandPath());
|
|
1744
|
+
}
|
|
1745
|
+
if (arg.type === 'choice') {
|
|
1746
|
+
if (!Array.isArray(arg.choices) || arg.choices.length === 0) {
|
|
1747
|
+
throw new CommanderError('ConfigurationError', `argument "${arg.name}" of type "choice" must declare a non-empty choices array`, this.#getCommandPath());
|
|
1748
|
+
}
|
|
1749
|
+
if (arg.choices.some(choice => typeof choice !== 'string')) {
|
|
1750
|
+
throw new CommanderError('ConfigurationError', `argument "${arg.name}" choices must be string[]`, this.#getCommandPath());
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
if (arg.default !== undefined) {
|
|
1754
|
+
this.#validateArgumentDefaultValue(arg);
|
|
1518
1755
|
}
|
|
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());
|
|
1756
|
+
if (arg.kind === 'variadic' || arg.kind === 'some') {
|
|
1757
|
+
if (this.#arguments.some(a => a.kind === 'variadic' || a.kind === 'some')) {
|
|
1758
|
+
throw new CommanderError('ConfigurationError', 'only one variadic/some argument is allowed', this.#getCommandPath());
|
|
1522
1759
|
}
|
|
1523
1760
|
}
|
|
1524
1761
|
if (this.#arguments.length > 0) {
|
|
1525
1762
|
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());
|
|
1763
|
+
if (last.kind === 'variadic' || last.kind === 'some') {
|
|
1764
|
+
throw new CommanderError('ConfigurationError', 'variadic/some argument must be the last argument', this.#getCommandPath());
|
|
1528
1765
|
}
|
|
1529
1766
|
}
|
|
1530
1767
|
if (arg.kind === 'required') {
|
|
1531
|
-
const hasOptional = this.#arguments.some(a => a.kind === 'optional' || a.kind === 'variadic');
|
|
1768
|
+
const hasOptional = this.#arguments.some(a => a.kind === 'optional' || a.kind === 'variadic' || a.kind === 'some');
|
|
1532
1769
|
if (hasOptional) {
|
|
1533
|
-
throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot come after optional/variadic arguments`, this.#getCommandPath());
|
|
1770
|
+
throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot come after optional/variadic/some arguments`, this.#getCommandPath());
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
#validateArgumentDefaultValue(arg) {
|
|
1775
|
+
if (typeof arg.default !== 'string') {
|
|
1776
|
+
throw new CommanderError('ConfigurationError', `default value for argument "${arg.name}" must match type "${arg.type}"`, this.#getCommandPath());
|
|
1777
|
+
}
|
|
1778
|
+
if (arg.type === 'choice') {
|
|
1779
|
+
const choices = arg.choices ?? [];
|
|
1780
|
+
if (!choices.includes(arg.default)) {
|
|
1781
|
+
throw new CommanderError('ConfigurationError', `default value for argument "${arg.name}" must be one of declared choices`, this.#getCommandPath());
|
|
1534
1782
|
}
|
|
1535
1783
|
}
|
|
1536
1784
|
}
|
|
@@ -1887,6 +2135,7 @@ class BashCompletion {
|
|
|
1887
2135
|
'',
|
|
1888
2136
|
`${funcName}() {`,
|
|
1889
2137
|
' local cur prev words cword',
|
|
2138
|
+
' local opts arg_choices prefer_value_choices',
|
|
1890
2139
|
' _init_completion || return',
|
|
1891
2140
|
'',
|
|
1892
2141
|
...this.#generateCommandCase(this.#meta, 1),
|
|
@@ -1906,13 +2155,15 @@ class BashCompletion {
|
|
|
1906
2155
|
for (const opt of cmd.options) {
|
|
1907
2156
|
const kebabLong = camelToKebabCase(opt.long);
|
|
1908
2157
|
if (opt.short)
|
|
1909
|
-
optParts.push(`-${opt.short}`);
|
|
1910
|
-
optParts.push(`--${kebabLong}`);
|
|
2158
|
+
optParts.push(this.#escapeWord(`-${opt.short}`));
|
|
2159
|
+
optParts.push(this.#escapeWord(`--${kebabLong}`));
|
|
1911
2160
|
if (!opt.takesValue) {
|
|
1912
|
-
optParts.push(`--no-${kebabLong}`);
|
|
2161
|
+
optParts.push(this.#escapeWord(`--no-${kebabLong}`));
|
|
1913
2162
|
}
|
|
1914
2163
|
}
|
|
1915
|
-
const subParts = cmd.subcommands
|
|
2164
|
+
const subParts = cmd.subcommands
|
|
2165
|
+
.flatMap(sub => [sub.name, ...sub.aliases])
|
|
2166
|
+
.map(value => this.#escapeWord(value));
|
|
1916
2167
|
const allOpts = [...optParts, ...subParts].join(' ');
|
|
1917
2168
|
if (cmd.subcommands.length > 0) {
|
|
1918
2169
|
lines.push(`${indent}case "\${words[${depth}]}" in`);
|
|
@@ -1924,14 +2175,105 @@ class BashCompletion {
|
|
|
1924
2175
|
}
|
|
1925
2176
|
lines.push(`${indent} *)`);
|
|
1926
2177
|
lines.push(`${indent} opts="${allOpts}"`);
|
|
2178
|
+
this.#appendChoiceLogicForCommand(lines, `${indent} `, cmd, depth);
|
|
1927
2179
|
lines.push(`${indent} ;;`);
|
|
1928
2180
|
lines.push(`${indent}esac`);
|
|
1929
2181
|
}
|
|
1930
2182
|
else {
|
|
1931
2183
|
lines.push(`${indent}opts="${allOpts}"`);
|
|
2184
|
+
this.#appendChoiceLogicForCommand(lines, indent, cmd, depth);
|
|
1932
2185
|
}
|
|
1933
2186
|
return lines;
|
|
1934
2187
|
}
|
|
2188
|
+
#serializeWordList(words) {
|
|
2189
|
+
return words.map(choice => this.#escapeWord(choice)).join(' ');
|
|
2190
|
+
}
|
|
2191
|
+
#appendChoiceLogicForCommand(lines, indent, cmd, depth) {
|
|
2192
|
+
const valueOptions = cmd.options.filter(opt => opt.takesValue);
|
|
2193
|
+
const valueOptionsWithChoices = valueOptions.filter(opt => opt.choices && opt.choices.length > 0);
|
|
2194
|
+
const valueLongPatterns = valueOptions.map(opt => `--${camelToKebabCase(opt.long)}`);
|
|
2195
|
+
const valueShortPatterns = valueOptions
|
|
2196
|
+
.map(opt => opt.short)
|
|
2197
|
+
.filter((short) => typeof short === 'string');
|
|
2198
|
+
lines.push(`${indent}prefer_value_choices=0`);
|
|
2199
|
+
if (valueOptionsWithChoices.length > 0) {
|
|
2200
|
+
lines.push(`${indent}if [[ "$cur" != -* ]]; then`);
|
|
2201
|
+
lines.push(`${indent} case "$prev" in`);
|
|
2202
|
+
for (const opt of valueOptionsWithChoices) {
|
|
2203
|
+
const patterns = [`--${camelToKebabCase(opt.long)}`];
|
|
2204
|
+
if (opt.short) {
|
|
2205
|
+
patterns.push(`-${opt.short}`);
|
|
2206
|
+
}
|
|
2207
|
+
lines.push(`${indent} ${patterns.join('|')})`);
|
|
2208
|
+
lines.push(`${indent} opts="${this.#serializeWordList(opt.choices ?? [])}"`);
|
|
2209
|
+
lines.push(`${indent} prefer_value_choices=1`);
|
|
2210
|
+
lines.push(`${indent} ;;`);
|
|
2211
|
+
}
|
|
2212
|
+
lines.push(`${indent} esac`);
|
|
2213
|
+
lines.push(`${indent}fi`);
|
|
2214
|
+
}
|
|
2215
|
+
lines.push(`${indent}if [[ $prefer_value_choices -eq 0 ]]; then`);
|
|
2216
|
+
lines.push(`${indent} positional_count=0`);
|
|
2217
|
+
lines.push(`${indent} expect_value=0`);
|
|
2218
|
+
lines.push(`${indent} for ((idx=${depth}; idx<cword; idx++)); do`);
|
|
2219
|
+
lines.push(`${indent} token="\${words[idx]}"`);
|
|
2220
|
+
lines.push(`${indent} if [[ $expect_value -eq 1 ]]; then`);
|
|
2221
|
+
lines.push(`${indent} expect_value=0`);
|
|
2222
|
+
lines.push(`${indent} continue`);
|
|
2223
|
+
lines.push(`${indent} fi`);
|
|
2224
|
+
lines.push(`${indent} if [[ "$token" == --* ]]; then`);
|
|
2225
|
+
lines.push(`${indent} if [[ "$token" == *=* ]]; then`);
|
|
2226
|
+
lines.push(`${indent} continue`);
|
|
2227
|
+
lines.push(`${indent} fi`);
|
|
2228
|
+
if (valueLongPatterns.length > 0) {
|
|
2229
|
+
lines.push(`${indent} case "$token" in`);
|
|
2230
|
+
lines.push(`${indent} ${valueLongPatterns.join('|')}) expect_value=1 ;;`);
|
|
2231
|
+
lines.push(`${indent} esac`);
|
|
2232
|
+
}
|
|
2233
|
+
lines.push(`${indent} continue`);
|
|
2234
|
+
lines.push(`${indent} fi`);
|
|
2235
|
+
lines.push(`${indent} if [[ "$token" == -* && "$token" != "-" ]]; then`);
|
|
2236
|
+
lines.push(`${indent} if [[ \${#token} -eq 2 ]]; then`);
|
|
2237
|
+
if (valueShortPatterns.length > 0) {
|
|
2238
|
+
lines.push(`${indent} case "\${token:1:1}" in`);
|
|
2239
|
+
lines.push(`${indent} ${valueShortPatterns.join('|')}) expect_value=1 ;;`);
|
|
2240
|
+
lines.push(`${indent} esac`);
|
|
2241
|
+
}
|
|
2242
|
+
lines.push(`${indent} fi`);
|
|
2243
|
+
lines.push(`${indent} continue`);
|
|
2244
|
+
lines.push(`${indent} fi`);
|
|
2245
|
+
lines.push(`${indent} positional_count=$((positional_count + 1))`);
|
|
2246
|
+
lines.push(`${indent} done`);
|
|
2247
|
+
lines.push(`${indent} if [[ $expect_value -eq 1 ]]; then`);
|
|
2248
|
+
lines.push(`${indent} opts=""`);
|
|
2249
|
+
lines.push(`${indent} prefer_value_choices=1`);
|
|
2250
|
+
lines.push(`${indent} elif [[ "$cur" != -* ]]; then`);
|
|
2251
|
+
lines.push(`${indent} arg_slot=-1`);
|
|
2252
|
+
lines.push(`${indent} arg_count=${cmd.arguments.length}`);
|
|
2253
|
+
const hasRestArgument = cmd.arguments.length > 0 &&
|
|
2254
|
+
(cmd.arguments[cmd.arguments.length - 1].kind === 'variadic' ||
|
|
2255
|
+
cmd.arguments[cmd.arguments.length - 1].kind === 'some');
|
|
2256
|
+
lines.push(`${indent} has_rest=${hasRestArgument ? 1 : 0}`);
|
|
2257
|
+
lines.push(`${indent} if [[ $has_rest -eq 1 && $positional_count -ge $((arg_count - 1)) ]]; then`);
|
|
2258
|
+
lines.push(`${indent} arg_slot=$((arg_count - 1))`);
|
|
2259
|
+
lines.push(`${indent} elif [[ $positional_count -lt $arg_count ]]; then`);
|
|
2260
|
+
lines.push(`${indent} arg_slot=$positional_count`);
|
|
2261
|
+
lines.push(`${indent} fi`);
|
|
2262
|
+
lines.push(`${indent} case "$arg_slot" in`);
|
|
2263
|
+
for (let index = 0; index < cmd.arguments.length; index += 1) {
|
|
2264
|
+
const arg = cmd.arguments[index];
|
|
2265
|
+
if (arg.type !== 'choice' || !arg.choices || arg.choices.length === 0) {
|
|
2266
|
+
continue;
|
|
2267
|
+
}
|
|
2268
|
+
lines.push(`${indent} ${index}) opts="${this.#serializeWordList(arg.choices)}" ;;`);
|
|
2269
|
+
}
|
|
2270
|
+
lines.push(`${indent} esac`);
|
|
2271
|
+
lines.push(`${indent} fi`);
|
|
2272
|
+
lines.push(`${indent}fi`);
|
|
2273
|
+
}
|
|
2274
|
+
#escapeWord(word) {
|
|
2275
|
+
return word.replace(/([\\\s'"`$!])/g, '\\$1');
|
|
2276
|
+
}
|
|
1935
2277
|
#sanitizeName(name) {
|
|
1936
2278
|
return name.replace(/[^a-zA-Z0-9]/g, '_');
|
|
1937
2279
|
}
|
|
@@ -1939,15 +2281,19 @@ class BashCompletion {
|
|
|
1939
2281
|
class FishCompletion {
|
|
1940
2282
|
#meta;
|
|
1941
2283
|
#programName;
|
|
2284
|
+
#slotMatcherName;
|
|
1942
2285
|
constructor(meta, programName) {
|
|
1943
2286
|
this.#meta = meta;
|
|
1944
2287
|
this.#programName = programName;
|
|
2288
|
+
this.#slotMatcherName = `__${this.#sanitizeName(programName)}_match_arg_slot`;
|
|
1945
2289
|
}
|
|
1946
2290
|
generate() {
|
|
1947
2291
|
const lines = [
|
|
1948
2292
|
`# Fish completion for ${this.#programName}`,
|
|
1949
2293
|
'# Generated by @guanghechen/commander',
|
|
1950
2294
|
'',
|
|
2295
|
+
...this.#generateSlotMatcherFunction(),
|
|
2296
|
+
'',
|
|
1951
2297
|
...this.#generateCommandCompletions(this.#meta, []),
|
|
1952
2298
|
'',
|
|
1953
2299
|
];
|
|
@@ -1967,7 +2313,7 @@ class FishCompletion {
|
|
|
1967
2313
|
line += ` -l ${kebabLong}`;
|
|
1968
2314
|
line += ` -d '${this.#escape(opt.desc)}'`;
|
|
1969
2315
|
if (opt.choices && opt.choices.length > 0) {
|
|
1970
|
-
line += ` -xa '${opt.choices.join(' ')}'`;
|
|
2316
|
+
line += ` -xa '${opt.choices.map(choice => this.#escapeChoice(choice)).join(' ')}'`;
|
|
1971
2317
|
}
|
|
1972
2318
|
lines.push(line);
|
|
1973
2319
|
if (!opt.takesValue) {
|
|
@@ -1979,6 +2325,36 @@ class FishCompletion {
|
|
|
1979
2325
|
lines.push(noLine);
|
|
1980
2326
|
}
|
|
1981
2327
|
}
|
|
2328
|
+
const valueOptionLongs = cmd.options
|
|
2329
|
+
.filter(opt => opt.takesValue)
|
|
2330
|
+
.map(opt => camelToKebabCase(opt.long))
|
|
2331
|
+
.join(',');
|
|
2332
|
+
const valueOptionShorts = cmd.options
|
|
2333
|
+
.filter(opt => opt.takesValue && opt.short)
|
|
2334
|
+
.map(opt => opt.short)
|
|
2335
|
+
.join(',');
|
|
2336
|
+
const argCount = cmd.arguments.length;
|
|
2337
|
+
const hasRestArgument = argCount > 0 &&
|
|
2338
|
+
(cmd.arguments[argCount - 1].kind === 'variadic' ||
|
|
2339
|
+
cmd.arguments[argCount - 1].kind === 'some');
|
|
2340
|
+
for (let index = 0; index < cmd.arguments.length; index += 1) {
|
|
2341
|
+
const arg = cmd.arguments[index];
|
|
2342
|
+
if (arg.type !== 'choice' || !arg.choices || arg.choices.length === 0) {
|
|
2343
|
+
continue;
|
|
2344
|
+
}
|
|
2345
|
+
let line = `complete -c ${this.#programName}`;
|
|
2346
|
+
const slotCondition = `${this.#slotMatcherName} ${parentPath.length} ${argCount} ${hasRestArgument ? 1 : 0} ${index} '${valueOptionLongs}' '${valueOptionShorts}'`;
|
|
2347
|
+
if (condition) {
|
|
2348
|
+
line += ` -n '${condition}; and ${slotCondition}'`;
|
|
2349
|
+
}
|
|
2350
|
+
else {
|
|
2351
|
+
line += ` -n '${slotCondition}'`;
|
|
2352
|
+
}
|
|
2353
|
+
line += ` -f`;
|
|
2354
|
+
line += ` -a '${arg.choices.map(choice => this.#escapeChoice(choice)).join(' ')}'`;
|
|
2355
|
+
line += ` -d '${this.#escape(`Argument: ${arg.name}`)}'`;
|
|
2356
|
+
lines.push(line);
|
|
2357
|
+
}
|
|
1982
2358
|
for (const sub of cmd.subcommands) {
|
|
1983
2359
|
let line = `complete -c ${this.#programName}`;
|
|
1984
2360
|
if (isRoot) {
|
|
@@ -2002,7 +2378,7 @@ class FishCompletion {
|
|
|
2002
2378
|
aliasLine += ` -d 'Alias for ${sub.name}'`;
|
|
2003
2379
|
lines.push(aliasLine);
|
|
2004
2380
|
}
|
|
2005
|
-
const newPath = [...parentPath, sub.name];
|
|
2381
|
+
const newPath = [...parentPath, [sub.name, ...sub.aliases]];
|
|
2006
2382
|
lines.push(...this.#generateCommandCompletions(sub, newPath));
|
|
2007
2383
|
}
|
|
2008
2384
|
return lines;
|
|
@@ -2010,7 +2386,7 @@ class FishCompletion {
|
|
|
2010
2386
|
#buildCondition(path) {
|
|
2011
2387
|
if (path.length === 0)
|
|
2012
2388
|
return '';
|
|
2013
|
-
return `__fish_seen_subcommand_from ${
|
|
2389
|
+
return path.map(level => `__fish_seen_subcommand_from ${level.join(' ')}`).join('; and ');
|
|
2014
2390
|
}
|
|
2015
2391
|
#getSubcommandNames(cmd) {
|
|
2016
2392
|
return cmd.subcommands.flatMap(sub => [sub.name, ...sub.aliases]);
|
|
@@ -2018,6 +2394,68 @@ class FishCompletion {
|
|
|
2018
2394
|
#escape(s) {
|
|
2019
2395
|
return s.replace(/'/g, "\\'");
|
|
2020
2396
|
}
|
|
2397
|
+
#escapeChoice(s) {
|
|
2398
|
+
return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\s/g, '\\ ');
|
|
2399
|
+
}
|
|
2400
|
+
#sanitizeName(name) {
|
|
2401
|
+
return name.replace(/[^a-zA-Z0-9]/g, '_');
|
|
2402
|
+
}
|
|
2403
|
+
#generateSlotMatcherFunction() {
|
|
2404
|
+
return [
|
|
2405
|
+
`function ${this.#slotMatcherName} --argument-names depth arg_count has_rest target_index long_opts short_opts`,
|
|
2406
|
+
' set -l tokens (commandline -opc)',
|
|
2407
|
+
' set -l start (math $depth + 2)',
|
|
2408
|
+
' set -l positional 0',
|
|
2409
|
+
' set -l expect_value 0',
|
|
2410
|
+
' set -l i $start',
|
|
2411
|
+
' set -l token_count (count $tokens)',
|
|
2412
|
+
' set -l long_list (string split "," -- $long_opts)',
|
|
2413
|
+
' set -l short_list (string split "," -- $short_opts)',
|
|
2414
|
+
' while test $i -le $token_count',
|
|
2415
|
+
' set -l token $tokens[$i]',
|
|
2416
|
+
' if test $expect_value -eq 1',
|
|
2417
|
+
' set expect_value 0',
|
|
2418
|
+
' set i (math $i + 1)',
|
|
2419
|
+
' continue',
|
|
2420
|
+
' end',
|
|
2421
|
+
' if string match -q -- "--*" $token',
|
|
2422
|
+
' if string match -q -- "*=*" $token',
|
|
2423
|
+
' set i (math $i + 1)',
|
|
2424
|
+
' continue',
|
|
2425
|
+
' end',
|
|
2426
|
+
' set -l opt_name (string replace -r "^--" "" -- $token)',
|
|
2427
|
+
' if contains -- $opt_name $long_list',
|
|
2428
|
+
' set expect_value 1',
|
|
2429
|
+
' end',
|
|
2430
|
+
' set i (math $i + 1)',
|
|
2431
|
+
' continue',
|
|
2432
|
+
' end',
|
|
2433
|
+
' if test "$token" != "-"; and string match -q -- "-*" $token',
|
|
2434
|
+
' set -l raw_short (string replace -r "^-" "" -- $token)',
|
|
2435
|
+
' if test (string length -- $raw_short) -eq 1',
|
|
2436
|
+
' if contains -- $raw_short $short_list',
|
|
2437
|
+
' set expect_value 1',
|
|
2438
|
+
' end',
|
|
2439
|
+
' end',
|
|
2440
|
+
' set i (math $i + 1)',
|
|
2441
|
+
' continue',
|
|
2442
|
+
' end',
|
|
2443
|
+
' set positional (math $positional + 1)',
|
|
2444
|
+
' set i (math $i + 1)',
|
|
2445
|
+
' end',
|
|
2446
|
+
' if test $expect_value -eq 1',
|
|
2447
|
+
' return 1',
|
|
2448
|
+
' end',
|
|
2449
|
+
' set -l slot -1',
|
|
2450
|
+
' if test $has_rest -eq 1; and test $positional -ge (math $arg_count - 1)',
|
|
2451
|
+
' set slot (math $arg_count - 1)',
|
|
2452
|
+
' else if test $positional -lt $arg_count',
|
|
2453
|
+
' set slot $positional',
|
|
2454
|
+
' end',
|
|
2455
|
+
' test $slot -eq $target_index',
|
|
2456
|
+
'end',
|
|
2457
|
+
];
|
|
2458
|
+
}
|
|
2021
2459
|
}
|
|
2022
2460
|
class PwshCompletion {
|
|
2023
2461
|
#meta;
|
|
@@ -2043,16 +2481,105 @@ class PwshCompletion {
|
|
|
2043
2481
|
'',
|
|
2044
2482
|
' # Find current command context',
|
|
2045
2483
|
' $cmd = $commands',
|
|
2484
|
+
' $commandDepth = 1',
|
|
2046
2485
|
' foreach ($word in $words[1..($words.Count - 1)]) {',
|
|
2047
2486
|
' if ($word.StartsWith("-")) { continue }',
|
|
2048
2487
|
' if ($cmd.subcommands -and $cmd.subcommands.ContainsKey($word)) {',
|
|
2049
2488
|
' $cmd = $cmd.subcommands[$word]',
|
|
2489
|
+
' $commandDepth += 1',
|
|
2050
2490
|
' }',
|
|
2051
2491
|
' }',
|
|
2052
2492
|
'',
|
|
2053
2493
|
' # Generate completions',
|
|
2054
2494
|
' $completions = @()',
|
|
2055
2495
|
'',
|
|
2496
|
+
' # Option value slot (always higher priority than arguments)',
|
|
2497
|
+
' $previous = if ($words.Count -ge 2) { $words[$words.Count - 2] } else { $null }',
|
|
2498
|
+
' if ($previous) {',
|
|
2499
|
+
' foreach ($opt in $cmd.options) {',
|
|
2500
|
+
' $isLong = $previous -eq "--$($opt.long)"',
|
|
2501
|
+
' $isShort = $opt.short -and $previous -eq "-$($opt.short)"',
|
|
2502
|
+
' if ($isLong -or $isShort) {',
|
|
2503
|
+
' if ($opt.choices) {',
|
|
2504
|
+
' foreach ($choice in $opt.choices) {',
|
|
2505
|
+
' if ($choice -like "$current*") {',
|
|
2506
|
+
' $completions += [System.Management.Automation.CompletionResult]::new(',
|
|
2507
|
+
' $choice,',
|
|
2508
|
+
' $choice,',
|
|
2509
|
+
' "ParameterValue",',
|
|
2510
|
+
' $choice',
|
|
2511
|
+
' )',
|
|
2512
|
+
' }',
|
|
2513
|
+
' }',
|
|
2514
|
+
' }',
|
|
2515
|
+
' return $completions',
|
|
2516
|
+
' }',
|
|
2517
|
+
' }',
|
|
2518
|
+
' }',
|
|
2519
|
+
'',
|
|
2520
|
+
' # Determine argument slot',
|
|
2521
|
+
' $positionalCount = 0',
|
|
2522
|
+
' $expectValue = $false',
|
|
2523
|
+
' for ($i = $commandDepth; $i -lt ($words.Count - 1); $i += 1) {',
|
|
2524
|
+
' $token = $words[$i]',
|
|
2525
|
+
' if ($expectValue) {',
|
|
2526
|
+
' $expectValue = $false',
|
|
2527
|
+
' continue',
|
|
2528
|
+
' }',
|
|
2529
|
+
' if ($token.StartsWith("--")) {',
|
|
2530
|
+
' if ($token.Contains("=")) { continue }',
|
|
2531
|
+
' foreach ($opt in $cmd.options) {',
|
|
2532
|
+
' if ($token -eq "--$($opt.long)" -and $opt.takesValue) {',
|
|
2533
|
+
' $expectValue = $true',
|
|
2534
|
+
' break',
|
|
2535
|
+
' }',
|
|
2536
|
+
' }',
|
|
2537
|
+
' continue',
|
|
2538
|
+
' }',
|
|
2539
|
+
' if ($token.StartsWith("-") -and $token -ne "-") {',
|
|
2540
|
+
' if ($token.Length -eq 2) {',
|
|
2541
|
+
' foreach ($opt in $cmd.options) {',
|
|
2542
|
+
' if ($opt.short -and $token -eq "-$($opt.short)" -and $opt.takesValue) {',
|
|
2543
|
+
' $expectValue = $true',
|
|
2544
|
+
' break',
|
|
2545
|
+
' }',
|
|
2546
|
+
' }',
|
|
2547
|
+
' }',
|
|
2548
|
+
' continue',
|
|
2549
|
+
' }',
|
|
2550
|
+
' $positionalCount += 1',
|
|
2551
|
+
' }',
|
|
2552
|
+
' if ($expectValue) {',
|
|
2553
|
+
' return $completions',
|
|
2554
|
+
' }',
|
|
2555
|
+
' if (-not $current.StartsWith("-") -and $cmd.arguments -and $cmd.arguments.Count -gt 0) {',
|
|
2556
|
+
' $argSlot = -1',
|
|
2557
|
+
' $argCount = $cmd.arguments.Count',
|
|
2558
|
+
' $lastArg = $cmd.arguments[$argCount - 1]',
|
|
2559
|
+
' $hasRest = $lastArg.kind -eq "variadic" -or $lastArg.kind -eq "some"',
|
|
2560
|
+
' if ($hasRest -and $positionalCount -ge ($argCount - 1)) {',
|
|
2561
|
+
' $argSlot = $argCount - 1',
|
|
2562
|
+
' } elseif ($positionalCount -lt $argCount) {',
|
|
2563
|
+
' $argSlot = $positionalCount',
|
|
2564
|
+
' }',
|
|
2565
|
+
' if ($argSlot -ge 0) {',
|
|
2566
|
+
' $argMeta = $cmd.arguments[$argSlot]',
|
|
2567
|
+
' if ($argMeta.choices) {',
|
|
2568
|
+
' foreach ($choice in $argMeta.choices) {',
|
|
2569
|
+
' if ($choice -like "$current*") {',
|
|
2570
|
+
' $completions += [System.Management.Automation.CompletionResult]::new(',
|
|
2571
|
+
' $choice,',
|
|
2572
|
+
' $choice,',
|
|
2573
|
+
' "ParameterValue",',
|
|
2574
|
+
' $choice',
|
|
2575
|
+
' )',
|
|
2576
|
+
' }',
|
|
2577
|
+
' }',
|
|
2578
|
+
' return $completions',
|
|
2579
|
+
' }',
|
|
2580
|
+
' }',
|
|
2581
|
+
' }',
|
|
2582
|
+
'',
|
|
2056
2583
|
' # Options',
|
|
2057
2584
|
' if ($current.StartsWith("-")) {',
|
|
2058
2585
|
' foreach ($opt in $cmd.options) {',
|
|
@@ -2061,7 +2588,7 @@ class PwshCompletion {
|
|
|
2061
2588
|
' "--$($opt.long)",',
|
|
2062
2589
|
' $opt.long,',
|
|
2063
2590
|
' "ParameterName",',
|
|
2064
|
-
' $opt.
|
|
2591
|
+
' $opt.description',
|
|
2065
2592
|
' )',
|
|
2066
2593
|
' }',
|
|
2067
2594
|
' if ($opt.isBoolean -and "--no-$($opt.long)" -like "$current*") {',
|
|
@@ -2069,7 +2596,7 @@ class PwshCompletion {
|
|
|
2069
2596
|
' "--no-$($opt.long)",',
|
|
2070
2597
|
' "no-$($opt.long)",',
|
|
2071
2598
|
' "ParameterName",',
|
|
2072
|
-
' $opt.
|
|
2599
|
+
' $opt.description',
|
|
2073
2600
|
' )',
|
|
2074
2601
|
' }',
|
|
2075
2602
|
' if ($opt.short -and "-$($opt.short)" -like "$current*") {',
|
|
@@ -2077,7 +2604,7 @@ class PwshCompletion {
|
|
|
2077
2604
|
' "-$($opt.short)",',
|
|
2078
2605
|
' $opt.short,',
|
|
2079
2606
|
' "ParameterName",',
|
|
2080
|
-
' $opt.
|
|
2607
|
+
' $opt.description',
|
|
2081
2608
|
' )',
|
|
2082
2609
|
' }',
|
|
2083
2610
|
' }',
|
|
@@ -2091,7 +2618,7 @@ class PwshCompletion {
|
|
|
2091
2618
|
' $sub,',
|
|
2092
2619
|
' $sub,',
|
|
2093
2620
|
' "Command",',
|
|
2094
|
-
' $cmd.subcommands[$sub].
|
|
2621
|
+
' $cmd.subcommands[$sub].description',
|
|
2095
2622
|
' )',
|
|
2096
2623
|
' }',
|
|
2097
2624
|
' }',
|
|
@@ -2115,8 +2642,25 @@ class PwshCompletion {
|
|
|
2115
2642
|
lines.push(`${indent} long = '${kebabLong}'`);
|
|
2116
2643
|
lines.push(`${indent} description = '${this.#escape(opt.desc)}'`);
|
|
2117
2644
|
lines.push(`${indent} isBoolean = $${!opt.takesValue}`);
|
|
2645
|
+
lines.push(`${indent} takesValue = $${opt.takesValue}`);
|
|
2118
2646
|
if (opt.choices) {
|
|
2119
|
-
lines.push(`${indent} choices = @('${opt.choices
|
|
2647
|
+
lines.push(`${indent} choices = @('${opt.choices
|
|
2648
|
+
.map(choice => this.#escape(choice))
|
|
2649
|
+
.join("', '")}')`);
|
|
2650
|
+
}
|
|
2651
|
+
lines.push(`${indent} }`);
|
|
2652
|
+
}
|
|
2653
|
+
lines.push(`${indent})`);
|
|
2654
|
+
lines.push(`${indent}arguments = @(`);
|
|
2655
|
+
for (const arg of cmd.arguments) {
|
|
2656
|
+
lines.push(`${indent} @{`);
|
|
2657
|
+
lines.push(`${indent} name = '${this.#escape(arg.name)}'`);
|
|
2658
|
+
lines.push(`${indent} kind = '${arg.kind}'`);
|
|
2659
|
+
lines.push(`${indent} type = '${arg.type}'`);
|
|
2660
|
+
if (arg.choices && arg.choices.length > 0) {
|
|
2661
|
+
lines.push(`${indent} choices = @('${arg.choices
|
|
2662
|
+
.map(choice => this.#escape(choice))
|
|
2663
|
+
.join("', '")}')`);
|
|
2120
2664
|
}
|
|
2121
2665
|
lines.push(`${indent} }`);
|
|
2122
2666
|
}
|