@guanghechen/commander 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/lib/cjs/index.cjs +251 -25
- package/lib/esm/index.mjs +251 -25
- package/lib/types/index.d.ts +14 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 3.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Implement option bubbling with shift/apply flow
|
|
8
|
+
- Add `shift()` method for bottom-up option consumption (leaf → root)
|
|
9
|
+
- Refactor `run()` with new flow: route → split → shift → apply → action
|
|
10
|
+
- Add `UnexpectedArgument` error type for positional args before `--`
|
|
11
|
+
|
|
3
12
|
All notable changes to this project will be documented in this file. See
|
|
4
13
|
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
14
|
|
package/lib/cjs/index.cjs
CHANGED
|
@@ -126,34 +126,39 @@ class Command {
|
|
|
126
126
|
const { argv, envs, reporter } = params;
|
|
127
127
|
try {
|
|
128
128
|
const processedArgv = this.#processHelpSubcommand(argv);
|
|
129
|
-
const {
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
129
|
+
const { chain, remaining } = this.#routeChain(processedArgv);
|
|
130
|
+
const leafCommand = chain[chain.length - 1];
|
|
131
|
+
const { optionTokens, restArgs } = this.#splitAtDoubleDash(remaining);
|
|
132
|
+
const leafOptions = leafCommand.#getMergedOptions();
|
|
133
|
+
const hasUserHelp = leafCommand.#options.some(o => o.long === 'help');
|
|
134
|
+
const hasUserVersion = leafCommand.#options.some(o => o.long === 'version');
|
|
135
|
+
if (!hasUserHelp && leafCommand.#hasHelpFlag(optionTokens, leafOptions)) {
|
|
136
|
+
console.log(leafCommand.formatHelp());
|
|
135
137
|
return;
|
|
136
138
|
}
|
|
137
|
-
if (!hasUserVersion &&
|
|
138
|
-
console.log(
|
|
139
|
+
if (!hasUserVersion && leafCommand.#hasVersionFlag(optionTokens, leafOptions)) {
|
|
140
|
+
console.log(leafCommand.version ?? 'unknown');
|
|
139
141
|
return;
|
|
140
142
|
}
|
|
141
|
-
const
|
|
143
|
+
const optsMap = this.#shiftChain(chain, optionTokens);
|
|
142
144
|
const ctx = {
|
|
143
|
-
cmd:
|
|
145
|
+
cmd: leafCommand,
|
|
144
146
|
envs,
|
|
145
147
|
reporter: reporter ?? new DefaultReporter(),
|
|
146
148
|
argv,
|
|
147
149
|
};
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
this.#applyChain(chain, optsMap, ctx);
|
|
151
|
+
const mergedOpts = this.#mergeOpts(chain, optsMap);
|
|
152
|
+
const args = restArgs;
|
|
153
|
+
const requiredArgs = leafCommand.#arguments.filter(a => a.kind === 'required');
|
|
154
|
+
if (args.length < requiredArgs.length) {
|
|
155
|
+
const missing = requiredArgs.slice(args.length).map(a => a.name);
|
|
156
|
+
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, leafCommand.#getCommandPath());
|
|
152
157
|
}
|
|
153
|
-
const actionParams = { ctx, opts, args };
|
|
154
|
-
if (
|
|
158
|
+
const actionParams = { ctx, opts: mergedOpts, args };
|
|
159
|
+
if (leafCommand.#action) {
|
|
155
160
|
try {
|
|
156
|
-
await
|
|
161
|
+
await leafCommand.#action(actionParams);
|
|
157
162
|
}
|
|
158
163
|
catch (err) {
|
|
159
164
|
if (err instanceof Error) {
|
|
@@ -165,11 +170,11 @@ class Command {
|
|
|
165
170
|
process.exit(1);
|
|
166
171
|
}
|
|
167
172
|
}
|
|
168
|
-
else if (
|
|
169
|
-
console.log(
|
|
173
|
+
else if (leafCommand.#subcommands.length > 0) {
|
|
174
|
+
console.log(leafCommand.formatHelp());
|
|
170
175
|
}
|
|
171
176
|
else {
|
|
172
|
-
throw new CommanderError('ConfigurationError', `no action defined for command "${
|
|
177
|
+
throw new CommanderError('ConfigurationError', `no action defined for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
173
178
|
}
|
|
174
179
|
}
|
|
175
180
|
catch (err) {
|
|
@@ -247,6 +252,82 @@ class Command {
|
|
|
247
252
|
}
|
|
248
253
|
return { opts, args };
|
|
249
254
|
}
|
|
255
|
+
shift(tokens) {
|
|
256
|
+
return this.#shiftWithShadowed(tokens, new Set());
|
|
257
|
+
}
|
|
258
|
+
#shiftWithShadowed(tokens, shadowed) {
|
|
259
|
+
const allDirectOptions = this.#getMergedOptions();
|
|
260
|
+
const directOptions = allDirectOptions.filter(o => !shadowed.has(o.long));
|
|
261
|
+
const opts = {};
|
|
262
|
+
for (const opt of directOptions) {
|
|
263
|
+
if (opt.default !== undefined) {
|
|
264
|
+
opts[opt.long] = opt.default;
|
|
265
|
+
}
|
|
266
|
+
else if (opt.type === 'boolean') {
|
|
267
|
+
opts[opt.long] = false;
|
|
268
|
+
}
|
|
269
|
+
else if (opt.type === 'string[]' || opt.type === 'number[]') {
|
|
270
|
+
opts[opt.long] = [];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
let remaining = [...tokens];
|
|
274
|
+
const resolverOptions = directOptions.filter(o => o.resolver);
|
|
275
|
+
for (const opt of resolverOptions) {
|
|
276
|
+
const result = opt.resolver(remaining);
|
|
277
|
+
opts[opt.long] = result.value;
|
|
278
|
+
remaining = result.remaining;
|
|
279
|
+
}
|
|
280
|
+
const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(directOptions, true);
|
|
281
|
+
const normalizedTokens = this.#normalizeArgv(remaining, booleanOptions);
|
|
282
|
+
const finalRemaining = [];
|
|
283
|
+
let i = 0;
|
|
284
|
+
while (i < normalizedTokens.length) {
|
|
285
|
+
const token = normalizedTokens[i];
|
|
286
|
+
if (token.startsWith('--')) {
|
|
287
|
+
const consumed = this.#tryConsumeLongOption(normalizedTokens, i, optionByLong, opts);
|
|
288
|
+
if (consumed > 0) {
|
|
289
|
+
i += consumed;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
finalRemaining.push(token);
|
|
293
|
+
i += 1;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (token.startsWith('-') && token.length > 1) {
|
|
297
|
+
const result = this.#tryConsumeShortOption(normalizedTokens, i, optionByShort, opts);
|
|
298
|
+
if (result.consumed) {
|
|
299
|
+
i = result.nextIdx;
|
|
300
|
+
if (result.remainingToken) {
|
|
301
|
+
finalRemaining.push(result.remainingToken);
|
|
302
|
+
}
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
finalRemaining.push(token);
|
|
306
|
+
i += 1;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
finalRemaining.push(token);
|
|
310
|
+
i += 1;
|
|
311
|
+
}
|
|
312
|
+
for (const opt of directOptions) {
|
|
313
|
+
if (opt.required && opts[opt.long] === undefined) {
|
|
314
|
+
throw new CommanderError('MissingRequired', `missing required option "--${opt.long}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
for (const opt of directOptions) {
|
|
318
|
+
if (opt.choices && opts[opt.long] !== undefined) {
|
|
319
|
+
const value = opts[opt.long];
|
|
320
|
+
const values = Array.isArray(value) ? value : [value];
|
|
321
|
+
const choices = opt.choices;
|
|
322
|
+
for (const v of values) {
|
|
323
|
+
if (!choices.includes(v)) {
|
|
324
|
+
throw new CommanderError('InvalidChoice', `invalid value "${v}" for option "--${opt.long}". Allowed: ${opt.choices.join(', ')}`, this.#getCommandPath());
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return { opts, remaining: finalRemaining };
|
|
330
|
+
}
|
|
250
331
|
formatHelp() {
|
|
251
332
|
const lines = [];
|
|
252
333
|
const allOptions = this.#getMergedOptions();
|
|
@@ -369,7 +450,8 @@ class Command {
|
|
|
369
450
|
}
|
|
370
451
|
return argv;
|
|
371
452
|
}
|
|
372
|
-
#
|
|
453
|
+
#routeChain(argv) {
|
|
454
|
+
const chain = [this];
|
|
373
455
|
let current = this;
|
|
374
456
|
let idx = 0;
|
|
375
457
|
while (idx < argv.length) {
|
|
@@ -380,9 +462,62 @@ class Command {
|
|
|
380
462
|
if (!entry)
|
|
381
463
|
break;
|
|
382
464
|
current = entry.command;
|
|
465
|
+
chain.push(current);
|
|
383
466
|
idx += 1;
|
|
384
467
|
}
|
|
385
|
-
return {
|
|
468
|
+
return { chain, remaining: argv.slice(idx) };
|
|
469
|
+
}
|
|
470
|
+
#splitAtDoubleDash(tokens) {
|
|
471
|
+
const ddIdx = tokens.indexOf('--');
|
|
472
|
+
if (ddIdx === -1) {
|
|
473
|
+
return { optionTokens: tokens, restArgs: [] };
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
optionTokens: tokens.slice(0, ddIdx),
|
|
477
|
+
restArgs: tokens.slice(ddIdx + 1),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
#shiftChain(chain, tokens) {
|
|
481
|
+
const optsMap = new Map();
|
|
482
|
+
let remaining = [...tokens];
|
|
483
|
+
const shadowed = new Set();
|
|
484
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
485
|
+
const cmd = chain[i];
|
|
486
|
+
const result = cmd.#shiftWithShadowed(remaining, shadowed);
|
|
487
|
+
optsMap.set(cmd, result.opts);
|
|
488
|
+
remaining = result.remaining;
|
|
489
|
+
for (const opt of cmd.#options) {
|
|
490
|
+
shadowed.add(opt.long);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (remaining.length > 0) {
|
|
494
|
+
const leafCommand = chain[chain.length - 1];
|
|
495
|
+
const firstToken = remaining[0];
|
|
496
|
+
if (firstToken.startsWith('-')) {
|
|
497
|
+
throw new CommanderError('UnknownOption', `unknown option "${firstToken}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
throw new CommanderError('UnexpectedArgument', `unexpected argument "${firstToken}". Positional arguments must come after "--"`, leafCommand.#getCommandPath());
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return optsMap;
|
|
504
|
+
}
|
|
505
|
+
#applyChain(chain, optsMap, ctx) {
|
|
506
|
+
for (const cmd of chain) {
|
|
507
|
+
const opts = optsMap.get(cmd) ?? {};
|
|
508
|
+
for (const opt of cmd.#getMergedOptions()) {
|
|
509
|
+
if (opt.apply && opts[opt.long] !== undefined) {
|
|
510
|
+
opt.apply(opts[opt.long], ctx);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
#mergeOpts(chain, optsMap) {
|
|
516
|
+
const merged = {};
|
|
517
|
+
for (const cmd of chain) {
|
|
518
|
+
Object.assign(merged, optsMap.get(cmd) ?? {});
|
|
519
|
+
}
|
|
520
|
+
return merged;
|
|
386
521
|
}
|
|
387
522
|
#parseLongOption(argv, idx, optionByLong, opts) {
|
|
388
523
|
const token = argv[idx];
|
|
@@ -545,9 +680,6 @@ class Command {
|
|
|
545
680
|
}
|
|
546
681
|
}
|
|
547
682
|
}
|
|
548
|
-
#isBuiltinOption(opt) {
|
|
549
|
-
return opt === BUILTIN_HELP_OPTION || opt === BUILTIN_VERSION_OPTION;
|
|
550
|
-
}
|
|
551
683
|
#buildOptionMaps(allOptions, excludeResolver = false) {
|
|
552
684
|
const optionByLong = new Map();
|
|
553
685
|
const optionByShort = new Map();
|
|
@@ -644,6 +776,100 @@ class Command {
|
|
|
644
776
|
#getCommandPath() {
|
|
645
777
|
return this.#name;
|
|
646
778
|
}
|
|
779
|
+
#tryConsumeLongOption(tokens, idx, optionByLong, opts) {
|
|
780
|
+
const token = tokens[idx];
|
|
781
|
+
const eqIdx = token.indexOf('=');
|
|
782
|
+
let optName;
|
|
783
|
+
let inlineValue;
|
|
784
|
+
if (eqIdx !== -1) {
|
|
785
|
+
optName = token.slice(2, eqIdx);
|
|
786
|
+
inlineValue = token.slice(eqIdx + 1);
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
optName = token.slice(2);
|
|
790
|
+
}
|
|
791
|
+
const opt = optionByLong.get(optName);
|
|
792
|
+
if (!opt) {
|
|
793
|
+
return 0;
|
|
794
|
+
}
|
|
795
|
+
if (opt.type === 'boolean') {
|
|
796
|
+
if (inlineValue !== undefined) {
|
|
797
|
+
if (inlineValue === 'true') {
|
|
798
|
+
opts[optName] = true;
|
|
799
|
+
}
|
|
800
|
+
else if (inlineValue === 'false') {
|
|
801
|
+
opts[optName] = false;
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
throw new CommanderError('InvalidBooleanValue', `invalid value "${inlineValue}" for boolean option "--${optName}". Use "true" or "false"`, this.#getCommandPath());
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
808
|
+
opts[optName] = true;
|
|
809
|
+
}
|
|
810
|
+
return 1;
|
|
811
|
+
}
|
|
812
|
+
let value;
|
|
813
|
+
let consumed = 1;
|
|
814
|
+
if (inlineValue !== undefined) {
|
|
815
|
+
value = inlineValue;
|
|
816
|
+
}
|
|
817
|
+
else if (idx + 1 < tokens.length) {
|
|
818
|
+
value = tokens[idx + 1];
|
|
819
|
+
consumed = 2;
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
throw new CommanderError('MissingValue', `option "--${optName}" requires a value`, this.#getCommandPath());
|
|
823
|
+
}
|
|
824
|
+
this.#applyValue(opt, value, opts);
|
|
825
|
+
return consumed;
|
|
826
|
+
}
|
|
827
|
+
#tryConsumeShortOption(tokens, idx, optionByShort, opts) {
|
|
828
|
+
const token = tokens[idx];
|
|
829
|
+
if (token.includes('=')) {
|
|
830
|
+
const firstFlag = token[1];
|
|
831
|
+
if (!optionByShort.has(firstFlag)) {
|
|
832
|
+
return { consumed: false, nextIdx: idx + 1 };
|
|
833
|
+
}
|
|
834
|
+
throw new CommanderError('UnsupportedShortSyntax', `"-${token.slice(1)}" is not supported. Use "-${token[1]} ${token.slice(3)}" instead`, this.#getCommandPath());
|
|
835
|
+
}
|
|
836
|
+
const flags = token.slice(1);
|
|
837
|
+
let j = 0;
|
|
838
|
+
const consumedFlags = [];
|
|
839
|
+
const unconsumedFlags = [];
|
|
840
|
+
let nextIdx = idx + 1;
|
|
841
|
+
while (j < flags.length) {
|
|
842
|
+
const flag = flags[j];
|
|
843
|
+
const opt = optionByShort.get(flag);
|
|
844
|
+
if (!opt) {
|
|
845
|
+
unconsumedFlags.push(...flags.slice(j).split(''));
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
848
|
+
consumedFlags.push(flag);
|
|
849
|
+
if (opt.type === 'boolean') {
|
|
850
|
+
opts[opt.long] = true;
|
|
851
|
+
j += 1;
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
if (j < flags.length - 1) {
|
|
855
|
+
throw new CommanderError('UnsupportedShortSyntax', `"-${flags}" is not supported. Use "-${flags.slice(0, j + 1)} ${flags.slice(j + 1)}" or separate options`, this.#getCommandPath());
|
|
856
|
+
}
|
|
857
|
+
if (idx + 1 < tokens.length && !tokens[idx + 1].startsWith('-')) {
|
|
858
|
+
const value = tokens[idx + 1];
|
|
859
|
+
this.#applyValue(opt, value, opts);
|
|
860
|
+
nextIdx = idx + 2;
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
throw new CommanderError('MissingValue', `option "-${flag}" requires a value`, this.#getCommandPath());
|
|
864
|
+
}
|
|
865
|
+
j += 1;
|
|
866
|
+
}
|
|
867
|
+
if (consumedFlags.length > 0) {
|
|
868
|
+
const remainingToken = unconsumedFlags.length > 0 ? `-${unconsumedFlags.join('')}` : undefined;
|
|
869
|
+
return { consumed: true, nextIdx, remainingToken };
|
|
870
|
+
}
|
|
871
|
+
return { consumed: false, nextIdx: idx + 1 };
|
|
872
|
+
}
|
|
647
873
|
}
|
|
648
874
|
|
|
649
875
|
class CompletionCommand extends Command {
|
package/lib/esm/index.mjs
CHANGED
|
@@ -104,34 +104,39 @@ class Command {
|
|
|
104
104
|
const { argv, envs, reporter } = params;
|
|
105
105
|
try {
|
|
106
106
|
const processedArgv = this.#processHelpSubcommand(argv);
|
|
107
|
-
const {
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
const { chain, remaining } = this.#routeChain(processedArgv);
|
|
108
|
+
const leafCommand = chain[chain.length - 1];
|
|
109
|
+
const { optionTokens, restArgs } = this.#splitAtDoubleDash(remaining);
|
|
110
|
+
const leafOptions = leafCommand.#getMergedOptions();
|
|
111
|
+
const hasUserHelp = leafCommand.#options.some(o => o.long === 'help');
|
|
112
|
+
const hasUserVersion = leafCommand.#options.some(o => o.long === 'version');
|
|
113
|
+
if (!hasUserHelp && leafCommand.#hasHelpFlag(optionTokens, leafOptions)) {
|
|
114
|
+
console.log(leafCommand.formatHelp());
|
|
113
115
|
return;
|
|
114
116
|
}
|
|
115
|
-
if (!hasUserVersion &&
|
|
116
|
-
console.log(
|
|
117
|
+
if (!hasUserVersion && leafCommand.#hasVersionFlag(optionTokens, leafOptions)) {
|
|
118
|
+
console.log(leafCommand.version ?? 'unknown');
|
|
117
119
|
return;
|
|
118
120
|
}
|
|
119
|
-
const
|
|
121
|
+
const optsMap = this.#shiftChain(chain, optionTokens);
|
|
120
122
|
const ctx = {
|
|
121
|
-
cmd:
|
|
123
|
+
cmd: leafCommand,
|
|
122
124
|
envs,
|
|
123
125
|
reporter: reporter ?? new DefaultReporter(),
|
|
124
126
|
argv,
|
|
125
127
|
};
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
this.#applyChain(chain, optsMap, ctx);
|
|
129
|
+
const mergedOpts = this.#mergeOpts(chain, optsMap);
|
|
130
|
+
const args = restArgs;
|
|
131
|
+
const requiredArgs = leafCommand.#arguments.filter(a => a.kind === 'required');
|
|
132
|
+
if (args.length < requiredArgs.length) {
|
|
133
|
+
const missing = requiredArgs.slice(args.length).map(a => a.name);
|
|
134
|
+
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, leafCommand.#getCommandPath());
|
|
130
135
|
}
|
|
131
|
-
const actionParams = { ctx, opts, args };
|
|
132
|
-
if (
|
|
136
|
+
const actionParams = { ctx, opts: mergedOpts, args };
|
|
137
|
+
if (leafCommand.#action) {
|
|
133
138
|
try {
|
|
134
|
-
await
|
|
139
|
+
await leafCommand.#action(actionParams);
|
|
135
140
|
}
|
|
136
141
|
catch (err) {
|
|
137
142
|
if (err instanceof Error) {
|
|
@@ -143,11 +148,11 @@ class Command {
|
|
|
143
148
|
process.exit(1);
|
|
144
149
|
}
|
|
145
150
|
}
|
|
146
|
-
else if (
|
|
147
|
-
console.log(
|
|
151
|
+
else if (leafCommand.#subcommands.length > 0) {
|
|
152
|
+
console.log(leafCommand.formatHelp());
|
|
148
153
|
}
|
|
149
154
|
else {
|
|
150
|
-
throw new CommanderError('ConfigurationError', `no action defined for command "${
|
|
155
|
+
throw new CommanderError('ConfigurationError', `no action defined for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
151
156
|
}
|
|
152
157
|
}
|
|
153
158
|
catch (err) {
|
|
@@ -225,6 +230,82 @@ class Command {
|
|
|
225
230
|
}
|
|
226
231
|
return { opts, args };
|
|
227
232
|
}
|
|
233
|
+
shift(tokens) {
|
|
234
|
+
return this.#shiftWithShadowed(tokens, new Set());
|
|
235
|
+
}
|
|
236
|
+
#shiftWithShadowed(tokens, shadowed) {
|
|
237
|
+
const allDirectOptions = this.#getMergedOptions();
|
|
238
|
+
const directOptions = allDirectOptions.filter(o => !shadowed.has(o.long));
|
|
239
|
+
const opts = {};
|
|
240
|
+
for (const opt of directOptions) {
|
|
241
|
+
if (opt.default !== undefined) {
|
|
242
|
+
opts[opt.long] = opt.default;
|
|
243
|
+
}
|
|
244
|
+
else if (opt.type === 'boolean') {
|
|
245
|
+
opts[opt.long] = false;
|
|
246
|
+
}
|
|
247
|
+
else if (opt.type === 'string[]' || opt.type === 'number[]') {
|
|
248
|
+
opts[opt.long] = [];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
let remaining = [...tokens];
|
|
252
|
+
const resolverOptions = directOptions.filter(o => o.resolver);
|
|
253
|
+
for (const opt of resolverOptions) {
|
|
254
|
+
const result = opt.resolver(remaining);
|
|
255
|
+
opts[opt.long] = result.value;
|
|
256
|
+
remaining = result.remaining;
|
|
257
|
+
}
|
|
258
|
+
const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(directOptions, true);
|
|
259
|
+
const normalizedTokens = this.#normalizeArgv(remaining, booleanOptions);
|
|
260
|
+
const finalRemaining = [];
|
|
261
|
+
let i = 0;
|
|
262
|
+
while (i < normalizedTokens.length) {
|
|
263
|
+
const token = normalizedTokens[i];
|
|
264
|
+
if (token.startsWith('--')) {
|
|
265
|
+
const consumed = this.#tryConsumeLongOption(normalizedTokens, i, optionByLong, opts);
|
|
266
|
+
if (consumed > 0) {
|
|
267
|
+
i += consumed;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
finalRemaining.push(token);
|
|
271
|
+
i += 1;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (token.startsWith('-') && token.length > 1) {
|
|
275
|
+
const result = this.#tryConsumeShortOption(normalizedTokens, i, optionByShort, opts);
|
|
276
|
+
if (result.consumed) {
|
|
277
|
+
i = result.nextIdx;
|
|
278
|
+
if (result.remainingToken) {
|
|
279
|
+
finalRemaining.push(result.remainingToken);
|
|
280
|
+
}
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
finalRemaining.push(token);
|
|
284
|
+
i += 1;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
finalRemaining.push(token);
|
|
288
|
+
i += 1;
|
|
289
|
+
}
|
|
290
|
+
for (const opt of directOptions) {
|
|
291
|
+
if (opt.required && opts[opt.long] === undefined) {
|
|
292
|
+
throw new CommanderError('MissingRequired', `missing required option "--${opt.long}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
for (const opt of directOptions) {
|
|
296
|
+
if (opt.choices && opts[opt.long] !== undefined) {
|
|
297
|
+
const value = opts[opt.long];
|
|
298
|
+
const values = Array.isArray(value) ? value : [value];
|
|
299
|
+
const choices = opt.choices;
|
|
300
|
+
for (const v of values) {
|
|
301
|
+
if (!choices.includes(v)) {
|
|
302
|
+
throw new CommanderError('InvalidChoice', `invalid value "${v}" for option "--${opt.long}". Allowed: ${opt.choices.join(', ')}`, this.#getCommandPath());
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return { opts, remaining: finalRemaining };
|
|
308
|
+
}
|
|
228
309
|
formatHelp() {
|
|
229
310
|
const lines = [];
|
|
230
311
|
const allOptions = this.#getMergedOptions();
|
|
@@ -347,7 +428,8 @@ class Command {
|
|
|
347
428
|
}
|
|
348
429
|
return argv;
|
|
349
430
|
}
|
|
350
|
-
#
|
|
431
|
+
#routeChain(argv) {
|
|
432
|
+
const chain = [this];
|
|
351
433
|
let current = this;
|
|
352
434
|
let idx = 0;
|
|
353
435
|
while (idx < argv.length) {
|
|
@@ -358,9 +440,62 @@ class Command {
|
|
|
358
440
|
if (!entry)
|
|
359
441
|
break;
|
|
360
442
|
current = entry.command;
|
|
443
|
+
chain.push(current);
|
|
361
444
|
idx += 1;
|
|
362
445
|
}
|
|
363
|
-
return {
|
|
446
|
+
return { chain, remaining: argv.slice(idx) };
|
|
447
|
+
}
|
|
448
|
+
#splitAtDoubleDash(tokens) {
|
|
449
|
+
const ddIdx = tokens.indexOf('--');
|
|
450
|
+
if (ddIdx === -1) {
|
|
451
|
+
return { optionTokens: tokens, restArgs: [] };
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
optionTokens: tokens.slice(0, ddIdx),
|
|
455
|
+
restArgs: tokens.slice(ddIdx + 1),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
#shiftChain(chain, tokens) {
|
|
459
|
+
const optsMap = new Map();
|
|
460
|
+
let remaining = [...tokens];
|
|
461
|
+
const shadowed = new Set();
|
|
462
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
463
|
+
const cmd = chain[i];
|
|
464
|
+
const result = cmd.#shiftWithShadowed(remaining, shadowed);
|
|
465
|
+
optsMap.set(cmd, result.opts);
|
|
466
|
+
remaining = result.remaining;
|
|
467
|
+
for (const opt of cmd.#options) {
|
|
468
|
+
shadowed.add(opt.long);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (remaining.length > 0) {
|
|
472
|
+
const leafCommand = chain[chain.length - 1];
|
|
473
|
+
const firstToken = remaining[0];
|
|
474
|
+
if (firstToken.startsWith('-')) {
|
|
475
|
+
throw new CommanderError('UnknownOption', `unknown option "${firstToken}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
throw new CommanderError('UnexpectedArgument', `unexpected argument "${firstToken}". Positional arguments must come after "--"`, leafCommand.#getCommandPath());
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return optsMap;
|
|
482
|
+
}
|
|
483
|
+
#applyChain(chain, optsMap, ctx) {
|
|
484
|
+
for (const cmd of chain) {
|
|
485
|
+
const opts = optsMap.get(cmd) ?? {};
|
|
486
|
+
for (const opt of cmd.#getMergedOptions()) {
|
|
487
|
+
if (opt.apply && opts[opt.long] !== undefined) {
|
|
488
|
+
opt.apply(opts[opt.long], ctx);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
#mergeOpts(chain, optsMap) {
|
|
494
|
+
const merged = {};
|
|
495
|
+
for (const cmd of chain) {
|
|
496
|
+
Object.assign(merged, optsMap.get(cmd) ?? {});
|
|
497
|
+
}
|
|
498
|
+
return merged;
|
|
364
499
|
}
|
|
365
500
|
#parseLongOption(argv, idx, optionByLong, opts) {
|
|
366
501
|
const token = argv[idx];
|
|
@@ -523,9 +658,6 @@ class Command {
|
|
|
523
658
|
}
|
|
524
659
|
}
|
|
525
660
|
}
|
|
526
|
-
#isBuiltinOption(opt) {
|
|
527
|
-
return opt === BUILTIN_HELP_OPTION || opt === BUILTIN_VERSION_OPTION;
|
|
528
|
-
}
|
|
529
661
|
#buildOptionMaps(allOptions, excludeResolver = false) {
|
|
530
662
|
const optionByLong = new Map();
|
|
531
663
|
const optionByShort = new Map();
|
|
@@ -622,6 +754,100 @@ class Command {
|
|
|
622
754
|
#getCommandPath() {
|
|
623
755
|
return this.#name;
|
|
624
756
|
}
|
|
757
|
+
#tryConsumeLongOption(tokens, idx, optionByLong, opts) {
|
|
758
|
+
const token = tokens[idx];
|
|
759
|
+
const eqIdx = token.indexOf('=');
|
|
760
|
+
let optName;
|
|
761
|
+
let inlineValue;
|
|
762
|
+
if (eqIdx !== -1) {
|
|
763
|
+
optName = token.slice(2, eqIdx);
|
|
764
|
+
inlineValue = token.slice(eqIdx + 1);
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
optName = token.slice(2);
|
|
768
|
+
}
|
|
769
|
+
const opt = optionByLong.get(optName);
|
|
770
|
+
if (!opt) {
|
|
771
|
+
return 0;
|
|
772
|
+
}
|
|
773
|
+
if (opt.type === 'boolean') {
|
|
774
|
+
if (inlineValue !== undefined) {
|
|
775
|
+
if (inlineValue === 'true') {
|
|
776
|
+
opts[optName] = true;
|
|
777
|
+
}
|
|
778
|
+
else if (inlineValue === 'false') {
|
|
779
|
+
opts[optName] = false;
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
throw new CommanderError('InvalidBooleanValue', `invalid value "${inlineValue}" for boolean option "--${optName}". Use "true" or "false"`, this.#getCommandPath());
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
opts[optName] = true;
|
|
787
|
+
}
|
|
788
|
+
return 1;
|
|
789
|
+
}
|
|
790
|
+
let value;
|
|
791
|
+
let consumed = 1;
|
|
792
|
+
if (inlineValue !== undefined) {
|
|
793
|
+
value = inlineValue;
|
|
794
|
+
}
|
|
795
|
+
else if (idx + 1 < tokens.length) {
|
|
796
|
+
value = tokens[idx + 1];
|
|
797
|
+
consumed = 2;
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
throw new CommanderError('MissingValue', `option "--${optName}" requires a value`, this.#getCommandPath());
|
|
801
|
+
}
|
|
802
|
+
this.#applyValue(opt, value, opts);
|
|
803
|
+
return consumed;
|
|
804
|
+
}
|
|
805
|
+
#tryConsumeShortOption(tokens, idx, optionByShort, opts) {
|
|
806
|
+
const token = tokens[idx];
|
|
807
|
+
if (token.includes('=')) {
|
|
808
|
+
const firstFlag = token[1];
|
|
809
|
+
if (!optionByShort.has(firstFlag)) {
|
|
810
|
+
return { consumed: false, nextIdx: idx + 1 };
|
|
811
|
+
}
|
|
812
|
+
throw new CommanderError('UnsupportedShortSyntax', `"-${token.slice(1)}" is not supported. Use "-${token[1]} ${token.slice(3)}" instead`, this.#getCommandPath());
|
|
813
|
+
}
|
|
814
|
+
const flags = token.slice(1);
|
|
815
|
+
let j = 0;
|
|
816
|
+
const consumedFlags = [];
|
|
817
|
+
const unconsumedFlags = [];
|
|
818
|
+
let nextIdx = idx + 1;
|
|
819
|
+
while (j < flags.length) {
|
|
820
|
+
const flag = flags[j];
|
|
821
|
+
const opt = optionByShort.get(flag);
|
|
822
|
+
if (!opt) {
|
|
823
|
+
unconsumedFlags.push(...flags.slice(j).split(''));
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
826
|
+
consumedFlags.push(flag);
|
|
827
|
+
if (opt.type === 'boolean') {
|
|
828
|
+
opts[opt.long] = true;
|
|
829
|
+
j += 1;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if (j < flags.length - 1) {
|
|
833
|
+
throw new CommanderError('UnsupportedShortSyntax', `"-${flags}" is not supported. Use "-${flags.slice(0, j + 1)} ${flags.slice(j + 1)}" or separate options`, this.#getCommandPath());
|
|
834
|
+
}
|
|
835
|
+
if (idx + 1 < tokens.length && !tokens[idx + 1].startsWith('-')) {
|
|
836
|
+
const value = tokens[idx + 1];
|
|
837
|
+
this.#applyValue(opt, value, opts);
|
|
838
|
+
nextIdx = idx + 2;
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
throw new CommanderError('MissingValue', `option "-${flag}" requires a value`, this.#getCommandPath());
|
|
842
|
+
}
|
|
843
|
+
j += 1;
|
|
844
|
+
}
|
|
845
|
+
if (consumedFlags.length > 0) {
|
|
846
|
+
const remainingToken = unconsumedFlags.length > 0 ? `-${unconsumedFlags.join('')}` : undefined;
|
|
847
|
+
return { consumed: true, nextIdx, remainingToken };
|
|
848
|
+
}
|
|
849
|
+
return { consumed: false, nextIdx: idx + 1 };
|
|
850
|
+
}
|
|
625
851
|
}
|
|
626
852
|
|
|
627
853
|
class CompletionCommand extends Command {
|
package/lib/types/index.d.ts
CHANGED
|
@@ -112,8 +112,15 @@ interface IParseResult {
|
|
|
112
112
|
/** Parsed positional arguments */
|
|
113
113
|
args: string[];
|
|
114
114
|
}
|
|
115
|
+
/** shift() method result */
|
|
116
|
+
interface IShiftResult {
|
|
117
|
+
/** Options consumed by this command */
|
|
118
|
+
opts: Record<string, unknown>;
|
|
119
|
+
/** Tokens not consumed, to be passed to parent */
|
|
120
|
+
remaining: string[];
|
|
121
|
+
}
|
|
115
122
|
/** Error kinds for command parsing */
|
|
116
|
-
type ICommanderErrorKind = 'UnknownOption' | 'MissingValue' | 'InvalidType' | 'UnsupportedShortSyntax' | 'OptionConflict' | 'MissingRequired' | 'InvalidChoice' | 'InvalidBooleanValue' | 'MissingRequiredArgument' | 'ConfigurationError';
|
|
123
|
+
type ICommanderErrorKind = 'UnknownOption' | 'UnexpectedArgument' | 'MissingValue' | 'InvalidType' | 'UnsupportedShortSyntax' | 'OptionConflict' | 'MissingRequired' | 'InvalidChoice' | 'InvalidBooleanValue' | 'MissingRequiredArgument' | 'ConfigurationError';
|
|
117
124
|
/** Commander error with structured information */
|
|
118
125
|
declare class CommanderError extends Error {
|
|
119
126
|
readonly kind: ICommanderErrorKind;
|
|
@@ -177,6 +184,11 @@ declare class Command implements ICommand {
|
|
|
177
184
|
subcommand(name: string, cmd: Command): this;
|
|
178
185
|
run(params: IRunParams): Promise<void>;
|
|
179
186
|
parse(argv: string[]): IParseResult;
|
|
187
|
+
/**
|
|
188
|
+
* Shift options from tokens that this command recognizes.
|
|
189
|
+
* Unrecognized tokens are returned in `remaining` for parent commands.
|
|
190
|
+
*/
|
|
191
|
+
shift(tokens: string[]): IShiftResult;
|
|
180
192
|
formatHelp(): string;
|
|
181
193
|
getCompletionMeta(): ICompletionMeta;
|
|
182
194
|
}
|
|
@@ -227,4 +239,4 @@ declare class PwshCompletion {
|
|
|
227
239
|
}
|
|
228
240
|
|
|
229
241
|
export { BashCompletion, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion };
|
|
230
|
-
export type { IAction, IActionParams, IArgument, IArgumentKind, ICommand, ICommandConfig, ICommandContext, ICommanderErrorKind, ICompletionCommandConfig, ICompletionMeta, ICompletionOptionMeta, ICompletionPaths, IOption, IOptionType, IParseResult, IReporter, IRunParams, IShellType };
|
|
242
|
+
export type { IAction, IActionParams, IArgument, IArgumentKind, ICommand, ICommandConfig, ICommandContext, ICommanderErrorKind, ICompletionCommandConfig, ICompletionMeta, ICompletionOptionMeta, ICompletionPaths, IOption, IOptionType, IParseResult, IReporter, IRunParams, IShellType, IShiftResult };
|