UNPKG

8.1 kBPlain TextView Raw
1import { EventEmitter } from "https://deno.land/std@0.80.0/node/events.ts";
2import mri from "https://cdn.skypack.dev/mri";
3import Command, { GlobalCommand, CommandConfig, HelpCallback, CommandExample } from "./Command.ts";
4import { OptionConfig } from "./Option.ts";
5import { getMriOptions, setDotProp, setByType, getFileName, camelcaseOptionName } from "./utils.ts";
6import { processArgs } from "./deno.ts";
7interface ParsedArgv {
8 args: ReadonlyArray<string>;
9 options: {
10 [k: string]: any;
11 };
12}
13
14class CAC extends EventEmitter {
15 /** The program name to display in help and version message */
16 name: string;
17 commands: Command[];
18 globalCommand: GlobalCommand;
19 matchedCommand?: Command;
20 matchedCommandName?: string;
21 /**
22 * Raw CLI arguments
23 */
24
25 rawArgs: string[];
26 /**
27 * Parsed CLI arguments
28 */
29
30 args: ParsedArgv['args'];
31 /**
32 * Parsed CLI options, camelCased
33 */
34
35 options: ParsedArgv['options'];
36 showHelpOnExit?: boolean;
37 showVersionOnExit?: boolean;
38 /**
39 * @param name The program name to display in help and version message
40 */
41
42 constructor(name = '') {
43 super();
44 this.name = name;
45 this.commands = [];
46 this.rawArgs = [];
47 this.args = [];
48 this.options = {};
49 this.globalCommand = new GlobalCommand(this);
50 this.globalCommand.usage('<command> [options]');
51 }
52 /**
53 * Add a global usage text.
54 *
55 * This is not used by sub-commands.
56 */
57
58
59 usage(text: string) {
60 this.globalCommand.usage(text);
61 return this;
62 }
63 /**
64 * Add a sub-command
65 */
66
67
68 command(rawName: string, description?: string, config?: CommandConfig) {
69 const command = new Command(rawName, description || '', config, this);
70 command.globalCommand = this.globalCommand;
71 this.commands.push(command);
72 return command;
73 }
74 /**
75 * Add a global CLI option.
76 *
77 * Which is also applied to sub-commands.
78 */
79
80
81 option(rawName: string, description: string, config?: OptionConfig) {
82 this.globalCommand.option(rawName, description, config);
83 return this;
84 }
85 /**
86 * Show help message when `-h, --help` flags appear.
87 *
88 */
89
90
91 help(callback?: HelpCallback) {
92 this.globalCommand.option('-h, --help', 'Display this message');
93 this.globalCommand.helpCallback = callback;
94 this.showHelpOnExit = true;
95 return this;
96 }
97 /**
98 * Show version number when `-v, --version` flags appear.
99 *
100 */
101
102
103 version(version: string, customFlags = '-v, --version') {
104 this.globalCommand.version(version, customFlags);
105 this.showVersionOnExit = true;
106 return this;
107 }
108 /**
109 * Add a global example.
110 *
111 * This example added here will not be used by sub-commands.
112 */
113
114
115 example(example: CommandExample) {
116 this.globalCommand.example(example);
117 return this;
118 }
119 /**
120 * Output the corresponding help message
121 * When a sub-command is matched, output the help message for the command
122 * Otherwise output the global one.
123 *
124 */
125
126
127 outputHelp() {
128 if (this.matchedCommand) {
129 this.matchedCommand.outputHelp();
130 } else {
131 this.globalCommand.outputHelp();
132 }
133 }
134 /**
135 * Output the version number.
136 *
137 */
138
139
140 outputVersion() {
141 this.globalCommand.outputVersion();
142 }
143
144 private setParsedInfo({
145 args,
146 options
147 }: ParsedArgv, matchedCommand?: Command, matchedCommandName?: string) {
148 this.args = args;
149 this.options = options;
150
151 if (matchedCommand) {
152 this.matchedCommand = matchedCommand;
153 }
154
155 if (matchedCommandName) {
156 this.matchedCommandName = matchedCommandName;
157 }
158
159 return this;
160 }
161
162 unsetMatchedCommand() {
163 this.matchedCommand = undefined;
164 this.matchedCommandName = undefined;
165 }
166 /**
167 * Parse argv
168 */
169
170
171 parse(argv = processArgs, {
172 /** Whether to run the action for matched command */
173 run = true
174 } = {}): ParsedArgv {
175 this.rawArgs = argv;
176
177 if (!this.name) {
178 this.name = argv[1] ? getFileName(argv[1]) : 'cli';
179 }
180
181 let shouldParse = true; // Search sub-commands
182
183 for (const command of this.commands) {
184 const parsed = this.mri(argv.slice(2), command);
185 const commandName = parsed.args[0];
186
187 if (command.isMatched(commandName)) {
188 shouldParse = false;
189 const parsedInfo = { ...parsed,
190 args: parsed.args.slice(1)
191 };
192 this.setParsedInfo(parsedInfo, command, commandName);
193 this.emit(`command:${commandName}`, command);
194 }
195 }
196
197 if (shouldParse) {
198 // Search the default command
199 for (const command of this.commands) {
200 if (command.name === '') {
201 shouldParse = false;
202 const parsed = this.mri(argv.slice(2), command);
203 this.setParsedInfo(parsed, command);
204 this.emit(`command:!`, command);
205 }
206 }
207 }
208
209 if (shouldParse) {
210 const parsed = this.mri(argv.slice(2));
211 this.setParsedInfo(parsed);
212 }
213
214 if (this.options.help && this.showHelpOnExit) {
215 this.outputHelp();
216 run = false;
217 this.unsetMatchedCommand();
218 }
219
220 if (this.options.version && this.showVersionOnExit) {
221 this.outputVersion();
222 run = false;
223 this.unsetMatchedCommand();
224 }
225
226 const parsedArgv = {
227 args: this.args,
228 options: this.options
229 };
230
231 if (run) {
232 this.runMatchedCommand();
233 }
234
235 if (!this.matchedCommand && this.args[0]) {
236 this.emit('command:*');
237 }
238
239 return parsedArgv;
240 }
241
242 private mri(argv: string[],
243 /** Matched command */
244 command?: Command): ParsedArgv {
245 // All added options
246 const cliOptions = [...this.globalCommand.options, ...(command ? command.options : [])];
247 const mriOptions = getMriOptions(cliOptions); // Extract everything after `--` since mri doesn't support it
248
249 let argsAfterDoubleDashes: string[] = [];
250 const doubleDashesIndex = argv.indexOf('--');
251
252 if (doubleDashesIndex > -1) {
253 argsAfterDoubleDashes = argv.slice(doubleDashesIndex + 1);
254 argv = argv.slice(0, doubleDashesIndex);
255 }
256
257 let parsed = mri(argv, mriOptions);
258 parsed = Object.keys(parsed).reduce((res, name) => {
259 return { ...res,
260 [camelcaseOptionName(name)]: parsed[name]
261 };
262 }, {
263 _: []
264 });
265 const args = parsed._;
266 const options: {
267 [k: string]: any;
268 } = {
269 '--': argsAfterDoubleDashes
270 }; // Set option default value
271
272 const ignoreDefault = command && command.config.ignoreOptionDefaultValue ? command.config.ignoreOptionDefaultValue : this.globalCommand.config.ignoreOptionDefaultValue;
273 let transforms = Object.create(null);
274
275 for (const cliOption of cliOptions) {
276 if (!ignoreDefault && cliOption.config.default !== undefined) {
277 for (const name of cliOption.names) {
278 options[name] = cliOption.config.default;
279 }
280 } // If options type is defined
281
282
283 if (Array.isArray(cliOption.config.type)) {
284 if (transforms[cliOption.name] === undefined) {
285 transforms[cliOption.name] = Object.create(null);
286 transforms[cliOption.name]['shouldTransform'] = true;
287 transforms[cliOption.name]['transformFunction'] = cliOption.config.type[0];
288 }
289 }
290 } // Set option values (support dot-nested property name)
291
292
293 for (const key of Object.keys(parsed)) {
294 if (key !== '_') {
295 const keys = key.split('.');
296 setDotProp(options, keys, parsed[key]);
297 setByType(options, transforms);
298 }
299 }
300
301 return {
302 args,
303 options
304 };
305 }
306
307 runMatchedCommand() {
308 const {
309 args,
310 options,
311 matchedCommand: command
312 } = this;
313 if (!command || !command.commandAction) return;
314 command.checkUnknownOptions();
315 command.checkOptionValue();
316 command.checkRequiredArgs();
317 const actionArgs: any[] = [];
318 command.args.forEach((arg, index) => {
319 if (arg.variadic) {
320 actionArgs.push(args.slice(index));
321 } else {
322 actionArgs.push(args[index]);
323 }
324 });
325 actionArgs.push(options);
326 return command.commandAction.apply(this, actionArgs);
327 }
328
329}
330
331export default CAC;
\No newline at end of file