v / vlib / cli
Raw file | 338 loc (309 sloc) | 8.72 KB | Latest commit hash 017ace6ea
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
13// or chain of commands.
14pub struct Command {
15pub 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`.
39pub 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.
70pub fn (cmd Command) is_root() bool {
71 return isnil(cmd.parent)
72}
73
74// root returns the root `Command` of the command chain.
75pub 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.
83pub 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.
91pub 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`.
98pub 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.
109pub 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`.
118pub 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`.
125pub 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`.
133pub 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`.
153fn (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`.
169fn (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
181fn (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
206fn (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
243fn (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
252fn (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
262fn (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
272fn (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
283fn (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.
294pub 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.
307pub 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
316fn (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
325fn (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]
335fn eprintln_exit(message string) {
336 eprintln(message)
337 exit(1)
338}