| 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 or chain of commands. |
| 13 | pub struct Command { |
| 14 | pub 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 | |
| 48 | pub struct CommandFlag { |
| 49 | pub mut: |
| 50 | command bool = true |
| 51 | flag bool = true |
| 52 | } |
| 53 | |
| 54 | type Defaults = CommandFlag | bool |
| 55 | |
| 56 | // str returns the `string` representation of the `Command`. |
| 57 | pub 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. |
| 117 | pub fn (cmd &Command) is_root() bool { |
| 118 | return isnil(cmd.parent) |
| 119 | } |
| 120 | |
| 121 | // root returns the root `Command` of the command chain. |
| 122 | pub 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. |
| 130 | pub 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. |
| 138 | pub 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`. |
| 145 | pub 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. |
| 163 | pub 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`. |
| 172 | pub 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`. |
| 179 | pub 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. |
| 187 | fn (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`. |
| 221 | pub 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`. |
| 238 | fn (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`. |
| 254 | fn (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 | |
| 266 | fn (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 | |
| 289 | fn (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 | |
| 327 | fn (cmd &Command) display_name() string { |
| 328 | if cmd.alias == '' { |
| 329 | return cmd.name |
| 330 | } |
| 331 | return '${cmd.name} (${cmd.alias})' |
| 332 | } |
| 333 | |
| 334 | fn (cmd &Command) matches(token string) bool { |
| 335 | return cmd.name == token || (cmd.alias != '' && cmd.alias == token) |
| 336 | } |
| 337 | |
| 338 | fn (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 | |
| 347 | fn (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 | |
| 358 | fn (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 | |
| 369 | fn (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 | |
| 380 | fn (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. |
| 390 | pub 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. |
| 403 | pub 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 | |
| 412 | fn (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 | |
| 421 | fn (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] |
| 431 | fn eprintln_exit(message string) { |
| 432 | eprintln(message) |
| 433 | exit(1) |
| 434 | } |
| 435 | |