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