@guanghechen/commander 4.4.0 → 4.5.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 +14 -0
- package/README.md +44 -0
- package/lib/cjs/index.cjs +93 -45
- package/lib/esm/index.mjs +93 -46
- package/lib/types/index.d.ts +16 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 4.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add built-in coerce factories for numeric option parsing in commander.
|
|
8
|
+
|
|
9
|
+
## 4.4.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- fix(commander): centralize option policy and enforce version flag semantics — subcommands with
|
|
14
|
+
their own `version` now correctly expose `--version`; commands without `version` reject
|
|
15
|
+
`--version` instead of treating it as a boolean option.
|
|
16
|
+
|
|
3
17
|
## 4.4.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -202,6 +202,50 @@ new Command({ name: 'example', description: 'Option types demo' })
|
|
|
202
202
|
})
|
|
203
203
|
```
|
|
204
204
|
|
|
205
|
+
### Built-in Coerce Factories
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import { Coerce, Command } from '@guanghechen/commander'
|
|
209
|
+
|
|
210
|
+
new Command({ name: 'example', desc: 'Coerce demo' })
|
|
211
|
+
.option({
|
|
212
|
+
long: 'offset',
|
|
213
|
+
type: 'number',
|
|
214
|
+
args: 'required',
|
|
215
|
+
coerce: Coerce.integer('--offset'),
|
|
216
|
+
desc: 'Signed offset',
|
|
217
|
+
})
|
|
218
|
+
.option({
|
|
219
|
+
long: 'parallel',
|
|
220
|
+
type: 'number',
|
|
221
|
+
args: 'required',
|
|
222
|
+
coerce: Coerce.positiveInteger('--parallel'),
|
|
223
|
+
desc: 'Parallel workers',
|
|
224
|
+
})
|
|
225
|
+
.option({
|
|
226
|
+
long: 'duration',
|
|
227
|
+
type: 'number',
|
|
228
|
+
args: 'required',
|
|
229
|
+
coerce: Coerce.positiveNumber('--duration'),
|
|
230
|
+
desc: 'Duration in seconds',
|
|
231
|
+
})
|
|
232
|
+
.option({
|
|
233
|
+
long: 'scale',
|
|
234
|
+
type: 'number',
|
|
235
|
+
args: 'required',
|
|
236
|
+
coerce: Coerce.number('--scale'),
|
|
237
|
+
desc: 'Scale factor',
|
|
238
|
+
})
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Default error message format:
|
|
242
|
+
|
|
243
|
+
```text
|
|
244
|
+
{name} is expected as {coerce type}, but got {raw}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
You can still override the message via `Coerce.xxx(name, 'custom error message')`.
|
|
248
|
+
|
|
205
249
|
### Help Examples
|
|
206
250
|
|
|
207
251
|
```typescript
|
package/lib/cjs/index.cjs
CHANGED
|
@@ -368,30 +368,29 @@ class Command {
|
|
|
368
368
|
const routeResult = this.#route(processedArgv);
|
|
369
369
|
const { chain, remaining } = routeResult;
|
|
370
370
|
const leafCommand = chain[chain.length - 1];
|
|
371
|
-
const rootCommand = chain[0];
|
|
372
371
|
const tokenizeResult = tokenize(remaining, leafCommand.#getCommandPath());
|
|
373
372
|
const { optionTokens, restArgs } = tokenizeResult;
|
|
374
|
-
const
|
|
375
|
-
const
|
|
376
|
-
if (
|
|
377
|
-
const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs);
|
|
373
|
+
const optionPolicyMap = this.#buildOptionPolicyMap(chain);
|
|
374
|
+
const leafPolicy = this.#mustGetOptionPolicy(optionPolicyMap, leafCommand);
|
|
375
|
+
if (leafPolicy.enableBuiltinHelp && this.#hasFlag(optionTokens, 'help', 'h')) {
|
|
376
|
+
const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs, leafPolicy);
|
|
378
377
|
console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
|
|
379
378
|
return;
|
|
380
379
|
}
|
|
381
|
-
if (
|
|
380
|
+
if (leafPolicy.enableBuiltinVersion) {
|
|
382
381
|
if (this.#hasFlag(optionTokens, 'version', 'V')) {
|
|
383
382
|
console.log(leafCommand.#version);
|
|
384
383
|
return;
|
|
385
384
|
}
|
|
386
385
|
}
|
|
387
|
-
const resolveResult = this.#resolve(chain, optionTokens);
|
|
386
|
+
const resolveResult = this.#resolve(chain, optionTokens, optionPolicyMap);
|
|
388
387
|
const ctx = {
|
|
389
388
|
cmd: leafCommand,
|
|
390
389
|
envs,
|
|
391
390
|
reporter: reporter$1 ?? this.#reporter ?? new reporter.Reporter(),
|
|
392
391
|
argv,
|
|
393
392
|
};
|
|
394
|
-
const parseResult = this.#parse(chain, resolveResult, ctx, restArgs);
|
|
393
|
+
const parseResult = this.#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs);
|
|
395
394
|
const actionParams = {
|
|
396
395
|
ctx: parseResult.ctx,
|
|
397
396
|
opts: parseResult.opts,
|
|
@@ -402,7 +401,7 @@ class Command {
|
|
|
402
401
|
await leafCommand.#runAction(actionParams);
|
|
403
402
|
}
|
|
404
403
|
else if (leafCommand.#subcommandsList.length > 0) {
|
|
405
|
-
const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs);
|
|
404
|
+
const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs, leafPolicy);
|
|
406
405
|
console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
|
|
407
406
|
}
|
|
408
407
|
else {
|
|
@@ -426,14 +425,15 @@ class Command {
|
|
|
426
425
|
const leafCommand = chain[chain.length - 1];
|
|
427
426
|
const tokenizeResult = tokenize(remaining, leafCommand.#getCommandPath());
|
|
428
427
|
const { optionTokens, restArgs } = tokenizeResult;
|
|
429
|
-
const
|
|
428
|
+
const optionPolicyMap = this.#buildOptionPolicyMap(chain);
|
|
429
|
+
const resolveResult = this.#resolve(chain, optionTokens, optionPolicyMap);
|
|
430
430
|
const ctx = {
|
|
431
431
|
cmd: leafCommand,
|
|
432
432
|
envs,
|
|
433
433
|
reporter: reporter$1 ?? this.#reporter ?? new reporter.Reporter(),
|
|
434
434
|
argv,
|
|
435
435
|
};
|
|
436
|
-
return this.#parse(chain, resolveResult, ctx, restArgs);
|
|
436
|
+
return this.#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs);
|
|
437
437
|
}
|
|
438
438
|
formatHelp() {
|
|
439
439
|
return this.#renderHelpPlain(this.#buildHelpData());
|
|
@@ -450,7 +450,7 @@ class Command {
|
|
|
450
450
|
return color && process.stdout.isTTY === true;
|
|
451
451
|
}
|
|
452
452
|
#buildHelpData() {
|
|
453
|
-
const allOptions = this.#
|
|
453
|
+
const allOptions = this.#resolveOptionPolicy().mergedOptions;
|
|
454
454
|
const commandPath = this.#getCommandPath();
|
|
455
455
|
let usage = `Usage: ${commandPath}`;
|
|
456
456
|
if (allOptions.length > 0)
|
|
@@ -587,7 +587,7 @@ class Command {
|
|
|
587
587
|
return lines.join('\n');
|
|
588
588
|
}
|
|
589
589
|
getCompletionMeta() {
|
|
590
|
-
const allOptions = this.#
|
|
590
|
+
const allOptions = this.#resolveOptionPolicy().mergedOptions;
|
|
591
591
|
const options = [];
|
|
592
592
|
for (const opt of allOptions) {
|
|
593
593
|
options.push({
|
|
@@ -675,14 +675,14 @@ class Command {
|
|
|
675
675
|
}
|
|
676
676
|
return { chain, remaining: argv.slice(idx) };
|
|
677
677
|
}
|
|
678
|
-
#resolve(chain, tokens) {
|
|
678
|
+
#resolve(chain, tokens, optionPolicyMap) {
|
|
679
679
|
const consumedTokens = new Map();
|
|
680
680
|
let remaining = [...tokens];
|
|
681
681
|
const shadowed = new Set();
|
|
682
682
|
for (let i = chain.length - 1; i >= 0; i--) {
|
|
683
683
|
const cmd = chain[i];
|
|
684
|
-
const
|
|
685
|
-
const result = cmd.#shift(remaining, shadowed,
|
|
684
|
+
const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
|
|
685
|
+
const result = cmd.#shift(remaining, shadowed, policy.mergedOptions);
|
|
686
686
|
consumedTokens.set(cmd, result.consumed);
|
|
687
687
|
remaining = result.remaining;
|
|
688
688
|
for (const opt of cmd.#options) {
|
|
@@ -699,8 +699,7 @@ class Command {
|
|
|
699
699
|
}
|
|
700
700
|
return { consumedTokens, argTokens };
|
|
701
701
|
}
|
|
702
|
-
#shift(tokens, shadowed,
|
|
703
|
-
const allOptions = this.#getMergedOptions(includeVersion);
|
|
702
|
+
#shift(tokens, shadowed, allOptions) {
|
|
704
703
|
const effectiveOptions = allOptions.filter(o => !shadowed.has(o.long));
|
|
705
704
|
const optionByLong = new Map();
|
|
706
705
|
const optionByShort = new Map();
|
|
@@ -768,18 +767,17 @@ class Command {
|
|
|
768
767
|
}
|
|
769
768
|
return { consumed, remaining };
|
|
770
769
|
}
|
|
771
|
-
#parse(chain, resolveResult, ctx, restArgs) {
|
|
770
|
+
#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs) {
|
|
772
771
|
const { consumedTokens, argTokens } = resolveResult;
|
|
773
772
|
const leafCommand = chain[chain.length - 1];
|
|
774
|
-
this.#validateMergedShortOptions(chain);
|
|
773
|
+
this.#validateMergedShortOptions(chain, optionPolicyMap);
|
|
775
774
|
const optsMap = new Map();
|
|
776
|
-
for (
|
|
777
|
-
const
|
|
778
|
-
const includeVersion = i === 0;
|
|
775
|
+
for (const cmd of chain) {
|
|
776
|
+
const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
|
|
779
777
|
const tokens = consumedTokens.get(cmd) ?? [];
|
|
780
|
-
const opts = cmd.#parseOptions(tokens,
|
|
778
|
+
const opts = cmd.#parseOptions(tokens, policy.mergedOptions, ctx.envs);
|
|
781
779
|
optsMap.set(cmd, opts);
|
|
782
|
-
for (const opt of
|
|
780
|
+
for (const opt of policy.mergedOptions) {
|
|
783
781
|
if (opt.apply && opts[opt.long] !== undefined) {
|
|
784
782
|
opt.apply(opts[opt.long], ctx);
|
|
785
783
|
}
|
|
@@ -793,8 +791,7 @@ class Command {
|
|
|
793
791
|
const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
|
|
794
792
|
return { ctx, opts: mergedOpts, args, rawArgs };
|
|
795
793
|
}
|
|
796
|
-
#parseOptions(tokens,
|
|
797
|
-
const allOptions = this.#getMergedOptions(includeVersion);
|
|
794
|
+
#parseOptions(tokens, allOptions, envs) {
|
|
798
795
|
const opts = {};
|
|
799
796
|
let sawColorToken = false;
|
|
800
797
|
for (const opt of allOptions) {
|
|
@@ -976,22 +973,30 @@ class Command {
|
|
|
976
973
|
}
|
|
977
974
|
return raw;
|
|
978
975
|
}
|
|
979
|
-
#
|
|
976
|
+
#hasUserOption(long) {
|
|
977
|
+
return this.#options.some(option => option.long === long);
|
|
978
|
+
}
|
|
979
|
+
#canUseBuiltinVersion() {
|
|
980
|
+
return this.#version !== undefined;
|
|
981
|
+
}
|
|
982
|
+
#resolveOptionPolicy() {
|
|
980
983
|
const optionMap = new Map();
|
|
981
|
-
const hasUserColor = this.#
|
|
982
|
-
const hasUserHelp = this.#
|
|
983
|
-
const hasUserVersion = this.#
|
|
984
|
-
const hasUserLogLevel = this.#
|
|
985
|
-
const hasUserSilent = this.#
|
|
986
|
-
const hasUserLogDate = this.#
|
|
987
|
-
const hasUserLogColorful = this.#
|
|
984
|
+
const hasUserColor = this.#hasUserOption('color');
|
|
985
|
+
const hasUserHelp = this.#hasUserOption('help');
|
|
986
|
+
const hasUserVersion = this.#hasUserOption('version');
|
|
987
|
+
const hasUserLogLevel = this.#hasUserOption('logLevel');
|
|
988
|
+
const hasUserSilent = this.#hasUserOption('silent');
|
|
989
|
+
const hasUserLogDate = this.#hasUserOption('logDate');
|
|
990
|
+
const hasUserLogColorful = this.#hasUserOption('logColorful');
|
|
991
|
+
const enableBuiltinHelp = !hasUserHelp;
|
|
992
|
+
const enableBuiltinVersion = !hasUserVersion && this.#canUseBuiltinVersion();
|
|
988
993
|
if (this.#builtin.option.color && !hasUserColor) {
|
|
989
994
|
optionMap.set('color', BUILTIN_COLOR_OPTION);
|
|
990
995
|
}
|
|
991
|
-
if (
|
|
996
|
+
if (enableBuiltinHelp) {
|
|
992
997
|
optionMap.set('help', BUILTIN_HELP_OPTION);
|
|
993
998
|
}
|
|
994
|
-
if (
|
|
999
|
+
if (enableBuiltinVersion) {
|
|
995
1000
|
optionMap.set('version', BUILTIN_VERSION_OPTION);
|
|
996
1001
|
}
|
|
997
1002
|
if (this.#builtin.option.logLevel && !hasUserLogLevel) {
|
|
@@ -1009,14 +1014,31 @@ class Command {
|
|
|
1009
1014
|
for (const opt of this.#options) {
|
|
1010
1015
|
optionMap.set(opt.long, opt);
|
|
1011
1016
|
}
|
|
1012
|
-
return
|
|
1017
|
+
return {
|
|
1018
|
+
mergedOptions: Array.from(optionMap.values()),
|
|
1019
|
+
enableBuiltinHelp,
|
|
1020
|
+
enableBuiltinVersion,
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
#buildOptionPolicyMap(chain) {
|
|
1024
|
+
const optionPolicyMap = new Map();
|
|
1025
|
+
for (const cmd of chain) {
|
|
1026
|
+
optionPolicyMap.set(cmd, cmd.#resolveOptionPolicy());
|
|
1027
|
+
}
|
|
1028
|
+
return optionPolicyMap;
|
|
1029
|
+
}
|
|
1030
|
+
#mustGetOptionPolicy(optionPolicyMap, cmd) {
|
|
1031
|
+
const policy = optionPolicyMap.get(cmd);
|
|
1032
|
+
if (policy !== undefined) {
|
|
1033
|
+
return policy;
|
|
1034
|
+
}
|
|
1035
|
+
throw new CommanderError('ConfigurationError', `missing option policy for command "${cmd.#getCommandPath()}"`, this.#getCommandPath());
|
|
1013
1036
|
}
|
|
1014
|
-
#validateMergedShortOptions(chain) {
|
|
1037
|
+
#validateMergedShortOptions(chain, optionPolicyMap) {
|
|
1015
1038
|
const mergedByLong = new Map();
|
|
1016
|
-
for (
|
|
1017
|
-
const
|
|
1018
|
-
const
|
|
1019
|
-
for (const opt of cmd.#getMergedOptions(includeVersion)) {
|
|
1039
|
+
for (const cmd of chain) {
|
|
1040
|
+
const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
|
|
1041
|
+
for (const opt of policy.mergedOptions) {
|
|
1020
1042
|
mergedByLong.set(opt.long, opt);
|
|
1021
1043
|
}
|
|
1022
1044
|
}
|
|
@@ -1112,8 +1134,8 @@ class Command {
|
|
|
1112
1134
|
process.exit(1);
|
|
1113
1135
|
}
|
|
1114
1136
|
}
|
|
1115
|
-
#resolveHelpColorOption(tokens, envs) {
|
|
1116
|
-
const colorOption =
|
|
1137
|
+
#resolveHelpColorOption(tokens, envs, policy = this.#resolveOptionPolicy()) {
|
|
1138
|
+
const colorOption = policy.mergedOptions.find(opt => opt.long === 'color');
|
|
1117
1139
|
let color = !isNoColorEnabled(envs);
|
|
1118
1140
|
if (!colorOption || colorOption.type !== 'boolean' || colorOption.args !== 'none') {
|
|
1119
1141
|
return color;
|
|
@@ -1164,6 +1186,31 @@ class Command {
|
|
|
1164
1186
|
}
|
|
1165
1187
|
}
|
|
1166
1188
|
|
|
1189
|
+
class Coerce {
|
|
1190
|
+
constructor() { }
|
|
1191
|
+
static create(name, expectedType, validator, errorMessage) {
|
|
1192
|
+
return (rawValue) => {
|
|
1193
|
+
const value = Number(rawValue);
|
|
1194
|
+
if (!validator(value)) {
|
|
1195
|
+
throw new Error(errorMessage ?? `${name} is expected as ${expectedType}, but got ${rawValue}`);
|
|
1196
|
+
}
|
|
1197
|
+
return value;
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
static number(name, errorMessage) {
|
|
1201
|
+
return this.create(name, 'a finite number', value => Number.isFinite(value), errorMessage);
|
|
1202
|
+
}
|
|
1203
|
+
static integer(name, errorMessage) {
|
|
1204
|
+
return this.create(name, 'an integer', value => Number.isInteger(value), errorMessage);
|
|
1205
|
+
}
|
|
1206
|
+
static positiveInteger(name, errorMessage) {
|
|
1207
|
+
return this.create(name, 'a positive integer', value => Number.isInteger(value) && value > 0, errorMessage);
|
|
1208
|
+
}
|
|
1209
|
+
static positiveNumber(name, errorMessage) {
|
|
1210
|
+
return this.create(name, 'a positive number', value => Number.isFinite(value) && value > 0, errorMessage);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1167
1214
|
function camelToKebabCase(str) {
|
|
1168
1215
|
return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
1169
1216
|
}
|
|
@@ -1523,6 +1570,7 @@ class PwshCompletion {
|
|
|
1523
1570
|
}
|
|
1524
1571
|
|
|
1525
1572
|
exports.BashCompletion = BashCompletion;
|
|
1573
|
+
exports.Coerce = Coerce;
|
|
1526
1574
|
exports.Command = Command;
|
|
1527
1575
|
exports.CommanderError = CommanderError;
|
|
1528
1576
|
exports.CompletionCommand = CompletionCommand;
|
package/lib/esm/index.mjs
CHANGED
|
@@ -346,30 +346,29 @@ class Command {
|
|
|
346
346
|
const routeResult = this.#route(processedArgv);
|
|
347
347
|
const { chain, remaining } = routeResult;
|
|
348
348
|
const leafCommand = chain[chain.length - 1];
|
|
349
|
-
const rootCommand = chain[0];
|
|
350
349
|
const tokenizeResult = tokenize(remaining, leafCommand.#getCommandPath());
|
|
351
350
|
const { optionTokens, restArgs } = tokenizeResult;
|
|
352
|
-
const
|
|
353
|
-
const
|
|
354
|
-
if (
|
|
355
|
-
const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs);
|
|
351
|
+
const optionPolicyMap = this.#buildOptionPolicyMap(chain);
|
|
352
|
+
const leafPolicy = this.#mustGetOptionPolicy(optionPolicyMap, leafCommand);
|
|
353
|
+
if (leafPolicy.enableBuiltinHelp && this.#hasFlag(optionTokens, 'help', 'h')) {
|
|
354
|
+
const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs, leafPolicy);
|
|
356
355
|
console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
|
|
357
356
|
return;
|
|
358
357
|
}
|
|
359
|
-
if (
|
|
358
|
+
if (leafPolicy.enableBuiltinVersion) {
|
|
360
359
|
if (this.#hasFlag(optionTokens, 'version', 'V')) {
|
|
361
360
|
console.log(leafCommand.#version);
|
|
362
361
|
return;
|
|
363
362
|
}
|
|
364
363
|
}
|
|
365
|
-
const resolveResult = this.#resolve(chain, optionTokens);
|
|
364
|
+
const resolveResult = this.#resolve(chain, optionTokens, optionPolicyMap);
|
|
366
365
|
const ctx = {
|
|
367
366
|
cmd: leafCommand,
|
|
368
367
|
envs,
|
|
369
368
|
reporter: reporter ?? this.#reporter ?? new Reporter(),
|
|
370
369
|
argv,
|
|
371
370
|
};
|
|
372
|
-
const parseResult = this.#parse(chain, resolveResult, ctx, restArgs);
|
|
371
|
+
const parseResult = this.#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs);
|
|
373
372
|
const actionParams = {
|
|
374
373
|
ctx: parseResult.ctx,
|
|
375
374
|
opts: parseResult.opts,
|
|
@@ -380,7 +379,7 @@ class Command {
|
|
|
380
379
|
await leafCommand.#runAction(actionParams);
|
|
381
380
|
}
|
|
382
381
|
else if (leafCommand.#subcommandsList.length > 0) {
|
|
383
|
-
const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs);
|
|
382
|
+
const helpColor = leafCommand.#resolveHelpColorOption(optionTokens, envs, leafPolicy);
|
|
384
383
|
console.log(leafCommand.#formatHelpForDisplay({ color: helpColor }));
|
|
385
384
|
}
|
|
386
385
|
else {
|
|
@@ -404,14 +403,15 @@ class Command {
|
|
|
404
403
|
const leafCommand = chain[chain.length - 1];
|
|
405
404
|
const tokenizeResult = tokenize(remaining, leafCommand.#getCommandPath());
|
|
406
405
|
const { optionTokens, restArgs } = tokenizeResult;
|
|
407
|
-
const
|
|
406
|
+
const optionPolicyMap = this.#buildOptionPolicyMap(chain);
|
|
407
|
+
const resolveResult = this.#resolve(chain, optionTokens, optionPolicyMap);
|
|
408
408
|
const ctx = {
|
|
409
409
|
cmd: leafCommand,
|
|
410
410
|
envs,
|
|
411
411
|
reporter: reporter ?? this.#reporter ?? new Reporter(),
|
|
412
412
|
argv,
|
|
413
413
|
};
|
|
414
|
-
return this.#parse(chain, resolveResult, ctx, restArgs);
|
|
414
|
+
return this.#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs);
|
|
415
415
|
}
|
|
416
416
|
formatHelp() {
|
|
417
417
|
return this.#renderHelpPlain(this.#buildHelpData());
|
|
@@ -428,7 +428,7 @@ class Command {
|
|
|
428
428
|
return color && process.stdout.isTTY === true;
|
|
429
429
|
}
|
|
430
430
|
#buildHelpData() {
|
|
431
|
-
const allOptions = this.#
|
|
431
|
+
const allOptions = this.#resolveOptionPolicy().mergedOptions;
|
|
432
432
|
const commandPath = this.#getCommandPath();
|
|
433
433
|
let usage = `Usage: ${commandPath}`;
|
|
434
434
|
if (allOptions.length > 0)
|
|
@@ -565,7 +565,7 @@ class Command {
|
|
|
565
565
|
return lines.join('\n');
|
|
566
566
|
}
|
|
567
567
|
getCompletionMeta() {
|
|
568
|
-
const allOptions = this.#
|
|
568
|
+
const allOptions = this.#resolveOptionPolicy().mergedOptions;
|
|
569
569
|
const options = [];
|
|
570
570
|
for (const opt of allOptions) {
|
|
571
571
|
options.push({
|
|
@@ -653,14 +653,14 @@ class Command {
|
|
|
653
653
|
}
|
|
654
654
|
return { chain, remaining: argv.slice(idx) };
|
|
655
655
|
}
|
|
656
|
-
#resolve(chain, tokens) {
|
|
656
|
+
#resolve(chain, tokens, optionPolicyMap) {
|
|
657
657
|
const consumedTokens = new Map();
|
|
658
658
|
let remaining = [...tokens];
|
|
659
659
|
const shadowed = new Set();
|
|
660
660
|
for (let i = chain.length - 1; i >= 0; i--) {
|
|
661
661
|
const cmd = chain[i];
|
|
662
|
-
const
|
|
663
|
-
const result = cmd.#shift(remaining, shadowed,
|
|
662
|
+
const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
|
|
663
|
+
const result = cmd.#shift(remaining, shadowed, policy.mergedOptions);
|
|
664
664
|
consumedTokens.set(cmd, result.consumed);
|
|
665
665
|
remaining = result.remaining;
|
|
666
666
|
for (const opt of cmd.#options) {
|
|
@@ -677,8 +677,7 @@ class Command {
|
|
|
677
677
|
}
|
|
678
678
|
return { consumedTokens, argTokens };
|
|
679
679
|
}
|
|
680
|
-
#shift(tokens, shadowed,
|
|
681
|
-
const allOptions = this.#getMergedOptions(includeVersion);
|
|
680
|
+
#shift(tokens, shadowed, allOptions) {
|
|
682
681
|
const effectiveOptions = allOptions.filter(o => !shadowed.has(o.long));
|
|
683
682
|
const optionByLong = new Map();
|
|
684
683
|
const optionByShort = new Map();
|
|
@@ -746,18 +745,17 @@ class Command {
|
|
|
746
745
|
}
|
|
747
746
|
return { consumed, remaining };
|
|
748
747
|
}
|
|
749
|
-
#parse(chain, resolveResult, ctx, restArgs) {
|
|
748
|
+
#parse(chain, resolveResult, optionPolicyMap, ctx, restArgs) {
|
|
750
749
|
const { consumedTokens, argTokens } = resolveResult;
|
|
751
750
|
const leafCommand = chain[chain.length - 1];
|
|
752
|
-
this.#validateMergedShortOptions(chain);
|
|
751
|
+
this.#validateMergedShortOptions(chain, optionPolicyMap);
|
|
753
752
|
const optsMap = new Map();
|
|
754
|
-
for (
|
|
755
|
-
const
|
|
756
|
-
const includeVersion = i === 0;
|
|
753
|
+
for (const cmd of chain) {
|
|
754
|
+
const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
|
|
757
755
|
const tokens = consumedTokens.get(cmd) ?? [];
|
|
758
|
-
const opts = cmd.#parseOptions(tokens,
|
|
756
|
+
const opts = cmd.#parseOptions(tokens, policy.mergedOptions, ctx.envs);
|
|
759
757
|
optsMap.set(cmd, opts);
|
|
760
|
-
for (const opt of
|
|
758
|
+
for (const opt of policy.mergedOptions) {
|
|
761
759
|
if (opt.apply && opts[opt.long] !== undefined) {
|
|
762
760
|
opt.apply(opts[opt.long], ctx);
|
|
763
761
|
}
|
|
@@ -771,8 +769,7 @@ class Command {
|
|
|
771
769
|
const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
|
|
772
770
|
return { ctx, opts: mergedOpts, args, rawArgs };
|
|
773
771
|
}
|
|
774
|
-
#parseOptions(tokens,
|
|
775
|
-
const allOptions = this.#getMergedOptions(includeVersion);
|
|
772
|
+
#parseOptions(tokens, allOptions, envs) {
|
|
776
773
|
const opts = {};
|
|
777
774
|
let sawColorToken = false;
|
|
778
775
|
for (const opt of allOptions) {
|
|
@@ -954,22 +951,30 @@ class Command {
|
|
|
954
951
|
}
|
|
955
952
|
return raw;
|
|
956
953
|
}
|
|
957
|
-
#
|
|
954
|
+
#hasUserOption(long) {
|
|
955
|
+
return this.#options.some(option => option.long === long);
|
|
956
|
+
}
|
|
957
|
+
#canUseBuiltinVersion() {
|
|
958
|
+
return this.#version !== undefined;
|
|
959
|
+
}
|
|
960
|
+
#resolveOptionPolicy() {
|
|
958
961
|
const optionMap = new Map();
|
|
959
|
-
const hasUserColor = this.#
|
|
960
|
-
const hasUserHelp = this.#
|
|
961
|
-
const hasUserVersion = this.#
|
|
962
|
-
const hasUserLogLevel = this.#
|
|
963
|
-
const hasUserSilent = this.#
|
|
964
|
-
const hasUserLogDate = this.#
|
|
965
|
-
const hasUserLogColorful = this.#
|
|
962
|
+
const hasUserColor = this.#hasUserOption('color');
|
|
963
|
+
const hasUserHelp = this.#hasUserOption('help');
|
|
964
|
+
const hasUserVersion = this.#hasUserOption('version');
|
|
965
|
+
const hasUserLogLevel = this.#hasUserOption('logLevel');
|
|
966
|
+
const hasUserSilent = this.#hasUserOption('silent');
|
|
967
|
+
const hasUserLogDate = this.#hasUserOption('logDate');
|
|
968
|
+
const hasUserLogColorful = this.#hasUserOption('logColorful');
|
|
969
|
+
const enableBuiltinHelp = !hasUserHelp;
|
|
970
|
+
const enableBuiltinVersion = !hasUserVersion && this.#canUseBuiltinVersion();
|
|
966
971
|
if (this.#builtin.option.color && !hasUserColor) {
|
|
967
972
|
optionMap.set('color', BUILTIN_COLOR_OPTION);
|
|
968
973
|
}
|
|
969
|
-
if (
|
|
974
|
+
if (enableBuiltinHelp) {
|
|
970
975
|
optionMap.set('help', BUILTIN_HELP_OPTION);
|
|
971
976
|
}
|
|
972
|
-
if (
|
|
977
|
+
if (enableBuiltinVersion) {
|
|
973
978
|
optionMap.set('version', BUILTIN_VERSION_OPTION);
|
|
974
979
|
}
|
|
975
980
|
if (this.#builtin.option.logLevel && !hasUserLogLevel) {
|
|
@@ -987,14 +992,31 @@ class Command {
|
|
|
987
992
|
for (const opt of this.#options) {
|
|
988
993
|
optionMap.set(opt.long, opt);
|
|
989
994
|
}
|
|
990
|
-
return
|
|
995
|
+
return {
|
|
996
|
+
mergedOptions: Array.from(optionMap.values()),
|
|
997
|
+
enableBuiltinHelp,
|
|
998
|
+
enableBuiltinVersion,
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
#buildOptionPolicyMap(chain) {
|
|
1002
|
+
const optionPolicyMap = new Map();
|
|
1003
|
+
for (const cmd of chain) {
|
|
1004
|
+
optionPolicyMap.set(cmd, cmd.#resolveOptionPolicy());
|
|
1005
|
+
}
|
|
1006
|
+
return optionPolicyMap;
|
|
1007
|
+
}
|
|
1008
|
+
#mustGetOptionPolicy(optionPolicyMap, cmd) {
|
|
1009
|
+
const policy = optionPolicyMap.get(cmd);
|
|
1010
|
+
if (policy !== undefined) {
|
|
1011
|
+
return policy;
|
|
1012
|
+
}
|
|
1013
|
+
throw new CommanderError('ConfigurationError', `missing option policy for command "${cmd.#getCommandPath()}"`, this.#getCommandPath());
|
|
991
1014
|
}
|
|
992
|
-
#validateMergedShortOptions(chain) {
|
|
1015
|
+
#validateMergedShortOptions(chain, optionPolicyMap) {
|
|
993
1016
|
const mergedByLong = new Map();
|
|
994
|
-
for (
|
|
995
|
-
const
|
|
996
|
-
const
|
|
997
|
-
for (const opt of cmd.#getMergedOptions(includeVersion)) {
|
|
1017
|
+
for (const cmd of chain) {
|
|
1018
|
+
const policy = this.#mustGetOptionPolicy(optionPolicyMap, cmd);
|
|
1019
|
+
for (const opt of policy.mergedOptions) {
|
|
998
1020
|
mergedByLong.set(opt.long, opt);
|
|
999
1021
|
}
|
|
1000
1022
|
}
|
|
@@ -1090,8 +1112,8 @@ class Command {
|
|
|
1090
1112
|
process.exit(1);
|
|
1091
1113
|
}
|
|
1092
1114
|
}
|
|
1093
|
-
#resolveHelpColorOption(tokens, envs) {
|
|
1094
|
-
const colorOption =
|
|
1115
|
+
#resolveHelpColorOption(tokens, envs, policy = this.#resolveOptionPolicy()) {
|
|
1116
|
+
const colorOption = policy.mergedOptions.find(opt => opt.long === 'color');
|
|
1095
1117
|
let color = !isNoColorEnabled(envs);
|
|
1096
1118
|
if (!colorOption || colorOption.type !== 'boolean' || colorOption.args !== 'none') {
|
|
1097
1119
|
return color;
|
|
@@ -1142,6 +1164,31 @@ class Command {
|
|
|
1142
1164
|
}
|
|
1143
1165
|
}
|
|
1144
1166
|
|
|
1167
|
+
class Coerce {
|
|
1168
|
+
constructor() { }
|
|
1169
|
+
static create(name, expectedType, validator, errorMessage) {
|
|
1170
|
+
return (rawValue) => {
|
|
1171
|
+
const value = Number(rawValue);
|
|
1172
|
+
if (!validator(value)) {
|
|
1173
|
+
throw new Error(errorMessage ?? `${name} is expected as ${expectedType}, but got ${rawValue}`);
|
|
1174
|
+
}
|
|
1175
|
+
return value;
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
static number(name, errorMessage) {
|
|
1179
|
+
return this.create(name, 'a finite number', value => Number.isFinite(value), errorMessage);
|
|
1180
|
+
}
|
|
1181
|
+
static integer(name, errorMessage) {
|
|
1182
|
+
return this.create(name, 'an integer', value => Number.isInteger(value), errorMessage);
|
|
1183
|
+
}
|
|
1184
|
+
static positiveInteger(name, errorMessage) {
|
|
1185
|
+
return this.create(name, 'a positive integer', value => Number.isInteger(value) && value > 0, errorMessage);
|
|
1186
|
+
}
|
|
1187
|
+
static positiveNumber(name, errorMessage) {
|
|
1188
|
+
return this.create(name, 'a positive number', value => Number.isFinite(value) && value > 0, errorMessage);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1145
1192
|
function camelToKebabCase(str) {
|
|
1146
1193
|
return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
1147
1194
|
}
|
|
@@ -1500,4 +1547,4 @@ class PwshCompletion {
|
|
|
1500
1547
|
}
|
|
1501
1548
|
}
|
|
1502
1549
|
|
|
1503
|
-
export { BashCompletion, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, logColorfulOption, logDateOption, logLevelOption, silentOption };
|
|
1550
|
+
export { BashCompletion, Coerce, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, logColorfulOption, logDateOption, logLevelOption, silentOption };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -129,7 +129,7 @@ interface ICommandConfig {
|
|
|
129
129
|
name?: string;
|
|
130
130
|
/** Command description */
|
|
131
131
|
desc: string;
|
|
132
|
-
/** Version (for
|
|
132
|
+
/** Version (for built-in --version on this command) */
|
|
133
133
|
version?: string;
|
|
134
134
|
/** Built-in features configuration */
|
|
135
135
|
builtin?: boolean | ICommandBuiltinConfig;
|
|
@@ -308,6 +308,20 @@ declare class Command implements ICommand {
|
|
|
308
308
|
getCompletionMeta(): ICompletionMeta;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Pre-defined coerce factory methods for @guanghechen/commander.
|
|
313
|
+
*
|
|
314
|
+
* @module @guanghechen/commander/coerce
|
|
315
|
+
*/
|
|
316
|
+
declare class Coerce {
|
|
317
|
+
private constructor();
|
|
318
|
+
private static create;
|
|
319
|
+
static number(name: string, errorMessage?: string): (rawValue: string) => number;
|
|
320
|
+
static integer(name: string, errorMessage?: string): (rawValue: string) => number;
|
|
321
|
+
static positiveInteger(name: string, errorMessage?: string): (rawValue: string) => number;
|
|
322
|
+
static positiveNumber(name: string, errorMessage?: string): (rawValue: string) => number;
|
|
323
|
+
}
|
|
324
|
+
|
|
311
325
|
/**
|
|
312
326
|
* Shell completion generators
|
|
313
327
|
*
|
|
@@ -436,5 +450,5 @@ declare const logColorfulOption: ICommandOptionConfig<boolean>;
|
|
|
436
450
|
*/
|
|
437
451
|
declare const silentOption: ICommandOptionConfig<boolean>;
|
|
438
452
|
|
|
439
|
-
export { BashCompletion, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, logColorfulOption, logDateOption, logLevelOption, silentOption };
|
|
453
|
+
export { BashCompletion, Coerce, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion, logColorfulOption, logDateOption, logLevelOption, silentOption };
|
|
440
454
|
export type { ICommand, ICommandAction, ICommandActionParams, ICommandArgumentConfig, ICommandArgumentKind, ICommandArgumentType, ICommandBuiltinCommandConfig, ICommandBuiltinConfig, ICommandBuiltinOptionConfig, ICommandConfig, ICommandContext, ICommandExample, ICommandOptionArgs, ICommandOptionConfig, ICommandOptionType, ICommandParseResult, ICommandParsedArgs, ICommandParsedOpts, ICommandResolveResult, ICommandRouteResult, ICommandRunParams, ICommandShiftResult, ICommandToken, ICommandTokenType, ICommandTokenizeResult, ICommanderErrorKind, ICompletionCommandConfig, ICompletionMeta, ICompletionOptionMeta, ICompletionPaths, ICompletionShellType };
|