v2 / vlib / cli / command.v
434 lines · 397 sloc · 11.57 KB · 8e35f4d9848f7ad35d857a187dddbfd2eca5e19d
Raw
1module cli
2
3import term
4
5type FnCommandCallback = fn (cmd Command) !
6
7// str returns the `string` representation of the callback.
8pub fn (f FnCommandCallback) str() string {
9 return 'FnCommandCallback=>' + ptr_str(f)
10}
11
12// Command is a structured representation of a single command or chain of commands.
13pub struct Command {
14pub mut:
15 name string
16 alias 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_flags bool
25 sort_flags bool
26 sort_commands bool
27 parent &Command = unsafe { nil }
28 commands []Command
29 flags []Flag
30 required_args int
31 args []string
32 posix_mode bool
33 defaults struct {
34 pub:
35 help Defaults = true
36 man Defaults = true
37 version Defaults = true
38 mut:
39 parsed struct {
40 mut:
41 help CommandFlag
42 version CommandFlag
43 man CommandFlag
44 }
45 }
46}
47
48pub struct CommandFlag {
49pub mut:
50 command bool = true
51 flag bool = true
52}
53
54type Defaults = CommandFlag | bool
55
56// str returns the `string` representation of the `Command`.
57pub fn (cmd &Command) str() string {
58 mut res := []string{}
59 res << 'Command{'
60 res << ' name: "${cmd.name}"'
61 res << ' alias: "${cmd.alias}"'
62 res << ' usage: "${cmd.usage}"'
63 res << ' version: "${cmd.version}"'
64 res << ' description: "${cmd.description}"'
65 res << ' man_description: "${cmd.man_description}"'
66 res << ' disable_flags: ${cmd.disable_flags}'
67 res << ' sort_flags: ${cmd.sort_flags}'
68 res << ' sort_commands: ${cmd.sort_commands}'
69 res << ' cb execute: ${cmd.execute}'
70 res << ' cb pre_execute: ${cmd.pre_execute}'
71 res << ' cb post_execute: ${cmd.post_execute}'
72 if unsafe { cmd.parent == 0 } {
73 res << ' parent: &Command(0)'
74 } else {
75 res << ' parent: &Command{${cmd.parent.name} ...}'
76 }
77 res << ' commands: ${cmd.commands}'
78 res << ' flags: ${cmd.flags}'
79 res << ' required_args: ${cmd.required_args}'
80 res << ' args: ${cmd.args}'
81 res << ' posix_mode: ${cmd.posix_mode}'
82 match cmd.defaults.help {
83 bool {
84 res << ' defaults.help: ${cmd.defaults.help}'
85 }
86 CommandFlag {
87 res << ' defaults.help.command: ${cmd.defaults.help.command}'
88 res << ' defaults.help.flag: ${cmd.defaults.help.flag}'
89 }
90 }
91
92 match cmd.defaults.man {
93 bool {
94 res << ' defaults.man: ${cmd.defaults.man}'
95 }
96 CommandFlag {
97 res << ' defaults.man.command: ${cmd.defaults.man.command}'
98 res << ' defaults.man.flag: ${cmd.defaults.man.flag}'
99 }
100 }
101
102 match cmd.defaults.version {
103 bool {
104 res << ' defaults.version: ${cmd.defaults.version}'
105 }
106 CommandFlag {
107 res << ' defaults.version.command: ${cmd.defaults.version.command}'
108 res << ' defaults.version.flag: ${cmd.defaults.version.flag}'
109 }
110 }
111
112 res << '}'
113 return res.join('\n')
114}
115
116// is_root returns `true` if this `Command` has no parents.
117pub fn (cmd &Command) is_root() bool {
118 return isnil(cmd.parent)
119}
120
121// root returns the root `Command` of the command chain.
122pub fn (cmd &Command) root() Command {
123 if cmd.is_root() {
124 return *cmd
125 }
126 return cmd.parent.root()
127}
128
129// full_name returns the full `string` representation of all commands int the chain.
130pub fn (cmd &Command) full_name() string {
131 if cmd.is_root() {
132 return cmd.name
133 }
134 return cmd.parent.full_name() + ' ${cmd.name}'
135}
136
137// add_commands adds the `commands` array of `Command`s as sub-commands.
138pub fn (mut cmd Command) add_commands(commands []Command) {
139 for command in commands {
140 cmd.add_command(command)
141 }
142}
143
144// add_command adds `command` as a sub-command of this `Command`.
145pub fn (mut cmd Command) add_command(command Command) {
146 mut subcmd := command
147 for existing in cmd.commands {
148 if existing.name == subcmd.name {
149 eprintln_exit('Command with the name `${subcmd.name}` already exists')
150 }
151 if existing.alias != '' && existing.alias == subcmd.name {
152 eprintln_exit('Command with the name `${subcmd.name}` already exists as an alias')
153 }
154 if subcmd.alias != '' && existing.matches(subcmd.alias) {
155 eprintln_exit('Command alias `${subcmd.alias}` already exists')
156 }
157 }
158 subcmd.parent = unsafe { cmd }
159 cmd.commands << subcmd
160}
161
162// setup ensures that all sub-commands of this `Command` is linked as a chain.
163pub fn (mut cmd Command) setup() {
164 for mut subcmd in cmd.commands {
165 subcmd.parent = unsafe { cmd }
166 subcmd.posix_mode = cmd.posix_mode
167 subcmd.setup()
168 }
169}
170
171// add_flags adds the array `flags` to this `Command`.
172pub fn (mut cmd Command) add_flags(flags []Flag) {
173 for flag in flags {
174 cmd.add_flag(flag)
175 }
176}
177
178// add_flag adds `flag` to this `Command`.
179pub fn (mut cmd Command) add_flag(flag Flag) {
180 if cmd.flags.contains(flag.name) {
181 eprintln_exit('Flag with the name `${flag.name}` already exists')
182 }
183 cmd.flags << flag
184}
185
186// TODO: remove deprecated `disable_<>` switches after deprecation period.
187fn (mut cmd Command) parse_defaults() {
188 // Help
189 if cmd.defaults.help is bool {
190 cmd.defaults.parsed.help.flag = cmd.defaults.help
191 cmd.defaults.parsed.help.command = cmd.defaults.help
192 } else if cmd.defaults.help is CommandFlag {
193 cmd.defaults.parsed.help.flag = cmd.defaults.help.flag
194 cmd.defaults.parsed.help.command = cmd.defaults.help.command
195 }
196 // Version
197 if cmd.defaults.version is bool {
198 cmd.defaults.parsed.version.flag = cmd.defaults.version
199 cmd.defaults.parsed.version.command = cmd.defaults.version
200 } else if cmd.defaults.version is CommandFlag {
201 cmd.defaults.parsed.version.flag = cmd.defaults.version.flag
202 cmd.defaults.parsed.version.command = cmd.defaults.version.command
203 }
204 // Man
205 if cmd.defaults.man is bool {
206 cmd.defaults.parsed.man.flag = cmd.defaults.man
207 cmd.defaults.parsed.man.command = cmd.defaults.man
208 } else if cmd.defaults.man is CommandFlag {
209 cmd.defaults.parsed.man.flag = cmd.defaults.man.flag
210 cmd.defaults.parsed.man.command = cmd.defaults.man.command
211 }
212 // Add Flags
213 if !cmd.disable_flags {
214 cmd.add_default_flags()
215 }
216 // Add Commands
217 cmd.add_default_commands()
218}
219
220// parse parses the flags and commands from the given `args` into the `Command`.
221pub fn (mut cmd Command) parse(args []string) {
222 cmd.parse_defaults()
223 if cmd.sort_flags {
224 cmd.flags.sort(a.name < b.name)
225 }
226 if cmd.sort_commands {
227 cmd.commands.sort(a.name < b.name)
228 }
229 cmd.args = args[1..]
230 if !cmd.disable_flags {
231 cmd.parse_flags()
232 }
233 cmd.parse_commands()
234}
235
236// add_default_flags adds the commonly used `-h`/`--help`, `--man`, and
237// `-v`/`--version` flags to the `Command`.
238fn (mut cmd Command) add_default_flags() {
239 if cmd.defaults.parsed.help.flag && !cmd.flags.contains('help') {
240 use_help_abbrev := !cmd.flags.contains('h') && cmd.posix_mode
241 cmd.add_flag(help_flag(use_help_abbrev))
242 }
243 if cmd.defaults.parsed.version.flag && cmd.version != '' && !cmd.flags.contains('version') {
244 use_version_abbrev := !cmd.flags.contains('v') && cmd.posix_mode
245 cmd.add_flag(version_flag(use_version_abbrev))
246 }
247 if cmd.defaults.parsed.man.flag && !cmd.flags.contains('man') {
248 cmd.add_flag(man_flag())
249 }
250}
251
252// add_default_commands adds the command functions of the commonly
253// used `help`, `man`, and `version` subcommands to the `Command`.
254fn (mut cmd Command) add_default_commands() {
255 if cmd.defaults.parsed.help.command && !cmd.commands.contains('help') && cmd.is_root() {
256 cmd.add_command(help_cmd())
257 }
258 if cmd.defaults.parsed.version.command && cmd.version != '' && !cmd.commands.contains('version') {
259 cmd.add_command(version_cmd())
260 }
261 if cmd.defaults.parsed.man.command && !cmd.commands.contains('man') && cmd.is_root() {
262 cmd.add_command(man_cmd())
263 }
264}
265
266fn (mut cmd Command) parse_flags() {
267 for cmd.args.len > 0 {
268 if !cmd.args[0].starts_with('-') {
269 return
270 }
271 mut found := false
272 for mut flag in cmd.flags {
273 if flag.matches(cmd.args[0], cmd.posix_mode) {
274 found = true
275 flag.found = true
276 // Eat flag and its values, continue with reduced args.
277 cmd.args = flag.parse(cmd.args, cmd.posix_mode) or {
278 eprintln_exit('Failed to parse flag `${cmd.args[0]}`: ${err}')
279 }
280 break
281 }
282 }
283 if !found {
284 eprintln_exit('Command `${cmd.name}` has no flag `${cmd.args[0]}`')
285 }
286 }
287}
288
289fn (mut cmd Command) parse_commands() {
290 global_flags := cmd.flags.filter(it.global)
291 cmd.check_help_flag()
292 cmd.check_version_flag()
293 cmd.check_man_flag()
294 for i in 0 .. cmd.args.len {
295 arg := cmd.args[i]
296 for j in 0 .. cmd.commands.len {
297 mut command := cmd.commands[j]
298 if command.matches(arg) {
299 for flag in global_flags {
300 command.add_flag(flag)
301 }
302 command.parse(cmd.args[i..])
303 return
304 }
305 }
306 }
307 if cmd.is_root() && isnil(cmd.execute) {
308 if cmd.defaults.parsed.help.command {
309 cmd.execute_help()
310 return
311 }
312 }
313 // if no further command was found, execute current command
314 if cmd.required_args > 0 {
315 if cmd.required_args > cmd.args.len {
316 descriptor := if cmd.required_args == 1 { 'argument' } else { 'arguments' }
317 eprintln_exit('Command `${cmd.name}` needs at least ${cmd.required_args} ${descriptor}')
318 }
319 }
320 cmd.check_required_flags()
321
322 cmd.handle_cb(cmd.pre_execute, 'preexecution')
323 cmd.handle_cb(cmd.execute, 'execution')
324 cmd.handle_cb(cmd.post_execute, 'postexecution')
325}
326
327fn (cmd &Command) display_name() string {
328 if cmd.alias == '' {
329 return cmd.name
330 }
331 return '${cmd.name} (${cmd.alias})'
332}
333
334fn (cmd &Command) matches(token string) bool {
335 return cmd.name == token || (cmd.alias != '' && cmd.alias == token)
336}
337
338fn (mut cmd Command) handle_cb(cb FnCommandCallback, label string) {
339 if !isnil(cb) {
340 cb(*cmd) or {
341 label_message := term.ecolorize(term.bright_red, 'cli ${label} error:')
342 eprintln_exit('${label_message} ${err}')
343 }
344 }
345}
346
347fn (cmd &Command) check_help_flag() {
348 if cmd.defaults.parsed.help.flag && cmd.flags.contains('help') {
349 help_flag :=
350 cmd.flags.get_bool('help') or { return } // ignore error and handle command normally
351 if help_flag {
352 cmd.execute_help()
353 exit(0)
354 }
355 }
356}
357
358fn (cmd &Command) check_man_flag() {
359 if cmd.defaults.parsed.man.flag && cmd.flags.contains('man') {
360 man_flag :=
361 cmd.flags.get_bool('man') or { return } // ignore error and handle command normally
362 if man_flag {
363 cmd.execute_man()
364 exit(0)
365 }
366 }
367}
368
369fn (cmd &Command) check_version_flag() {
370 if cmd.defaults.parsed.version.flag && cmd.version != '' && cmd.flags.contains('version') {
371 version_flag :=
372 cmd.flags.get_bool('version') or { return } // ignore error and handle command normally
373 if version_flag {
374 print_version_for_command(cmd) or { panic(err) }
375 exit(0)
376 }
377 }
378}
379
380fn (cmd &Command) check_required_flags() {
381 for flag in cmd.flags {
382 if flag.required && flag.value.len == 0 {
383 full_name := cmd.full_name()
384 eprintln_exit('Flag `${flag.name}` is required by `${full_name}`')
385 }
386 }
387}
388
389// execute_help executes the callback registered for the `-h`/`--help` flag option.
390pub fn (cmd &Command) execute_help() {
391 if cmd.commands.contains('help') {
392 help_cmd :=
393 cmd.commands.get('help') or { return } // ignore error and handle command normally
394 if !isnil(help_cmd.execute) {
395 help_cmd.execute(help_cmd) or { panic(err) }
396 return
397 }
398 }
399 print(cmd.help_message())
400}
401
402// execute_man executes the callback registered for the `-man` flag option.
403pub fn (cmd &Command) execute_man() {
404 if cmd.commands.contains('man') {
405 man_cmd := cmd.commands.get('man') or { return }
406 man_cmd.execute(man_cmd) or { panic(err) }
407 } else {
408 print(cmd.manpage())
409 }
410}
411
412fn (cmds []Command) get(name string) !Command {
413 for cmd in cmds {
414 if cmd.name == name {
415 return cmd
416 }
417 }
418 return error('Command `${name}` not found in ${cmds}')
419}
420
421fn (cmds []Command) contains(name string) bool {
422 for cmd in cmds {
423 if cmd.name == name {
424 return true
425 }
426 }
427 return false
428}
429
430@[noreturn]
431fn eprintln_exit(message string) {
432 eprintln(message)
433 exit(1)
434}
435