UNPKG

6.86 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 const options = this.isGlobalCommand ? globalOptions : [...this.options, ...(globalOptions || [])];
155
156 if (options.length > 0) {
157 const longestOptionName = findLongest(options.map(option => option.rawName));
158 sections.push({
159 title: 'Options',
160 body: options.map(option => {
161 return ` ${padRight(option.rawName, longestOptionName.length)} ${option.description} ${option.config.default === undefined ? '' : `(default: ${option.config.default})`}`;
162 }).join('\n')
163 });
164 }
165
166 if (this.examples.length > 0) {
167 sections.push({
168 title: 'Examples',
169 body: this.examples.map(example => {
170 if (typeof example === 'function') {
171 return example(name);
172 }
173
174 return example;
175 }).join('\n')
176 });
177 }
178
179 if (helpCallback) {
180 sections = helpCallback(sections) || sections;
181 }
182
183 console.log(sections.map(section => {
184 return section.title ? `${section.title}:\n${section.body}` : section.body;
185 }).join('\n\n'));
186 }
187
188 outputVersion() {
189 const {
190 name
191 } = this.cli;
192 const {
193 versionNumber
194 } = this.cli.globalCommand;
195
196 if (versionNumber) {
197 console.log(`${name}/${versionNumber} ${platformInfo}`);
198 }
199 }
200
201 checkRequiredArgs() {
202 const minimalArgsCount = this.args.filter(arg => arg.required).length;
203
204 if (this.cli.args.length < minimalArgsCount) {
205 throw new CACError(`missing required args for command \`${this.rawName}\``);
206 }
207 }
208 /**
209 * Check if the parsed options contain any unknown options
210 *
211 * Exit and output error when true
212 */
213
214
215 checkUnknownOptions() {
216 const {
217 options,
218 globalCommand
219 } = this.cli;
220
221 if (!this.config.allowUnknownOptions) {
222 for (const name of Object.keys(options)) {
223 if (name !== '--' && !this.hasOption(name) && !globalCommand.hasOption(name)) {
224 throw new CACError(`Unknown option \`${name.length > 1 ? `--${name}` : `-${name}`}\``);
225 }
226 }
227 }
228 }
229 /**
230 * Check if the required string-type options exist
231 */
232
233
234 checkOptionValue() {
235 const {
236 options: parsedOptions,
237 globalCommand
238 } = this.cli;
239 const options = [...globalCommand.options, ...this.options];
240
241 for (const option of options) {
242 const value = parsedOptions[option.name.split('.')[0]]; // Check required option value
243
244 if (option.required) {
245 const hasNegated = options.some(o => o.negated && o.names.includes(option.name));
246
247 if (value === true || value === false && !hasNegated) {
248 throw new CACError(`option \`${option.rawName}\` value is missing`);
249 }
250 }
251 }
252 }
253
254}
255
256class GlobalCommand extends Command {
257 constructor(cli: CAC) {
258 super('@@global@@', '', {}, cli);
259 }
260
261}
262
263export type { HelpCallback, CommandExample, CommandConfig };
264export { GlobalCommand };
265export default Command;
\No newline at end of file