@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/esm/node.mjs
CHANGED
|
@@ -195,6 +195,116 @@ function kebabToCamelCase(str) {
|
|
|
195
195
|
function camelToKebabCase$1(str) {
|
|
196
196
|
return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
197
197
|
}
|
|
198
|
+
const ANSI_ESCAPE_REGEX = new RegExp(String.raw `\\x1B\\[[0-?]*[ -/]*[@-~]`, 'g');
|
|
199
|
+
const DECIMAL_INTEGER_REGEX = /^\d(?:_?\d)*$/;
|
|
200
|
+
const DECIMAL_FRACTION_REGEX = /^\d(?:_?\d)*$/;
|
|
201
|
+
const DECIMAL_EXPONENT_REGEX = /^[eE][+-]?\d(?:_?\d)*$/;
|
|
202
|
+
const BINARY_LITERAL_REGEX = /^0[bB][01](?:_?[01])*$/;
|
|
203
|
+
const OCTAL_LITERAL_REGEX = /^0[oO][0-7](?:_?[0-7])*$/;
|
|
204
|
+
const HEX_LITERAL_REGEX = /^0[xX][0-9a-fA-F](?:_?[0-9a-fA-F])*$/;
|
|
205
|
+
function stripAnsi(value) {
|
|
206
|
+
return value.replace(ANSI_ESCAPE_REGEX, '');
|
|
207
|
+
}
|
|
208
|
+
function isCombiningMark(codePoint) {
|
|
209
|
+
return ((codePoint >= 0x0300 && codePoint <= 0x036f) ||
|
|
210
|
+
(codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
|
|
211
|
+
(codePoint >= 0x1dc0 && codePoint <= 0x1dff) ||
|
|
212
|
+
(codePoint >= 0x20d0 && codePoint <= 0x20ff) ||
|
|
213
|
+
(codePoint >= 0xfe20 && codePoint <= 0xfe2f));
|
|
214
|
+
}
|
|
215
|
+
function isWideCodePoint(codePoint) {
|
|
216
|
+
if (codePoint < 0x1100) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
return (codePoint <= 0x115f ||
|
|
220
|
+
codePoint === 0x2329 ||
|
|
221
|
+
codePoint === 0x232a ||
|
|
222
|
+
(codePoint >= 0x2e80 && codePoint <= 0x3247 && codePoint !== 0x303f) ||
|
|
223
|
+
(codePoint >= 0x3250 && codePoint <= 0x4dbf) ||
|
|
224
|
+
(codePoint >= 0x4e00 && codePoint <= 0xa4c6) ||
|
|
225
|
+
(codePoint >= 0xa960 && codePoint <= 0xa97c) ||
|
|
226
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
227
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
228
|
+
(codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
|
|
229
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6b) ||
|
|
230
|
+
(codePoint >= 0xff01 && codePoint <= 0xff60) ||
|
|
231
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
232
|
+
(codePoint >= 0x1b000 && codePoint <= 0x1b001) ||
|
|
233
|
+
(codePoint >= 0x1f200 && codePoint <= 0x1f251) ||
|
|
234
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd));
|
|
235
|
+
}
|
|
236
|
+
function getDisplayWidth(value) {
|
|
237
|
+
const normalized = stripAnsi(value).normalize('NFC');
|
|
238
|
+
let width = 0;
|
|
239
|
+
for (const char of normalized) {
|
|
240
|
+
const codePoint = char.codePointAt(0);
|
|
241
|
+
if (codePoint === undefined || isCombiningMark(codePoint)) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
width += isWideCodePoint(codePoint) ? 2 : 1;
|
|
245
|
+
}
|
|
246
|
+
return width;
|
|
247
|
+
}
|
|
248
|
+
function padDisplayEnd(value, targetWidth) {
|
|
249
|
+
const width = getDisplayWidth(value);
|
|
250
|
+
if (width >= targetWidth) {
|
|
251
|
+
return value;
|
|
252
|
+
}
|
|
253
|
+
return value + ' '.repeat(targetWidth - width);
|
|
254
|
+
}
|
|
255
|
+
function isValidPrimitiveNumberLiteral(rawValue) {
|
|
256
|
+
if (rawValue.trim() !== rawValue || rawValue.length === 0) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
if (rawValue === 'NaN' || rawValue === 'Infinity' || rawValue === '-Infinity') {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
if (BINARY_LITERAL_REGEX.test(rawValue)) {
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
if (OCTAL_LITERAL_REGEX.test(rawValue)) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
if (HEX_LITERAL_REGEX.test(rawValue)) {
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
const sign = rawValue[0] === '+' || rawValue[0] === '-' ? rawValue[0] : '';
|
|
272
|
+
const body = sign ? rawValue.slice(1) : rawValue;
|
|
273
|
+
if (body.length === 0) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
const expIndex = body.search(/[eE]/);
|
|
277
|
+
const basePart = expIndex === -1 ? body : body.slice(0, expIndex);
|
|
278
|
+
const expPart = expIndex === -1 ? '' : body.slice(expIndex);
|
|
279
|
+
if (expPart && !DECIMAL_EXPONENT_REGEX.test(expPart)) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
if (basePart.includes('.')) {
|
|
283
|
+
const decimalParts = basePart.split('.');
|
|
284
|
+
if (decimalParts.length !== 2) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
const [intPart, fracPart] = decimalParts;
|
|
288
|
+
const intOk = intPart.length === 0 || DECIMAL_INTEGER_REGEX.test(intPart);
|
|
289
|
+
const fracOk = fracPart.length === 0 || DECIMAL_FRACTION_REGEX.test(fracPart);
|
|
290
|
+
if (!intOk || !fracOk) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
return intPart.length > 0 || fracPart.length > 0;
|
|
294
|
+
}
|
|
295
|
+
return DECIMAL_INTEGER_REGEX.test(basePart);
|
|
296
|
+
}
|
|
297
|
+
function parsePrimitiveNumber(rawValue) {
|
|
298
|
+
if (!isValidPrimitiveNumberLiteral(rawValue)) {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
const normalized = rawValue.replaceAll('_', '');
|
|
302
|
+
const value = Number(normalized);
|
|
303
|
+
if (!Number.isFinite(value)) {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
return value;
|
|
307
|
+
}
|
|
198
308
|
function tokenizeLongOption(arg, commandPath) {
|
|
199
309
|
const eqIdx = arg.indexOf('=');
|
|
200
310
|
const namePart = eqIdx !== -1 ? arg.slice(0, eqIdx) : arg;
|
|
@@ -431,9 +541,13 @@ class Command {
|
|
|
431
541
|
if (cmd.#parent && cmd.#parent !== this) {
|
|
432
542
|
throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
|
|
433
543
|
}
|
|
544
|
+
const occupied = this.#subcommandsMap.get(name);
|
|
545
|
+
if (occupied && occupied !== cmd) {
|
|
546
|
+
throw new CommanderError('ConfigurationError', `subcommand name/alias "${name}" conflicts with an existing command`, this.#getCommandPath());
|
|
547
|
+
}
|
|
434
548
|
const existing = this.#subcommandsList.find(e => e.command === cmd);
|
|
435
549
|
if (existing) {
|
|
436
|
-
if (existing.aliases.includes(name)) {
|
|
550
|
+
if (existing.name === name || existing.aliases.includes(name)) {
|
|
437
551
|
return this;
|
|
438
552
|
}
|
|
439
553
|
existing.aliases.push(name);
|
|
@@ -562,10 +676,32 @@ class Command {
|
|
|
562
676
|
else if (arg.kind === 'optional') {
|
|
563
677
|
usage += ` [${arg.name}]`;
|
|
564
678
|
}
|
|
679
|
+
else if (arg.kind === 'some') {
|
|
680
|
+
usage += ` <${arg.name}...>`;
|
|
681
|
+
}
|
|
565
682
|
else {
|
|
566
683
|
usage += ` [${arg.name}...]`;
|
|
567
684
|
}
|
|
568
685
|
}
|
|
686
|
+
const argumentsLines = [];
|
|
687
|
+
for (const arg of this.#arguments) {
|
|
688
|
+
const sig = arg.kind === 'required'
|
|
689
|
+
? `<${arg.name}>`
|
|
690
|
+
: arg.kind === 'optional'
|
|
691
|
+
? `[${arg.name}]`
|
|
692
|
+
: arg.kind === 'some'
|
|
693
|
+
? `<${arg.name}...>`
|
|
694
|
+
: `[${arg.name}...]`;
|
|
695
|
+
const metadata = [`[type: ${arg.type}]`];
|
|
696
|
+
if (arg.kind === 'optional' && arg.default !== undefined) {
|
|
697
|
+
metadata.push(`[default: ${JSON.stringify(arg.default)}]`);
|
|
698
|
+
}
|
|
699
|
+
if (arg.choices && arg.choices.length > 0) {
|
|
700
|
+
metadata.push(`[choices: ${arg.choices.map(choice => JSON.stringify(choice)).join(', ')}]`);
|
|
701
|
+
}
|
|
702
|
+
const desc = metadata.length > 0 ? `${arg.desc} ${metadata.join(' ')}` : arg.desc;
|
|
703
|
+
argumentsLines.push({ sig, desc });
|
|
704
|
+
}
|
|
569
705
|
const options = [];
|
|
570
706
|
for (const opt of allOptions) {
|
|
571
707
|
const kebabLong = camelToKebabCase$1(opt.long);
|
|
@@ -579,7 +715,7 @@ class Command {
|
|
|
579
715
|
desc += ` (default: ${JSON.stringify(opt.default)})`;
|
|
580
716
|
}
|
|
581
717
|
if (opt.choices) {
|
|
582
|
-
desc += ` [choices: ${opt.choices.join(', ')}]`;
|
|
718
|
+
desc += ` [choices: ${opt.choices.map(choice => JSON.stringify(choice)).join(', ')}]`;
|
|
583
719
|
}
|
|
584
720
|
options.push({ sig, desc });
|
|
585
721
|
if (opt.type === 'boolean' &&
|
|
@@ -611,6 +747,7 @@ class Command {
|
|
|
611
747
|
return {
|
|
612
748
|
desc: this.#desc,
|
|
613
749
|
usage,
|
|
750
|
+
arguments: argumentsLines,
|
|
614
751
|
options,
|
|
615
752
|
commands,
|
|
616
753
|
examples,
|
|
@@ -618,25 +755,29 @@ class Command {
|
|
|
618
755
|
}
|
|
619
756
|
#renderHelpPlain(helpData) {
|
|
620
757
|
const lines = [];
|
|
758
|
+
const labelWidth = this.#getHelpLabelWidth(helpData);
|
|
621
759
|
lines.push(helpData.desc);
|
|
622
760
|
lines.push('');
|
|
623
761
|
lines.push(helpData.usage);
|
|
624
762
|
lines.push('');
|
|
763
|
+
if (helpData.arguments.length > 0) {
|
|
764
|
+
lines.push('Arguments:');
|
|
765
|
+
for (const { sig, desc } of helpData.arguments) {
|
|
766
|
+
lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth));
|
|
767
|
+
}
|
|
768
|
+
lines.push('');
|
|
769
|
+
}
|
|
625
770
|
if (helpData.options.length > 0) {
|
|
626
771
|
lines.push('Options:');
|
|
627
|
-
const maxSigLen = Math.max(...helpData.options.map(line => line.sig.length));
|
|
628
772
|
for (const { sig, desc } of helpData.options) {
|
|
629
|
-
|
|
630
|
-
lines.push(` ${sig}${padding}${desc}`);
|
|
773
|
+
lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth));
|
|
631
774
|
}
|
|
632
775
|
lines.push('');
|
|
633
776
|
}
|
|
634
777
|
if (helpData.commands.length > 0) {
|
|
635
778
|
lines.push('Commands:');
|
|
636
|
-
const maxNameLen = Math.max(...helpData.commands.map(line => line.name.length));
|
|
637
779
|
for (const { name, desc } of helpData.commands) {
|
|
638
|
-
|
|
639
|
-
lines.push(` ${name}${padding}${desc}`);
|
|
780
|
+
lines.push(this.#renderAlignedHelpLine(name, desc, labelWidth));
|
|
640
781
|
}
|
|
641
782
|
lines.push('');
|
|
642
783
|
}
|
|
@@ -653,25 +794,29 @@ class Command {
|
|
|
653
794
|
}
|
|
654
795
|
#renderHelpTerminal(helpData) {
|
|
655
796
|
const lines = [];
|
|
797
|
+
const labelWidth = this.#getHelpLabelWidth(helpData);
|
|
656
798
|
lines.push(helpData.desc);
|
|
657
799
|
lines.push('');
|
|
658
800
|
lines.push(styleText(helpData.usage, TERMINAL_STYLE.bold));
|
|
659
801
|
lines.push('');
|
|
802
|
+
if (helpData.arguments.length > 0) {
|
|
803
|
+
lines.push(styleText('Arguments:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
|
|
804
|
+
for (const { sig, desc } of helpData.arguments) {
|
|
805
|
+
lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth, value => styleText(value, TERMINAL_STYLE.cyan)));
|
|
806
|
+
}
|
|
807
|
+
lines.push('');
|
|
808
|
+
}
|
|
660
809
|
if (helpData.options.length > 0) {
|
|
661
810
|
lines.push(styleText('Options:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
|
|
662
|
-
const maxSigLen = Math.max(...helpData.options.map(line => line.sig.length));
|
|
663
811
|
for (const { sig, desc } of helpData.options) {
|
|
664
|
-
|
|
665
|
-
lines.push(` ${styleText(sig, TERMINAL_STYLE.cyan)}${padding}${desc}`);
|
|
812
|
+
lines.push(this.#renderAlignedHelpLine(sig, desc, labelWidth, value => styleText(value, TERMINAL_STYLE.cyan)));
|
|
666
813
|
}
|
|
667
814
|
lines.push('');
|
|
668
815
|
}
|
|
669
816
|
if (helpData.commands.length > 0) {
|
|
670
817
|
lines.push(styleText('Commands:', TERMINAL_STYLE.bold, TERMINAL_STYLE.underline));
|
|
671
|
-
const maxNameLen = Math.max(...helpData.commands.map(line => line.name.length));
|
|
672
818
|
for (const { name, desc } of helpData.commands) {
|
|
673
|
-
|
|
674
|
-
lines.push(` ${styleText(name, TERMINAL_STYLE.cyan)}${padding}${desc}`);
|
|
819
|
+
lines.push(this.#renderAlignedHelpLine(name, desc, labelWidth, value => styleText(value, TERMINAL_STYLE.cyan)));
|
|
675
820
|
}
|
|
676
821
|
lines.push('');
|
|
677
822
|
}
|
|
@@ -686,16 +831,41 @@ class Command {
|
|
|
686
831
|
}
|
|
687
832
|
return lines.join('\n');
|
|
688
833
|
}
|
|
834
|
+
#getHelpLabelWidth(helpData) {
|
|
835
|
+
const labels = [
|
|
836
|
+
...helpData.arguments.map(line => line.sig),
|
|
837
|
+
...helpData.options.map(line => line.sig),
|
|
838
|
+
...helpData.commands.map(line => line.name),
|
|
839
|
+
];
|
|
840
|
+
if (labels.length === 0) {
|
|
841
|
+
return 0;
|
|
842
|
+
}
|
|
843
|
+
return Math.max(...labels.map(getDisplayWidth));
|
|
844
|
+
}
|
|
845
|
+
#renderAlignedHelpLine(label, desc, labelWidth, styleLabel) {
|
|
846
|
+
const paddedLabel = padDisplayEnd(label, labelWidth);
|
|
847
|
+
const outputLabel = styleLabel ? styleLabel(paddedLabel) : paddedLabel;
|
|
848
|
+
return ` ${outputLabel} ${desc}`;
|
|
849
|
+
}
|
|
689
850
|
getCompletionMeta() {
|
|
690
851
|
const allOptions = this.#resolveOptionPolicy().mergedOptions;
|
|
691
852
|
const options = [];
|
|
853
|
+
const argumentsMeta = [];
|
|
692
854
|
for (const opt of allOptions) {
|
|
693
855
|
options.push({
|
|
694
856
|
long: opt.long,
|
|
695
857
|
short: opt.short,
|
|
696
858
|
desc: opt.desc,
|
|
697
859
|
takesValue: opt.args !== 'none',
|
|
698
|
-
choices: opt.choices,
|
|
860
|
+
choices: opt.choices?.map(choice => String(choice)),
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
for (const arg of this.#arguments) {
|
|
864
|
+
argumentsMeta.push({
|
|
865
|
+
name: arg.name,
|
|
866
|
+
kind: arg.kind,
|
|
867
|
+
type: arg.type,
|
|
868
|
+
choices: arg.type === 'choice' ? arg.choices?.map(choice => String(choice)) : undefined,
|
|
699
869
|
});
|
|
700
870
|
}
|
|
701
871
|
return {
|
|
@@ -703,6 +873,7 @@ class Command {
|
|
|
703
873
|
desc: this.#desc,
|
|
704
874
|
aliases: [],
|
|
705
875
|
options,
|
|
876
|
+
arguments: argumentsMeta,
|
|
706
877
|
subcommands: this.#subcommandsList.map(entry => {
|
|
707
878
|
const subMeta = entry.command.getCompletionMeta();
|
|
708
879
|
return {
|
|
@@ -1348,8 +1519,8 @@ class Command {
|
|
|
1348
1519
|
return opt.coerce(rawValue);
|
|
1349
1520
|
}
|
|
1350
1521
|
if (opt.type === 'number') {
|
|
1351
|
-
const num =
|
|
1352
|
-
if (
|
|
1522
|
+
const num = parsePrimitiveNumber(rawValue);
|
|
1523
|
+
if (num === undefined) {
|
|
1353
1524
|
throw new CommanderError('InvalidType', `invalid number "${rawValue}" for option "--${camelToKebabCase$1(opt.long)}"`, this.#getCommandPath());
|
|
1354
1525
|
}
|
|
1355
1526
|
return num;
|
|
@@ -1359,12 +1530,34 @@ class Command {
|
|
|
1359
1530
|
#parseArguments(rawArgs) {
|
|
1360
1531
|
const argumentDefs = this.#arguments;
|
|
1361
1532
|
const args = {};
|
|
1362
|
-
const
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1533
|
+
const missing = [];
|
|
1534
|
+
let remaining = rawArgs.length;
|
|
1535
|
+
for (const def of argumentDefs) {
|
|
1536
|
+
if (def.kind === 'required') {
|
|
1537
|
+
if (remaining === 0) {
|
|
1538
|
+
missing.push(def.name);
|
|
1539
|
+
}
|
|
1540
|
+
else {
|
|
1541
|
+
remaining -= 1;
|
|
1542
|
+
}
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
if (def.kind === 'optional') {
|
|
1546
|
+
if (remaining > 0) {
|
|
1547
|
+
remaining -= 1;
|
|
1548
|
+
}
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
if (def.kind === 'some') {
|
|
1552
|
+
if (remaining === 0) {
|
|
1553
|
+
missing.push(def.name);
|
|
1554
|
+
}
|
|
1555
|
+
remaining = 0;
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
remaining = 0;
|
|
1559
|
+
}
|
|
1560
|
+
if (missing.length > 0) {
|
|
1368
1561
|
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
|
|
1369
1562
|
}
|
|
1370
1563
|
let index = 0;
|
|
@@ -1375,47 +1568,65 @@ class Command {
|
|
|
1375
1568
|
index = rawArgs.length;
|
|
1376
1569
|
break;
|
|
1377
1570
|
}
|
|
1571
|
+
if (def.kind === 'some') {
|
|
1572
|
+
const rest = rawArgs.slice(index);
|
|
1573
|
+
if (rest.length === 0) {
|
|
1574
|
+
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${def.name}`, this.#getCommandPath());
|
|
1575
|
+
}
|
|
1576
|
+
args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
|
|
1577
|
+
index = rawArgs.length;
|
|
1578
|
+
break;
|
|
1579
|
+
}
|
|
1378
1580
|
const raw = rawArgs[index];
|
|
1379
1581
|
if (raw === undefined) {
|
|
1380
1582
|
if (def.kind === 'optional') {
|
|
1381
1583
|
args[def.name] = def.default ?? undefined;
|
|
1382
1584
|
continue;
|
|
1383
1585
|
}
|
|
1586
|
+
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${def.name}`, this.#getCommandPath());
|
|
1384
1587
|
}
|
|
1385
1588
|
else {
|
|
1386
1589
|
args[def.name] = this.#convertArgument(def, raw);
|
|
1387
1590
|
index += 1;
|
|
1388
1591
|
}
|
|
1389
1592
|
}
|
|
1390
|
-
const
|
|
1391
|
-
if (!
|
|
1593
|
+
const hasRestArgument = argumentDefs.some(a => a.kind === 'variadic' || a.kind === 'some');
|
|
1594
|
+
if (!hasRestArgument && index < rawArgs.length) {
|
|
1392
1595
|
throw new CommanderError('TooManyArguments', `too many arguments: expected ${argumentDefs.length}, got ${rawArgs.length}`, this.#getCommandPath());
|
|
1393
1596
|
}
|
|
1394
1597
|
return { args, rawArgs };
|
|
1395
1598
|
}
|
|
1396
1599
|
#convertArgument(def, raw) {
|
|
1600
|
+
let value;
|
|
1397
1601
|
if (def.coerce) {
|
|
1398
1602
|
try {
|
|
1399
|
-
|
|
1603
|
+
value = def.coerce(raw);
|
|
1400
1604
|
}
|
|
1401
1605
|
catch {
|
|
1402
1606
|
throw new CommanderError('InvalidType', `invalid value "${raw}" for argument "${def.name}"`, this.#getCommandPath());
|
|
1403
1607
|
}
|
|
1404
1608
|
}
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1609
|
+
else {
|
|
1610
|
+
value = raw;
|
|
1611
|
+
}
|
|
1612
|
+
if (typeof value !== 'string') {
|
|
1613
|
+
throw new CommanderError('InvalidType', `invalid value for argument "${def.name}": expected ${def.type}`, this.#getCommandPath());
|
|
1614
|
+
}
|
|
1615
|
+
if (def.type === 'choice') {
|
|
1616
|
+
const choices = def.choices ?? [];
|
|
1617
|
+
if (!choices.includes(value)) {
|
|
1618
|
+
throw new CommanderError('InvalidChoice', `invalid value "${value}" for argument "${def.name}". Allowed: ${choices
|
|
1619
|
+
.map(choice => JSON.stringify(choice))
|
|
1620
|
+
.join(', ')}`, this.#getCommandPath());
|
|
1409
1621
|
}
|
|
1410
|
-
return n;
|
|
1411
1622
|
}
|
|
1412
|
-
return
|
|
1623
|
+
return value;
|
|
1413
1624
|
}
|
|
1414
1625
|
#hasUserOption(long) {
|
|
1415
1626
|
return this.#options.some(option => option.long === long);
|
|
1416
1627
|
}
|
|
1417
1628
|
#supportsBuiltinVersion() {
|
|
1418
|
-
return this.#
|
|
1629
|
+
return this.#version !== undefined && this.#builtin.option.version;
|
|
1419
1630
|
}
|
|
1420
1631
|
#resolveOptionPolicy() {
|
|
1421
1632
|
const optionMap = new Map();
|
|
@@ -1495,6 +1706,9 @@ class Command {
|
|
|
1495
1706
|
if (!/^[a-z][a-zA-Z0-9]*$/.test(opt.long)) {
|
|
1496
1707
|
throw new CommanderError('ConfigurationError', `option long name must be camelCase: "${opt.long}"`, this.#getCommandPath());
|
|
1497
1708
|
}
|
|
1709
|
+
if (opt.short !== undefined && opt.short.length !== 1) {
|
|
1710
|
+
throw new CommanderError('ConfigurationError', `option short name must be a single character: "${opt.short}"`, this.#getCommandPath());
|
|
1711
|
+
}
|
|
1498
1712
|
if (opt.required && opt.default !== undefined) {
|
|
1499
1713
|
throw new CommanderError('ConfigurationError', `option "--${opt.long}" cannot be both required and have a default value`, this.#getCommandPath());
|
|
1500
1714
|
}
|
|
@@ -1511,24 +1725,58 @@ class Command {
|
|
|
1511
1725
|
}
|
|
1512
1726
|
}
|
|
1513
1727
|
#validateArgumentConfig(arg) {
|
|
1514
|
-
if (arg.kind
|
|
1515
|
-
|
|
1728
|
+
if (arg.kind !== 'required' &&
|
|
1729
|
+
arg.kind !== 'optional' &&
|
|
1730
|
+
arg.kind !== 'variadic' &&
|
|
1731
|
+
arg.kind !== 'some') {
|
|
1732
|
+
throw new CommanderError('ConfigurationError', `argument "${arg.name}" must specify a valid kind`, this.#getCommandPath());
|
|
1733
|
+
}
|
|
1734
|
+
if (arg.type !== 'string' && arg.type !== 'choice') {
|
|
1735
|
+
throw new CommanderError('ConfigurationError', `argument "${arg.name}" must specify a valid type`, this.#getCommandPath());
|
|
1736
|
+
}
|
|
1737
|
+
if (arg.default !== undefined && arg.kind !== 'optional') {
|
|
1738
|
+
throw new CommanderError('ConfigurationError', `only optional argument "${arg.name}" can have a default value`, this.#getCommandPath());
|
|
1739
|
+
}
|
|
1740
|
+
if (arg.type === 'string' && arg.choices !== undefined) {
|
|
1741
|
+
throw new CommanderError('ConfigurationError', `argument "${arg.name}" of type "string" cannot declare choices`, this.#getCommandPath());
|
|
1742
|
+
}
|
|
1743
|
+
if (arg.type === 'choice') {
|
|
1744
|
+
if (!Array.isArray(arg.choices) || arg.choices.length === 0) {
|
|
1745
|
+
throw new CommanderError('ConfigurationError', `argument "${arg.name}" of type "choice" must declare a non-empty choices array`, this.#getCommandPath());
|
|
1746
|
+
}
|
|
1747
|
+
if (arg.choices.some(choice => typeof choice !== 'string')) {
|
|
1748
|
+
throw new CommanderError('ConfigurationError', `argument "${arg.name}" choices must be string[]`, this.#getCommandPath());
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
if (arg.default !== undefined) {
|
|
1752
|
+
this.#validateArgumentDefaultValue(arg);
|
|
1516
1753
|
}
|
|
1517
|
-
if (arg.kind === 'variadic') {
|
|
1518
|
-
if (this.#arguments.some(a => a.kind === 'variadic')) {
|
|
1519
|
-
throw new CommanderError('ConfigurationError', 'only one variadic argument is allowed', this.#getCommandPath());
|
|
1754
|
+
if (arg.kind === 'variadic' || arg.kind === 'some') {
|
|
1755
|
+
if (this.#arguments.some(a => a.kind === 'variadic' || a.kind === 'some')) {
|
|
1756
|
+
throw new CommanderError('ConfigurationError', 'only one variadic/some argument is allowed', this.#getCommandPath());
|
|
1520
1757
|
}
|
|
1521
1758
|
}
|
|
1522
1759
|
if (this.#arguments.length > 0) {
|
|
1523
1760
|
const last = this.#arguments[this.#arguments.length - 1];
|
|
1524
|
-
if (last.kind === 'variadic') {
|
|
1525
|
-
throw new CommanderError('ConfigurationError', 'variadic argument must be the last argument', this.#getCommandPath());
|
|
1761
|
+
if (last.kind === 'variadic' || last.kind === 'some') {
|
|
1762
|
+
throw new CommanderError('ConfigurationError', 'variadic/some argument must be the last argument', this.#getCommandPath());
|
|
1526
1763
|
}
|
|
1527
1764
|
}
|
|
1528
1765
|
if (arg.kind === 'required') {
|
|
1529
|
-
const hasOptional = this.#arguments.some(a => a.kind === 'optional' || a.kind === 'variadic');
|
|
1766
|
+
const hasOptional = this.#arguments.some(a => a.kind === 'optional' || a.kind === 'variadic' || a.kind === 'some');
|
|
1530
1767
|
if (hasOptional) {
|
|
1531
|
-
throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot come after optional/variadic arguments`, this.#getCommandPath());
|
|
1768
|
+
throw new CommanderError('ConfigurationError', `required argument "${arg.name}" cannot come after optional/variadic/some arguments`, this.#getCommandPath());
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
#validateArgumentDefaultValue(arg) {
|
|
1773
|
+
if (typeof arg.default !== 'string') {
|
|
1774
|
+
throw new CommanderError('ConfigurationError', `default value for argument "${arg.name}" must match type "${arg.type}"`, this.#getCommandPath());
|
|
1775
|
+
}
|
|
1776
|
+
if (arg.type === 'choice') {
|
|
1777
|
+
const choices = arg.choices ?? [];
|
|
1778
|
+
if (!choices.includes(arg.default)) {
|
|
1779
|
+
throw new CommanderError('ConfigurationError', `default value for argument "${arg.name}" must be one of declared choices`, this.#getCommandPath());
|
|
1532
1780
|
}
|
|
1533
1781
|
}
|
|
1534
1782
|
}
|
|
@@ -1885,6 +2133,7 @@ class BashCompletion {
|
|
|
1885
2133
|
'',
|
|
1886
2134
|
`${funcName}() {`,
|
|
1887
2135
|
' local cur prev words cword',
|
|
2136
|
+
' local opts arg_choices prefer_value_choices',
|
|
1888
2137
|
' _init_completion || return',
|
|
1889
2138
|
'',
|
|
1890
2139
|
...this.#generateCommandCase(this.#meta, 1),
|
|
@@ -1904,13 +2153,15 @@ class BashCompletion {
|
|
|
1904
2153
|
for (const opt of cmd.options) {
|
|
1905
2154
|
const kebabLong = camelToKebabCase(opt.long);
|
|
1906
2155
|
if (opt.short)
|
|
1907
|
-
optParts.push(`-${opt.short}`);
|
|
1908
|
-
optParts.push(`--${kebabLong}`);
|
|
2156
|
+
optParts.push(this.#escapeWord(`-${opt.short}`));
|
|
2157
|
+
optParts.push(this.#escapeWord(`--${kebabLong}`));
|
|
1909
2158
|
if (!opt.takesValue) {
|
|
1910
|
-
optParts.push(`--no-${kebabLong}`);
|
|
2159
|
+
optParts.push(this.#escapeWord(`--no-${kebabLong}`));
|
|
1911
2160
|
}
|
|
1912
2161
|
}
|
|
1913
|
-
const subParts = cmd.subcommands
|
|
2162
|
+
const subParts = cmd.subcommands
|
|
2163
|
+
.flatMap(sub => [sub.name, ...sub.aliases])
|
|
2164
|
+
.map(value => this.#escapeWord(value));
|
|
1914
2165
|
const allOpts = [...optParts, ...subParts].join(' ');
|
|
1915
2166
|
if (cmd.subcommands.length > 0) {
|
|
1916
2167
|
lines.push(`${indent}case "\${words[${depth}]}" in`);
|
|
@@ -1922,14 +2173,105 @@ class BashCompletion {
|
|
|
1922
2173
|
}
|
|
1923
2174
|
lines.push(`${indent} *)`);
|
|
1924
2175
|
lines.push(`${indent} opts="${allOpts}"`);
|
|
2176
|
+
this.#appendChoiceLogicForCommand(lines, `${indent} `, cmd, depth);
|
|
1925
2177
|
lines.push(`${indent} ;;`);
|
|
1926
2178
|
lines.push(`${indent}esac`);
|
|
1927
2179
|
}
|
|
1928
2180
|
else {
|
|
1929
2181
|
lines.push(`${indent}opts="${allOpts}"`);
|
|
2182
|
+
this.#appendChoiceLogicForCommand(lines, indent, cmd, depth);
|
|
1930
2183
|
}
|
|
1931
2184
|
return lines;
|
|
1932
2185
|
}
|
|
2186
|
+
#serializeWordList(words) {
|
|
2187
|
+
return words.map(choice => this.#escapeWord(choice)).join(' ');
|
|
2188
|
+
}
|
|
2189
|
+
#appendChoiceLogicForCommand(lines, indent, cmd, depth) {
|
|
2190
|
+
const valueOptions = cmd.options.filter(opt => opt.takesValue);
|
|
2191
|
+
const valueOptionsWithChoices = valueOptions.filter(opt => opt.choices && opt.choices.length > 0);
|
|
2192
|
+
const valueLongPatterns = valueOptions.map(opt => `--${camelToKebabCase(opt.long)}`);
|
|
2193
|
+
const valueShortPatterns = valueOptions
|
|
2194
|
+
.map(opt => opt.short)
|
|
2195
|
+
.filter((short) => typeof short === 'string');
|
|
2196
|
+
lines.push(`${indent}prefer_value_choices=0`);
|
|
2197
|
+
if (valueOptionsWithChoices.length > 0) {
|
|
2198
|
+
lines.push(`${indent}if [[ "$cur" != -* ]]; then`);
|
|
2199
|
+
lines.push(`${indent} case "$prev" in`);
|
|
2200
|
+
for (const opt of valueOptionsWithChoices) {
|
|
2201
|
+
const patterns = [`--${camelToKebabCase(opt.long)}`];
|
|
2202
|
+
if (opt.short) {
|
|
2203
|
+
patterns.push(`-${opt.short}`);
|
|
2204
|
+
}
|
|
2205
|
+
lines.push(`${indent} ${patterns.join('|')})`);
|
|
2206
|
+
lines.push(`${indent} opts="${this.#serializeWordList(opt.choices ?? [])}"`);
|
|
2207
|
+
lines.push(`${indent} prefer_value_choices=1`);
|
|
2208
|
+
lines.push(`${indent} ;;`);
|
|
2209
|
+
}
|
|
2210
|
+
lines.push(`${indent} esac`);
|
|
2211
|
+
lines.push(`${indent}fi`);
|
|
2212
|
+
}
|
|
2213
|
+
lines.push(`${indent}if [[ $prefer_value_choices -eq 0 ]]; then`);
|
|
2214
|
+
lines.push(`${indent} positional_count=0`);
|
|
2215
|
+
lines.push(`${indent} expect_value=0`);
|
|
2216
|
+
lines.push(`${indent} for ((idx=${depth}; idx<cword; idx++)); do`);
|
|
2217
|
+
lines.push(`${indent} token="\${words[idx]}"`);
|
|
2218
|
+
lines.push(`${indent} if [[ $expect_value -eq 1 ]]; then`);
|
|
2219
|
+
lines.push(`${indent} expect_value=0`);
|
|
2220
|
+
lines.push(`${indent} continue`);
|
|
2221
|
+
lines.push(`${indent} fi`);
|
|
2222
|
+
lines.push(`${indent} if [[ "$token" == --* ]]; then`);
|
|
2223
|
+
lines.push(`${indent} if [[ "$token" == *=* ]]; then`);
|
|
2224
|
+
lines.push(`${indent} continue`);
|
|
2225
|
+
lines.push(`${indent} fi`);
|
|
2226
|
+
if (valueLongPatterns.length > 0) {
|
|
2227
|
+
lines.push(`${indent} case "$token" in`);
|
|
2228
|
+
lines.push(`${indent} ${valueLongPatterns.join('|')}) expect_value=1 ;;`);
|
|
2229
|
+
lines.push(`${indent} esac`);
|
|
2230
|
+
}
|
|
2231
|
+
lines.push(`${indent} continue`);
|
|
2232
|
+
lines.push(`${indent} fi`);
|
|
2233
|
+
lines.push(`${indent} if [[ "$token" == -* && "$token" != "-" ]]; then`);
|
|
2234
|
+
lines.push(`${indent} if [[ \${#token} -eq 2 ]]; then`);
|
|
2235
|
+
if (valueShortPatterns.length > 0) {
|
|
2236
|
+
lines.push(`${indent} case "\${token:1:1}" in`);
|
|
2237
|
+
lines.push(`${indent} ${valueShortPatterns.join('|')}) expect_value=1 ;;`);
|
|
2238
|
+
lines.push(`${indent} esac`);
|
|
2239
|
+
}
|
|
2240
|
+
lines.push(`${indent} fi`);
|
|
2241
|
+
lines.push(`${indent} continue`);
|
|
2242
|
+
lines.push(`${indent} fi`);
|
|
2243
|
+
lines.push(`${indent} positional_count=$((positional_count + 1))`);
|
|
2244
|
+
lines.push(`${indent} done`);
|
|
2245
|
+
lines.push(`${indent} if [[ $expect_value -eq 1 ]]; then`);
|
|
2246
|
+
lines.push(`${indent} opts=""`);
|
|
2247
|
+
lines.push(`${indent} prefer_value_choices=1`);
|
|
2248
|
+
lines.push(`${indent} elif [[ "$cur" != -* ]]; then`);
|
|
2249
|
+
lines.push(`${indent} arg_slot=-1`);
|
|
2250
|
+
lines.push(`${indent} arg_count=${cmd.arguments.length}`);
|
|
2251
|
+
const hasRestArgument = cmd.arguments.length > 0 &&
|
|
2252
|
+
(cmd.arguments[cmd.arguments.length - 1].kind === 'variadic' ||
|
|
2253
|
+
cmd.arguments[cmd.arguments.length - 1].kind === 'some');
|
|
2254
|
+
lines.push(`${indent} has_rest=${hasRestArgument ? 1 : 0}`);
|
|
2255
|
+
lines.push(`${indent} if [[ $has_rest -eq 1 && $positional_count -ge $((arg_count - 1)) ]]; then`);
|
|
2256
|
+
lines.push(`${indent} arg_slot=$((arg_count - 1))`);
|
|
2257
|
+
lines.push(`${indent} elif [[ $positional_count -lt $arg_count ]]; then`);
|
|
2258
|
+
lines.push(`${indent} arg_slot=$positional_count`);
|
|
2259
|
+
lines.push(`${indent} fi`);
|
|
2260
|
+
lines.push(`${indent} case "$arg_slot" in`);
|
|
2261
|
+
for (let index = 0; index < cmd.arguments.length; index += 1) {
|
|
2262
|
+
const arg = cmd.arguments[index];
|
|
2263
|
+
if (arg.type !== 'choice' || !arg.choices || arg.choices.length === 0) {
|
|
2264
|
+
continue;
|
|
2265
|
+
}
|
|
2266
|
+
lines.push(`${indent} ${index}) opts="${this.#serializeWordList(arg.choices)}" ;;`);
|
|
2267
|
+
}
|
|
2268
|
+
lines.push(`${indent} esac`);
|
|
2269
|
+
lines.push(`${indent} fi`);
|
|
2270
|
+
lines.push(`${indent}fi`);
|
|
2271
|
+
}
|
|
2272
|
+
#escapeWord(word) {
|
|
2273
|
+
return word.replace(/([\\\s'"`$!])/g, '\\$1');
|
|
2274
|
+
}
|
|
1933
2275
|
#sanitizeName(name) {
|
|
1934
2276
|
return name.replace(/[^a-zA-Z0-9]/g, '_');
|
|
1935
2277
|
}
|
|
@@ -1937,15 +2279,19 @@ class BashCompletion {
|
|
|
1937
2279
|
class FishCompletion {
|
|
1938
2280
|
#meta;
|
|
1939
2281
|
#programName;
|
|
2282
|
+
#slotMatcherName;
|
|
1940
2283
|
constructor(meta, programName) {
|
|
1941
2284
|
this.#meta = meta;
|
|
1942
2285
|
this.#programName = programName;
|
|
2286
|
+
this.#slotMatcherName = `__${this.#sanitizeName(programName)}_match_arg_slot`;
|
|
1943
2287
|
}
|
|
1944
2288
|
generate() {
|
|
1945
2289
|
const lines = [
|
|
1946
2290
|
`# Fish completion for ${this.#programName}`,
|
|
1947
2291
|
'# Generated by @guanghechen/commander',
|
|
1948
2292
|
'',
|
|
2293
|
+
...this.#generateSlotMatcherFunction(),
|
|
2294
|
+
'',
|
|
1949
2295
|
...this.#generateCommandCompletions(this.#meta, []),
|
|
1950
2296
|
'',
|
|
1951
2297
|
];
|
|
@@ -1965,7 +2311,7 @@ class FishCompletion {
|
|
|
1965
2311
|
line += ` -l ${kebabLong}`;
|
|
1966
2312
|
line += ` -d '${this.#escape(opt.desc)}'`;
|
|
1967
2313
|
if (opt.choices && opt.choices.length > 0) {
|
|
1968
|
-
line += ` -xa '${opt.choices.join(' ')}'`;
|
|
2314
|
+
line += ` -xa '${opt.choices.map(choice => this.#escapeChoice(choice)).join(' ')}'`;
|
|
1969
2315
|
}
|
|
1970
2316
|
lines.push(line);
|
|
1971
2317
|
if (!opt.takesValue) {
|
|
@@ -1977,6 +2323,36 @@ class FishCompletion {
|
|
|
1977
2323
|
lines.push(noLine);
|
|
1978
2324
|
}
|
|
1979
2325
|
}
|
|
2326
|
+
const valueOptionLongs = cmd.options
|
|
2327
|
+
.filter(opt => opt.takesValue)
|
|
2328
|
+
.map(opt => camelToKebabCase(opt.long))
|
|
2329
|
+
.join(',');
|
|
2330
|
+
const valueOptionShorts = cmd.options
|
|
2331
|
+
.filter(opt => opt.takesValue && opt.short)
|
|
2332
|
+
.map(opt => opt.short)
|
|
2333
|
+
.join(',');
|
|
2334
|
+
const argCount = cmd.arguments.length;
|
|
2335
|
+
const hasRestArgument = argCount > 0 &&
|
|
2336
|
+
(cmd.arguments[argCount - 1].kind === 'variadic' ||
|
|
2337
|
+
cmd.arguments[argCount - 1].kind === 'some');
|
|
2338
|
+
for (let index = 0; index < cmd.arguments.length; index += 1) {
|
|
2339
|
+
const arg = cmd.arguments[index];
|
|
2340
|
+
if (arg.type !== 'choice' || !arg.choices || arg.choices.length === 0) {
|
|
2341
|
+
continue;
|
|
2342
|
+
}
|
|
2343
|
+
let line = `complete -c ${this.#programName}`;
|
|
2344
|
+
const slotCondition = `${this.#slotMatcherName} ${parentPath.length} ${argCount} ${hasRestArgument ? 1 : 0} ${index} '${valueOptionLongs}' '${valueOptionShorts}'`;
|
|
2345
|
+
if (condition) {
|
|
2346
|
+
line += ` -n '${condition}; and ${slotCondition}'`;
|
|
2347
|
+
}
|
|
2348
|
+
else {
|
|
2349
|
+
line += ` -n '${slotCondition}'`;
|
|
2350
|
+
}
|
|
2351
|
+
line += ` -f`;
|
|
2352
|
+
line += ` -a '${arg.choices.map(choice => this.#escapeChoice(choice)).join(' ')}'`;
|
|
2353
|
+
line += ` -d '${this.#escape(`Argument: ${arg.name}`)}'`;
|
|
2354
|
+
lines.push(line);
|
|
2355
|
+
}
|
|
1980
2356
|
for (const sub of cmd.subcommands) {
|
|
1981
2357
|
let line = `complete -c ${this.#programName}`;
|
|
1982
2358
|
if (isRoot) {
|
|
@@ -2000,7 +2376,7 @@ class FishCompletion {
|
|
|
2000
2376
|
aliasLine += ` -d 'Alias for ${sub.name}'`;
|
|
2001
2377
|
lines.push(aliasLine);
|
|
2002
2378
|
}
|
|
2003
|
-
const newPath = [...parentPath, sub.name];
|
|
2379
|
+
const newPath = [...parentPath, [sub.name, ...sub.aliases]];
|
|
2004
2380
|
lines.push(...this.#generateCommandCompletions(sub, newPath));
|
|
2005
2381
|
}
|
|
2006
2382
|
return lines;
|
|
@@ -2008,7 +2384,7 @@ class FishCompletion {
|
|
|
2008
2384
|
#buildCondition(path) {
|
|
2009
2385
|
if (path.length === 0)
|
|
2010
2386
|
return '';
|
|
2011
|
-
return `__fish_seen_subcommand_from ${
|
|
2387
|
+
return path.map(level => `__fish_seen_subcommand_from ${level.join(' ')}`).join('; and ');
|
|
2012
2388
|
}
|
|
2013
2389
|
#getSubcommandNames(cmd) {
|
|
2014
2390
|
return cmd.subcommands.flatMap(sub => [sub.name, ...sub.aliases]);
|
|
@@ -2016,6 +2392,68 @@ class FishCompletion {
|
|
|
2016
2392
|
#escape(s) {
|
|
2017
2393
|
return s.replace(/'/g, "\\'");
|
|
2018
2394
|
}
|
|
2395
|
+
#escapeChoice(s) {
|
|
2396
|
+
return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\s/g, '\\ ');
|
|
2397
|
+
}
|
|
2398
|
+
#sanitizeName(name) {
|
|
2399
|
+
return name.replace(/[^a-zA-Z0-9]/g, '_');
|
|
2400
|
+
}
|
|
2401
|
+
#generateSlotMatcherFunction() {
|
|
2402
|
+
return [
|
|
2403
|
+
`function ${this.#slotMatcherName} --argument-names depth arg_count has_rest target_index long_opts short_opts`,
|
|
2404
|
+
' set -l tokens (commandline -opc)',
|
|
2405
|
+
' set -l start (math $depth + 2)',
|
|
2406
|
+
' set -l positional 0',
|
|
2407
|
+
' set -l expect_value 0',
|
|
2408
|
+
' set -l i $start',
|
|
2409
|
+
' set -l token_count (count $tokens)',
|
|
2410
|
+
' set -l long_list (string split "," -- $long_opts)',
|
|
2411
|
+
' set -l short_list (string split "," -- $short_opts)',
|
|
2412
|
+
' while test $i -le $token_count',
|
|
2413
|
+
' set -l token $tokens[$i]',
|
|
2414
|
+
' if test $expect_value -eq 1',
|
|
2415
|
+
' set expect_value 0',
|
|
2416
|
+
' set i (math $i + 1)',
|
|
2417
|
+
' continue',
|
|
2418
|
+
' end',
|
|
2419
|
+
' if string match -q -- "--*" $token',
|
|
2420
|
+
' if string match -q -- "*=*" $token',
|
|
2421
|
+
' set i (math $i + 1)',
|
|
2422
|
+
' continue',
|
|
2423
|
+
' end',
|
|
2424
|
+
' set -l opt_name (string replace -r "^--" "" -- $token)',
|
|
2425
|
+
' if contains -- $opt_name $long_list',
|
|
2426
|
+
' set expect_value 1',
|
|
2427
|
+
' end',
|
|
2428
|
+
' set i (math $i + 1)',
|
|
2429
|
+
' continue',
|
|
2430
|
+
' end',
|
|
2431
|
+
' if test "$token" != "-"; and string match -q -- "-*" $token',
|
|
2432
|
+
' set -l raw_short (string replace -r "^-" "" -- $token)',
|
|
2433
|
+
' if test (string length -- $raw_short) -eq 1',
|
|
2434
|
+
' if contains -- $raw_short $short_list',
|
|
2435
|
+
' set expect_value 1',
|
|
2436
|
+
' end',
|
|
2437
|
+
' end',
|
|
2438
|
+
' set i (math $i + 1)',
|
|
2439
|
+
' continue',
|
|
2440
|
+
' end',
|
|
2441
|
+
' set positional (math $positional + 1)',
|
|
2442
|
+
' set i (math $i + 1)',
|
|
2443
|
+
' end',
|
|
2444
|
+
' if test $expect_value -eq 1',
|
|
2445
|
+
' return 1',
|
|
2446
|
+
' end',
|
|
2447
|
+
' set -l slot -1',
|
|
2448
|
+
' if test $has_rest -eq 1; and test $positional -ge (math $arg_count - 1)',
|
|
2449
|
+
' set slot (math $arg_count - 1)',
|
|
2450
|
+
' else if test $positional -lt $arg_count',
|
|
2451
|
+
' set slot $positional',
|
|
2452
|
+
' end',
|
|
2453
|
+
' test $slot -eq $target_index',
|
|
2454
|
+
'end',
|
|
2455
|
+
];
|
|
2456
|
+
}
|
|
2019
2457
|
}
|
|
2020
2458
|
class PwshCompletion {
|
|
2021
2459
|
#meta;
|
|
@@ -2041,16 +2479,105 @@ class PwshCompletion {
|
|
|
2041
2479
|
'',
|
|
2042
2480
|
' # Find current command context',
|
|
2043
2481
|
' $cmd = $commands',
|
|
2482
|
+
' $commandDepth = 1',
|
|
2044
2483
|
' foreach ($word in $words[1..($words.Count - 1)]) {',
|
|
2045
2484
|
' if ($word.StartsWith("-")) { continue }',
|
|
2046
2485
|
' if ($cmd.subcommands -and $cmd.subcommands.ContainsKey($word)) {',
|
|
2047
2486
|
' $cmd = $cmd.subcommands[$word]',
|
|
2487
|
+
' $commandDepth += 1',
|
|
2048
2488
|
' }',
|
|
2049
2489
|
' }',
|
|
2050
2490
|
'',
|
|
2051
2491
|
' # Generate completions',
|
|
2052
2492
|
' $completions = @()',
|
|
2053
2493
|
'',
|
|
2494
|
+
' # Option value slot (always higher priority than arguments)',
|
|
2495
|
+
' $previous = if ($words.Count -ge 2) { $words[$words.Count - 2] } else { $null }',
|
|
2496
|
+
' if ($previous) {',
|
|
2497
|
+
' foreach ($opt in $cmd.options) {',
|
|
2498
|
+
' $isLong = $previous -eq "--$($opt.long)"',
|
|
2499
|
+
' $isShort = $opt.short -and $previous -eq "-$($opt.short)"',
|
|
2500
|
+
' if ($isLong -or $isShort) {',
|
|
2501
|
+
' if ($opt.choices) {',
|
|
2502
|
+
' foreach ($choice in $opt.choices) {',
|
|
2503
|
+
' if ($choice -like "$current*") {',
|
|
2504
|
+
' $completions += [System.Management.Automation.CompletionResult]::new(',
|
|
2505
|
+
' $choice,',
|
|
2506
|
+
' $choice,',
|
|
2507
|
+
' "ParameterValue",',
|
|
2508
|
+
' $choice',
|
|
2509
|
+
' )',
|
|
2510
|
+
' }',
|
|
2511
|
+
' }',
|
|
2512
|
+
' }',
|
|
2513
|
+
' return $completions',
|
|
2514
|
+
' }',
|
|
2515
|
+
' }',
|
|
2516
|
+
' }',
|
|
2517
|
+
'',
|
|
2518
|
+
' # Determine argument slot',
|
|
2519
|
+
' $positionalCount = 0',
|
|
2520
|
+
' $expectValue = $false',
|
|
2521
|
+
' for ($i = $commandDepth; $i -lt ($words.Count - 1); $i += 1) {',
|
|
2522
|
+
' $token = $words[$i]',
|
|
2523
|
+
' if ($expectValue) {',
|
|
2524
|
+
' $expectValue = $false',
|
|
2525
|
+
' continue',
|
|
2526
|
+
' }',
|
|
2527
|
+
' if ($token.StartsWith("--")) {',
|
|
2528
|
+
' if ($token.Contains("=")) { continue }',
|
|
2529
|
+
' foreach ($opt in $cmd.options) {',
|
|
2530
|
+
' if ($token -eq "--$($opt.long)" -and $opt.takesValue) {',
|
|
2531
|
+
' $expectValue = $true',
|
|
2532
|
+
' break',
|
|
2533
|
+
' }',
|
|
2534
|
+
' }',
|
|
2535
|
+
' continue',
|
|
2536
|
+
' }',
|
|
2537
|
+
' if ($token.StartsWith("-") -and $token -ne "-") {',
|
|
2538
|
+
' if ($token.Length -eq 2) {',
|
|
2539
|
+
' foreach ($opt in $cmd.options) {',
|
|
2540
|
+
' if ($opt.short -and $token -eq "-$($opt.short)" -and $opt.takesValue) {',
|
|
2541
|
+
' $expectValue = $true',
|
|
2542
|
+
' break',
|
|
2543
|
+
' }',
|
|
2544
|
+
' }',
|
|
2545
|
+
' }',
|
|
2546
|
+
' continue',
|
|
2547
|
+
' }',
|
|
2548
|
+
' $positionalCount += 1',
|
|
2549
|
+
' }',
|
|
2550
|
+
' if ($expectValue) {',
|
|
2551
|
+
' return $completions',
|
|
2552
|
+
' }',
|
|
2553
|
+
' if (-not $current.StartsWith("-") -and $cmd.arguments -and $cmd.arguments.Count -gt 0) {',
|
|
2554
|
+
' $argSlot = -1',
|
|
2555
|
+
' $argCount = $cmd.arguments.Count',
|
|
2556
|
+
' $lastArg = $cmd.arguments[$argCount - 1]',
|
|
2557
|
+
' $hasRest = $lastArg.kind -eq "variadic" -or $lastArg.kind -eq "some"',
|
|
2558
|
+
' if ($hasRest -and $positionalCount -ge ($argCount - 1)) {',
|
|
2559
|
+
' $argSlot = $argCount - 1',
|
|
2560
|
+
' } elseif ($positionalCount -lt $argCount) {',
|
|
2561
|
+
' $argSlot = $positionalCount',
|
|
2562
|
+
' }',
|
|
2563
|
+
' if ($argSlot -ge 0) {',
|
|
2564
|
+
' $argMeta = $cmd.arguments[$argSlot]',
|
|
2565
|
+
' if ($argMeta.choices) {',
|
|
2566
|
+
' foreach ($choice in $argMeta.choices) {',
|
|
2567
|
+
' if ($choice -like "$current*") {',
|
|
2568
|
+
' $completions += [System.Management.Automation.CompletionResult]::new(',
|
|
2569
|
+
' $choice,',
|
|
2570
|
+
' $choice,',
|
|
2571
|
+
' "ParameterValue",',
|
|
2572
|
+
' $choice',
|
|
2573
|
+
' )',
|
|
2574
|
+
' }',
|
|
2575
|
+
' }',
|
|
2576
|
+
' return $completions',
|
|
2577
|
+
' }',
|
|
2578
|
+
' }',
|
|
2579
|
+
' }',
|
|
2580
|
+
'',
|
|
2054
2581
|
' # Options',
|
|
2055
2582
|
' if ($current.StartsWith("-")) {',
|
|
2056
2583
|
' foreach ($opt in $cmd.options) {',
|
|
@@ -2059,7 +2586,7 @@ class PwshCompletion {
|
|
|
2059
2586
|
' "--$($opt.long)",',
|
|
2060
2587
|
' $opt.long,',
|
|
2061
2588
|
' "ParameterName",',
|
|
2062
|
-
' $opt.
|
|
2589
|
+
' $opt.description',
|
|
2063
2590
|
' )',
|
|
2064
2591
|
' }',
|
|
2065
2592
|
' if ($opt.isBoolean -and "--no-$($opt.long)" -like "$current*") {',
|
|
@@ -2067,7 +2594,7 @@ class PwshCompletion {
|
|
|
2067
2594
|
' "--no-$($opt.long)",',
|
|
2068
2595
|
' "no-$($opt.long)",',
|
|
2069
2596
|
' "ParameterName",',
|
|
2070
|
-
' $opt.
|
|
2597
|
+
' $opt.description',
|
|
2071
2598
|
' )',
|
|
2072
2599
|
' }',
|
|
2073
2600
|
' if ($opt.short -and "-$($opt.short)" -like "$current*") {',
|
|
@@ -2075,7 +2602,7 @@ class PwshCompletion {
|
|
|
2075
2602
|
' "-$($opt.short)",',
|
|
2076
2603
|
' $opt.short,',
|
|
2077
2604
|
' "ParameterName",',
|
|
2078
|
-
' $opt.
|
|
2605
|
+
' $opt.description',
|
|
2079
2606
|
' )',
|
|
2080
2607
|
' }',
|
|
2081
2608
|
' }',
|
|
@@ -2089,7 +2616,7 @@ class PwshCompletion {
|
|
|
2089
2616
|
' $sub,',
|
|
2090
2617
|
' $sub,',
|
|
2091
2618
|
' "Command",',
|
|
2092
|
-
' $cmd.subcommands[$sub].
|
|
2619
|
+
' $cmd.subcommands[$sub].description',
|
|
2093
2620
|
' )',
|
|
2094
2621
|
' }',
|
|
2095
2622
|
' }',
|
|
@@ -2113,8 +2640,25 @@ class PwshCompletion {
|
|
|
2113
2640
|
lines.push(`${indent} long = '${kebabLong}'`);
|
|
2114
2641
|
lines.push(`${indent} description = '${this.#escape(opt.desc)}'`);
|
|
2115
2642
|
lines.push(`${indent} isBoolean = $${!opt.takesValue}`);
|
|
2643
|
+
lines.push(`${indent} takesValue = $${opt.takesValue}`);
|
|
2116
2644
|
if (opt.choices) {
|
|
2117
|
-
lines.push(`${indent} choices = @('${opt.choices
|
|
2645
|
+
lines.push(`${indent} choices = @('${opt.choices
|
|
2646
|
+
.map(choice => this.#escape(choice))
|
|
2647
|
+
.join("', '")}')`);
|
|
2648
|
+
}
|
|
2649
|
+
lines.push(`${indent} }`);
|
|
2650
|
+
}
|
|
2651
|
+
lines.push(`${indent})`);
|
|
2652
|
+
lines.push(`${indent}arguments = @(`);
|
|
2653
|
+
for (const arg of cmd.arguments) {
|
|
2654
|
+
lines.push(`${indent} @{`);
|
|
2655
|
+
lines.push(`${indent} name = '${this.#escape(arg.name)}'`);
|
|
2656
|
+
lines.push(`${indent} kind = '${arg.kind}'`);
|
|
2657
|
+
lines.push(`${indent} type = '${arg.type}'`);
|
|
2658
|
+
if (arg.choices && arg.choices.length > 0) {
|
|
2659
|
+
lines.push(`${indent} choices = @('${arg.choices
|
|
2660
|
+
.map(choice => this.#escape(choice))
|
|
2661
|
+
.join("', '")}')`);
|
|
2118
2662
|
}
|
|
2119
2663
|
lines.push(`${indent} }`);
|
|
2120
2664
|
}
|