1 | module cli |
2 | |
3 | import term |
4 | |
5 | type FnCommandCallback = fn (cmd Command) ! |
6 | |
7 | // str returns the `string` representation of the callback. |
8 | pub fn (f FnCommandCallback) str() string { |
9 | return 'FnCommandCallback=>' + ptr_str(f) |
10 | } |
11 | |
12 | // Command is a structured representation of a single command |
13 | // or chain of commands. |
14 | pub struct Command { |
15 | pub mut: |
16 | name string |
17 | usage string |
18 | description string |
19 | man_description string |
20 | version string |
21 | pre_execute FnCommandCallback = unsafe { nil } |
22 | execute FnCommandCallback = unsafe { nil } |
23 | post_execute FnCommandCallback = unsafe { nil } |
24 | disable_help bool |
25 | disable_man bool |
26 | disable_version bool |
27 | disable_flags bool |
28 | sort_flags bool |
29 | sort_commands bool |
30 | parent &Command = unsafe { nil } |
31 | commands []Command |
32 | flags []Flag |
33 | required_args int |
34 | args []string |
35 | posix_mode bool |
36 | } |
37 | |
38 | // str returns the `string` representation of the `Command`. |
39 | pub fn (cmd Command) str() string { |
40 | mut res := []string{} |
41 | res << 'Command{' |
42 | res << ' name: "${cmd.name}"' |
43 | res << ' usage: "${cmd.usage}"' |
44 | res << ' version: "${cmd.version}"' |
45 | res << ' description: "${cmd.description}"' |
46 | res << ' man_description: "${cmd.man_description}"' |
47 | res << ' disable_help: ${cmd.disable_help}' |
48 | res << ' disable_man: ${cmd.disable_man}' |
49 | res << ' disable_flags: ${cmd.disable_flags}' |
50 | res << ' disable_version: ${cmd.disable_version}' |
51 | res << ' sort_flags: ${cmd.sort_flags}' |
52 | res << ' sort_commands: ${cmd.sort_commands}' |
53 | res << ' cb execute: ${cmd.execute}' |
54 | res << ' cb pre_execute: ${cmd.pre_execute}' |
55 | res << ' cb post_execute: ${cmd.post_execute}' |
56 | if unsafe { cmd.parent == 0 } { |
57 | res << ' parent: &Command(0)' |
58 | } else { |
59 | res << ' parent: &Command{${cmd.parent.name} ...}' |
60 | } |
61 | res << ' commands: ${cmd.commands}' |
62 | res << ' flags: ${cmd.flags}' |
63 | res << ' required_args: ${cmd.required_args}' |
64 | res << ' args: ${cmd.args}' |
65 | res << '}' |
66 | return res.join('\n') |
67 | } |
68 | |
69 | // is_root returns `true` if this `Command` has no parents. |
70 | pub fn (cmd Command) is_root() bool { |
71 | return isnil(cmd.parent) |
72 | } |
73 | |
74 | // root returns the root `Command` of the command chain. |
75 | pub fn (cmd Command) root() Command { |
76 | if cmd.is_root() { |
77 | return cmd |
78 | } |
79 | return cmd.parent.root() |
80 | } |
81 | |
82 | // full_name returns the full `string` representation of all commands int the chain. |
83 | pub fn (cmd Command) full_name() string { |
84 | if cmd.is_root() { |
85 | return cmd.name |
86 | } |
87 | return cmd.parent.full_name() + ' ${cmd.name}' |
88 | } |
89 | |
90 | // add_commands adds the `commands` array of `Command`s as sub-commands. |
91 | pub fn (mut cmd Command) add_commands(commands []Command) { |
92 | for command in commands { |
93 | cmd.add_command(command) |
94 | } |
95 | } |
96 | |
97 | // add_command adds `command` as a sub-command of this `Command`. |
98 | pub fn (mut cmd Command) add_command(command Command) { |
99 | mut subcmd := command |
100 | if cmd.commands.contains(subcmd.name) { |
101 | eprintln_exit('Command with the name `${subcmd.name}` already exists') |
102 | } |
103 | subcmd.parent = unsafe { cmd } |
104 | cmd.commands << subcmd |
105 | } |
106 | |
107 | // setup ensures that all sub-commands of this `Command` |
108 | // is linked as a chain. |
109 | pub fn (mut cmd Command) setup() { |
110 | for mut subcmd in cmd.commands { |
111 | subcmd.parent = unsafe { cmd } |
112 | subcmd.posix_mode = cmd.posix_mode |
113 | subcmd.setup() |
114 | } |
115 | } |
116 | |
117 | // add_flags adds the array `flags` to this `Command`. |
118 | pub fn (mut cmd Command) add_flags(flags []Flag) { |
119 | for flag in flags { |
120 | cmd.add_flag(flag) |
121 | } |
122 | } |
123 | |
124 | // add_flag adds `flag` to this `Command`. |
125 | pub fn (mut cmd Command) add_flag(flag Flag) { |
126 | if cmd.flags.contains(flag.name) { |
127 | eprintln_exit('Flag with the name `${flag.name}` already exists') |
128 | } |
129 | cmd.flags << flag |
130 | } |
131 | |
132 | // parse parses `args` into this structured `Command`. |
133 | pub fn (mut cmd Command) parse(args []string) { |
134 | if !cmd.disable_flags { |
135 | cmd.add_default_flags() |
136 | } |
137 | cmd.add_default_commands() |
138 | if cmd.sort_flags { |
139 | cmd.flags.sort(a.name < b.name) |
140 | } |
141 | if cmd.sort_commands { |
142 | cmd.commands.sort(a.name < b.name) |
143 | } |
144 | cmd.args = args[1..] |
145 | if !cmd.disable_flags { |
146 | cmd.parse_flags() |
147 | } |
148 | cmd.parse_commands() |
149 | } |
150 | |
151 | // add_default_flags adds the commonly used `-h`/`--help` and |
152 | // `-v`/`--version` flags to the `Command`. |
153 | fn (mut cmd Command) add_default_flags() { |
154 | if !cmd.disable_help && !cmd.flags.contains('help') { |
155 | use_help_abbrev := !cmd.flags.contains('h') && cmd.posix_mode |
156 | cmd.add_flag(help_flag(use_help_abbrev)) |
157 | } |
158 | if !cmd.disable_version && cmd.version != '' && !cmd.flags.contains('version') { |
159 | use_version_abbrev := !cmd.flags.contains('v') && cmd.posix_mode |
160 | cmd.add_flag(version_flag(use_version_abbrev)) |
161 | } |
162 | if !cmd.disable_man && !cmd.flags.contains('man') { |
163 | cmd.add_flag(man_flag()) |
164 | } |
165 | } |
166 | |
167 | // add_default_commands adds the command functions of the |
168 | // commonly used `help` and `version` flags to the `Command`. |
169 | fn (mut cmd Command) add_default_commands() { |
170 | if !cmd.disable_help && !cmd.commands.contains('help') && cmd.is_root() { |
171 | cmd.add_command(help_cmd()) |
172 | } |
173 | if !cmd.disable_version && cmd.version != '' && !cmd.commands.contains('version') { |
174 | cmd.add_command(version_cmd()) |
175 | } |
176 | if !cmd.disable_man && !cmd.commands.contains('man') && cmd.is_root() { |
177 | cmd.add_command(man_cmd()) |
178 | } |
179 | } |
180 | |
181 | fn (mut cmd Command) parse_flags() { |
182 | for { |
183 | if cmd.args.len < 1 || !cmd.args[0].starts_with('-') { |
184 | break |
185 | } |
186 | mut found := false |
187 | for i in 0 .. cmd.flags.len { |
188 | unsafe { |
189 | mut flag := &cmd.flags[i] |
190 | if flag.matches(cmd.args, cmd.posix_mode) { |
191 | found = true |
192 | flag.found = true |
193 | cmd.args = flag.parse(cmd.args, cmd.posix_mode) or { |
194 | eprintln_exit('Failed to parse flag `${cmd.args[0]}`: ${err}') |
195 | } |
196 | break |
197 | } |
198 | } |
199 | } |
200 | if !found { |
201 | eprintln_exit('Command `${cmd.name}` has no flag `${cmd.args[0]}`') |
202 | } |
203 | } |
204 | } |
205 | |
206 | fn (mut cmd Command) parse_commands() { |
207 | global_flags := cmd.flags.filter(it.global) |
208 | cmd.check_help_flag() |
209 | cmd.check_version_flag() |
210 | cmd.check_man_flag() |
211 | for i in 0 .. cmd.args.len { |
212 | arg := cmd.args[i] |
213 | for j in 0 .. cmd.commands.len { |
214 | mut command := cmd.commands[j] |
215 | if command.name == arg { |
216 | for flag in global_flags { |
217 | command.add_flag(flag) |
218 | } |
219 | command.parse(cmd.args[i..]) |
220 | return |
221 | } |
222 | } |
223 | } |
224 | if cmd.is_root() && isnil(cmd.execute) { |
225 | if !cmd.disable_help { |
226 | cmd.execute_help() |
227 | return |
228 | } |
229 | } |
230 | // if no further command was found, execute current command |
231 | if cmd.required_args > 0 { |
232 | if cmd.required_args > cmd.args.len { |
233 | eprintln_exit('Command `${cmd.name}` needs at least ${cmd.required_args} arguments') |
234 | } |
235 | } |
236 | cmd.check_required_flags() |
237 | |
238 | cmd.handle_cb(cmd.pre_execute, 'preexecution') |
239 | cmd.handle_cb(cmd.execute, 'execution') |
240 | cmd.handle_cb(cmd.post_execute, 'postexecution') |
241 | } |
242 | |
243 | fn (mut cmd Command) handle_cb(cb FnCommandCallback, label string) { |
244 | if !isnil(cb) { |
245 | cb(*cmd) or { |
246 | label_message := term.ecolorize(term.bright_red, 'cli ${label} error:') |
247 | eprintln_exit('${label_message} ${err}') |
248 | } |
249 | } |
250 | } |
251 | |
252 | fn (cmd Command) check_help_flag() { |
253 | if !cmd.disable_help && cmd.flags.contains('help') { |
254 | help_flag := cmd.flags.get_bool('help') or { return } // ignore error and handle command normally |
255 | if help_flag { |
256 | cmd.execute_help() |
257 | exit(0) |
258 | } |
259 | } |
260 | } |
261 | |
262 | fn (cmd Command) check_man_flag() { |
263 | if !cmd.disable_man && cmd.flags.contains('man') { |
264 | man_flag := cmd.flags.get_bool('man') or { return } // ignore error and handle command normally |
265 | if man_flag { |
266 | cmd.execute_man() |
267 | exit(0) |
268 | } |
269 | } |
270 | } |
271 | |
272 | fn (cmd Command) check_version_flag() { |
273 | if !cmd.disable_version && cmd.version != '' && cmd.flags.contains('version') { |
274 | version_flag := cmd.flags.get_bool('version') or { return } // ignore error and handle command normally |
275 | if version_flag { |
276 | version_cmd := cmd.commands.get('version') or { return } // ignore error and handle command normally |
277 | version_cmd.execute(version_cmd) or { panic(err) } |
278 | exit(0) |
279 | } |
280 | } |
281 | } |
282 | |
283 | fn (cmd Command) check_required_flags() { |
284 | for flag in cmd.flags { |
285 | if flag.required && flag.value.len == 0 { |
286 | full_name := cmd.full_name() |
287 | eprintln_exit('Flag `${flag.name}` is required by `${full_name}`') |
288 | } |
289 | } |
290 | } |
291 | |
292 | // execute_help executes the callback registered |
293 | // for the `-h`/`--help` flag option. |
294 | pub fn (cmd Command) execute_help() { |
295 | if cmd.commands.contains('help') { |
296 | help_cmd := cmd.commands.get('help') or { return } // ignore error and handle command normally |
297 | if !isnil(help_cmd.execute) { |
298 | help_cmd.execute(help_cmd) or { panic(err) } |
299 | return |
300 | } |
301 | } |
302 | print(cmd.help_message()) |
303 | } |
304 | |
305 | // execute_help executes the callback registered |
306 | // for the `-man` flag option. |
307 | pub fn (cmd Command) execute_man() { |
308 | if cmd.commands.contains('man') { |
309 | man_cmd := cmd.commands.get('man') or { return } |
310 | man_cmd.execute(man_cmd) or { panic(err) } |
311 | } else { |
312 | print(cmd.manpage()) |
313 | } |
314 | } |
315 | |
316 | fn (cmds []Command) get(name string) !Command { |
317 | for cmd in cmds { |
318 | if cmd.name == name { |
319 | return cmd |
320 | } |
321 | } |
322 | return error('Command `${name}` not found in ${cmds}') |
323 | } |
324 | |
325 | fn (cmds []Command) contains(name string) bool { |
326 | for cmd in cmds { |
327 | if cmd.name == name { |
328 | return true |
329 | } |
330 | } |
331 | return false |
332 | } |
333 | |
334 | [noreturn] |
335 | fn eprintln_exit(message string) { |
336 | eprintln(message) |
337 | exit(1) |
338 | } |