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