UNPKG

15.3 kBJavaScriptView Raw
1"use strict";
2var __importDefault = (this && this.__importDefault) || function (mod) {
3 return (mod && mod.__esModule) ? mod : { "default": mod };
4};
5var __importStar = (this && this.__importStar) || function (mod) {
6 if (mod && mod.__esModule) return mod;
7 var result = {};
8 if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
9 result["default"] = mod;
10 return result;
11};
12Object.defineProperty(exports, "__esModule", { value: true });
13const option_1 = __importDefault(require("./option"));
14const argument_1 = __importDefault(require("./argument"));
15const error_1 = require("./error");
16const utils = __importStar(require("./utils"));
17// jsdoc, see constructor.
18class Command {
19 /**
20 * class of command.
21 * ```
22 * cmd -v sub --path foo/bar buzz.txt
23 * ↑ this one!
24 * ```
25 * @param name name and flags pass flags pass 'foo'(sub command) or 'foo <bar>'(sub command & required argument) or 'foo [bar]'(sub command & optional argument) or 'foo <bar...>'(sub command & required variadic argument) or 'foo [bar...]'(sub command & optional variadic argument).
26 * @class
27 */
28 constructor(name) {
29 /**
30 * @private
31 */
32 this._help = new option_1.default("-h, --help", "display help");
33 /**
34 * e.g. bar
35 * @private
36 */
37 this._rest = [];
38 /**
39 * e.g.
40 * ```
41 * git -p clone git@github.com:vvakame/dtsm.git
42 * ↑ this!
43 * ```
44 * @type {Array}
45 */
46 this.options = [];
47 /**
48 * e.g.
49 * ```
50 * git -p clone git@github.com:vvakame/dtsm.git
51 * ↑ this!
52 * ```
53 * @type {Array}
54 */
55 this.subCommands = [];
56 /**
57 * parsed option values.
58 * @type {any}
59 */
60 this.parsedOpts = {};
61 /**
62 * parsed option arguments.
63 * @type {any}
64 */
65 this.parsedArgs = {};
66 /**
67 * unknown options.
68 * @type {Array}
69 */
70 this.unknownOptions = [];
71 let args = name.split(/\s+/);
72 this.name = args.shift();
73 let findOptional = false;
74 let findVariadic = false;
75 this.args = args.map(argStr => {
76 if (findVariadic) {
77 throw new error_1.CommandpostError({
78 parts: [argStr],
79 message: "parameter can not placed after variadic parameter",
80 reason: error_1.ErrorReason.ParameterCantPlacedAfterVariadic,
81 });
82 }
83 let arg = new argument_1.default(argStr);
84 if (arg.required && findOptional) {
85 throw new error_1.CommandpostError({
86 parts: [argStr],
87 message: "parameter can not placed after variadic parameter",
88 reason: error_1.ErrorReason.ParameterCannPlacedAfterOptional,
89 });
90 }
91 if (!arg.required) {
92 findOptional = true;
93 }
94 if (arg.variadic) {
95 findVariadic = true;
96 }
97 return arg;
98 });
99 this._action = () => {
100 process.stdout.write(this.helpText() + "\n");
101 };
102 }
103 /**
104 * set description for this command.
105 * @param desc
106 * @returns {Command}
107 * @method
108 */
109 description(desc) {
110 this._description = desc;
111 return this;
112 }
113 /**
114 * set usage for this command.
115 * @param usage
116 * @returns {Command}
117 * @method
118 */
119 usage(usage) {
120 this._usage = usage;
121 return this;
122 }
123 /**
124 * add option for this command.
125 * see {@link Option}.
126 * @param flags
127 * @param description
128 * @param defaultValue
129 * @returns {Command}
130 */
131 option(flags, description, defaultValue) {
132 let option = new option_1.default(flags, description, defaultValue);
133 this.options.push(option);
134 return this;
135 }
136 /**
137 * allow unknown option.
138 * by default, An error occured if unknown option is included.
139 * @param flag
140 * @returns {Command}
141 */
142 allowUnknownOption(flag = true) {
143 this._allowUnknownOption = flag;
144 return this;
145 }
146 /**
147 * add action at this command selected.
148 * @param fn
149 * @returns {Command}
150 */
151 action(fn) {
152 this._action = fn;
153 return this;
154 }
155 /**
156 * create sub command.
157 * @param name
158 * @returns {Command<Opt2, Arg2>} new command instance
159 */
160 subCommand(name) {
161 let command = new Command(name);
162 command.parent = this;
163 this.subCommands.push(command);
164 return command;
165 }
166 /**
167 * check arg is matches this command.
168 * @param arg
169 * @returns {boolean}
170 */
171 is(arg) {
172 return this.name === arg;
173 }
174 /**
175 * add help this option.
176 * in general case, use default help option.
177 * @param flags
178 * @param description
179 * @returns {Command}
180 */
181 help(flags, description) {
182 this._help = new option_1.default(flags, description);
183 return this;
184 }
185 /**
186 * add show version option to this command.
187 * @param version
188 * @param flags
189 * @param description
190 * @returns {Command}
191 */
192 version(version, flags, description = "output the version number") {
193 this._version = new option_1.default(flags, description);
194 this._versionStr = version;
195 return this;
196 }
197 /**
198 * exec action of command.
199 * this method MUST call after parse process.
200 * @returns {Promise<{}>}
201 */
202 exec() {
203 return Promise.resolve(this._action(this.parsedOpts, this.parsedArgs, this._rest));
204 }
205 /**
206 * parse argv.
207 * @param argv
208 * @returns {Promise<{}>}
209 */
210 parse(argv) {
211 return Promise
212 .resolve(null)
213 .then(() => {
214 let rest = this._parseRawArgs(argv);
215 // resolve help action
216 if (this._args.some(arg => this._help.is(arg))) {
217 // include help option. (help for this command
218 process.stdout.write(this.helpText() + "\n");
219 process.exit(0);
220 return Promise.resolve({});
221 }
222 let subCommand;
223 if (this.parent == null) {
224 // only for top level (why? because I can't decide which is natural syntax between `foo help bar buzz` and `foo bar help buzz`.
225 if (this._rest.some(arg => this._help.name() === arg)) {
226 // include help sub command. (help for deeper level sub command
227 if (rest[0]) {
228 subCommand = this.subCommands.filter(cmd => cmd.is(rest[0]))[0];
229 if (subCommand) {
230 process.stdout.write(subCommand.helpText() + "\n");
231 process.exit(0);
232 return Promise.resolve({});
233 }
234 }
235 // TODO raise error? pass through?
236 }
237 }
238 // resolve version option
239 if (this._version && this._args.some(arg => this._version.is(arg))) {
240 process.stdout.write((this._versionStr || "unknown") + "\n");
241 process.exit(0);
242 return Promise.resolve({});
243 }
244 if (rest[0]) {
245 subCommand = this.subCommands.filter(cmd => cmd.is(rest[0]))[0];
246 if (subCommand) {
247 return subCommand.parse(rest.slice(1));
248 }
249 }
250 return this.exec();
251 });
252 }
253 /**
254 * @returns {*}
255 * @private
256 */
257 _getAncestorsAndMe() {
258 if (!this.parent) {
259 return [this];
260 }
261 else {
262 return this.parent._getAncestorsAndMe().concat([this]);
263 }
264 }
265 /**
266 * @param args
267 * @returns {string[]}
268 * @private
269 */
270 _parseRawArgs(args) {
271 args = args.slice(0);
272 let target = [];
273 let rest = [];
274 for (let i = 0; i < args.length; i++) {
275 let arg = args[i];
276 if (arg === "--") {
277 // Honor option terminator
278 target = target.concat(args.slice(i));
279 break;
280 }
281 let cmd = this.subCommands.filter(cmd => cmd.is(arg))[0];
282 if (cmd) {
283 rest = args.slice(i);
284 break;
285 }
286 target.push(arg);
287 }
288 this._rawArgs = target.slice(0);
289 this._args = this._normalize(target);
290 this._rest = this._parseOptions(this._args);
291 let cmds = this._getAncestorsAndMe();
292 let allowUnknownOption = cmds.reverse().map(cmd => cmd._allowUnknownOption).filter(allowUnknownOption => typeof allowUnknownOption !== "undefined")[0];
293 if (this.unknownOptions.length !== 0 && !allowUnknownOption) {
294 let errMsg = "unknown option";
295 errMsg += this.unknownOptions.length === 1 ? " " : "s ";
296 errMsg += this.unknownOptions.join(", ") + "\n";
297 errMsg += this.helpText();
298 throw new error_1.CommandpostError({
299 message: errMsg,
300 reason: error_1.ErrorReason.UnknownOption,
301 parts: this.unknownOptions,
302 params: {
303 origin: this,
304 args,
305 },
306 });
307 }
308 if (this._matchSubCommand(rest)) {
309 return rest;
310 }
311 this._rest = this._parseArgs(this._rest);
312 return rest;
313 }
314 /**
315 * @param rest
316 * @returns {boolean}
317 * @private
318 */
319 _matchSubCommand(rest) {
320 if (rest == null || !rest[0]) {
321 return false;
322 }
323 let subCommand = this.subCommands.filter(cmd => cmd.is(rest[0]))[0];
324 return !!subCommand;
325 }
326 /**
327 * @param args
328 * @returns {string[]}
329 * @private
330 */
331 _parseOptions(args) {
332 args = args.slice(0);
333 let rest = [];
334 let processedOptions = [];
335 while (args.length !== 0) {
336 let arg = args.shift();
337 if (arg === "--") {
338 rest = rest.concat(args);
339 break;
340 }
341 let opt = this.options.filter(opt => opt.is(arg))[0];
342 if (!opt) {
343 rest.push(arg);
344 if (arg.indexOf("-") === 0 && !this._help.is(arg) && (!this._version || !this._version.is(arg))) {
345 this.unknownOptions.push(arg);
346 }
347 continue;
348 }
349 args = opt.parse(this.parsedOpts, [arg].concat(args));
350 processedOptions.push(opt);
351 }
352 this.options
353 .filter(opt => processedOptions.indexOf(opt) === -1)
354 .forEach(opt => {
355 let optName = utils.kebabToLowerCamelCase(opt.name());
356 if (opt.required || opt.optional) {
357 // string[]
358 this.parsedOpts[optName] = this.parsedOpts[optName] || [];
359 if (opt.defaultValue) {
360 this.parsedOpts[optName].push(opt.defaultValue);
361 }
362 }
363 else {
364 this.parsedOpts[optName] = opt.defaultValue;
365 }
366 });
367 return rest;
368 }
369 /**
370 * @param rest
371 * @returns {string[]}
372 * @private
373 */
374 _parseArgs(rest) {
375 rest = rest.slice(0);
376 this.args.forEach(argInfo => {
377 rest = argInfo.parse(this.parsedArgs, rest);
378 });
379 return rest;
380 }
381 /**
382 * @param args
383 * @returns {string[]}
384 * @private
385 */
386 _normalize(args) {
387 let result = [];
388 for (let i = 0; i < args.length; i++) {
389 let arg = args[i];
390 let lastOpt;
391 if (0 < i) {
392 lastOpt = this.options.filter(opt => opt.is(args[i - 1]))[0];
393 }
394 if (arg === "--") {
395 // Honor option terminator
396 result = result.concat(args.slice(i));
397 break;
398 }
399 else if (lastOpt && lastOpt.required) {
400 result.push(arg);
401 }
402 else if (/^-[^-]/.test(arg)) {
403 // expand combined short hand option. "-abc" to "-a -b -c"
404 arg.slice(1).split("").forEach(c => result.push("-" + c));
405 }
406 else if (/^--/.test(arg) && arg.indexOf("=") !== -1) {
407 result.push(arg.slice(0, arg.indexOf("=")), arg.slice(arg.indexOf("=") + 1));
408 }
409 else {
410 result.push(arg);
411 }
412 }
413 return result;
414 }
415 /**
416 * generate help text.
417 * @returns {string}
418 */
419 helpText() {
420 let result = "";
421 if (this._description) {
422 result += this._description + "\n\n";
423 }
424 // usage part
425 result += " Usage: ";
426 if (this._usage != null) {
427 result += this._usage;
428 }
429 else {
430 result += this._getAncestorsAndMe().map(cmd => cmd.name).join(" ") + " ";
431 if (this.options.length !== 0) {
432 result += "[options] ";
433 }
434 if (this.subCommands.length !== 0) {
435 result += "[command] ";
436 }
437 if (this.args.length !== 0) {
438 result += "[--] ";
439 result += this.args.map(arg => {
440 if (arg.required) {
441 return "<" + arg.name + (arg.variadic ? "..." : "") + ">";
442 }
443 else {
444 return "[" + arg.name + (arg.variadic ? "..." : "") + "]";
445 }
446 }).join(" ");
447 }
448 }
449 result += "\n\n";
450 // options part
451 if (this.options.length !== 0) {
452 result += " Options:\n\n";
453 let optionsMaxLength = utils.maxLength(this.options.map(opt => opt.flags));
454 result += this.options.map(opt => {
455 let result = " ";
456 result += utils.pad(opt.flags, optionsMaxLength);
457 result += " ";
458 result += opt.description || "";
459 result += "\n";
460 return result;
461 }).join("");
462 result += "\n\n";
463 }
464 // sub commands part
465 if (this.subCommands.length !== 0) {
466 result += " Commands:\n\n";
467 let subCommandsMaxLength = utils.maxLength(this.subCommands.map(cmd => cmd.name));
468 result += this.subCommands.map(cmd => {
469 let result = " ";
470 result += utils.pad(cmd.name, subCommandsMaxLength);
471 result += " ";
472 result += cmd._description || "";
473 result += "\n";
474 return result;
475 }).join("");
476 result += "\n\n";
477 }
478 return result;
479 }
480}
481exports.default = Command;
482//# sourceMappingURL=command.js.map
\No newline at end of file