UNPKG

6.99 kBPlain TextView Raw
1import CAC from "./CAC.ts";
2import Option, { OptionConfig } from "./Option.ts";
3import { removeBrackets, findAllBrackets, findLongest, padRight, CACError } from "./utils.ts";
4import { platformInfo } from "./deno.ts";
5interface CommandArg {
6 required: boolean;
7 value: string;
8 variadic: boolean;
9}
10interface HelpSection {
11 title?: string;
12 body: string;
13}
14interface CommandConfig {
15 allowUnknownOptions?: boolean;
16 ignoreOptionDefaultValue?: boolean;
17}
18type HelpCallback = (sections: HelpSection[]) => void | HelpSection[];
19type CommandExample = ((bin: string) => string) | string;
20
21class Command {
22 options: Option[];
23 aliasNames: string[];
24 /* Parsed command name */
25
26 name: string;
27 args: CommandArg[];
28 commandAction?: (...args: any[]) => any;
29 usageText?: string;
30 versionNumber?: string;
31 examples: CommandExample[];
32 helpCallback?: HelpCallback;
33 globalCommand?: GlobalCommand;
34
35 constructor(public rawName: string, public description: string, public config: CommandConfig = {}, public cli: CAC) {
36 this.options = [];
37 this.aliasNames = [];
38 this.name = removeBrackets(rawName);
39 this.args = findAllBrackets(rawName);
40 this.examples = [];
41 }
42
43 usage(text: string) {
44 this.usageText = text;
45 return this;
46 }
47
48 allowUnknownOptions() {
49 this.config.allowUnknownOptions = true;
50 return this;
51 }
52
53 ignoreOptionDefaultValue() {
54 this.config.ignoreOptionDefaultValue = true;
55 return this;
56 }
57
58 version(version: string, customFlags = '-v, --version') {
59 this.versionNumber = version;
60 this.option(customFlags, 'Display version number');
61 return this;
62 }
63
64 example(example: CommandExample) {
65 this.examples.push(example);
66 return this;
67 }
68 /**
69 * Add a option for this command
70 * @param rawName Raw option name(s)
71 * @param description Option description
72 * @param config Option config
73 */
74
75
76 option(rawName: string, description: string, config?: OptionConfig) {
77 const option = new Option(rawName, description, config);
78 this.options.push(option);
79 return this;
80 }
81
82 alias(name: string) {
83 this.aliasNames.push(name);
84 return this;
85 }
86
87 action(callback: (...args: any[]) => any) {
88 this.commandAction = callback;
89 return this;
90 }
91 /**
92 * Check if a command name is matched by this command
93 * @param name Command name
94 */
95
96
97 isMatched(name: string) {
98 return this.name === name || this.aliasNames.includes(name);
99 }
100
101 get isDefaultCommand() {
102 return this.name === '' || this.aliasNames.includes('!');
103 }
104
105 get isGlobalCommand(): boolean {
106 return this instanceof GlobalCommand;
107 }
108 /**
109 * Check if an option is registered in this command
110 * @param name Option name
111 */
112
113
114 hasOption(name: string) {
115 name = name.split('.')[0];
116 return this.options.find(option => {
117 return option.names.includes(name);
118 });
119 }
120
121 outputHelp() {
122 const {
123 name,
124 commands
125 } = this.cli;
126 const {
127 versionNumber,
128 options: globalOptions,
129 helpCallback
130 } = this.cli.globalCommand;
131 let sections: HelpSection[] = [{
132 body: `${name}${versionNumber ? `/${versionNumber}` : ''}`
133 }];
134 sections.push({
135 title: 'Usage',
136 body: ` $ ${name} ${this.usageText || this.rawName}`
137 });
138 const showCommands = (this.isGlobalCommand || this.isDefaultCommand) && commands.length > 0;
139
140 if (showCommands) {
141 const longestCommandName = findLongest(commands.map(command => command.rawName));
142 sections.push({
143 title: 'Commands',
144 body: commands.map(command => {
145 return ` ${padRight(command.rawName, longestCommandName.length)} ${command.description}`;
146 }).join('\n')
147 });
148 sections.push({
149 title: `For more info, run any command with the \`--help\` flag`,
150 body: commands.map(command => ` $ ${name}${command.name === '' ? '' : ` ${command.name}`} --help`).join('\n')
151 });
152 }
153
154 let options = this.isGlobalCommand ? globalOptions : [...this.options, ...(globalOptions || [])];
155
156 if (!this.isGlobalCommand && !this.isDefaultCommand) {
157 options = options.filter(option => option.name !== 'version');
158 }
159
160 if (options.length > 0) {
161 const longestOptionName = findLongest(options.map(option => option.rawName));
162 sections.push({
163 title: 'Options',
164 body: options.map(option => {
165 return ` ${padRight(option.rawName, longestOptionName.length)} ${option.description} ${option.config.default === undefined ? '' : `(default: ${option.config.default})`}`;
166 }).join('\n')
167 });
168 }
169
170 if (this.examples.length > 0) {
171 sections.push({
172 title: 'Examples',
173 body: this.examples.map(example => {
174 if (typeof example === 'function') {
175 return example(name);
176 }
177
178 return example;
179 }).join('\n')
180 });
181 }
182
183 if (helpCallback) {
184 sections = helpCallback(sections) || sections;
185 }
186
187 console.log(sections.map(section => {
188 return section.title ? `${section.title}:\n${section.body}` : section.body;
189 }).join('\n\n'));
190 }
191
192 outputVersion() {
193 const {
194 name
195 } = this.cli;
196 const {
197 versionNumber
198 } = this.cli.globalCommand;
199
200 if (versionNumber) {
201 console.log(`${name}/${versionNumber} ${platformInfo}`);
202 }
203 }
204
205 checkRequiredArgs() {
206 const minimalArgsCount = this.args.filter(arg => arg.required).length;
207
208 if (this.cli.args.length < minimalArgsCount) {
209 throw new CACError(`missing required args for command \`${this.rawName}\``);
210 }
211 }
212 /**
213 * Check if the parsed options contain any unknown options
214 *
215 * Exit and output error when true
216 */
217
218
219 checkUnknownOptions() {
220 const {
221 options,
222 globalCommand
223 } = this.cli;
224
225 if (!this.config.allowUnknownOptions) {
226 for (const name of Object.keys(options)) {
227 if (name !== '--' && !this.hasOption(name) && !globalCommand.hasOption(name)) {
228 throw new CACError(`Unknown option \`${name.length > 1 ? `--${name}` : `-${name}`}\``);
229 }
230 }
231 }
232 }
233 /**
234 * Check if the required string-type options exist
235 */
236
237
238 checkOptionValue() {
239 const {
240 options: parsedOptions,
241 globalCommand
242 } = this.cli;
243 const options = [...globalCommand.options, ...this.options];
244
245 for (const option of options) {
246 const value = parsedOptions[option.name.split('.')[0]]; // Check required option value
247
248 if (option.required) {
249 const hasNegated = options.some(o => o.negated && o.names.includes(option.name));
250
251 if (value === true || value === false && !hasNegated) {
252 throw new CACError(`option \`${option.rawName}\` value is missing`);
253 }
254 }
255 }
256 }
257
258}
259
260class GlobalCommand extends Command {
261 constructor(cli: CAC) {
262 super('@@global@@', '', {}, cli);
263 }
264
265}
266
267export type { HelpCallback, CommandExample, CommandConfig };
268export { GlobalCommand };
269export default Command;
\No newline at end of file