cli: extract improvements to vlib/cli, based on PR 5616 (without cmd/v2)

pull/5874/head
Delyan Angelov 2020-07-18 14:24:10 +03:00
parent 7ab6899538
commit 3a4f2dfe8b
6 changed files with 458 additions and 269 deletions

View File

@ -1,48 +1,105 @@
module cli module cli
fn nil() voidptr { return 0 } type FnCommandCallback fn (cmd Command)?
pub fn (f FnCommandCallback) str() string {
return 'FnCommandCallback=>' + ptr_str(f)
}
pub struct Command { pub struct Command {
pub mut: pub mut:
name string name string
description string usage string
version string description string
pre_execute fn(cmd Command) version string
execute fn(cmd Command) pre_execute FnCommandCallback
post_execute fn(cmd Command) execute FnCommandCallback
post_execute FnCommandCallback
disable_help bool disable_help bool
disable_version bool disable_version bool
disable_flags bool disable_flags bool
sort_flags bool = true
sort_flags bool = true sort_commands bool = true
sort_commands bool = true parent &Command = 0
commands []Command
parent &Command = nil() flags []Flag
commands []Command args []string
flags []Flag
args []string
} }
pub fn (cmd Command) full_name() string { pub fn (cmd Command) str() string {
if isnil(cmd.parent) { mut res := []string{}
return cmd.name res << 'Command{'
res << ' name: "$cmd.name"'
res << ' usage: "$cmd.usage"'
res << ' version: "$cmd.version"'
res << ' description: "$cmd.description"'
res << ' disable_help: $cmd.disable_help'
res << ' disable_flags: $cmd.disable_flags'
res << ' disable_version: $cmd.disable_version'
res << ' sort_flags: $cmd.sort_flags'
res << ' sort_commands: $cmd.sort_commands'
res << ' cb execute: $cmd.execute'
res << ' cb pre_execute: $cmd.pre_execute'
res << ' cb post_execute: $cmd.post_execute'
if cmd.parent == 0 {
res << ' parent: &Command(0)'
} else {
res << ' parent: &Command{$cmd.parent.name ...}'
} }
return cmd.parent.full_name() + ' ${cmd.name}' res << ' commands: $cmd.commands'
res << ' flags: $cmd.flags'
res << ' args: $cmd.args'
res << '}'
return res.join('\n')
}
pub fn (cmd Command) is_root() bool {
return isnil(cmd.parent)
} }
pub fn (cmd Command) root() Command { pub fn (cmd Command) root() Command {
if isnil(cmd.parent) { if cmd.is_root() {
return cmd return cmd
} }
return cmd.parent.root() return cmd.parent.root()
} }
pub fn (cmd Command) full_name() string {
if cmd.is_root() {
return cmd.name
}
return cmd.parent.full_name() + ' $cmd.name'
}
pub fn (mut cmd Command) add_commands(commands []Command) {
for command in commands {
cmd.add_command(command)
}
}
pub fn (mut cmd Command) add_command(command Command) { pub fn (mut cmd Command) add_command(command Command) {
for existing_cmd in cmd.commands {
if existing_cmd.name == command.name {
println("command with the same name \'$command.name\' already exists")
exit(1)
}
}
cmd.commands << command cmd.commands << command
} }
pub fn (mut cmd Command) add_flags(flags []Flag) {
for flag in flags {
cmd.add_flag(flag)
}
}
pub fn (mut cmd Command) add_flag(flag Flag) { pub fn (mut cmd Command) add_flag(flag Flag) {
for existing_flag in cmd.flags {
if existing_flag.name == flag.name {
println("flag with the same name \'$flag.name\' already exists")
exit(1)
}
}
cmd.flags << flag cmd.flags << flag
} }
@ -51,19 +108,16 @@ pub fn (mut cmd Command) parse(args []string) {
cmd.add_default_flags() cmd.add_default_flags()
} }
cmd.add_default_commands() cmd.add_default_commands()
if cmd.sort_flags { if cmd.sort_flags {
cmd.flags.sort() cmd.flags.sort()
} }
if cmd.sort_commands { if cmd.sort_commands {
cmd.commands.sort() cmd.commands.sort()
} }
cmd.args = args[1..] cmd.args = args[1..]
for i in 0..cmd.commands.len { for i in 0 .. cmd.commands.len {
cmd.commands[i].parent = cmd cmd.commands[i].parent = cmd
} }
if !cmd.disable_flags { if !cmd.disable_flags {
cmd.parse_flags() cmd.parse_flags()
} }
@ -72,18 +126,18 @@ pub fn (mut cmd Command) parse(args []string) {
fn (mut cmd Command) add_default_flags() { fn (mut cmd Command) add_default_flags() {
if !cmd.disable_help && !cmd.flags.contains('help') { if !cmd.disable_help && !cmd.flags.contains('help') {
cmd.add_flag(help_flag(!cmd.flags.contains('h'))) cmd.add_flag(help_flag(!cmd.flags.contains('h') && cmd.has_abbrev_flags()))
} }
if !cmd.disable_version && cmd.version != '' && !cmd.flags.contains('version') { if cmd.version != '' && !cmd.flags.contains('version') {
cmd.add_flag(version_flag(!cmd.flags.contains('v'))) cmd.add_flag(version_flag(!cmd.flags.contains('v') && cmd.has_abbrev_flags()))
} }
} }
fn (mut cmd Command) add_default_commands() { fn (mut cmd Command) add_default_commands() {
if !cmd.disable_help && !cmd.commands.contains('help') { if !cmd.disable_help && !cmd.commands.contains('help') && cmd.is_root() {
cmd.add_command(help_cmd()) cmd.add_command(help_cmd())
} }
if !cmd.disable_version && cmd.version != '' && !cmd.commands.contains('version') { if cmd.version != '' && !cmd.commands.contains('version') {
cmd.add_command(version_cmd()) cmd.add_command(version_cmd())
} }
} }
@ -94,19 +148,18 @@ fn (mut cmd Command) parse_flags() {
break break
} }
mut found := false mut found := false
for i in 0..cmd.flags.len { for i in 0 .. cmd.flags.len {
mut flag := &cmd.flags[i] mut flag := &cmd.flags[i]
if flag.matches(cmd.args) { if flag.matches(cmd.args, cmd.has_abbrev_flags()) {
found = true found = true
flag.found = true flag.found = true
cmd.args = flag.parse(cmd.args) or { cmd.args = flag.parse(cmd.args, cmd.has_abbrev_flags()) or {
println('failed to parse flag ${cmd.args[0]}: ${err}') println('failed to parse flag ${cmd.args[0]}: $err')
exit(1) exit(1)
} }
break break
} }
} }
if !found { if !found {
println('invalid flag: ${cmd.args[0]}') println('invalid flag: ${cmd.args[0]}')
exit(1) exit(1)
@ -116,13 +169,11 @@ fn (mut cmd Command) parse_flags() {
fn (mut cmd Command) parse_commands() { fn (mut cmd Command) parse_commands() {
global_flags := cmd.flags.filter(it.global) global_flags := cmd.flags.filter(it.global)
cmd.check_help_flag() cmd.check_help_flag()
cmd.check_version_flag() cmd.check_version_flag()
for i in 0 .. cmd.args.len {
for i in 0..cmd.args.len {
arg := cmd.args[i] arg := cmd.args[i]
for j in 0..cmd.commands.len { for j in 0 .. cmd.commands.len {
mut command := cmd.commands[j] mut command := cmd.commands[j]
if command.name == arg { if command.name == arg {
for flag in global_flags { for flag in global_flags {
@ -133,73 +184,97 @@ fn (mut cmd Command) parse_commands() {
} }
} }
} }
// if no further command was found, execute current command // if no further command was found, execute current command
if int(cmd.execute) == 0 { if int(cmd.execute) == 0 {
if !cmd.disable_help { if !cmd.disable_help {
help_cmd := cmd.commands.get('help') or { return } // ignore error and handle command normally cmd.execute_help()
help_cmd.execute(help_cmd)
} }
} else { } else {
cmd.check_required_flags() cmd.check_required_flags()
if int(cmd.pre_execute) > 0 { if int(cmd.pre_execute) > 0 {
cmd.pre_execute(*cmd) cmd.pre_execute(*cmd) or {
println('cli preexecution error: $err')
exit(1)
}
}
cmd.execute(*cmd) or {
println('cli execution error: $err')
exit(1)
} }
cmd.execute(*cmd)
if int(cmd.post_execute) > 0 { if int(cmd.post_execute) > 0 {
cmd.post_execute(*cmd) cmd.post_execute(*cmd) or {
println('cli postexecution error: $err')
exit(1)
}
} }
} }
} }
fn (mut cmd Command) check_help_flag() { fn (cmd Command) has_abbrev_flags() bool {
if cmd.disable_help { mut has_abbrev := false
return for flag in cmd.flags {
if flag.abbrev != '' {
has_abbrev = true
}
} }
if cmd.flags.contains('help') { return has_abbrev
help_flag := cmd.flags.get_bool('help') or { return } // ignore error and handle command normally }
fn (cmd Command) check_help_flag() {
if !cmd.disable_help && cmd.flags.contains('help') {
help_flag := cmd.flags.get_bool('help') or {
return
} // ignore error and handle command normally
if help_flag { if help_flag {
help_cmd := cmd.commands.get('help') or { return } // ignore error and handle command normally cmd.execute_help()
help_cmd.execute(help_cmd)
exit(0) exit(0)
} }
} }
} }
fn (mut cmd Command) check_version_flag() { fn (cmd Command) check_version_flag() {
if cmd.disable_version {
return
}
if cmd.version != '' && cmd.flags.contains('version') { if cmd.version != '' && cmd.flags.contains('version') {
version_flag := cmd.flags.get_bool('version') or { return } // ignore error and handle command normally version_flag := cmd.flags.get_bool('version') or {
return
} // ignore error and handle command normally
if version_flag { if version_flag {
version_cmd := cmd.commands.get('version') or { return } // ignore error and handle command normally version_cmd := cmd.commands.get('version') or {
return
} // ignore error and handle command normally
version_cmd.execute(version_cmd) version_cmd.execute(version_cmd)
exit(0) exit(0)
} }
} }
} }
fn (mut cmd Command) check_required_flags() { fn (cmd Command) check_required_flags() {
for flag in cmd.flags { for flag in cmd.flags {
if flag.required && flag.value == '' { if flag.required && flag.value == '' {
full_name := cmd.full_name() full_name := cmd.full_name()
println('flag \'${flag.name}\' is required by \'${full_name}\'') println("flag \'$flag.name\' is required by \'$full_name\'")
exit(1) exit(1)
} }
} }
} }
fn (cmd Command) execute_help() {
if cmd.commands.contains('help') {
help_cmd := cmd.commands.get('help') or {
return
} // ignore error and handle command normally
help_cmd.execute(help_cmd)
} else {
print(cmd.help_message())
}
}
fn (cmds []Command) get(name string) ?Command { fn (cmds []Command) get(name string) ?Command {
for cmd in cmds { for cmd in cmds {
if cmd.name == name { if cmd.name == name {
return cmd return cmd
} }
} }
return error('command \'${name}\' not found.') return error("command \'$name\' not found.")
} }
fn (cmds []Command) contains(name string) bool { fn (cmds []Command) contains(name string) bool {
@ -212,7 +287,7 @@ fn (cmds []Command) contains(name string) bool {
} }
fn (mut cmds []Command) sort() { fn (mut cmds []Command) sort() {
cmds.sort_with_compare(fn(a &Command, b &Command) int { cmds.sort_with_compare(fn (a, b &Command) int {
return compare_strings(&a.name, &b.name) return compare_strings(&a.name, &b.name)
}) })
} }

View File

@ -2,100 +2,95 @@ import cli
fn test_if_command_parses_empty_args() { fn test_if_command_parses_empty_args() {
mut cmd := cli.Command{ mut cmd := cli.Command{
name: 'command', name: 'command'
execute: empty_func, execute: empty_func
} }
cmd.parse(['command']) cmd.parse(['command'])
assert cmd.name == 'command' assert cmd.name == 'command' && compare_arrays(cmd.args, [])
&& compare_arrays(cmd.args, [])
} }
fn test_if_command_parses_args() { fn test_if_command_parses_args() {
mut cmd := cli.Command{ mut cmd := cli.Command{
name: 'command', name: 'command'
execute: empty_func, execute: empty_func
} }
cmd.parse(['command', 'arg0', 'arg1']) cmd.parse(['command', 'arg0', 'arg1'])
assert cmd.name == 'command' && compare_arrays(cmd.args, ['arg0', 'arg1'])
assert cmd.name == 'command'
&& compare_arrays(cmd.args, ['arg0', 'arg1'])
} }
fn test_if_subcommands_parse_args() { fn test_if_subcommands_parse_args() {
mut cmd := cli.Command{ mut cmd := cli.Command{
name: 'command', name: 'command'
} }
subcmd := cli.Command{ subcmd := cli.Command{
name: 'subcommand', name: 'subcommand'
execute: empty_func, execute: empty_func
} }
cmd.add_command(subcmd) cmd.add_command(subcmd)
cmd.parse(['command', 'subcommand', 'arg0', 'arg1']) cmd.parse(['command', 'subcommand', 'arg0', 'arg1'])
} }
fn if_subcommands_parse_args_func(cmd cli.Command) { fn if_subcommands_parse_args_func(cmd cli.Command) {
assert cmd.name == 'subcommand' assert cmd.name == 'subcommand' && compare_arrays(cmd.args, ['arg0', 'arg1'])
&& compare_arrays(cmd.args, ['arg0', 'arg1'])
} }
fn test_if_command_has_default_help_subcommand() { fn test_if_command_has_default_help_subcommand() {
mut cmd := cli.Command{ mut cmd := cli.Command{
name: 'command', name: 'command'
} }
cmd.parse(['command']) cmd.parse(['command'])
assert has_command(cmd, 'help') assert has_command(cmd, 'help')
} }
fn test_if_command_has_default_version_subcommand_if_version_is_set() { fn test_if_command_has_default_version_subcommand_if_version_is_set() {
mut cmd := cli.Command{ mut cmd := cli.Command{
name: 'command', name: 'command'
version: '1.0.0', version: '1.0.0'
} }
cmd.parse(['command']) cmd.parse(['command'])
assert has_command(cmd, 'version') assert has_command(cmd, 'version')
} }
fn test_if_flag_gets_set() { fn test_if_flag_gets_set() {
mut cmd := cli.Command{ mut cmd := cli.Command{
name: 'command', name: 'command'
execute: if_flag_gets_set_func, execute: fn (cmd cli.Command) ? {
flag := cmd.flags.get_string('flag')?
assert flag == 'value'
}
} }
cmd.add_flag(cli.Flag{ cmd.add_flag(cli.Flag{
flag: .string flag: .string
name: 'flag' name: 'flag'
}) })
cmd.parse(['command', '--flag', 'value']) cmd.parse(['command', '-flag', 'value'])
}
fn if_flag_gets_set_func(cmd cli.Command) {
flag := cmd.flags.get_string('flag') or { panic(err) }
assert flag == 'value'
} }
fn test_if_flag_gets_set_with_abbrev() { fn test_if_flag_gets_set_with_abbrev() {
mut cmd := cli.Command{ mut cmd := cli.Command{
name: 'command', name: 'command'
execute: if_flag_gets_set_with_abbrev_func, execute: fn (cmd cli.Command) ? {
flag := cmd.flags.get_string('flag')?
assert flag == 'value'
}
} }
cmd.add_flag(cli.Flag{ cmd.add_flag(cli.Flag{
flag: .string, flag: .string
name: 'flag', name: 'flag'
abbrev: 'f', abbrev: 'f'
}) })
cmd.parse(['command', '-f', 'value']) cmd.parse(['command', '-f', 'value'])
} cmd.parse(['command', '--flag', 'value'])
fn if_flag_gets_set_with_abbrev_func(cmd cli.Command) {
flag := cmd.flags.get_string('flag') or { panic(err) }
assert flag == 'value'
} }
fn test_if_multiple_flags_get_set() { fn test_if_multiple_flags_get_set() {
mut cmd := cli.Command{ mut cmd := cli.Command{
name: 'command', name: 'command'
execute: if_multiple_flags_get_set_func, execute: fn (cmd cli.Command) ? {
flag := cmd.flags.get_string('flag')?
value := cmd.flags.get_int('value')?
assert flag == 'value' && value == 42
}
} }
cmd.add_flag(cli.Flag{ cmd.add_flag(cli.Flag{
flag: .string flag: .string
@ -105,65 +100,57 @@ fn test_if_multiple_flags_get_set() {
flag: .int flag: .int
name: 'value' name: 'value'
}) })
cmd.parse(['command', '--flag', 'value', '--value', '42']) cmd.parse(['command', '-flag', 'value', '-value', '42'])
}
fn if_multiple_flags_get_set_func(cmd cli.Command) {
flag := cmd.flags.get_string('flag') or { panic(err) }
value := cmd.flags.get_int('value') or { panic(err) }
assert flag == 'value'
&& value == 42
} }
fn test_if_flag_gets_set_in_subcommand() { fn test_if_flag_gets_set_in_subcommand() {
mut cmd := cli.Command{ mut cmd := cli.Command{
name: 'command', name: 'command'
execute: empty_func, execute: empty_func
} }
mut subcmd := cli.Command{ mut subcmd := cli.Command{
name: 'subcommand', name: 'subcommand'
execute: if_flag_gets_set_in_subcommand_func execute: fn (cmd cli.Command) ? {
flag := cmd.flags.get_string('flag') or {
panic(err)
}
assert flag == 'value'
}
} }
subcmd.add_flag(cli.Flag{ subcmd.add_flag(cli.Flag{
flag: .string flag: .string
name: 'flag' name: 'flag'
}) })
cmd.add_command(subcmd) cmd.add_command(subcmd)
cmd.parse(['command', 'subcommand', '--flag', 'value']) cmd.parse(['command', 'subcommand', '-flag', 'value'])
}
fn if_flag_gets_set_in_subcommand_func(cmd cli.Command) {
flag := cmd.flags.get_string('flag') or { panic(err) }
assert flag == 'value'
} }
fn test_if_global_flag_gets_set_in_subcommand() { fn test_if_global_flag_gets_set_in_subcommand() {
mut cmd := cli.Command{ mut cmd := cli.Command{
name: 'command', name: 'command'
execute: empty_func, execute: empty_func
} }
cmd.add_flag(cli.Flag{ cmd.add_flag(cli.Flag{
flag: .string, flag: .string
name: 'flag', name: 'flag'
global: true, global: true
}) })
subcmd := cli.Command{ subcmd := cli.Command{
name: 'subcommand', name: 'subcommand'
execute: if_global_flag_gets_set_in_subcommand_func, execute: fn (cmd cli.Command) {
flag := cmd.flags.get_string('flag') or {
panic(err)
}
assert flag == 'value'
}
} }
cmd.add_command(subcmd) cmd.add_command(subcmd)
cmd.parse(['command', '--flag', 'value', 'subcommand']) cmd.parse(['command', '-flag', 'value', 'subcommand'])
} }
fn if_global_flag_gets_set_in_subcommand_func(cmd cli.Command) {
flag := cmd.flags.get_string('flag') or { panic(err) }
assert flag == 'value'
}
// helper functions // helper functions
fn empty_func(cmd cli.Command) ? {
fn empty_func(cmd cli.Command) {} }
fn has_command(cmd cli.Command, name string) bool { fn has_command(cmd cli.Command, name string) bool {
for subcmd in cmd.commands { for subcmd in cmd.commands {
@ -174,11 +161,11 @@ fn has_command(cmd cli.Command, name string) bool {
return false return false
} }
fn compare_arrays(array0 []string, array1 []string) bool { fn compare_arrays(array0, array1 []string) bool {
if array0.len != array1.len { if array0.len != array1.len {
return false return false
} }
for i in 0..array0.len { for i in 0 .. array0.len {
if array0[i] != array1[i] { if array0[i] != array1[i] {
return false return false
} }

View File

@ -9,16 +9,15 @@ pub enum FlagType {
pub struct Flag { pub struct Flag {
pub mut: pub mut:
flag FlagType flag FlagType
name string name string
abbrev string abbrev string
description string description string
global bool global bool
required bool required bool
value string value string
mut: mut:
found bool found bool
} }
pub fn (flags []Flag) get_all_found() []Flag { pub fn (flags []Flag) get_all_found() []Flag {
@ -26,73 +25,101 @@ pub fn (flags []Flag) get_all_found() []Flag {
} }
pub fn (flag Flag) get_bool() ?bool { pub fn (flag Flag) get_bool() ?bool {
if flag.flag != .bool { return error('invalid flag type') } if flag.flag != .bool {
return error('invalid flag type')
}
return flag.value == 'true' return flag.value == 'true'
} }
pub fn (flags []Flag) get_bool(name string) ?bool { pub fn (flags []Flag) get_bool(name string) ?bool {
flag := flags.get(name) or { return error(err) } flag := flags.get(name) or {
return error(err)
}
return flag.get_bool() return flag.get_bool()
} }
pub fn (flags []Flag) get_bool_or(name string, or_value bool) bool { pub fn (flags []Flag) get_bool_or(name string, or_value bool) bool {
value := flags.get_bool(name) or { return or_value } value := flags.get_bool(name) or {
return or_value
}
return value return value
} }
pub fn (flag Flag) get_int() ?int { pub fn (flag Flag) get_int() ?int {
if flag.flag != .int { return error('invalid flag type') } if flag.flag != .int {
return error('invalid flag type')
}
return flag.value.int() return flag.value.int()
} }
pub fn (flags []Flag) get_int(name string) ?int { pub fn (flags []Flag) get_int(name string) ?int {
flag := flags.get(name) or { return error(err) } flag := flags.get(name) or {
return error(err)
}
return flag.get_int() return flag.get_int()
} }
pub fn (flags []Flag) get_int_or(name string, or_value int) int { pub fn (flags []Flag) get_int_or(name string, or_value int) int {
value := flags.get_int(name) or { return or_value } value := flags.get_int(name) or {
return or_value
}
return value return value
} }
pub fn (flag Flag) get_float() ?f64 { pub fn (flag Flag) get_float() ?f64 {
if flag.flag != .float { return error('invalid flag type') } if flag.flag != .float {
return error('invalid flag type')
}
return flag.value.f64() return flag.value.f64()
} }
pub fn (flags []Flag) get_float(name string) ?f64 { pub fn (flags []Flag) get_float(name string) ?f64 {
flag := flags.get(name) or { return error(err) } flag := flags.get(name) or {
return error(err)
}
return flag.get_float() return flag.get_float()
} }
pub fn (flags []Flag) get_float_or(name string, or_value f64) f64 { pub fn (flags []Flag) get_float_or(name string, or_value f64) f64 {
value := flags.get_float(name) or { return or_value } value := flags.get_float(name) or {
return or_value
}
return value return value
} }
pub fn (flag Flag) get_string() ?string { pub fn (flag Flag) get_string() ?string {
if flag.flag != .string { return error('invalid flag type') } if flag.flag != .string {
return error('invalid flag type')
}
return flag.value return flag.value
} }
pub fn (flags []Flag) get_string(name string) ?string { pub fn (flags []Flag) get_string(name string) ?string {
flag := flags.get(name) or { return error(err) } flag := flags.get(name) or {
return error(err)
}
return flag.get_string() return flag.get_string()
} }
pub fn (flags []Flag) get_string_or(name string, or_value string) string { pub fn (flags []Flag) get_string_or(name, or_value string) string {
value := flags.get_string(name) or { return or_value } value := flags.get_string(name) or {
return or_value
}
return value return value
} }
// parse flag value from arguments and return arguments with all consumed element removed // parse flag value from arguments and return arguments with all consumed element removed
fn (mut flag Flag) parse(args []string) ?[]string { fn (mut flag Flag) parse(args []string, with_abbrev bool) ?[]string {
if flag.matches(args) { if flag.matches(args, with_abbrev) {
if flag.flag == .bool { if flag.flag == .bool {
new_args := flag.parse_bool(args) or { return error(err) } new_args := flag.parse_bool(args) or {
return error(err)
}
return new_args return new_args
} else { } else {
new_args := flag.parse_raw(args) or { return error(err) } new_args := flag.parse_raw(args) or {
return error(err)
}
return new_args return new_args
} }
} else { } else {
@ -101,12 +128,16 @@ fn (mut flag Flag) parse(args []string) ?[]string {
} }
// check if first arg matches flag // check if first arg matches flag
fn (mut flag Flag) matches(args []string) bool { fn (mut flag Flag) matches(args []string, with_abbrev bool) bool {
return if with_abbrev {
(flag.name != '' && args[0] == '--${flag.name}') || return (flag.name != '' && args[0] == '--$flag.name') ||
(flag.name != '' && args[0].starts_with('--${flag.name}=')) || (flag.name != '' && args[0].starts_with('--$flag.name=')) ||
(flag.abbrev != '' && args[0] == '-${flag.abbrev}') || (flag.abbrev != '' && args[0] == '-$flag.abbrev') ||
(flag.abbrev != '' && args[0].starts_with('-${flag.abbrev}=')) (flag.abbrev != '' && args[0].starts_with('-$flag.abbrev='))
} else {
return (flag.name != '' && args[0] == '-$flag.name') ||
(flag.name != '' && args[0].starts_with('-$flag.name='))
}
} }
fn (mut flag Flag) parse_raw(args []string) ?[]string { fn (mut flag Flag) parse_raw(args []string) ?[]string {
@ -117,7 +148,7 @@ fn (mut flag Flag) parse_raw(args []string) ?[]string {
flag.value = args[1] flag.value = args[1]
return args[2..] return args[2..]
} }
return error('missing argument for ${flag.name}') return error('missing argument for $flag.name')
} }
fn (mut flag Flag) parse_bool(args []string) ?[]string { fn (mut flag Flag) parse_bool(args []string) ?[]string {
@ -140,7 +171,7 @@ fn (flags []Flag) get(name string) ?Flag {
return flag return flag
} }
} }
return error('flag ${name} not found.') return error('flag $name not found.')
} }
fn (flags []Flag) contains(name string) bool { fn (flags []Flag) contains(name string) bool {
@ -153,7 +184,7 @@ fn (flags []Flag) contains(name string) bool {
} }
fn (mut flags []Flag) sort() { fn (mut flags []Flag) sort() {
flags.sort_with_compare(fn(a &Flag, b &Flag) int { flags.sort_with_compare(fn (a, b &Flag) int {
return compare_strings(&a.name, &b.name) return compare_strings(&a.name, &b.name)
}) })
} }

View File

@ -2,74 +2,126 @@ import cli
fn test_if_string_flag_parses() { fn test_if_string_flag_parses() {
mut flag := cli.Flag{ mut flag := cli.Flag{
flag: .string, flag: .string
name: 'flag', name: 'flag'
}
flag.parse(['-flag', 'value'], false) or {
panic(err)
} }
flag.parse(['--flag', 'value']) or { panic(err) }
assert flag.value == 'value' assert flag.value == 'value'
flag.parse(['-flag=value'], false) or {
flag.parse(['--flag=value']) or { panic(err) } panic(err)
}
assert flag.value == 'value' assert flag.value == 'value'
} }
fn test_if_bool_flag_parses() { fn test_if_bool_flag_parses() {
mut flag := cli.Flag{ mut flag := cli.Flag{
flag: .bool, flag: .bool
name: 'flag', name: 'flag'
} }
mut value := false mut value := false
flag.parse(['-flag'], false) or {
flag.parse(['--flag']) or { panic(err) } panic(err)
value = flag.get_bool() or { panic(err) } }
value = flag.get_bool() or {
panic(err)
}
assert value == true assert value == true
flag.parse(['-flag', 'true'], false) or {
flag.parse(['--flag', 'true']) or { panic(err) } panic(err)
value = flag.get_bool() or { panic(err) } }
value = flag.get_bool() or {
panic(err)
}
assert value == true assert value == true
flag.parse(['-flag=true'], false) or {
flag.parse(['--flag=true']) or { panic(err) } panic(err)
value = flag.get_bool() or { panic(err) } }
value = flag.get_bool() or {
panic(err)
}
assert value == true assert value == true
flag.parse(['-flag', 'false'], false) or {
flag.parse(['--flag', 'false']) or { panic(err) } panic(err)
value = flag.get_bool() or { panic(err) } }
value = flag.get_bool() or {
panic(err)
}
assert value == false assert value == false
flag.parse(['-flag=false'], false) or {
flag.parse(['--flag=false']) or { panic(err) } panic(err)
value = flag.get_bool() or { panic(err) } }
value = flag.get_bool() or {
panic(err)
}
assert value == false assert value == false
} }
fn test_if_int_flag_parses() { fn test_if_int_flag_parses() {
mut flag := cli.Flag{ mut flag := cli.Flag{
flag: .int, flag: .int
name: 'flag', name: 'flag'
} }
mut value := 0 mut value := 0
flag.parse(['-flag', '42'], false) or {
flag.parse(['--flag', '42']) or { panic(err) } panic(err)
value = flag.get_int() or { panic(err) } }
value = flag.get_int() or {
panic(err)
}
assert value == 42 assert value == 42
flag.parse(['-flag=42'], false) or {
flag.parse(['--flag=42']) or { panic(err) } panic(err)
value = flag.get_int() or { panic(err) } }
value = flag.get_int() or {
panic(err)
}
assert value == 42 assert value == 42
} }
fn test_if_float_flag_parses() { fn test_if_float_flag_parses() {
mut flag := cli.Flag{ mut flag := cli.Flag{
flag: .float, flag: .float
name: 'flag', name: 'flag'
} }
mut value := f64(0) mut value := f64(0)
flag.parse(['-flag', '3.14159'], false) or {
flag.parse(['--flag', '3.14159']) or { panic(err) } panic(err)
value = flag.get_float() or { panic(err) } }
value = flag.get_float() or {
panic(err)
}
assert value == 3.14159 assert value == 3.14159
flag.parse(['-flag=3.14159'], false) or {
flag.parse(['--flag=3.14159']) or { panic(err) } panic(err)
}
assert flag.value.f64() == 3.14159 assert flag.value.f64() == 3.14159
value = flag.get_float() or { panic(err) } value = flag.get_float() or {
panic(err)
}
assert value == 3.14159 assert value == 3.14159
} }
fn test_if_flag_parses_with_abbrev() {
mut flag := cli.Flag{
flag: .bool
name: 'flag'
abbrev: 'f'
}
mut value := false
flag.parse(['--flag'], true) or {
panic(err)
}
value = flag.get_bool() or {
panic(err)
}
assert value == true
flag.parse(['-flag'], true) or {
panic(err)
}
value = flag.get_bool() or {
panic(err)
}
assert value == true
}

View File

@ -4,72 +4,111 @@ import term
import strings import strings
const ( const (
base_indent_len = 2 base_indent_len = 2
min_description_indent_len = 20 min_description_indent_len = 20
spacing = 2 spacing = 2
) )
fn help_flag(with_abbrev bool) Flag { fn help_flag(with_abbrev bool) Flag {
sabbrev := if with_abbrev { 'h' } else { '' }
return Flag{ return Flag{
flag: .bool, flag: .bool
name: 'help', name: 'help'
abbrev: if with_abbrev { 'h' } else { '' }, abbrev: sabbrev
description: 'Prints help information', description: 'Prints help information'
} }
} }
fn help_cmd() Command { fn help_cmd() Command {
return Command{ return Command{
name: 'help', name: 'help'
description: 'Prints help information', usage: '<command>'
execute: help_func, description: 'Prints help information'
execute: print_help_for_command
} }
} }
fn help_func(help_cmd Command) { fn print_help_for_command(help_cmd Command) ? {
cmd := help_cmd.parent if help_cmd.args.len > 0 {
full_name := cmd.full_name() mut cmd := help_cmd.parent
for arg in help_cmd.args {
mut help := '' mut found := false
help += 'Usage: ${full_name}' for sub_cmd in cmd.commands {
if cmd.flags.len > 0 { help += ' [FLAGS]'} if sub_cmd.name == arg {
if cmd.commands.len > 0 { help += ' [COMMANDS]'} cmd = &sub_cmd
help += '\n\n' found = true
break
if cmd.description != '' { }
help += '${cmd.description}\n\n' }
if !found {
args := help_cmd.args.join(' ')
print('invalid command: $args')
return
}
}
print(cmd.help_message())
} else {
if help_cmd.parent != 0 {
print(help_cmd.parent.help_message())
}
} }
}
fn (cmd Command) help_message() string {
mut help := ''
help += 'Usage: $cmd.full_name()'
if cmd.flags.len > 0 {
help += ' [flags]'
}
if cmd.commands.len > 0 {
help += ' [commands]'
}
if cmd.usage.len > 0 {
help += ' $cmd.usage'
}
help += '\n\n'
if cmd.description != '' {
help += '$cmd.description\n\n'
}
mut abbrev_len := 0 mut abbrev_len := 0
mut name_len := min_description_indent_len mut name_len := min_description_indent_len
for flag in cmd.flags { if cmd.has_abbrev_flags() {
abbrev_len = max(abbrev_len, flag.abbrev.len + spacing + 1) // + 1 for '-' in front for flag in cmd.flags {
name_len = max(name_len, abbrev_len + flag.name.len + spacing + 2) // + 2 for '--' in front abbrev_len = max(abbrev_len, flag.abbrev.len + spacing + 1) // + 1 for '-' in front
name_len = max(name_len, abbrev_len + flag.name.len + spacing + 2) // + 2 for '--' in front
}
for command in cmd.commands {
name_len = max(name_len, command.name.len + spacing)
}
} else {
for flag in cmd.flags {
name_len = max(name_len, abbrev_len + flag.name.len + spacing + 1) // + 1 for '-' in front
}
for command in cmd.commands {
name_len = max(name_len, command.name.len + spacing)
}
} }
for command in cmd.commands {
name_len = max(name_len, command.name.len + spacing)
}
if cmd.flags.len > 0 { if cmd.flags.len > 0 {
help += 'Flags:\n' help += 'Flags:\n'
for flag in cmd.flags { for flag in cmd.flags {
mut flag_name := '' mut flag_name := ''
if flag.abbrev != '' { if flag.abbrev != '' && cmd.has_abbrev_flags() {
abbrev_indent := ' '.repeat(abbrev_len - flag.abbrev.len - 1) // - 1 for '-' in front abbrev_indent := ' '.repeat(abbrev_len - flag.abbrev.len - 1) // - 1 for '-' in front
flag_name = '-${flag.abbrev}${abbrev_indent}--${flag.name}' flag_name = '-$flag.abbrev$abbrev_indent--$flag.name'
} else { } else if cmd.has_abbrev_flags() {
abbrev_indent := ' '.repeat(abbrev_len) abbrev_indent := ' '.repeat(abbrev_len)
flag_name = '${abbrev_indent}--${flag.name}' flag_name = '$abbrev_indent--$flag.name'
} else {
flag_name = '-$flag.name'
} }
mut required := '' mut required := ''
if flag.required { if flag.required {
required = ' (required)' required = ' (required)'
} }
base_indent := ' '.repeat(base_indent_len) base_indent := ' '.repeat(base_indent_len)
description_indent := ' '.repeat(name_len - flag_name.len) description_indent := ' '.repeat(name_len - flag_name.len)
help += '${base_indent}${flag_name}${description_indent}' + help += '$base_indent$flag_name$description_indent' + pretty_description(flag.description +
pretty_description(flag.description + required, base_indent_len + name_len) + '\n' required, base_indent_len + name_len) + '\n'
} }
help += '\n' help += '\n'
} }
@ -78,14 +117,12 @@ fn help_func(help_cmd Command) {
for command in cmd.commands { for command in cmd.commands {
base_indent := ' '.repeat(base_indent_len) base_indent := ' '.repeat(base_indent_len)
description_indent := ' '.repeat(name_len - command.name.len) description_indent := ' '.repeat(name_len - command.name.len)
help += '$base_indent$command.name$description_indent' + pretty_description(command.description, name_len) +
help += '${base_indent}${command.name}${description_indent}' + '\n'
pretty_description(command.description, name_len) + '\n'
} }
help += '\n' help += '\n'
} }
return help
print(help)
} }
// pretty_description resizes description text depending on terminal width. // pretty_description resizes description text depending on terminal width.
@ -93,22 +130,27 @@ fn help_func(help_cmd Command) {
fn pretty_description(s string, indent_len int) string { fn pretty_description(s string, indent_len int) string {
width, _ := term.get_terminal_size() width, _ := term.get_terminal_size()
// Don't prettify if the terminal is that small, it won't be pretty anyway. // Don't prettify if the terminal is that small, it won't be pretty anyway.
if indent_len > width { if indent_len > width {
return s return s
} }
indent := ' '.repeat(indent_len) indent := ' '.repeat(indent_len)
chars_per_line := width - indent_len chars_per_line := width - indent_len
// Give us enough room, better a little bigger than smaller // Give us enough room, better a little bigger than smaller
mut acc := strings.new_builder(((s.len / chars_per_line) + 1) * (width + 1)) mut acc := strings.new_builder(((s.len / chars_per_line) + 1) * (width + 1))
for k, line in s.split('\n') { for k, line in s.split('\n') {
if k != 0 { acc.write('\n${indent}') } if k != 0 {
acc.write('\n$indent')
}
mut i := chars_per_line - 2 mut i := chars_per_line - 2
mut j := 0 mut j := 0
for ; i < line.len; i += chars_per_line - 2 { for ; i < line.len; i += chars_per_line - 2 {
for line.str[i] != ` ` { i-- } for line.str[i] != ` ` {
i--
}
// indent was already done the first iteration // indent was already done the first iteration
if j != 0 { acc.write(indent) } if j != 0 {
acc.write(indent)
}
acc.writeln(line[j..i].trim_space()) acc.writeln(line[j..i].trim_space())
j = i j = i
} }
@ -122,5 +164,6 @@ fn pretty_description(s string, indent_len int) string {
} }
fn max(a, b int) int { fn max(a, b int) int {
return if a > b {a} else {b} res := if a > b { a } else { b }
return res
} }

View File

@ -1,24 +1,25 @@
module cli module cli
fn version_flag(with_abbrev bool) Flag { fn version_flag(with_abbrev bool) Flag {
sabbrev := if with_abbrev { 'v' } else { '' }
return Flag{ return Flag{
flag: .bool, flag: .bool
name: 'version', name: 'version'
abbrev: if with_abbrev { 'v' } else { '' }, abbrev: sabbrev
description: 'Prints version information', description: 'Prints version information'
} }
} }
fn version_cmd() Command { fn version_cmd() Command {
return Command{ return Command{
name: 'version' name: 'version'
description: 'Prints version information', description: 'Prints version information'
execute: version_func, execute: version_func
} }
} }
fn version_func(version_cmd Command) { fn version_func(version_cmd Command) ? {
cmd := version_cmd.parent cmd := version_cmd.parent
version := '${cmd.name} v${cmd.version}' version := '$cmd.name version $cmd.version'
println(version) println(version)
} }