| 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 | // 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 | |
| 62 | pub struct CommandFlag { |
| 63 | pub mut: |
| 64 | command bool = true |
| 65 | flag bool = true |
| 66 | } |
| 67 | |
| 68 | type Defaults = CommandFlag | bool |
| 69 | |
| 70 | // str returns the `string` representation of the `Command`. |
| 71 | pub 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. |
| 134 | pub fn (cmd &Command) is_root() bool { |
| 135 | return isnil(cmd.parent) |
| 136 | } |
| 137 | |
| 138 | // root returns the root `Command` of the command chain. |
| 139 | pub 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. |
| 147 | pub 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. |
| 155 | pub 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`. |
| 162 | pub 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. |
| 180 | pub 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`. |
| 189 | pub 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`. |
| 196 | pub 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. |
| 204 | fn (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`. |
| 238 | pub 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`. |
| 255 | fn (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`. |
| 271 | fn (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 | |
| 283 | fn (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 | |
| 306 | fn (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 | |
| 344 | fn (cmd &Command) display_name() string { |
| 345 | if cmd.alias == '' { |
| 346 | return cmd.name |
| 347 | } |
| 348 | return '${cmd.name} (${cmd.alias})' |
| 349 | } |
| 350 | |
| 351 | fn (cmd &Command) matches(token string) bool { |
| 352 | return cmd.name == token || (cmd.alias != '' && cmd.alias == token) |
| 353 | } |
| 354 | |
| 355 | fn (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 | |
| 364 | fn (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 | |
| 375 | fn (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 | |
| 386 | fn (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 | |
| 397 | fn (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. |
| 407 | pub 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. |
| 419 | pub 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 | |
| 428 | fn (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 | |
| 437 | fn (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] |
| 447 | fn eprintln_exit(message string) { |
| 448 | eprintln(message) |
| 449 | exit(1) |
| 450 | } |
| 451 | |