2019-11-21 13:03:12 +01:00
module cli
2022-03-04 11:28:11 +01:00
import term
2022-10-21 03:14:33 +08:00
type FnCommandCallback = fn ( cmd Command ) !
2020-07-18 14:24:10 +03:00
2021-01-20 22:15:48 +01:00
// str returns the `string` representation of the callback.
2020-07-18 14:24:10 +03:00
pub fn ( f FnCommandCallback ) str ( ) string {
return ' F n C o m m a n d C a l l b a c k = > ' + ptr_str ( f )
}
2020-04-16 14:50:04 +02:00
2025-07-02 15:45:47 +03:00
// Command is a structured representation of a single command or chain of commands.
2019-11-21 13:03:12 +01:00
pub struct Command {
pub mut :
2020-07-18 14:24:10 +03:00
name string
usage string
description string
2022-04-03 03:12:47 -04:00
man_description string
2020-07-18 14:24:10 +03:00
version string
2023-08-03 21:06:32 +03:00
pre_execute FnCommandCallback = unsafe { nil }
execute FnCommandCallback = unsafe { nil }
post_execute FnCommandCallback = unsafe { nil }
2020-07-18 14:24:10 +03:00
disable_flags bool
2021-02-21 23:17:50 +09:00
sort_flags bool
sort_commands bool
2022-09-15 12:59:31 +08:00
parent & Command = unsafe { nil }
2020-07-18 14:24:10 +03:00
commands [ ] Command
flags [ ] Flag
2020-08-20 23:14:53 +02:00
required_args int
2020-07-18 14:24:10 +03:00
args [ ] string
2021-08-11 12:26:17 +03:00
posix_mode bool
2024-04-20 11:49:08 +02:00
defaults struct {
pub :
help Defaults = true
man Defaults = true
version Defaults = true
mut :
parsed struct {
mut :
help CommandFlag
version CommandFlag
man CommandFlag
}
}
}
pub struct CommandFlag {
pub mut :
command bool = true
flag bool = true
2020-07-18 14:24:10 +03:00
}
2024-04-20 11:49:08 +02:00
type Defaults = CommandFlag | bool
2021-01-20 22:15:48 +01:00
// str returns the `string` representation of the `Command`.
2024-10-17 16:04:23 +08:00
pub fn ( cmd & Command ) str ( ) string {
2020-07-18 14:24:10 +03:00
mut res := [ ] string { }
res << ' C o m m a n d { '
2022-11-15 21:53:13 +08:00
res << ' n a m e : " $ { cmd . name } " '
res << ' u s a g e : " $ { cmd . usage } " '
res << ' v e r s i o n : " $ { cmd . version } " '
res << ' d e s c r i p t i o n : " $ { cmd . description } " '
res << ' m a n _ d e s c r i p t i o n : " $ { cmd . man_description } " '
res << ' d i s a b l e _ f l a g s : $ { cmd . disable_flags } '
res << ' s o r t _ f l a g s : $ { cmd . sort_flags } '
res << ' s o r t _ c o m m a n d s : $ { cmd . sort_commands } '
res << ' c b e x e c u t e : $ { cmd . execute } '
res << ' c b p r e _ e x e c u t e : $ { cmd . pre_execute } '
res << ' c b p o s t _ e x e c u t e : $ { cmd . post_execute } '
2022-05-20 17:30:16 +02:00
if unsafe { cmd . parent == 0 } {
2020-07-18 14:24:10 +03:00
res << ' p a r e n t : & C o m m a n d ( 0 ) '
} else {
2022-11-15 21:53:13 +08:00
res << ' p a r e n t : & C o m m a n d { $ { cmd . parent . name } . . . } '
2020-07-18 14:24:10 +03:00
}
2022-11-15 21:53:13 +08:00
res << ' c o m m a n d s : $ { cmd . commands } '
res << ' f l a g s : $ { cmd . flags } '
res << ' r e q u i r e d _ a r g s : $ { cmd . required_args } '
res << ' a r g s : $ { cmd . args } '
2025-03-09 14:14:17 -06:00
res << ' p o s i x _ m o d e : $ { cmd . posix_mode } '
match cmd . defaults . help {
bool {
res << ' d e f a u l t s . h e l p : $ { cmd . defaults . help } '
}
CommandFlag {
res << ' d e f a u l t s . h e l p . c o m m a n d : $ { cmd . defaults . help . command } '
res << ' d e f a u l t s . h e l p . f l a g : $ { cmd . defaults . help . flag } '
}
}
match cmd . defaults . man {
bool {
res << ' d e f a u l t s . m a n : $ { cmd . defaults . man } '
}
CommandFlag {
res << ' d e f a u l t s . m a n . c o m m a n d : $ { cmd . defaults . man . command } '
res << ' d e f a u l t s . m a n . f l a g : $ { cmd . defaults . man . flag } '
}
}
match cmd . defaults . version {
bool {
res << ' d e f a u l t s . v e r s i o n : $ { cmd . defaults . version } '
}
CommandFlag {
res << ' d e f a u l t s . v e r s i o n . c o m m a n d : $ { cmd . defaults . version . command } '
res << ' d e f a u l t s . v e r s i o n . f l a g : $ { cmd . defaults . version . flag } '
}
}
2020-07-18 14:24:10 +03:00
res << ' } '
return res . join ( ' \n ' )
}
2019-11-21 13:03:12 +01:00
2021-01-20 22:15:48 +01:00
// is_root returns `true` if this `Command` has no parents.
2024-10-17 16:04:23 +08:00
pub fn ( cmd & Command ) is_root ( ) bool {
2020-07-18 14:24:10 +03:00
return isnil ( cmd . parent )
}
2020-07-02 11:10:03 +02:00
2021-01-20 22:15:48 +01:00
// root returns the root `Command` of the command chain.
2024-10-17 16:04:23 +08:00
pub fn ( cmd & Command ) root ( ) Command {
2020-07-18 14:24:10 +03:00
if cmd . is_root ( ) {
2024-10-17 16:04:23 +08:00
return * cmd
2020-07-18 14:24:10 +03:00
}
return cmd . parent . root ( )
2019-11-21 13:03:12 +01:00
}
2021-01-20 22:15:48 +01:00
// full_name returns the full `string` representation of all commands int the chain.
2024-10-17 16:04:23 +08:00
pub fn ( cmd & Command ) full_name ( ) string {
2020-07-18 14:24:10 +03:00
if cmd . is_root ( ) {
2019-11-21 13:03:12 +01:00
return cmd . name
}
2022-11-15 21:53:13 +08:00
return cmd . parent . full_name ( ) + ' $ { cmd . name } '
2019-11-21 13:03:12 +01:00
}
2021-01-20 22:15:48 +01:00
// add_commands adds the `commands` array of `Command`s as sub-commands.
2020-07-18 14:24:10 +03:00
pub fn ( mut cmd Command ) add_commands ( commands [ ] Command ) {
for command in commands {
cmd . add_command ( command )
2019-11-21 13:03:12 +01:00
}
}
2021-01-20 22:15:48 +01:00
// add_command adds `command` as a sub-command of this `Command`.
2020-05-17 19:51:18 +08:00
pub fn ( mut cmd Command ) add_command ( command Command ) {
2020-08-20 23:14:53 +02:00
mut subcmd := command
if cmd . commands . contains ( subcmd . name ) {
2022-11-15 21:53:13 +08:00
eprintln_exit ( ' C o m m a n d w i t h t h e n a m e ` $ { subcmd . name } ` a l r e a d y e x i s t s ' )
2020-07-18 14:24:10 +03:00
}
2021-05-07 14:58:48 +02:00
subcmd . parent = unsafe { cmd }
2020-08-20 23:14:53 +02:00
cmd . commands << subcmd
2019-11-21 13:03:12 +01:00
}
2025-07-02 15:45:47 +03:00
// setup ensures that all sub-commands of this `Command` is linked as a chain.
2021-01-05 20:25:25 +09:00
pub fn ( mut cmd Command ) setup ( ) {
for mut subcmd in cmd . commands {
2021-05-07 14:58:48 +02:00
subcmd . parent = unsafe { cmd }
2021-08-11 12:26:17 +03:00
subcmd . posix_mode = cmd . posix_mode
2021-01-05 20:25:25 +09:00
subcmd . setup ( )
}
}
2021-01-20 22:15:48 +01:00
// add_flags adds the array `flags` to this `Command`.
2020-07-18 14:24:10 +03:00
pub fn ( mut cmd Command ) add_flags ( flags [ ] Flag ) {
for flag in flags {
cmd . add_flag ( flag )
}
}
2021-01-20 22:15:48 +01:00
// add_flag adds `flag` to this `Command`.
2020-05-17 19:51:18 +08:00
pub fn ( mut cmd Command ) add_flag ( flag Flag ) {
2020-08-20 23:14:53 +02:00
if cmd . flags . contains ( flag . name ) {
2022-11-15 21:53:13 +08:00
eprintln_exit ( ' F l a g w i t h t h e n a m e ` $ { flag . name } ` a l r e a d y e x i s t s ' )
2020-07-18 14:24:10 +03:00
}
2019-11-21 13:03:12 +01:00
cmd . flags << flag
}
2024-04-20 11:49:08 +02:00
// TODO: remove deprecated `disable_<>` switches after deprecation period.
fn ( mut cmd Command ) parse_defaults ( ) {
// Help
if cmd . defaults . help is bool {
2025-05-05 09:31:32 +03:00
cmd . defaults . parsed . help . flag = cmd . defaults . help
cmd . defaults . parsed . help . command = cmd . defaults . help
2024-04-20 11:49:08 +02:00
} else if cmd . defaults . help is CommandFlag {
cmd . defaults . parsed . help . flag = cmd . defaults . help . flag
cmd . defaults . parsed . help . command = cmd . defaults . help . command
}
// Version
if cmd . defaults . version is bool {
2025-05-05 09:31:32 +03:00
cmd . defaults . parsed . version . flag = cmd . defaults . version
cmd . defaults . parsed . version . command = cmd . defaults . version
2024-04-20 11:49:08 +02:00
} else if cmd . defaults . version is CommandFlag {
cmd . defaults . parsed . version . flag = cmd . defaults . version . flag
cmd . defaults . parsed . version . command = cmd . defaults . version . command
}
// Man
if cmd . defaults . man is bool {
2025-05-05 09:31:32 +03:00
cmd . defaults . parsed . man . flag = cmd . defaults . man
cmd . defaults . parsed . man . command = cmd . defaults . man
2024-04-20 11:49:08 +02:00
} else if cmd . defaults . man is CommandFlag {
cmd . defaults . parsed . man . flag = cmd . defaults . man . flag
cmd . defaults . parsed . man . command = cmd . defaults . man . command
}
// Add Flags
2020-05-28 13:32:43 +02:00
if ! cmd . disable_flags {
cmd . add_default_flags ( )
}
2024-04-20 11:49:08 +02:00
// Add Commands
2019-11-21 13:03:12 +01:00
cmd . add_default_commands ( )
2024-04-20 11:49:08 +02:00
}
// parse parses the flags and commands from the given `args` into the `Command`.
pub fn ( mut cmd Command ) parse ( args [ ] string ) {
cmd . parse_defaults ( )
2020-07-02 11:10:03 +02:00
if cmd . sort_flags {
2020-08-12 06:11:40 +02:00
cmd . flags . sort ( a . name < b . name )
2020-07-02 11:10:03 +02:00
}
if cmd . sort_commands {
2020-08-12 06:11:40 +02:00
cmd . commands . sort ( a . name < b . name )
2020-07-02 11:10:03 +02:00
}
2019-11-30 12:37:34 +03:00
cmd . args = args [ 1 .. ]
2020-05-28 13:32:43 +02:00
if ! cmd . disable_flags {
cmd . parse_flags ( )
}
2019-11-21 13:03:12 +01:00
cmd . parse_commands ( )
}
2025-03-09 14:14:17 -06:00
// add_default_flags adds the commonly used `-h`/`--help`, `--man`, and
2021-01-20 22:15:48 +01:00
// `-v`/`--version` flags to the `Command`.
2020-05-17 19:51:18 +08:00
fn ( mut cmd Command ) add_default_flags ( ) {
2024-04-20 11:49:08 +02:00
if cmd . defaults . parsed . help . flag && ! cmd . flags . contains ( ' h e l p ' ) {
2021-08-11 12:26:17 +03:00
use_help_abbrev := ! cmd . flags . contains ( ' h ' ) && cmd . posix_mode
2020-08-20 23:14:53 +02:00
cmd . add_flag ( help_flag ( use_help_abbrev ) )
2019-11-21 13:03:12 +01:00
}
2024-04-20 11:49:08 +02:00
if cmd . defaults . parsed . version . flag && cmd . version != ' ' && ! cmd . flags . contains ( ' v e r s i o n ' ) {
2021-08-11 12:26:17 +03:00
use_version_abbrev := ! cmd . flags . contains ( ' v ' ) && cmd . posix_mode
2020-08-20 23:14:53 +02:00
cmd . add_flag ( version_flag ( use_version_abbrev ) )
2019-11-21 13:03:12 +01:00
}
2024-04-20 11:49:08 +02:00
if cmd . defaults . parsed . man . flag && ! cmd . flags . contains ( ' m a n ' ) {
2022-04-03 03:12:47 -04:00
cmd . add_flag ( man_flag ( ) )
}
2019-11-21 13:03:12 +01:00
}
2025-03-09 14:14:17 -06:00
// add_default_commands adds the command functions of the commonly
// used `help`, `man`, and `version` subcommands to the `Command`.
2020-05-17 19:51:18 +08:00
fn ( mut cmd Command ) add_default_commands ( ) {
2024-04-20 11:49:08 +02:00
if cmd . defaults . parsed . help . command && ! cmd . commands . contains ( ' h e l p ' ) && cmd . is_root ( ) {
2019-11-21 13:03:12 +01:00
cmd . add_command ( help_cmd ( ) )
}
2024-04-20 11:49:08 +02:00
if cmd . defaults . parsed . version . command && cmd . version != ' ' && ! cmd . commands . contains ( ' v e r s i o n ' ) {
2019-11-21 13:03:12 +01:00
cmd . add_command ( version_cmd ( ) )
}
2024-04-20 11:49:08 +02:00
if cmd . defaults . parsed . man . command && ! cmd . commands . contains ( ' m a n ' ) && cmd . is_root ( ) {
2022-04-03 03:12:47 -04:00
cmd . add_command ( man_cmd ( ) )
}
2019-11-21 13:03:12 +01:00
}
2020-05-17 19:51:18 +08:00
fn ( mut cmd Command ) parse_flags ( ) {
2024-05-01 20:07:25 +02:00
for cmd . args . len > 0 {
if ! cmd . args [ 0 ] . starts_with ( ' - ' ) {
return
2019-11-21 13:03:12 +01:00
}
mut found := false
2024-05-01 20:07:25 +02:00
for mut flag in cmd . flags {
if flag . matches ( cmd . args [ 0 ] , cmd . posix_mode ) {
2024-04-20 11:49:08 +02:00
found = true
flag . found = true
2024-05-01 20:07:25 +02:00
// Eat flag and its values, continue with reduced args.
2024-04-20 11:49:08 +02:00
cmd . args = flag . parse ( cmd . args , cmd . posix_mode ) or {
eprintln_exit ( ' F a i l e d t o p a r s e f l a g ` $ { cmd . args [ 0 ] } ` : $ { err } ' )
2019-11-21 13:03:12 +01:00
}
2024-04-20 11:49:08 +02:00
break
2019-11-21 13:03:12 +01:00
}
}
if ! found {
2022-11-15 21:53:13 +08:00
eprintln_exit ( ' C o m m a n d ` $ { cmd . name } ` h a s n o f l a g ` $ { cmd . args [ 0 ] } ` ' )
2019-11-21 13:03:12 +01:00
}
}
}
2020-07-01 10:54:34 +02:00
fn ( mut cmd Command ) parse_commands ( ) {
global_flags := cmd . flags . filter ( it . global )
2019-11-21 13:03:12 +01:00
cmd . check_help_flag ( )
cmd . check_version_flag ( )
2022-04-03 03:12:47 -04:00
cmd . check_man_flag ( )
2020-07-18 14:24:10 +03:00
for i in 0 .. cmd . args . len {
2019-11-21 13:03:12 +01:00
arg := cmd . args [ i ]
2020-07-18 14:24:10 +03:00
for j in 0 .. cmd . commands . len {
2019-11-21 13:03:12 +01:00
mut command := cmd . commands [ j ]
if command . name == arg {
for flag in global_flags {
command . add_flag ( flag )
}
2019-11-30 12:37:34 +03:00
command . parse ( cmd . args [ i .. ] )
2019-11-21 13:03:12 +01:00
return
}
}
}
2021-02-20 20:30:39 +05:00
if cmd . is_root ( ) && isnil ( cmd . execute ) {
2024-04-20 11:49:08 +02:00
if cmd . defaults . parsed . help . command {
2020-10-17 18:06:27 +02:00
cmd . execute_help ( )
return
}
}
2020-07-01 10:54:34 +02:00
// if no further command was found, execute current command
2020-10-07 05:39:13 +02:00
if cmd . required_args > 0 {
if cmd . required_args > cmd . args . len {
2024-03-08 01:57:58 +07:00
descriptor := if cmd . required_args == 1 { ' a r g u m e n t ' } else { ' a r g u m e n t s ' }
eprintln_exit ( ' C o m m a n d ` $ { cmd . name } ` n e e d s a t l e a s t $ { cmd . required_args } $ { descriptor } ' )
2020-08-20 23:14:53 +02:00
}
2020-10-07 05:39:13 +02:00
}
cmd . check_required_flags ( )
2022-03-04 11:28:11 +01:00
cmd . handle_cb ( cmd . pre_execute , ' p r e e x e c u t i o n ' )
cmd . handle_cb ( cmd . execute , ' e x e c u t i o n ' )
cmd . handle_cb ( cmd . post_execute , ' p o s t e x e c u t i o n ' )
}
fn ( mut cmd Command ) handle_cb ( cb FnCommandCallback , label string ) {
if ! isnil ( cb ) {
cb ( * cmd ) or {
2022-11-15 21:53:13 +08:00
label_message := term . ecolorize ( term . bright_red , ' c l i $ { label } e r r o r : ' )
eprintln_exit ( ' $ { label_message } $ { err } ' )
2020-03-10 23:11:17 +08:00
}
2019-11-21 13:03:12 +01:00
}
}
2024-10-17 16:04:23 +08:00
fn ( cmd & Command ) check_help_flag ( ) {
2024-04-20 11:49:08 +02:00
if cmd . defaults . parsed . help . flag && cmd . flags . contains ( ' h e l p ' ) {
2020-12-22 04:23:40 +09:00
help_flag := cmd . flags . get_bool ( ' h e l p ' ) or { return } // ignore error and handle command normally
2019-11-21 13:03:12 +01:00
if help_flag {
2020-07-18 14:24:10 +03:00
cmd . execute_help ( )
2019-11-21 13:03:12 +01:00
exit ( 0 )
}
}
}
2024-10-17 16:04:23 +08:00
fn ( cmd & Command ) check_man_flag ( ) {
2024-04-20 11:49:08 +02:00
if cmd . defaults . parsed . man . flag && cmd . flags . contains ( ' m a n ' ) {
2022-04-03 03:12:47 -04:00
man_flag := cmd . flags . get_bool ( ' m a n ' ) or { return } // ignore error and handle command normally
if man_flag {
cmd . execute_man ( )
exit ( 0 )
}
}
}
2024-10-17 16:04:23 +08:00
fn ( cmd & Command ) check_version_flag ( ) {
2024-04-20 11:49:08 +02:00
if cmd . defaults . parsed . version . flag && cmd . version != ' ' && cmd . flags . contains ( ' v e r s i o n ' ) {
2020-12-22 04:23:40 +09:00
version_flag := cmd . flags . get_bool ( ' v e r s i o n ' ) or { return } // ignore error and handle command normally
2019-11-21 13:03:12 +01:00
if version_flag {
2024-05-08 13:11:40 +02:00
print_version_for_command ( cmd ) or { panic ( err ) }
2019-11-21 13:03:12 +01:00
exit ( 0 )
}
}
}
2024-10-17 16:04:23 +08:00
fn ( cmd & Command ) check_required_flags ( ) {
2019-11-21 13:03:12 +01:00
for flag in cmd . flags {
2021-02-19 04:43:18 -06:00
if flag . required && flag . value . len == 0 {
2019-11-21 13:03:12 +01:00
full_name := cmd . full_name ( )
2022-11-15 21:53:13 +08:00
eprintln_exit ( ' F l a g ` $ { flag . name } ` i s r e q u i r e d b y ` $ { full_name } ` ' )
2019-11-21 13:03:12 +01:00
}
}
}
2025-07-02 15:45:47 +03:00
// execute_help executes the callback registered for the `-h`/`--help` flag option.
2024-10-17 16:04:23 +08:00
pub fn ( cmd & Command ) execute_help ( ) {
2020-07-18 14:24:10 +03:00
if cmd . commands . contains ( ' h e l p ' ) {
2020-12-22 04:23:40 +09:00
help_cmd := cmd . commands . get ( ' h e l p ' ) or { return } // ignore error and handle command normally
2023-07-04 00:48:53 -03:00
if ! isnil ( help_cmd . execute ) {
help_cmd . execute ( help_cmd ) or { panic ( err ) }
return
}
2020-07-18 14:24:10 +03:00
}
2023-07-04 00:48:53 -03:00
print ( cmd . help_message ( ) )
2020-07-18 14:24:10 +03:00
}
2025-07-02 15:45:47 +03:00
// execute_man executes the callback registered for the `-man` flag option.
2024-10-17 16:04:23 +08:00
pub fn ( cmd & Command ) execute_man ( ) {
2022-04-03 03:12:47 -04:00
if cmd . commands . contains ( ' m a n ' ) {
man_cmd := cmd . commands . get ( ' m a n ' ) or { return }
man_cmd . execute ( man_cmd ) or { panic ( err ) }
} else {
print ( cmd . manpage ( ) )
}
}
2022-10-21 03:14:33 +08:00
fn ( cmds [ ] Command ) get ( name string ) ! Command {
2019-11-21 13:03:12 +01:00
for cmd in cmds {
if cmd . name == name {
2020-07-02 11:10:03 +02:00
return cmd
2019-11-21 13:03:12 +01:00
}
}
2022-11-15 21:53:13 +08:00
return error ( ' C o m m a n d ` $ { name } ` n o t f o u n d i n $ { cmds } ' )
2019-11-21 13:03:12 +01:00
}
2020-07-02 11:10:03 +02:00
fn ( cmds [ ] Command ) contains ( name string ) bool {
2019-11-21 13:03:12 +01:00
for cmd in cmds {
if cmd . name == name {
2020-07-02 11:10:03 +02:00
return true
2019-11-21 13:03:12 +01:00
}
}
2020-07-02 11:10:03 +02:00
return false
}
2022-03-04 11:28:11 +01:00
2023-11-15 16:16:01 +11:00
@ [ noreturn ]
2022-03-04 11:28:11 +01:00
fn eprintln_exit ( message string ) {
eprintln ( message )
exit ( 1 )
}