@guanghechen/commander 3.3.0 → 4.0.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 +483 -502
- package/lib/esm/index.mjs +483 -502
- package/lib/types/index.d.ts +150 -81
- package/package.json +1 -1
package/lib/esm/index.mjs
CHANGED
|
@@ -29,41 +29,131 @@ class DefaultReporter {
|
|
|
29
29
|
console.error(message, ...args);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
const LONG_OPTION_REGEX = /^--[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
33
|
+
const NEGATIVE_OPTION_REGEX = /^--no-[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
34
|
+
function kebabToCamelCase(str) {
|
|
35
|
+
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
36
|
+
}
|
|
37
|
+
function camelToKebabCase$1(str) {
|
|
38
|
+
return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
39
|
+
}
|
|
40
|
+
function tokenizeLongOption(arg, commandPath) {
|
|
41
|
+
const eqIdx = arg.indexOf('=');
|
|
42
|
+
const namePart = eqIdx !== -1 ? arg.slice(0, eqIdx) : arg;
|
|
43
|
+
const valuePart = eqIdx !== -1 ? arg.slice(eqIdx) : '';
|
|
44
|
+
if (namePart.includes('_')) {
|
|
45
|
+
throw new CommanderError('InvalidOptionFormat', `invalid option "${arg}": use '-' instead of '_'`, commandPath);
|
|
46
|
+
}
|
|
47
|
+
const lowerName = namePart.toLowerCase();
|
|
48
|
+
if (lowerName === '--no' || lowerName === '--no-') {
|
|
49
|
+
throw new CommanderError('InvalidNegativeOption', `invalid negative option syntax "${arg}"`, commandPath);
|
|
50
|
+
}
|
|
51
|
+
if (lowerName.startsWith('--no-')) {
|
|
52
|
+
if (valuePart !== '') {
|
|
53
|
+
throw new CommanderError('NegativeOptionWithValue', `"${namePart}" does not accept a value`, commandPath);
|
|
54
|
+
}
|
|
55
|
+
if (!NEGATIVE_OPTION_REGEX.test(lowerName)) {
|
|
56
|
+
throw new CommanderError('InvalidOptionFormat', `invalid option format "${arg}"`, commandPath);
|
|
57
|
+
}
|
|
58
|
+
const camelName = kebabToCamelCase(lowerName.slice(5));
|
|
59
|
+
return {
|
|
60
|
+
original: arg,
|
|
61
|
+
resolved: `--${camelName}=false`,
|
|
62
|
+
name: camelName,
|
|
63
|
+
type: 'long',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (!LONG_OPTION_REGEX.test(lowerName)) {
|
|
67
|
+
throw new CommanderError('InvalidOptionFormat', `invalid option format "${arg}"`, commandPath);
|
|
68
|
+
}
|
|
69
|
+
const camelName = kebabToCamelCase(lowerName.slice(2));
|
|
70
|
+
return {
|
|
71
|
+
original: arg,
|
|
72
|
+
resolved: `--${camelName}${valuePart}`,
|
|
73
|
+
name: camelName,
|
|
74
|
+
type: 'long',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function tokenizeShortOptions(arg, commandPath) {
|
|
78
|
+
if (arg.includes('=')) {
|
|
79
|
+
throw new CommanderError('UnsupportedShortSyntax', `"${arg}" is not supported. Use "-${arg[1]} ${arg.slice(3)}" instead`, commandPath);
|
|
80
|
+
}
|
|
81
|
+
const flags = arg.slice(1);
|
|
82
|
+
return flags.split('').map(flag => ({
|
|
83
|
+
original: `-${flag}`,
|
|
84
|
+
resolved: `-${flag}`,
|
|
85
|
+
name: flag,
|
|
86
|
+
type: 'short',
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
function tokenize(argv, commandPath) {
|
|
90
|
+
const optionTokens = [];
|
|
91
|
+
const restArgs = [];
|
|
92
|
+
let passThrough = false;
|
|
93
|
+
for (const arg of argv) {
|
|
94
|
+
if (arg === '--') {
|
|
95
|
+
passThrough = true;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (passThrough) {
|
|
99
|
+
restArgs.push(arg);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (arg.startsWith('--')) {
|
|
103
|
+
optionTokens.push(tokenizeLongOption(arg, commandPath));
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (arg.startsWith('-') && arg.length > 1) {
|
|
107
|
+
optionTokens.push(...tokenizeShortOptions(arg, commandPath));
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
optionTokens.push({
|
|
111
|
+
original: arg,
|
|
112
|
+
resolved: arg,
|
|
113
|
+
name: '',
|
|
114
|
+
type: 'none',
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return { optionTokens, restArgs };
|
|
118
|
+
}
|
|
32
119
|
const BUILTIN_HELP_OPTION = {
|
|
33
120
|
long: 'help',
|
|
34
121
|
short: 'h',
|
|
35
122
|
type: 'boolean',
|
|
36
|
-
|
|
123
|
+
args: 'none',
|
|
124
|
+
desc: 'Show help information',
|
|
37
125
|
};
|
|
38
126
|
const BUILTIN_VERSION_OPTION = {
|
|
39
127
|
long: 'version',
|
|
40
128
|
short: 'V',
|
|
41
129
|
type: 'boolean',
|
|
42
|
-
|
|
130
|
+
args: 'none',
|
|
131
|
+
desc: 'Show version number',
|
|
43
132
|
};
|
|
44
133
|
class Command {
|
|
45
134
|
#name;
|
|
46
|
-
#
|
|
135
|
+
#desc;
|
|
47
136
|
#version;
|
|
48
137
|
#helpSubcommandEnabled;
|
|
49
138
|
#reporter;
|
|
50
139
|
#parent;
|
|
51
140
|
#options = [];
|
|
52
141
|
#arguments = [];
|
|
53
|
-
#
|
|
54
|
-
#
|
|
142
|
+
#subcommandsList = [];
|
|
143
|
+
#subcommandsMap = new Map();
|
|
144
|
+
#action = undefined;
|
|
55
145
|
constructor(config) {
|
|
56
146
|
this.#name = config.name ?? '';
|
|
57
|
-
this.#
|
|
147
|
+
this.#desc = config.desc;
|
|
58
148
|
this.#version = config.version;
|
|
59
149
|
this.#helpSubcommandEnabled = config.help ?? false;
|
|
60
150
|
this.#reporter = config.reporter;
|
|
61
151
|
}
|
|
62
152
|
get name() {
|
|
63
|
-
return this.#name;
|
|
153
|
+
return this.#name || undefined;
|
|
64
154
|
}
|
|
65
155
|
get description() {
|
|
66
|
-
return this.#
|
|
156
|
+
return this.#desc;
|
|
67
157
|
}
|
|
68
158
|
get version() {
|
|
69
159
|
return this.#version;
|
|
@@ -77,6 +167,9 @@ class Command {
|
|
|
77
167
|
get arguments() {
|
|
78
168
|
return [...this.#arguments];
|
|
79
169
|
}
|
|
170
|
+
get subcommands() {
|
|
171
|
+
return new Map(this.#subcommandsMap);
|
|
172
|
+
}
|
|
80
173
|
option(opt) {
|
|
81
174
|
this.#validateOptionConfig(opt);
|
|
82
175
|
this.#checkOptionUniqueness(opt);
|
|
@@ -99,14 +192,16 @@ class Command {
|
|
|
99
192
|
if (cmd.#parent && cmd.#parent !== this) {
|
|
100
193
|
throw new CommanderError('ConfigurationError', `command "${cmd.#name}" already has a parent`, this.#getCommandPath());
|
|
101
194
|
}
|
|
102
|
-
const existing = this.#
|
|
195
|
+
const existing = this.#subcommandsList.find(e => e.command === cmd);
|
|
103
196
|
if (existing) {
|
|
104
197
|
existing.aliases.push(name);
|
|
198
|
+
this.#subcommandsMap.set(name, cmd);
|
|
105
199
|
}
|
|
106
200
|
else {
|
|
107
201
|
cmd.#name = name;
|
|
108
202
|
cmd.#parent = this;
|
|
109
|
-
this.#
|
|
203
|
+
this.#subcommandsList.push({ name, aliases: [], command: cmd });
|
|
204
|
+
this.#subcommandsMap.set(name, cmd);
|
|
110
205
|
}
|
|
111
206
|
return this;
|
|
112
207
|
}
|
|
@@ -114,52 +209,42 @@ class Command {
|
|
|
114
209
|
const { argv, envs, reporter } = params;
|
|
115
210
|
try {
|
|
116
211
|
const processedArgv = this.#processHelpSubcommand(argv);
|
|
117
|
-
const
|
|
212
|
+
const routeResult = this.#route(processedArgv);
|
|
213
|
+
const { chain, remaining } = routeResult;
|
|
118
214
|
const leafCommand = chain[chain.length - 1];
|
|
119
215
|
const rootCommand = chain[0];
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
const { optionTokens, restArgs } = this.#splitAtDoubleDash(remaining);
|
|
123
|
-
const leafOptions = leafCommand.#getMergedOptions(leafCommand === rootCommand);
|
|
216
|
+
const tokenizeResult = tokenize(remaining, leafCommand.#getCommandPath());
|
|
217
|
+
const { optionTokens, restArgs } = tokenizeResult;
|
|
124
218
|
const hasUserHelp = leafCommand.#options.some(o => o.long === 'help');
|
|
125
219
|
const hasUserVersion = leafCommand.#options.some(o => o.long === 'version');
|
|
126
|
-
if (!hasUserHelp &&
|
|
220
|
+
if (!hasUserHelp && this.#hasFlag(optionTokens, 'help', 'h')) {
|
|
127
221
|
console.log(leafCommand.formatHelp());
|
|
128
222
|
return;
|
|
129
223
|
}
|
|
130
|
-
if (!hasUserVersion && leafCommand === rootCommand) {
|
|
131
|
-
if (
|
|
132
|
-
console.log(leafCommand
|
|
224
|
+
if (!hasUserVersion && leafCommand === rootCommand && leafCommand.#version) {
|
|
225
|
+
if (this.#hasFlag(optionTokens, 'version', 'V')) {
|
|
226
|
+
console.log(leafCommand.#version);
|
|
133
227
|
return;
|
|
134
228
|
}
|
|
135
229
|
}
|
|
136
|
-
const
|
|
230
|
+
const resolveResult = this.#resolve(chain, optionTokens);
|
|
137
231
|
const ctx = {
|
|
138
232
|
cmd: leafCommand,
|
|
139
233
|
envs,
|
|
140
234
|
reporter: reporter ?? this.#reporter ?? new DefaultReporter(),
|
|
141
235
|
argv,
|
|
142
236
|
};
|
|
143
|
-
this.#
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
237
|
+
const parseResult = this.#parse(chain, resolveResult, ctx, restArgs);
|
|
238
|
+
const actionParams = {
|
|
239
|
+
ctx: parseResult.ctx,
|
|
240
|
+
opts: parseResult.opts,
|
|
241
|
+
args: parseResult.args,
|
|
242
|
+
rawArgs: parseResult.rawArgs,
|
|
243
|
+
};
|
|
148
244
|
if (leafCommand.#action) {
|
|
149
|
-
|
|
150
|
-
await leafCommand.#action(actionParams);
|
|
151
|
-
}
|
|
152
|
-
catch (err) {
|
|
153
|
-
if (err instanceof Error) {
|
|
154
|
-
console.error(`Error: ${err.message}`);
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
console.error('Error: action failed');
|
|
158
|
-
}
|
|
159
|
-
process.exit(1);
|
|
160
|
-
}
|
|
245
|
+
await leafCommand.#runAction(actionParams);
|
|
161
246
|
}
|
|
162
|
-
else if (leafCommand.#
|
|
247
|
+
else if (leafCommand.#subcommandsList.length > 0) {
|
|
163
248
|
console.log(leafCommand.formatHelp());
|
|
164
249
|
}
|
|
165
250
|
else {
|
|
@@ -175,105 +260,33 @@ class Command {
|
|
|
175
260
|
throw err;
|
|
176
261
|
}
|
|
177
262
|
}
|
|
178
|
-
parse(
|
|
263
|
+
parse(params) {
|
|
264
|
+
const { argv, envs, reporter } = params;
|
|
179
265
|
const processedArgv = this.#processHelpSubcommand(argv);
|
|
180
|
-
const
|
|
266
|
+
const routeResult = this.#route(processedArgv);
|
|
267
|
+
const { chain, remaining } = routeResult;
|
|
181
268
|
const leafCommand = chain[chain.length - 1];
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return this.#shiftWithShadowed(tokens, new Set());
|
|
193
|
-
}
|
|
194
|
-
#shiftWithShadowed(tokens, shadowed, includeVersion = !this.#parent) {
|
|
195
|
-
const allDirectOptions = this.#getMergedOptions(includeVersion);
|
|
196
|
-
const directOptions = allDirectOptions.filter(o => !shadowed.has(o.long));
|
|
197
|
-
const opts = {};
|
|
198
|
-
for (const opt of directOptions) {
|
|
199
|
-
if (opt.default !== undefined) {
|
|
200
|
-
opts[opt.long] = opt.default;
|
|
201
|
-
}
|
|
202
|
-
else if (opt.type === 'boolean') {
|
|
203
|
-
opts[opt.long] = false;
|
|
204
|
-
}
|
|
205
|
-
else if (opt.type === 'string[]' || opt.type === 'number[]') {
|
|
206
|
-
opts[opt.long] = [];
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
let remaining = [...tokens];
|
|
210
|
-
const resolverOptions = directOptions.filter(o => o.resolver);
|
|
211
|
-
for (const opt of resolverOptions) {
|
|
212
|
-
const result = opt.resolver(remaining);
|
|
213
|
-
opts[opt.long] = result.value;
|
|
214
|
-
remaining = result.remaining;
|
|
215
|
-
}
|
|
216
|
-
const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(directOptions, true);
|
|
217
|
-
const normalizedTokens = this.#normalizeArgv(remaining, booleanOptions);
|
|
218
|
-
const finalRemaining = [];
|
|
219
|
-
let i = 0;
|
|
220
|
-
while (i < normalizedTokens.length) {
|
|
221
|
-
const token = normalizedTokens[i];
|
|
222
|
-
if (token.startsWith('--')) {
|
|
223
|
-
const consumed = this.#tryConsumeLongOption(normalizedTokens, i, optionByLong, opts);
|
|
224
|
-
if (consumed > 0) {
|
|
225
|
-
i += consumed;
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
finalRemaining.push(token);
|
|
229
|
-
i += 1;
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
if (token.startsWith('-') && token.length > 1) {
|
|
233
|
-
const result = this.#tryConsumeShortOption(normalizedTokens, i, optionByShort, opts);
|
|
234
|
-
if (result.consumed) {
|
|
235
|
-
i = result.nextIdx;
|
|
236
|
-
if (result.remainingToken) {
|
|
237
|
-
finalRemaining.push(result.remainingToken);
|
|
238
|
-
}
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
finalRemaining.push(token);
|
|
242
|
-
i += 1;
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
finalRemaining.push(token);
|
|
246
|
-
i += 1;
|
|
247
|
-
}
|
|
248
|
-
for (const opt of directOptions) {
|
|
249
|
-
if (opt.required && opts[opt.long] === undefined) {
|
|
250
|
-
throw new CommanderError('MissingRequired', `missing required option "--${opt.long}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
for (const opt of directOptions) {
|
|
254
|
-
if (opt.choices && opts[opt.long] !== undefined) {
|
|
255
|
-
const value = opts[opt.long];
|
|
256
|
-
const values = Array.isArray(value) ? value : [value];
|
|
257
|
-
const choices = opt.choices;
|
|
258
|
-
for (const v of values) {
|
|
259
|
-
if (!choices.includes(v)) {
|
|
260
|
-
throw new CommanderError('InvalidChoice', `invalid value "${v}" for option "--${opt.long}". Allowed: ${opt.choices.join(', ')}`, this.#getCommandPath());
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
return { opts, remaining: finalRemaining };
|
|
269
|
+
const tokenizeResult = tokenize(remaining, leafCommand.#getCommandPath());
|
|
270
|
+
const { optionTokens, restArgs } = tokenizeResult;
|
|
271
|
+
const resolveResult = this.#resolve(chain, optionTokens);
|
|
272
|
+
const ctx = {
|
|
273
|
+
cmd: leafCommand,
|
|
274
|
+
envs,
|
|
275
|
+
reporter: reporter ?? this.#reporter ?? new DefaultReporter(),
|
|
276
|
+
argv,
|
|
277
|
+
};
|
|
278
|
+
return this.#parse(chain, resolveResult, ctx, restArgs);
|
|
266
279
|
}
|
|
267
280
|
formatHelp() {
|
|
268
281
|
const lines = [];
|
|
269
282
|
const allOptions = this.#getMergedOptions();
|
|
270
|
-
lines.push(this.#
|
|
283
|
+
lines.push(this.#desc);
|
|
271
284
|
lines.push('');
|
|
272
285
|
const commandPath = this.#getCommandPath();
|
|
273
286
|
let usage = `Usage: ${commandPath}`;
|
|
274
287
|
if (allOptions.length > 0)
|
|
275
288
|
usage += ' [options]';
|
|
276
|
-
if (this.#
|
|
289
|
+
if (this.#subcommandsList.length > 0)
|
|
277
290
|
usage += ' [command]';
|
|
278
291
|
for (const arg of this.#arguments) {
|
|
279
292
|
if (arg.kind === 'required') {
|
|
@@ -292,24 +305,24 @@ class Command {
|
|
|
292
305
|
lines.push('Options:');
|
|
293
306
|
const optLines = [];
|
|
294
307
|
for (const opt of allOptions) {
|
|
308
|
+
const kebabLong = camelToKebabCase$1(opt.long);
|
|
295
309
|
let sig = opt.short ? `-${opt.short}, ` : ' ';
|
|
296
|
-
sig += `--${
|
|
297
|
-
|
|
298
|
-
if (effectiveType !== 'boolean') {
|
|
310
|
+
sig += `--${kebabLong}`;
|
|
311
|
+
if (opt.args !== 'none') {
|
|
299
312
|
sig += ' <value>';
|
|
300
313
|
}
|
|
301
|
-
let desc = opt.
|
|
302
|
-
if (opt.default !== undefined &&
|
|
314
|
+
let desc = opt.desc;
|
|
315
|
+
if (opt.default !== undefined && opt.type !== 'boolean') {
|
|
303
316
|
desc += ` (default: ${JSON.stringify(opt.default)})`;
|
|
304
317
|
}
|
|
305
318
|
if (opt.choices) {
|
|
306
319
|
desc += ` [choices: ${opt.choices.join(', ')}]`;
|
|
307
320
|
}
|
|
308
321
|
optLines.push({ sig, desc });
|
|
309
|
-
if (
|
|
322
|
+
if (opt.type === 'boolean' && opt.args === 'none') {
|
|
310
323
|
optLines.push({
|
|
311
|
-
sig: ` --no-${
|
|
312
|
-
desc: `Negate --${
|
|
324
|
+
sig: ` --no-${kebabLong}`,
|
|
325
|
+
desc: `Negate --${kebabLong}`,
|
|
313
326
|
});
|
|
314
327
|
}
|
|
315
328
|
}
|
|
@@ -320,19 +333,19 @@ class Command {
|
|
|
320
333
|
}
|
|
321
334
|
lines.push('');
|
|
322
335
|
}
|
|
323
|
-
const showHelpSubcommand = this.#helpSubcommandEnabled && this.#
|
|
324
|
-
if (this.#
|
|
336
|
+
const showHelpSubcommand = this.#helpSubcommandEnabled && this.#subcommandsList.length > 0;
|
|
337
|
+
if (this.#subcommandsList.length > 0) {
|
|
325
338
|
lines.push('Commands:');
|
|
326
339
|
const cmdLines = [];
|
|
327
340
|
if (showHelpSubcommand) {
|
|
328
341
|
cmdLines.push({ name: 'help', desc: 'Show help for a command' });
|
|
329
342
|
}
|
|
330
|
-
for (const entry of this.#
|
|
343
|
+
for (const entry of this.#subcommandsList) {
|
|
331
344
|
let name = entry.name;
|
|
332
345
|
if (entry.aliases.length > 0) {
|
|
333
346
|
name += `, ${entry.aliases.join(', ')}`;
|
|
334
347
|
}
|
|
335
|
-
cmdLines.push({ name, desc: entry.command.#
|
|
348
|
+
cmdLines.push({ name, desc: entry.command.#desc });
|
|
336
349
|
}
|
|
337
350
|
const maxNameLen = Math.max(...cmdLines.map(l => l.name.length));
|
|
338
351
|
for (const { name, desc } of cmdLines) {
|
|
@@ -347,21 +360,20 @@ class Command {
|
|
|
347
360
|
const allOptions = this.#getMergedOptions();
|
|
348
361
|
const options = [];
|
|
349
362
|
for (const opt of allOptions) {
|
|
350
|
-
const effectiveType = opt.type ?? 'string';
|
|
351
363
|
options.push({
|
|
352
364
|
long: opt.long,
|
|
353
365
|
short: opt.short,
|
|
354
|
-
|
|
355
|
-
takesValue:
|
|
366
|
+
desc: opt.desc,
|
|
367
|
+
takesValue: opt.args !== 'none',
|
|
356
368
|
choices: opt.choices,
|
|
357
369
|
});
|
|
358
370
|
}
|
|
359
371
|
return {
|
|
360
372
|
name: this.#name,
|
|
361
|
-
|
|
373
|
+
desc: this.#desc,
|
|
362
374
|
aliases: [],
|
|
363
375
|
options,
|
|
364
|
-
subcommands: this.#
|
|
376
|
+
subcommands: this.#subcommandsList.map(entry => {
|
|
365
377
|
const subMeta = entry.command.getCompletionMeta();
|
|
366
378
|
return {
|
|
367
379
|
...subMeta,
|
|
@@ -376,17 +388,17 @@ class Command {
|
|
|
376
388
|
return argv;
|
|
377
389
|
if (argv.length < 1 || argv[0] !== 'help')
|
|
378
390
|
return argv;
|
|
379
|
-
if (argv.length === 1 || this.#
|
|
391
|
+
if (argv.length === 1 || this.#subcommandsList.length === 0) {
|
|
380
392
|
return ['--help'];
|
|
381
393
|
}
|
|
382
394
|
const subName = argv[1];
|
|
383
|
-
const entry = this.#
|
|
395
|
+
const entry = this.#subcommandsList.find(e => e.name === subName || e.aliases.includes(subName));
|
|
384
396
|
if (entry) {
|
|
385
397
|
return [subName, '--help', ...argv.slice(2)];
|
|
386
398
|
}
|
|
387
399
|
return argv;
|
|
388
400
|
}
|
|
389
|
-
#
|
|
401
|
+
#route(argv) {
|
|
390
402
|
const chain = [this];
|
|
391
403
|
let current = this;
|
|
392
404
|
let idx = 0;
|
|
@@ -394,7 +406,7 @@ class Command {
|
|
|
394
406
|
const token = argv[idx];
|
|
395
407
|
if (token.startsWith('-'))
|
|
396
408
|
break;
|
|
397
|
-
const entry = current.#
|
|
409
|
+
const entry = current.#subcommandsList.find(e => e.name === token || e.aliases.includes(token));
|
|
398
410
|
if (!entry)
|
|
399
411
|
break;
|
|
400
412
|
current = entry.command;
|
|
@@ -403,93 +415,299 @@ class Command {
|
|
|
403
415
|
}
|
|
404
416
|
return { chain, remaining: argv.slice(idx) };
|
|
405
417
|
}
|
|
406
|
-
#
|
|
407
|
-
const
|
|
408
|
-
if (ddIdx === -1) {
|
|
409
|
-
return { optionTokens: tokens, restArgs: [] };
|
|
410
|
-
}
|
|
411
|
-
return {
|
|
412
|
-
optionTokens: tokens.slice(0, ddIdx),
|
|
413
|
-
restArgs: tokens.slice(ddIdx + 1),
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
#shiftChain(chain, tokens, includeRootVersion) {
|
|
417
|
-
const optsMap = new Map();
|
|
418
|
+
#resolve(chain, tokens) {
|
|
419
|
+
const consumedTokens = new Map();
|
|
418
420
|
let remaining = [...tokens];
|
|
419
|
-
const rootCommand = chain[0];
|
|
420
421
|
const shadowed = new Set();
|
|
421
422
|
for (let i = chain.length - 1; i >= 0; i--) {
|
|
422
423
|
const cmd = chain[i];
|
|
423
|
-
const includeVersion =
|
|
424
|
-
const result = cmd.#
|
|
425
|
-
|
|
424
|
+
const includeVersion = i === 0;
|
|
425
|
+
const result = cmd.#shift(remaining, shadowed, includeVersion);
|
|
426
|
+
consumedTokens.set(cmd, result.consumed);
|
|
426
427
|
remaining = result.remaining;
|
|
427
428
|
for (const opt of cmd.#options) {
|
|
428
429
|
shadowed.add(opt.long);
|
|
429
430
|
}
|
|
430
431
|
}
|
|
431
|
-
const
|
|
432
|
+
const argTokens = [];
|
|
432
433
|
for (const token of remaining) {
|
|
433
|
-
if (token.
|
|
434
|
+
if (token.type !== 'none') {
|
|
434
435
|
const leafCommand = chain[chain.length - 1];
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
436
|
+
throw new CommanderError('UnknownOption', `unknown option "${token.original}" for command "${leafCommand.#getCommandPath()}"`, leafCommand.#getCommandPath());
|
|
437
|
+
}
|
|
438
|
+
argTokens.push(token);
|
|
439
|
+
}
|
|
440
|
+
return { consumedTokens, argTokens };
|
|
441
|
+
}
|
|
442
|
+
#shift(tokens, shadowed, includeVersion) {
|
|
443
|
+
const allOptions = this.#getMergedOptions(includeVersion);
|
|
444
|
+
const effectiveOptions = allOptions.filter(o => !shadowed.has(o.long));
|
|
445
|
+
const optionByLong = new Map();
|
|
446
|
+
const optionByShort = new Map();
|
|
447
|
+
for (const opt of effectiveOptions) {
|
|
448
|
+
optionByLong.set(opt.long, opt);
|
|
449
|
+
if (opt.short) {
|
|
450
|
+
optionByShort.set(opt.short, opt);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const consumed = [];
|
|
454
|
+
const remaining = [];
|
|
455
|
+
let i = 0;
|
|
456
|
+
while (i < tokens.length) {
|
|
457
|
+
const token = tokens[i];
|
|
458
|
+
if (token.type === 'long') {
|
|
459
|
+
const opt = optionByLong.get(token.name);
|
|
460
|
+
if (opt) {
|
|
461
|
+
consumed.push(token);
|
|
462
|
+
if (opt.args === 'required') {
|
|
463
|
+
if (!token.resolved.includes('=') && i + 1 < tokens.length) {
|
|
464
|
+
i += 1;
|
|
465
|
+
consumed.push(tokens[i]);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
else if (opt.args === 'variadic') {
|
|
469
|
+
if (!token.resolved.includes('=')) {
|
|
470
|
+
while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
471
|
+
i += 1;
|
|
472
|
+
consumed.push(tokens[i]);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
i += 1;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
remaining.push(token);
|
|
480
|
+
i += 1;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (token.type === 'short') {
|
|
484
|
+
const opt = optionByShort.get(token.name);
|
|
485
|
+
if (opt) {
|
|
486
|
+
consumed.push(token);
|
|
487
|
+
if (opt.args === 'required') {
|
|
488
|
+
if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
489
|
+
i += 1;
|
|
490
|
+
consumed.push(tokens[i]);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
else if (opt.args === 'variadic') {
|
|
494
|
+
while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
495
|
+
i += 1;
|
|
496
|
+
consumed.push(tokens[i]);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
i += 1;
|
|
500
|
+
continue;
|
|
438
501
|
}
|
|
439
|
-
|
|
502
|
+
remaining.push(token);
|
|
503
|
+
i += 1;
|
|
504
|
+
continue;
|
|
440
505
|
}
|
|
441
|
-
|
|
506
|
+
remaining.push(token);
|
|
507
|
+
i += 1;
|
|
442
508
|
}
|
|
443
|
-
return {
|
|
509
|
+
return { consumed, remaining };
|
|
444
510
|
}
|
|
445
|
-
#
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
511
|
+
#parse(chain, resolveResult, ctx, restArgs) {
|
|
512
|
+
const { consumedTokens, argTokens } = resolveResult;
|
|
513
|
+
const leafCommand = chain[chain.length - 1];
|
|
514
|
+
this.#validateMergedShortOptions(chain);
|
|
515
|
+
const optsMap = new Map();
|
|
516
|
+
for (let i = 0; i < chain.length; i++) {
|
|
517
|
+
const cmd = chain[i];
|
|
518
|
+
const includeVersion = i === 0;
|
|
519
|
+
const tokens = consumedTokens.get(cmd) ?? [];
|
|
520
|
+
const opts = cmd.#parseOptions(tokens, includeVersion);
|
|
521
|
+
optsMap.set(cmd, opts);
|
|
522
|
+
for (const opt of cmd.#getMergedOptions(includeVersion)) {
|
|
449
523
|
if (opt.apply && opts[opt.long] !== undefined) {
|
|
450
524
|
opt.apply(opts[opt.long], ctx);
|
|
451
525
|
}
|
|
452
526
|
}
|
|
453
527
|
}
|
|
454
|
-
|
|
455
|
-
#mergeOpts(chain, optsMap) {
|
|
456
|
-
const merged = {};
|
|
528
|
+
const mergedOpts = {};
|
|
457
529
|
for (const cmd of chain) {
|
|
458
|
-
Object.assign(
|
|
530
|
+
Object.assign(mergedOpts, optsMap.get(cmd) ?? {});
|
|
459
531
|
}
|
|
460
|
-
|
|
532
|
+
const rawArgStrings = [...argTokens.map(t => t.original), ...restArgs];
|
|
533
|
+
const { args, rawArgs } = leafCommand.#parseArguments(rawArgStrings);
|
|
534
|
+
return { ctx, opts: mergedOpts, args, rawArgs };
|
|
461
535
|
}
|
|
462
|
-
#
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
536
|
+
#parseOptions(tokens, includeVersion) {
|
|
537
|
+
const allOptions = this.#getMergedOptions(includeVersion);
|
|
538
|
+
const opts = {};
|
|
539
|
+
for (const opt of allOptions) {
|
|
540
|
+
if (opt.default !== undefined) {
|
|
541
|
+
opts[opt.long] = opt.default;
|
|
542
|
+
}
|
|
543
|
+
else if (opt.type === 'boolean' && opt.args === 'none') {
|
|
544
|
+
opts[opt.long] = false;
|
|
545
|
+
}
|
|
546
|
+
else if (opt.args === 'variadic') {
|
|
547
|
+
opts[opt.long] = [];
|
|
548
|
+
}
|
|
467
549
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
550
|
+
const optionByLong = new Map();
|
|
551
|
+
const optionByShort = new Map();
|
|
552
|
+
for (const opt of allOptions) {
|
|
553
|
+
optionByLong.set(opt.long, opt);
|
|
554
|
+
if (opt.short) {
|
|
555
|
+
optionByShort.set(opt.short, opt);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
let i = 0;
|
|
559
|
+
while (i < tokens.length) {
|
|
560
|
+
const token = tokens[i];
|
|
561
|
+
const opt = token.type === 'long' ? optionByLong.get(token.name) : optionByShort.get(token.name);
|
|
562
|
+
if (!opt) {
|
|
563
|
+
i += 1;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const isNegativeToken = token.original.toLowerCase().startsWith('--no-');
|
|
567
|
+
if (isNegativeToken && !(opt.type === 'boolean' && opt.args === 'none')) {
|
|
568
|
+
throw new CommanderError('NegativeOptionType', `"--no-${camelToKebabCase$1(opt.long)}" can only be used with boolean options`, this.#getCommandPath());
|
|
569
|
+
}
|
|
570
|
+
if (opt.type === 'boolean' && opt.args === 'none') {
|
|
571
|
+
const eqIdx = token.resolved.indexOf('=');
|
|
572
|
+
if (eqIdx !== -1) {
|
|
573
|
+
const value = token.resolved.slice(eqIdx + 1);
|
|
574
|
+
if (value === 'true') {
|
|
575
|
+
opts[opt.long] = true;
|
|
479
576
|
}
|
|
480
|
-
|
|
481
|
-
|
|
577
|
+
else if (value === 'false') {
|
|
578
|
+
opts[opt.long] = false;
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
throw new CommanderError('InvalidBooleanValue', `invalid value "${value}" for boolean option "--${camelToKebabCase$1(opt.long)}". Use "true" or "false"`, this.#getCommandPath());
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
opts[opt.long] = true;
|
|
482
586
|
}
|
|
587
|
+
i += 1;
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (opt.args === 'required') {
|
|
591
|
+
const eqIdx = token.resolved.indexOf('=');
|
|
592
|
+
let rawValue;
|
|
593
|
+
if (eqIdx !== -1) {
|
|
594
|
+
rawValue = token.resolved.slice(eqIdx + 1);
|
|
595
|
+
}
|
|
596
|
+
else if (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
597
|
+
rawValue = tokens[i + 1].original;
|
|
598
|
+
i += 1;
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
throw new CommanderError('MissingValue', `option "--${camelToKebabCase$1(opt.long)}" requires a value`, this.#getCommandPath());
|
|
602
|
+
}
|
|
603
|
+
opts[opt.long] = this.#convertValue(opt, rawValue);
|
|
604
|
+
i += 1;
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
if (opt.args === 'variadic') {
|
|
608
|
+
const values = Array.isArray(opts[opt.long]) ? opts[opt.long] : [];
|
|
609
|
+
const eqIdx = token.resolved.indexOf('=');
|
|
610
|
+
if (eqIdx !== -1) {
|
|
611
|
+
values.push(this.#convertValue(opt, token.resolved.slice(eqIdx + 1)));
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
while (i + 1 < tokens.length && tokens[i + 1].type === 'none') {
|
|
615
|
+
i += 1;
|
|
616
|
+
values.push(this.#convertValue(opt, tokens[i].original));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
opts[opt.long] = values;
|
|
620
|
+
i += 1;
|
|
621
|
+
continue;
|
|
483
622
|
}
|
|
623
|
+
i += 1;
|
|
484
624
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
625
|
+
for (const opt of allOptions) {
|
|
626
|
+
if (opt.required && opts[opt.long] === undefined) {
|
|
627
|
+
throw new CommanderError('MissingRequired', `missing required option "--${camelToKebabCase$1(opt.long)}"`, this.#getCommandPath());
|
|
628
|
+
}
|
|
489
629
|
}
|
|
490
|
-
|
|
491
|
-
opts[opt.long]
|
|
630
|
+
for (const opt of allOptions) {
|
|
631
|
+
if (opt.choices && opts[opt.long] !== undefined) {
|
|
632
|
+
const value = opts[opt.long];
|
|
633
|
+
const values = Array.isArray(value) ? value : [value];
|
|
634
|
+
const choices = opt.choices;
|
|
635
|
+
for (const v of values) {
|
|
636
|
+
if (!choices.includes(v)) {
|
|
637
|
+
throw new CommanderError('InvalidChoice', `invalid value "${v}" for option "--${camelToKebabCase$1(opt.long)}". Allowed: ${opt.choices.join(', ')}`, this.#getCommandPath());
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return opts;
|
|
643
|
+
}
|
|
644
|
+
#convertValue(opt, rawValue) {
|
|
645
|
+
if (opt.coerce) {
|
|
646
|
+
return opt.coerce(rawValue);
|
|
647
|
+
}
|
|
648
|
+
if (opt.type === 'number') {
|
|
649
|
+
const num = Number(rawValue);
|
|
650
|
+
if (Number.isNaN(num)) {
|
|
651
|
+
throw new CommanderError('InvalidType', `invalid number "${rawValue}" for option "--${camelToKebabCase$1(opt.long)}"`, this.#getCommandPath());
|
|
652
|
+
}
|
|
653
|
+
return num;
|
|
654
|
+
}
|
|
655
|
+
return rawValue;
|
|
656
|
+
}
|
|
657
|
+
#parseArguments(rawArgs) {
|
|
658
|
+
const argumentDefs = this.#arguments;
|
|
659
|
+
const args = {};
|
|
660
|
+
const requiredCount = argumentDefs.filter(a => a.kind === 'required').length;
|
|
661
|
+
if (rawArgs.length < requiredCount) {
|
|
662
|
+
const missing = argumentDefs
|
|
663
|
+
.filter(a => a.kind === 'required')
|
|
664
|
+
.slice(rawArgs.length)
|
|
665
|
+
.map(a => a.name);
|
|
666
|
+
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
|
|
667
|
+
}
|
|
668
|
+
let index = 0;
|
|
669
|
+
for (const def of argumentDefs) {
|
|
670
|
+
if (def.kind === 'variadic') {
|
|
671
|
+
const rest = rawArgs.slice(index);
|
|
672
|
+
args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
|
|
673
|
+
index = rawArgs.length;
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
const raw = rawArgs[index];
|
|
677
|
+
if (raw === undefined) {
|
|
678
|
+
if (def.kind === 'optional') {
|
|
679
|
+
args[def.name] = def.default ?? undefined;
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
args[def.name] = this.#convertArgument(def, raw);
|
|
685
|
+
index += 1;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
const hasVariadic = argumentDefs.some(a => a.kind === 'variadic');
|
|
689
|
+
if (!hasVariadic && index < rawArgs.length) {
|
|
690
|
+
throw new CommanderError('TooManyArguments', `too many arguments: expected ${argumentDefs.length}, got ${rawArgs.length}`, this.#getCommandPath());
|
|
691
|
+
}
|
|
692
|
+
return { args, rawArgs };
|
|
693
|
+
}
|
|
694
|
+
#convertArgument(def, raw) {
|
|
695
|
+
if (def.coerce) {
|
|
696
|
+
try {
|
|
697
|
+
return def.coerce(raw);
|
|
698
|
+
}
|
|
699
|
+
catch {
|
|
700
|
+
throw new CommanderError('InvalidType', `invalid value "${raw}" for argument "${def.name}"`, this.#getCommandPath());
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (def.type === 'number') {
|
|
704
|
+
const n = Number(raw);
|
|
705
|
+
if (Number.isNaN(n)) {
|
|
706
|
+
throw new CommanderError('InvalidType', `invalid number "${raw}" for argument "${def.name}"`, this.#getCommandPath());
|
|
707
|
+
}
|
|
708
|
+
return n;
|
|
492
709
|
}
|
|
710
|
+
return raw;
|
|
493
711
|
}
|
|
494
712
|
#getMergedOptions(includeVersion = !this.#parent) {
|
|
495
713
|
const optionMap = new Map();
|
|
@@ -506,11 +724,11 @@ class Command {
|
|
|
506
724
|
}
|
|
507
725
|
return Array.from(optionMap.values());
|
|
508
726
|
}
|
|
509
|
-
#validateMergedShortOptions(chain
|
|
727
|
+
#validateMergedShortOptions(chain) {
|
|
510
728
|
const mergedByLong = new Map();
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const includeVersion =
|
|
729
|
+
for (let i = 0; i < chain.length; i++) {
|
|
730
|
+
const cmd = chain[i];
|
|
731
|
+
const includeVersion = i === 0;
|
|
514
732
|
for (const opt of cmd.#getMergedOptions(includeVersion)) {
|
|
515
733
|
mergedByLong.set(opt.long, opt);
|
|
516
734
|
}
|
|
@@ -527,8 +745,17 @@ class Command {
|
|
|
527
745
|
}
|
|
528
746
|
}
|
|
529
747
|
#validateOptionConfig(opt) {
|
|
530
|
-
if (opt.
|
|
531
|
-
throw new CommanderError('ConfigurationError', `option
|
|
748
|
+
if (opt.type === 'boolean' && opt.args !== 'none') {
|
|
749
|
+
throw new CommanderError('ConfigurationError', `boolean option "--${opt.long}" must have args: 'none'`, this.#getCommandPath());
|
|
750
|
+
}
|
|
751
|
+
if ((opt.type === 'string' || opt.type === 'number') && opt.args === 'none') {
|
|
752
|
+
throw new CommanderError('ConfigurationError', `${opt.type} option "--${opt.long}" must have args: 'required' or 'variadic'`, this.#getCommandPath());
|
|
753
|
+
}
|
|
754
|
+
if (opt.long.startsWith('no')) {
|
|
755
|
+
throw new CommanderError('ConfigurationError', `option long name cannot start with "no": "${opt.long}"`, this.#getCommandPath());
|
|
756
|
+
}
|
|
757
|
+
if (!/^[a-z][a-zA-Z0-9]*$/.test(opt.long)) {
|
|
758
|
+
throw new CommanderError('ConfigurationError', `option long name must be camelCase: "${opt.long}"`, this.#getCommandPath());
|
|
532
759
|
}
|
|
533
760
|
if (opt.required && opt.default !== undefined) {
|
|
534
761
|
throw new CommanderError('ConfigurationError', `option "--${opt.long}" cannot be both required and have a default value`, this.#getCommandPath());
|
|
@@ -567,154 +794,33 @@ class Command {
|
|
|
567
794
|
}
|
|
568
795
|
}
|
|
569
796
|
}
|
|
570
|
-
#
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const missing = argumentDefs
|
|
576
|
-
.filter(a => a.kind === 'required')
|
|
577
|
-
.slice(rawArgs.length)
|
|
578
|
-
.map(a => a.name);
|
|
579
|
-
throw new CommanderError('MissingRequiredArgument', `missing required argument(s): ${missing.join(', ')}`, this.#getCommandPath());
|
|
797
|
+
async #runAction(params) {
|
|
798
|
+
if (!this.#action)
|
|
799
|
+
return;
|
|
800
|
+
try {
|
|
801
|
+
await this.#action(params);
|
|
580
802
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const rest = rawArgs.slice(index);
|
|
585
|
-
args[def.name] = rest.map(raw => this.#convertArgument(def, raw));
|
|
586
|
-
index = rawArgs.length;
|
|
587
|
-
break;
|
|
588
|
-
}
|
|
589
|
-
const raw = rawArgs[index];
|
|
590
|
-
if (raw === undefined) {
|
|
591
|
-
if (def.kind === 'optional') {
|
|
592
|
-
args[def.name] = def.default ?? undefined;
|
|
593
|
-
continue;
|
|
594
|
-
}
|
|
803
|
+
catch (err) {
|
|
804
|
+
if (err instanceof Error) {
|
|
805
|
+
console.error(`Error: ${err.message}`);
|
|
595
806
|
}
|
|
596
807
|
else {
|
|
597
|
-
|
|
598
|
-
index += 1;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
const hasVariadic = argumentDefs.some(a => a.kind === 'variadic');
|
|
602
|
-
if (!hasVariadic && index < rawArgs.length) {
|
|
603
|
-
throw new CommanderError('TooManyArguments', `too many arguments: expected ${argumentDefs.length}, got ${rawArgs.length}`, this.#getCommandPath());
|
|
604
|
-
}
|
|
605
|
-
return { args, rawArgs };
|
|
606
|
-
}
|
|
607
|
-
#convertArgument(def, raw) {
|
|
608
|
-
if (def.coerce) {
|
|
609
|
-
try {
|
|
610
|
-
return def.coerce(raw);
|
|
611
|
-
}
|
|
612
|
-
catch {
|
|
613
|
-
throw new CommanderError('InvalidType', `invalid value "${raw}" for argument "${def.name}"`, this.#getCommandPath());
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
if (def.type === 'number') {
|
|
617
|
-
const n = Number(raw);
|
|
618
|
-
if (Number.isNaN(n)) {
|
|
619
|
-
throw new CommanderError('InvalidType', `invalid number "${raw}" for argument "${def.name}"`, this.#getCommandPath());
|
|
620
|
-
}
|
|
621
|
-
return n;
|
|
622
|
-
}
|
|
623
|
-
return raw;
|
|
624
|
-
}
|
|
625
|
-
#buildOptionMaps(allOptions, excludeResolver = false) {
|
|
626
|
-
const optionByLong = new Map();
|
|
627
|
-
const optionByShort = new Map();
|
|
628
|
-
const booleanOptions = new Set();
|
|
629
|
-
for (const opt of allOptions) {
|
|
630
|
-
if (excludeResolver && opt.resolver)
|
|
631
|
-
continue;
|
|
632
|
-
optionByLong.set(opt.long, opt);
|
|
633
|
-
if (opt.short) {
|
|
634
|
-
optionByShort.set(opt.short, opt);
|
|
635
|
-
}
|
|
636
|
-
if (opt.type === 'boolean') {
|
|
637
|
-
booleanOptions.add(opt.long);
|
|
808
|
+
console.error('Error: action failed');
|
|
638
809
|
}
|
|
810
|
+
process.exit(1);
|
|
639
811
|
}
|
|
640
|
-
return { optionByLong, optionByShort, booleanOptions };
|
|
641
812
|
}
|
|
642
|
-
#
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
#hasVersionFlag(argv, allOptions) {
|
|
646
|
-
return this.#hasBuiltinFlag(argv, 'version', 'V', allOptions);
|
|
647
|
-
}
|
|
648
|
-
#hasBuiltinFlag(argv, flagLong, flagShort, allOptions) {
|
|
649
|
-
const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(allOptions);
|
|
650
|
-
const normalizedArgv = this.#normalizeArgv(argv, booleanOptions);
|
|
651
|
-
for (let i = 0; i < normalizedArgv.length; i++) {
|
|
652
|
-
const arg = normalizedArgv[i];
|
|
653
|
-
if (arg === '--') {
|
|
654
|
-
break;
|
|
655
|
-
}
|
|
656
|
-
if (arg === `--${flagLong}` || (flagShort && arg === `-${flagShort}`)) {
|
|
813
|
+
#hasFlag(tokens, longName, shortName) {
|
|
814
|
+
for (const token of tokens) {
|
|
815
|
+
if (token.type === 'long' && token.name === longName) {
|
|
657
816
|
return true;
|
|
658
817
|
}
|
|
659
|
-
if (
|
|
660
|
-
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
return false;
|
|
664
|
-
}
|
|
665
|
-
#optionConsumesNextValue(arg, optionByLong, optionByShort) {
|
|
666
|
-
if (arg.startsWith('--')) {
|
|
667
|
-
const eqIdx = arg.indexOf('=');
|
|
668
|
-
if (eqIdx !== -1) {
|
|
669
|
-
return false;
|
|
670
|
-
}
|
|
671
|
-
const optName = arg.slice(2);
|
|
672
|
-
const opt = optionByLong.get(optName);
|
|
673
|
-
if (!opt) {
|
|
674
|
-
return false;
|
|
675
|
-
}
|
|
676
|
-
const type = opt.type ?? 'string';
|
|
677
|
-
return type !== 'boolean';
|
|
678
|
-
}
|
|
679
|
-
if (arg.startsWith('-') && arg.length === 2) {
|
|
680
|
-
const opt = optionByShort.get(arg[1]);
|
|
681
|
-
if (!opt) {
|
|
682
|
-
return false;
|
|
818
|
+
if (token.type === 'short' && token.name === shortName) {
|
|
819
|
+
return true;
|
|
683
820
|
}
|
|
684
|
-
const type = opt.type ?? 'string';
|
|
685
|
-
return type !== 'boolean';
|
|
686
821
|
}
|
|
687
822
|
return false;
|
|
688
823
|
}
|
|
689
|
-
#normalizeArgv(argv, booleanOptions) {
|
|
690
|
-
const result = [];
|
|
691
|
-
let seenDoubleDash = false;
|
|
692
|
-
for (const arg of argv) {
|
|
693
|
-
if (arg === '--') {
|
|
694
|
-
seenDoubleDash = true;
|
|
695
|
-
result.push(arg);
|
|
696
|
-
continue;
|
|
697
|
-
}
|
|
698
|
-
if (!seenDoubleDash && arg.startsWith('--no-')) {
|
|
699
|
-
const eqIdx = arg.indexOf('=');
|
|
700
|
-
if (eqIdx !== -1) {
|
|
701
|
-
const optName = arg.slice(5, eqIdx);
|
|
702
|
-
if (booleanOptions.has(optName)) {
|
|
703
|
-
throw new CommanderError('InvalidBooleanValue', `"--no-${optName}" does not accept a value`, this.#getCommandPath());
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
else {
|
|
707
|
-
const optName = arg.slice(5);
|
|
708
|
-
if (booleanOptions.has(optName)) {
|
|
709
|
-
result.push(`--${optName}=false`);
|
|
710
|
-
continue;
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
result.push(arg);
|
|
715
|
-
}
|
|
716
|
-
return result;
|
|
717
|
-
}
|
|
718
824
|
#getCommandPath() {
|
|
719
825
|
const parts = [];
|
|
720
826
|
let current = this;
|
|
@@ -726,130 +832,41 @@ class Command {
|
|
|
726
832
|
}
|
|
727
833
|
return parts.join(' ') || this.#name;
|
|
728
834
|
}
|
|
729
|
-
#tryConsumeLongOption(tokens, idx, optionByLong, opts) {
|
|
730
|
-
const token = tokens[idx];
|
|
731
|
-
const eqIdx = token.indexOf('=');
|
|
732
|
-
let optName;
|
|
733
|
-
let inlineValue;
|
|
734
|
-
if (eqIdx !== -1) {
|
|
735
|
-
optName = token.slice(2, eqIdx);
|
|
736
|
-
inlineValue = token.slice(eqIdx + 1);
|
|
737
|
-
}
|
|
738
|
-
else {
|
|
739
|
-
optName = token.slice(2);
|
|
740
|
-
}
|
|
741
|
-
const opt = optionByLong.get(optName);
|
|
742
|
-
if (!opt) {
|
|
743
|
-
return 0;
|
|
744
|
-
}
|
|
745
|
-
if (opt.type === 'boolean') {
|
|
746
|
-
if (inlineValue !== undefined) {
|
|
747
|
-
if (inlineValue === 'true') {
|
|
748
|
-
opts[optName] = true;
|
|
749
|
-
}
|
|
750
|
-
else if (inlineValue === 'false') {
|
|
751
|
-
opts[optName] = false;
|
|
752
|
-
}
|
|
753
|
-
else {
|
|
754
|
-
throw new CommanderError('InvalidBooleanValue', `invalid value "${inlineValue}" for boolean option "--${optName}". Use "true" or "false"`, this.#getCommandPath());
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
else {
|
|
758
|
-
opts[optName] = true;
|
|
759
|
-
}
|
|
760
|
-
return 1;
|
|
761
|
-
}
|
|
762
|
-
let value;
|
|
763
|
-
let consumed = 1;
|
|
764
|
-
if (inlineValue !== undefined) {
|
|
765
|
-
value = inlineValue;
|
|
766
|
-
}
|
|
767
|
-
else if (idx + 1 < tokens.length) {
|
|
768
|
-
value = tokens[idx + 1];
|
|
769
|
-
consumed = 2;
|
|
770
|
-
}
|
|
771
|
-
else {
|
|
772
|
-
throw new CommanderError('MissingValue', `option "--${optName}" requires a value`, this.#getCommandPath());
|
|
773
|
-
}
|
|
774
|
-
this.#applyValue(opt, value, opts);
|
|
775
|
-
return consumed;
|
|
776
|
-
}
|
|
777
|
-
#tryConsumeShortOption(tokens, idx, optionByShort, opts) {
|
|
778
|
-
const token = tokens[idx];
|
|
779
|
-
if (token.includes('=')) {
|
|
780
|
-
const firstFlag = token[1];
|
|
781
|
-
if (!optionByShort.has(firstFlag)) {
|
|
782
|
-
return { consumed: false, nextIdx: idx + 1 };
|
|
783
|
-
}
|
|
784
|
-
throw new CommanderError('UnsupportedShortSyntax', `"-${token.slice(1)}" is not supported. Use "-${token[1]} ${token.slice(3)}" instead`, this.#getCommandPath());
|
|
785
|
-
}
|
|
786
|
-
const flags = token.slice(1);
|
|
787
|
-
let j = 0;
|
|
788
|
-
const consumedFlags = [];
|
|
789
|
-
const unconsumedFlags = [];
|
|
790
|
-
let nextIdx = idx + 1;
|
|
791
|
-
while (j < flags.length) {
|
|
792
|
-
const flag = flags[j];
|
|
793
|
-
const opt = optionByShort.get(flag);
|
|
794
|
-
if (!opt) {
|
|
795
|
-
unconsumedFlags.push(...flags.slice(j).split(''));
|
|
796
|
-
break;
|
|
797
|
-
}
|
|
798
|
-
consumedFlags.push(flag);
|
|
799
|
-
if (opt.type === 'boolean') {
|
|
800
|
-
opts[opt.long] = true;
|
|
801
|
-
j += 1;
|
|
802
|
-
continue;
|
|
803
|
-
}
|
|
804
|
-
if (j < flags.length - 1) {
|
|
805
|
-
throw new CommanderError('UnsupportedShortSyntax', `"-${flags}" is not supported. Use "-${flags.slice(0, j + 1)} ${flags.slice(j + 1)}" or separate options`, this.#getCommandPath());
|
|
806
|
-
}
|
|
807
|
-
if (idx + 1 < tokens.length && !tokens[idx + 1].startsWith('-')) {
|
|
808
|
-
const value = tokens[idx + 1];
|
|
809
|
-
this.#applyValue(opt, value, opts);
|
|
810
|
-
nextIdx = idx + 2;
|
|
811
|
-
}
|
|
812
|
-
else {
|
|
813
|
-
throw new CommanderError('MissingValue', `option "-${flag}" requires a value`, this.#getCommandPath());
|
|
814
|
-
}
|
|
815
|
-
j += 1;
|
|
816
|
-
}
|
|
817
|
-
if (consumedFlags.length > 0) {
|
|
818
|
-
const remainingToken = unconsumedFlags.length > 0 ? `-${unconsumedFlags.join('')}` : undefined;
|
|
819
|
-
return { consumed: true, nextIdx, remainingToken };
|
|
820
|
-
}
|
|
821
|
-
return { consumed: false, nextIdx: idx + 1 };
|
|
822
|
-
}
|
|
823
835
|
}
|
|
824
836
|
|
|
837
|
+
function camelToKebabCase(str) {
|
|
838
|
+
return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
839
|
+
}
|
|
825
840
|
class CompletionCommand extends Command {
|
|
826
841
|
constructor(root, config) {
|
|
827
842
|
const paths = config.paths;
|
|
828
|
-
const programName = config.programName ?? root.name;
|
|
829
|
-
super({
|
|
830
|
-
description: 'Generate shell completion script',
|
|
831
|
-
});
|
|
843
|
+
const programName = config.programName ?? root.name ?? 'program';
|
|
844
|
+
super({ desc: 'Generate shell completion script' });
|
|
832
845
|
this.option({
|
|
833
846
|
long: 'bash',
|
|
834
847
|
type: 'boolean',
|
|
835
|
-
|
|
848
|
+
args: 'none',
|
|
849
|
+
desc: 'Generate Bash completion script',
|
|
836
850
|
})
|
|
837
851
|
.option({
|
|
838
852
|
long: 'fish',
|
|
839
853
|
type: 'boolean',
|
|
840
|
-
|
|
854
|
+
args: 'none',
|
|
855
|
+
desc: 'Generate Fish completion script',
|
|
841
856
|
})
|
|
842
857
|
.option({
|
|
843
858
|
long: 'pwsh',
|
|
844
859
|
type: 'boolean',
|
|
845
|
-
|
|
860
|
+
args: 'none',
|
|
861
|
+
desc: 'Generate PowerShell completion script',
|
|
846
862
|
})
|
|
847
863
|
.option({
|
|
848
864
|
long: 'write',
|
|
849
865
|
short: 'w',
|
|
850
866
|
type: 'string',
|
|
851
|
-
|
|
852
|
-
|
|
867
|
+
args: 'required',
|
|
868
|
+
desc: 'Write to file (use shell default path if empty)',
|
|
869
|
+
default: undefined,
|
|
853
870
|
})
|
|
854
871
|
.action(({ opts }) => {
|
|
855
872
|
const meta = root.getCompletionMeta();
|
|
@@ -905,45 +922,6 @@ function expandHome(filepath) {
|
|
|
905
922
|
}
|
|
906
923
|
return filepath;
|
|
907
924
|
}
|
|
908
|
-
function resolveOptionalStringOption(argv, longName, shortName) {
|
|
909
|
-
const remaining = [];
|
|
910
|
-
let value;
|
|
911
|
-
for (let i = 0; i < argv.length; i++) {
|
|
912
|
-
const arg = argv[i];
|
|
913
|
-
if (arg.startsWith(`--${longName}=`)) {
|
|
914
|
-
value = arg.slice(`--${longName}=`.length);
|
|
915
|
-
continue;
|
|
916
|
-
}
|
|
917
|
-
if (arg === `--${longName}`) {
|
|
918
|
-
const next = argv[i + 1];
|
|
919
|
-
if (next !== undefined && !next.startsWith('-')) {
|
|
920
|
-
value = next;
|
|
921
|
-
i += 1;
|
|
922
|
-
}
|
|
923
|
-
else {
|
|
924
|
-
value = '';
|
|
925
|
-
}
|
|
926
|
-
continue;
|
|
927
|
-
}
|
|
928
|
-
if (arg.startsWith(`-${shortName}=`)) {
|
|
929
|
-
value = arg.slice(`-${shortName}=`.length);
|
|
930
|
-
continue;
|
|
931
|
-
}
|
|
932
|
-
if (arg === `-${shortName}`) {
|
|
933
|
-
const next = argv[i + 1];
|
|
934
|
-
if (next !== undefined && !next.startsWith('-')) {
|
|
935
|
-
value = next;
|
|
936
|
-
i += 1;
|
|
937
|
-
}
|
|
938
|
-
else {
|
|
939
|
-
value = '';
|
|
940
|
-
}
|
|
941
|
-
continue;
|
|
942
|
-
}
|
|
943
|
-
remaining.push(arg);
|
|
944
|
-
}
|
|
945
|
-
return { value, remaining };
|
|
946
|
-
}
|
|
947
925
|
class BashCompletion {
|
|
948
926
|
#meta;
|
|
949
927
|
#programName;
|
|
@@ -976,11 +954,12 @@ class BashCompletion {
|
|
|
976
954
|
const lines = [];
|
|
977
955
|
const optParts = [];
|
|
978
956
|
for (const opt of cmd.options) {
|
|
957
|
+
const kebabLong = camelToKebabCase(opt.long);
|
|
979
958
|
if (opt.short)
|
|
980
959
|
optParts.push(`-${opt.short}`);
|
|
981
|
-
optParts.push(`--${
|
|
960
|
+
optParts.push(`--${kebabLong}`);
|
|
982
961
|
if (!opt.takesValue) {
|
|
983
|
-
optParts.push(`--no-${
|
|
962
|
+
optParts.push(`--no-${kebabLong}`);
|
|
984
963
|
}
|
|
985
964
|
}
|
|
986
965
|
const subParts = cmd.subcommands.flatMap(sub => [sub.name, ...sub.aliases]);
|
|
@@ -1029,13 +1008,14 @@ class FishCompletion {
|
|
|
1029
1008
|
const isRoot = parentPath.length === 0;
|
|
1030
1009
|
const condition = this.#buildCondition(parentPath);
|
|
1031
1010
|
for (const opt of cmd.options) {
|
|
1011
|
+
const kebabLong = camelToKebabCase(opt.long);
|
|
1032
1012
|
let line = `complete -c ${this.#programName}`;
|
|
1033
1013
|
if (condition)
|
|
1034
1014
|
line += ` -n '${condition}'`;
|
|
1035
1015
|
if (opt.short)
|
|
1036
1016
|
line += ` -s ${opt.short}`;
|
|
1037
|
-
line += ` -l ${
|
|
1038
|
-
line += ` -d '${this.#escape(opt.
|
|
1017
|
+
line += ` -l ${kebabLong}`;
|
|
1018
|
+
line += ` -d '${this.#escape(opt.desc)}'`;
|
|
1039
1019
|
if (opt.choices && opt.choices.length > 0) {
|
|
1040
1020
|
line += ` -xa '${opt.choices.join(' ')}'`;
|
|
1041
1021
|
}
|
|
@@ -1044,8 +1024,8 @@ class FishCompletion {
|
|
|
1044
1024
|
let noLine = `complete -c ${this.#programName}`;
|
|
1045
1025
|
if (condition)
|
|
1046
1026
|
noLine += ` -n '${condition}'`;
|
|
1047
|
-
noLine += ` -l no-${
|
|
1048
|
-
noLine += ` -d '${this.#escape(opt.
|
|
1027
|
+
noLine += ` -l no-${kebabLong}`;
|
|
1028
|
+
noLine += ` -d '${this.#escape(opt.desc)}'`;
|
|
1049
1029
|
lines.push(noLine);
|
|
1050
1030
|
}
|
|
1051
1031
|
}
|
|
@@ -1058,7 +1038,7 @@ class FishCompletion {
|
|
|
1058
1038
|
line += ` -n '${condition}; and not __fish_seen_subcommand_from ${this.#getSubcommandNames(cmd).join(' ')}'`;
|
|
1059
1039
|
}
|
|
1060
1040
|
line += ` -a ${sub.name}`;
|
|
1061
|
-
line += ` -d '${this.#escape(sub.
|
|
1041
|
+
line += ` -d '${this.#escape(sub.desc)}'`;
|
|
1062
1042
|
lines.push(line);
|
|
1063
1043
|
for (const alias of sub.aliases) {
|
|
1064
1044
|
let aliasLine = `complete -c ${this.#programName}`;
|
|
@@ -1131,7 +1111,7 @@ class PwshCompletion {
|
|
|
1131
1111
|
' "--$($opt.long)",',
|
|
1132
1112
|
' $opt.long,',
|
|
1133
1113
|
' "ParameterName",',
|
|
1134
|
-
' $opt.
|
|
1114
|
+
' $opt.desc',
|
|
1135
1115
|
' )',
|
|
1136
1116
|
' }',
|
|
1137
1117
|
' if ($opt.isBoolean -and "--no-$($opt.long)" -like "$current*") {',
|
|
@@ -1139,7 +1119,7 @@ class PwshCompletion {
|
|
|
1139
1119
|
' "--no-$($opt.long)",',
|
|
1140
1120
|
' "no-$($opt.long)",',
|
|
1141
1121
|
' "ParameterName",',
|
|
1142
|
-
' $opt.
|
|
1122
|
+
' $opt.desc',
|
|
1143
1123
|
' )',
|
|
1144
1124
|
' }',
|
|
1145
1125
|
' if ($opt.short -and "-$($opt.short)" -like "$current*") {',
|
|
@@ -1147,7 +1127,7 @@ class PwshCompletion {
|
|
|
1147
1127
|
' "-$($opt.short)",',
|
|
1148
1128
|
' $opt.short,',
|
|
1149
1129
|
' "ParameterName",',
|
|
1150
|
-
' $opt.
|
|
1130
|
+
' $opt.desc',
|
|
1151
1131
|
' )',
|
|
1152
1132
|
' }',
|
|
1153
1133
|
' }',
|
|
@@ -1161,7 +1141,7 @@ class PwshCompletion {
|
|
|
1161
1141
|
' $sub,',
|
|
1162
1142
|
' $sub,',
|
|
1163
1143
|
' "Command",',
|
|
1164
|
-
' $cmd.subcommands[$sub].
|
|
1144
|
+
' $cmd.subcommands[$sub].desc',
|
|
1165
1145
|
' )',
|
|
1166
1146
|
' }',
|
|
1167
1147
|
' }',
|
|
@@ -1175,14 +1155,15 @@ class PwshCompletion {
|
|
|
1175
1155
|
}
|
|
1176
1156
|
#generateCommandHash(cmd, indent) {
|
|
1177
1157
|
const lines = [];
|
|
1178
|
-
lines.push(`${indent}description = '${this.#escape(cmd.
|
|
1158
|
+
lines.push(`${indent}description = '${this.#escape(cmd.desc)}'`);
|
|
1179
1159
|
lines.push(`${indent}options = @(`);
|
|
1180
1160
|
for (const opt of cmd.options) {
|
|
1161
|
+
const kebabLong = camelToKebabCase(opt.long);
|
|
1181
1162
|
lines.push(`${indent} @{`);
|
|
1182
1163
|
if (opt.short)
|
|
1183
1164
|
lines.push(`${indent} short = '${opt.short}'`);
|
|
1184
|
-
lines.push(`${indent} long = '${
|
|
1185
|
-
lines.push(`${indent} description = '${this.#escape(opt.
|
|
1165
|
+
lines.push(`${indent} long = '${kebabLong}'`);
|
|
1166
|
+
lines.push(`${indent} description = '${this.#escape(opt.desc)}'`);
|
|
1186
1167
|
lines.push(`${indent} isBoolean = $${!opt.takesValue}`);
|
|
1187
1168
|
if (opt.choices) {
|
|
1188
1169
|
lines.push(`${indent} choices = @('${opt.choices.join("', '")}')`);
|