vlib: cli module

pull/2831/head
Tim Basel 2019-11-21 13:03:12 +01:00 committed by Alexander Medvednikov
parent 8c7f5d5cd8
commit 597a6fead2
8 changed files with 691 additions and 0 deletions

1
examples/.gitignore vendored
View File

@ -1,3 +1,4 @@
/cli
/hello_world /hello_world
/json /json
/links_scraper /links_scraper

50
examples/cli.v 100644
View File

@ -0,0 +1,50 @@
module main
import (
cli
os
)
fn main() {
mut cmd := cli.Command{
name: 'cli',
description: 'An example of the cli library',
version: '1.0.0',
}
mut greet_cmd := cli.Command{
name: 'greet',
description: 'Prints greeting in different languages',
execute: greet_func,
}
greet_cmd.add_flag(cli.Flag{
flag: .string,
required: true,
name: 'language',
abbrev: 'l',
description: 'Language of the message'
})
greet_cmd.add_flag(cli.Flag{
flag: .int,
name: 'times',
value: '3',
description: 'Number of times the message gets printed'
})
cmd.add_command(greet_cmd)
cmd.parse(os.args)
}
fn greet_func(cmd cli.Command) {
language := cmd.flags.get_string('language') or { panic('failed to get \'language\' flag: $err') }
times := cmd.flags.get_int('times') or { panic('failed to get \'times\' flag: $err') }
for i := 0; i < times; i++ {
match language {
'english' { println('Hello World') }
'german' { println('Hallo Welt') }
'dutch' { println('Hallo Wereld') }
else { println('unsupported language') }
}
}
}

190
vlib/cli/command.v 100644
View File

@ -0,0 +1,190 @@
module cli
pub struct Command {
pub mut:
name string
description string
version string
execute fn(cmd Command)
disable_help bool
disable_version bool
parent &Command
commands []Command
flags []Flag
args []string
}
pub fn (cmd Command) full_name() string {
if isnil(cmd.parent) {
return cmd.name
}
return cmd.parent.full_name() + ' ${cmd.name}'
}
pub fn (cmd Command) root() Command {
if isnil(cmd.parent) {
return cmd
}
return cmd.parent.root()
}
pub fn (cmd mut Command) add_command(command Command) {
cmd.commands << command
}
pub fn (cmd mut Command) add_flag(flag Flag) {
cmd.flags << flag
}
pub fn (cmd mut Command) parse(args []string) {
cmd.add_default_flags()
cmd.add_default_commands()
cmd.args = args.right(1)
for i := 0; i < cmd.commands.len; i++ {
cmd.commands[i].parent = cmd
}
cmd.parse_flags()
cmd.parse_commands()
}
fn (cmd mut Command) add_default_flags() {
if !cmd.disable_help && !cmd.flags.contains('help') && !cmd.flags.contains('h') {
cmd.add_flag(help_flag())
}
if !cmd.disable_version && cmd.version != '' && !cmd.flags.contains('version') && !cmd.flags.contains('v') {
cmd.add_flag(version_flag())
}
}
fn (cmd mut Command) add_default_commands() {
if !cmd.disable_help && !cmd.commands.contains('help') {
cmd.add_command(help_cmd())
}
if !cmd.disable_version && cmd.version != '' && !cmd.commands.contains('version') {
cmd.add_command(version_cmd())
}
}
fn (cmd mut Command) parse_flags() {
for {
if cmd.args.len < 1 || !cmd.args[0].starts_with('-') {
break
}
mut found := false
for i := 0; i < cmd.flags.len; i++ {
mut flag := &cmd.flags[i]
if flag.matches(cmd.args) {
found = true
mut args := flag.parse(cmd.args) or { // TODO: fix once options types can be assigned to struct variables
println('failed to parse flag ${cmd.args[0]}: ${err}')
exit(1)
}
cmd.args = args
break
}
}
if !found {
println('invalid flag: ${cmd.args[0]}')
exit(1)
}
}
}
fn (cmd mut Command) parse_commands() {
flags := cmd.flags
global_flags := flags.filter(it.global) // TODO: fix once filter can be applied to struct variable
cmd.check_help_flag()
cmd.check_version_flag()
for i := 0; i < cmd.args.len; i++ {
arg := cmd.args[i]
for j := 0; j < cmd.commands.len; j++ {
mut command := cmd.commands[j]
if command.name == arg {
for flag in global_flags {
command.add_flag(flag)
}
command.parse(cmd.args.right(i))
return
}
}
}
// if no further command was found execute current command
if int(cmd.execute) == 0 {
if !cmd.disable_help {
help_cmd := cmd.commands.get('help') or { return } // ignore error and handle command normally
execute := help_cmd.execute
execute(help_cmd)
}
} else {
cmd.check_required_flags()
execute := cmd.execute
execute(cmd) // TODO: fix once higher order function can be execute on struct variable
}
}
fn (cmd mut Command) check_help_flag() {
if cmd.disable_help {
return
}
if cmd.flags.contains('help') {
help_flag := cmd.flags.get_bool('help') or { return } // ignore error and handle command normally
if help_flag {
help_cmd := cmd.commands.get('help') or { return } // ignore error and handle command normally
execute := help_cmd.execute
execute(help_cmd)
exit(0)
}
}
}
fn (cmd mut Command) check_version_flag() {
if cmd.disable_version {
return
}
if cmd.version != '' && cmd.flags.contains('version') {
version_flag := cmd.flags.get_bool('version') or { return } // ignore error and handle command normally
if version_flag {
version_cmd := cmd.commands.get('version') or { return } // ignore error and handle command normally
execute := version_cmd.execute
execute(version_cmd)
exit(0)
}
}
}
fn (cmd mut Command) check_required_flags() {
for flag in cmd.flags {
if flag.required && flag.value == '' {
full_name := cmd.full_name()
println('flag \'${flag.name}\' is required by \'${full_name}\'')
exit(1)
}
}
}
fn (cmds []Command) contains(name string) bool {
for cmd in cmds {
if cmd.name == name {
return true
}
}
return false
}
fn (cmds []Command) get(name string) ?Command {
for cmd in cmds {
if cmd.name == name {
return cmd
}
}
return error('command \'${name}\' not found.')
}

View File

@ -0,0 +1,187 @@
import cli
fn test_if_command_parses_empty_args() {
mut cmd := cli.Command{
name: 'command',
execute: empty_func,
}
cmd.parse(['command'])
assert cmd.name == 'command'
&& compare_arrays(cmd.args, [])
}
fn test_if_command_parses_args() {
mut cmd := cli.Command{
name: 'command',
execute: empty_func,
}
cmd.parse(['command', 'arg0', 'arg1'])
assert cmd.name == 'command'
&& compare_arrays(cmd.args, ['arg0', 'arg1'])
}
fn test_if_subcommands_parse_args() {
mut cmd := cli.Command{
name: 'command',
}
subcmd := cli.Command{
name: 'subcommand',
execute: empty_func,
}
cmd.add_command(subcmd)
cmd.parse(['command', 'subcommand', 'arg0', 'arg1'])
}
fn if_subcommands_parse_args_func(cmd cli.Command) {
assert cmd.name == 'subcommand'
&& compare_arrays(cmd.args, ['arg0', 'arg1'])
}
fn test_if_command_has_default_help_subcommand() {
mut cmd := cli.Command{
name: 'command',
}
cmd.parse(['command'])
assert has_command(cmd, 'help')
}
fn test_if_command_has_default_version_subcommand_if_version_is_set() {
mut cmd := cli.Command{
name: 'command',
version: '1.0.0',
}
cmd.parse(['command'])
assert has_command(cmd, 'version')
}
fn test_if_flag_gets_set() {
mut cmd := cli.Command{
name: 'command',
execute: if_flag_gets_set_func,
}
cmd.add_flag(cli.Flag{
flag: .string
name: 'flag'
})
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() {
mut cmd := cli.Command{
name: 'command',
execute: if_flag_gets_set_with_abbrev_func,
}
cmd.add_flag(cli.Flag{
flag: .string,
name: 'flag',
abbrev: 'f',
})
cmd.parse(['command', '-f', '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() {
mut cmd := cli.Command{
name: 'command',
execute: if_multiple_flags_get_set_func,
}
cmd.add_flag(cli.Flag{
flag: .string
name: 'flag'
})
cmd.add_flag(cli.Flag{
flag: .int
name: 'value'
})
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() {
mut cmd := cli.Command{
name: 'command',
execute: empty_func,
}
mut subcmd := cli.Command{
name: 'subcommand',
execute: if_flag_gets_set_in_subcommand_func
}
subcmd.add_flag(cli.Flag{
flag: .string
name: 'flag'
})
cmd.add_command(subcmd)
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() {
mut cmd := cli.Command{
name: 'command',
execute: empty_func,
}
cmd.add_flag(cli.Flag{
flag: .string,
name: 'flag',
global: true,
})
subcmd := cli.Command{
name: 'subcommand',
execute: if_global_flag_gets_set_in_subcommand_func,
}
cmd.add_command(subcmd)
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
fn empty_func(cmd cli.Command) {}
fn has_command(cmd cli.Command, name string) bool {
for subcmd in cmd.commands {
if subcmd.name == name {
return true
}
}
return false
}
fn compare_arrays(array0 []string, array1 []string) bool {
if array0.len != array1.len {
return false
}
for i := 0; i < array0.len; i++ {
if array0[i] != array1[i] {
return false
}
}
return true
}

110
vlib/cli/flag.v 100644
View File

@ -0,0 +1,110 @@
module cli
pub enum FlagType {
bool
int
float
string
}
pub struct Flag {
pub mut:
flag FlagType
name string
abbrev string
description string
global bool
required bool
value string
}
pub fn (flags []Flag) get_bool(name string) ?bool {
flag := flags.get(name) or { return error(err) }
if flag.flag != .bool { return error('invalid flag type') }
return flag.value == 'true'
}
pub fn (flags []Flag) get_int(name string) ?int {
flag := flags.get(name) or { return error(err) }
if flag.flag != .int { return error('invalid flag type') }
return flag.value.int()
}
pub fn (flags []Flag) get_float(name string) ?f32 {
flag := flags.get(name) or { return error(err) }
if flag.flag != .float { return error('invalid flag type') }
return flag.value.f32()
}
pub fn (flags []Flag) get_string(name string) ?string {
flag := flags.get(name) or { return error(err) }
if flag.flag != .string { return error('invalid flag type') }
return flag.value
}
// parse flag value from arguments and return arguments with all consumed element removed
fn (flag mut Flag) parse(args []string) ?[]string {
if flag.matches(args) {
if flag.flag == .bool {
new_args := flag.parse_bool(args) or { return error(err) }
return new_args
} else {
new_args := flag.parse_raw(args) or { return error(err) }
return new_args
}
} else {
return args
}
}
// check if first arg matches flag
fn (flag mut Flag) matches(args []string) bool {
return
(flag.name != '' && args[0].starts_with('--${flag.name}')) ||
(flag.abbrev != '' && args[0].starts_with('-${flag.abbrev}'))
}
fn (flag mut Flag) parse_raw(args []string) ?[]string {
if args[0].len > flag.name.len && args[0].contains('=') {
println('1')
flag.value = args[0].split('=')[1]
return args.right(1)
} else if args.len >= 2 {
flag.value = args[1]
return args.right(2)
}
return error('missing argument for ${flag.name}')
}
fn (flag mut Flag) parse_bool(args []string) ?[]string {
if args[0].len > flag.name.len && args[0].contains('=') {
flag.value = args[0].split('=')[1]
return args.right(1)
} else if args.len >= 2 {
if args[1] in ['true', 'false'] {
flag.value = args[1]
return args.right(2)
}
}
flag.value = 'true'
return args.right(1)
}
fn (flags []Flag) get(name string) ?Flag {
for flag in flags {
if flag.name == name {
return flag
}
}
return error('flag ${name} not found.')
}
fn (flags []Flag) contains(name string) bool {
for flag in flags {
if flag.name == name || flag.abbrev == name {
return true
}
}
return false
}

View File

@ -0,0 +1,56 @@
import cli
fn test_if_string_flag_parses() {
mut flag := cli.Flag{
flag: .string,
name: 'flag',
}
flag.parse(['--flag', 'value']) or { panic(err) }
assert flag.value == 'value'
flag.parse(['--flag=value']) or { panic(err) }
assert flag.value == 'value'
}
fn test_if_bool_flag_parses() {
mut flag := cli.Flag{
flag: .bool,
name: 'flag',
}
flag.parse(['--flag']) or { panic(err) }
assert flag.value == 'true'
flag.parse(['--flag', 'true']) or { panic(err) }
assert flag.value == 'true'
flag.parse(['--flag=true']) or { panic(err) }
assert flag.value == 'true'
}
fn test_if_int_flag_parses() {
mut flag := cli.Flag{
flag: .int,
name: 'flag',
}
flag.parse(['--flag', '42']) or { panic(err) }
assert flag.value.int() == 42
flag.parse(['--flag=42']) or { panic(err) }
assert flag.value.int() == 42
}
fn test_if_float_flag_parses() {
mut flag := cli.Flag{
flag: .float,
name: 'flag',
}
flag.parse(['--flag', '3.14159']) or { panic(err) }
assert flag.value.f32() == 3.14159
flag.parse(['--flag=3.14159']) or { panic(err) }
assert flag.value.f32() == 3.14159
}

73
vlib/cli/help.v 100644
View File

@ -0,0 +1,73 @@
module cli
const (
BASE_INDENT = 2
ABBREV_INDENT = 5
DESCRIPTION_INDENT = 20
)
fn help_flag() Flag {
return Flag{
flag: .bool,
name: 'help',
abbrev: 'h',
description: 'Prints help information',
}
}
fn help_cmd() Command {
return Command{
name: 'help',
description: 'Prints help information',
execute: help_func,
}
}
fn help_func(help_cmd cli.Command) {
cmd := help_cmd.parent
full_name := cmd.full_name()
mut help := ''
help += 'Usage: ${full_name}'
if cmd.flags.len > 0 { help += ' [FLAGS]'}
if cmd.commands.len > 0 { help += ' [COMMANDS]'}
help += '\n\n'
if cmd.description != '' {
help += '${cmd.description}\n\n'
}
if cmd.flags.len > 0 {
help += 'Flags:\n'
for flag in cmd.flags {
mut flag_name := ''
if flag.abbrev != '' {
abbrev_indent := ' '.repeat(ABBREV_INDENT-(flag.abbrev.len+1))
flag_name = '-${flag.abbrev}${abbrev_indent}--${flag.name}'
} else {
abbrev_indent := ' '.repeat(ABBREV_INDENT-(flag.abbrev.len))
flag_name = '${abbrev_indent}--${flag.name}'
}
mut required := ''
if flag.required {
required = ' (required)'
}
base_indent := ' '.repeat(BASE_INDENT)
description_indent := ' '.repeat(DESCRIPTION_INDENT-flag_name.len)
help += '${base_indent}${flag_name}${description_indent}${flag.description}${required}\n'
}
help += '\n'
}
if cmd.commands.len > 0 {
help += 'Commands:\n'
for command in cmd.commands {
base_indent := ' '.repeat(BASE_INDENT)
description_indent := ' '.repeat(DESCRIPTION_INDENT-command.name.len)
help += '${base_indent}${command.name}${description_indent}${command.description}\n'
}
help += '\n'
}
print(help)
}

24
vlib/cli/version.v 100644
View File

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