1 | import CAC from "./CAC.ts";
|
2 | import Option, { OptionConfig } from "./Option.ts";
|
3 | import { removeBrackets, findAllBrackets, findLongest, padRight, CACError } from "./utils.ts";
|
4 | import { platformInfo } from "./deno.ts";
|
5 | interface CommandArg {
|
6 | required: boolean;
|
7 | value: string;
|
8 | variadic: boolean;
|
9 | }
|
10 | interface HelpSection {
|
11 | title?: string;
|
12 | body: string;
|
13 | }
|
14 | interface CommandConfig {
|
15 | allowUnknownOptions?: boolean;
|
16 | ignoreOptionDefaultValue?: boolean;
|
17 | }
|
18 | type HelpCallback = (sections: HelpSection[]) => void | HelpSection[];
|
19 | type CommandExample = ((bin: string) => string) | string;
|
20 |
|
21 | class 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 |
|
70 |
|
71 |
|
72 |
|
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 |
|
93 |
|
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 |
|
110 |
|
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 |
|
214 |
|
215 |
|
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 |
|
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]];
|
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 |
|
260 | class GlobalCommand extends Command {
|
261 | constructor(cli: CAC) {
|
262 | super('@@global@@', '', {}, cli);
|
263 | }
|
264 |
|
265 | }
|
266 |
|
267 | export type { HelpCallback, CommandExample, CommandConfig };
|
268 | export { GlobalCommand };
|
269 | export default Command; |
\ | No newline at end of file |