aboutsummaryrefslogtreecommitdiff
path: root/v_windows/v/vlib/cli
diff options
context:
space:
mode:
Diffstat (limited to 'v_windows/v/vlib/cli')
-rw-r--r--v_windows/v/vlib/cli/README.md30
-rw-r--r--v_windows/v/vlib/cli/command.v307
-rw-r--r--v_windows/v/vlib/cli/command_test.v222
-rw-r--r--v_windows/v/vlib/cli/flag.v296
-rw-r--r--v_windows/v/vlib/cli/flag_test.v216
-rw-r--r--v_windows/v/vlib/cli/help.v176
-rw-r--r--v_windows/v/vlib/cli/help_test.v65
-rw-r--r--v_windows/v/vlib/cli/version.v25
8 files changed, 1337 insertions, 0 deletions
diff --git a/v_windows/v/vlib/cli/README.md b/v_windows/v/vlib/cli/README.md
new file mode 100644
index 0000000..93d8d62
--- /dev/null
+++ b/v_windows/v/vlib/cli/README.md
@@ -0,0 +1,30 @@
+Usage example:
+
+```v
+module main
+
+import os
+import cli
+
+fn main() {
+ mut app := cli.Command{
+ name: 'example-app'
+ description: 'example-app'
+ execute: fn (cmd cli.Command) ? {
+ println('hello app')
+ return
+ }
+ commands: [
+ cli.Command{
+ name: 'sub'
+ execute: fn (cmd cli.Command) ? {
+ println('hello subcommand')
+ return
+ }
+ },
+ ]
+ }
+ app.setup()
+ app.parse(os.args)
+}
+```
diff --git a/v_windows/v/vlib/cli/command.v b/v_windows/v/vlib/cli/command.v
new file mode 100644
index 0000000..d242e00
--- /dev/null
+++ b/v_windows/v/vlib/cli/command.v
@@ -0,0 +1,307 @@
+module cli
+
+type FnCommandCallback = fn (cmd Command) ?
+
+// str returns the `string` representation of the callback.
+pub fn (f FnCommandCallback) str() string {
+ return 'FnCommandCallback=>' + ptr_str(f)
+}
+
+// Command is a structured representation of a single command
+// or chain of commands.
+pub struct Command {
+pub mut:
+ name string
+ usage string
+ description string
+ version string
+ pre_execute FnCommandCallback
+ execute FnCommandCallback
+ post_execute FnCommandCallback
+ disable_help bool
+ disable_version bool
+ disable_flags bool
+ sort_flags bool
+ sort_commands bool
+ parent &Command = 0
+ commands []Command
+ flags []Flag
+ required_args int
+ args []string
+ posix_mode bool
+}
+
+// str returns the `string` representation of the `Command`.
+pub fn (cmd Command) str() string {
+ mut res := []string{}
+ 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 ...}'
+ }
+ res << ' commands: $cmd.commands'
+ res << ' flags: $cmd.flags'
+ res << ' required_args: $cmd.required_args'
+ res << ' args: $cmd.args'
+ res << '}'
+ return res.join('\n')
+}
+
+// is_root returns `true` if this `Command` has no parents.
+pub fn (cmd Command) is_root() bool {
+ return isnil(cmd.parent)
+}
+
+// root returns the root `Command` of the command chain.
+pub fn (cmd Command) root() Command {
+ if cmd.is_root() {
+ return cmd
+ }
+ return cmd.parent.root()
+}
+
+// full_name returns the full `string` representation of all commands int the chain.
+pub fn (cmd Command) full_name() string {
+ if cmd.is_root() {
+ return cmd.name
+ }
+ return cmd.parent.full_name() + ' $cmd.name'
+}
+
+// add_commands adds the `commands` array of `Command`s as sub-commands.
+pub fn (mut cmd Command) add_commands(commands []Command) {
+ for command in commands {
+ cmd.add_command(command)
+ }
+}
+
+// add_command adds `command` as a sub-command of this `Command`.
+pub fn (mut cmd Command) add_command(command Command) {
+ mut subcmd := command
+ if cmd.commands.contains(subcmd.name) {
+ println('Command with the name `$subcmd.name` already exists')
+ exit(1)
+ }
+ subcmd.parent = unsafe { cmd }
+ cmd.commands << subcmd
+}
+
+// setup ensures that all sub-commands of this `Command`
+// is linked as a chain.
+pub fn (mut cmd Command) setup() {
+ for mut subcmd in cmd.commands {
+ subcmd.parent = unsafe { cmd }
+ subcmd.posix_mode = cmd.posix_mode
+ subcmd.setup()
+ }
+}
+
+// add_flags adds the array `flags` to this `Command`.
+pub fn (mut cmd Command) add_flags(flags []Flag) {
+ for flag in flags {
+ cmd.add_flag(flag)
+ }
+}
+
+// add_flag adds `flag` to this `Command`.
+pub fn (mut cmd Command) add_flag(flag Flag) {
+ if cmd.flags.contains(flag.name) {
+ println('Flag with the name `$flag.name` already exists')
+ exit(1)
+ }
+ cmd.flags << flag
+}
+
+// parse parses `args` into this structured `Command`.
+pub fn (mut cmd Command) parse(args []string) {
+ if !cmd.disable_flags {
+ cmd.add_default_flags()
+ }
+ cmd.add_default_commands()
+ if cmd.sort_flags {
+ cmd.flags.sort(a.name < b.name)
+ }
+ if cmd.sort_commands {
+ cmd.commands.sort(a.name < b.name)
+ }
+ cmd.args = args[1..]
+ if !cmd.disable_flags {
+ cmd.parse_flags()
+ }
+ cmd.parse_commands()
+}
+
+// add_default_flags adds the commonly used `-h`/`--help` and
+// `-v`/`--version` flags to the `Command`.
+fn (mut cmd Command) add_default_flags() {
+ if !cmd.disable_help && !cmd.flags.contains('help') {
+ use_help_abbrev := !cmd.flags.contains('h') && cmd.posix_mode
+ cmd.add_flag(help_flag(use_help_abbrev))
+ }
+ if !cmd.disable_version && cmd.version != '' && !cmd.flags.contains('version') {
+ use_version_abbrev := !cmd.flags.contains('v') && cmd.posix_mode
+ cmd.add_flag(version_flag(use_version_abbrev))
+ }
+}
+
+// add_default_commands adds the command functions of the
+// commonly used `help` and `version` flags to the `Command`.
+fn (mut cmd Command) add_default_commands() {
+ if !cmd.disable_help && !cmd.commands.contains('help') && cmd.is_root() {
+ cmd.add_command(help_cmd())
+ }
+ if !cmd.disable_version && cmd.version != '' && !cmd.commands.contains('version') {
+ cmd.add_command(version_cmd())
+ }
+}
+
+fn (mut cmd Command) parse_flags() {
+ for {
+ if cmd.args.len < 1 || !cmd.args[0].starts_with('-') {
+ break
+ }
+ mut found := false
+ for i in 0 .. cmd.flags.len {
+ unsafe {
+ mut flag := &cmd.flags[i]
+ if flag.matches(cmd.args, cmd.posix_mode) {
+ found = true
+ flag.found = true
+ cmd.args = flag.parse(cmd.args, cmd.posix_mode) or {
+ println('Failed to parse flag `${cmd.args[0]}`: $err')
+ exit(1)
+ }
+ break
+ }
+ }
+ }
+ if !found {
+ println('Command `$cmd.name` has no flag `${cmd.args[0]}`')
+ exit(1)
+ }
+ }
+}
+
+fn (mut cmd Command) parse_commands() {
+ global_flags := cmd.flags.filter(it.global)
+ cmd.check_help_flag()
+ cmd.check_version_flag()
+ for i in 0 .. cmd.args.len {
+ arg := cmd.args[i]
+ for j in 0 .. cmd.commands.len {
+ mut command := cmd.commands[j]
+ if command.name == arg {
+ for flag in global_flags {
+ command.add_flag(flag)
+ }
+ command.parse(cmd.args[i..])
+ return
+ }
+ }
+ }
+ if cmd.is_root() && isnil(cmd.execute) {
+ if !cmd.disable_help {
+ cmd.execute_help()
+ return
+ }
+ }
+ // if no further command was found, execute current command
+ if cmd.required_args > 0 {
+ if cmd.required_args > cmd.args.len {
+ eprintln('Command `$cmd.name` needs at least $cmd.required_args arguments')
+ exit(1)
+ }
+ }
+ cmd.check_required_flags()
+ if !isnil(cmd.pre_execute) {
+ cmd.pre_execute(*cmd) or {
+ eprintln('cli preexecution error: $err')
+ exit(1)
+ }
+ }
+ if !isnil(cmd.execute) {
+ cmd.execute(*cmd) or {
+ eprintln('cli execution error: $err')
+ exit(1)
+ }
+ }
+ if !isnil(cmd.post_execute) {
+ cmd.post_execute(*cmd) or {
+ eprintln('cli postexecution error: $err')
+ exit(1)
+ }
+ }
+}
+
+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 {
+ cmd.execute_help()
+ exit(0)
+ }
+ }
+}
+
+fn (cmd Command) check_version_flag() {
+ if !cmd.disable_version && 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
+ version_cmd.execute(version_cmd) or { panic(err) }
+ exit(0)
+ }
+ }
+}
+
+fn (cmd Command) check_required_flags() {
+ for flag in cmd.flags {
+ if flag.required && flag.value.len == 0 {
+ full_name := cmd.full_name()
+ println('Flag `$flag.name` is required by `$full_name`')
+ exit(1)
+ }
+ }
+}
+
+// execute_help executes the callback registered
+// for the `-h`/`--help` flag option.
+pub 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) or { panic(err) }
+ } else {
+ print(cmd.help_message())
+ }
+}
+
+fn (cmds []Command) get(name string) ?Command {
+ for cmd in cmds {
+ if cmd.name == name {
+ return cmd
+ }
+ }
+ return error('Command `$name` not found in $cmds')
+}
+
+fn (cmds []Command) contains(name string) bool {
+ for cmd in cmds {
+ if cmd.name == name {
+ return true
+ }
+ }
+ return false
+}
diff --git a/v_windows/v/vlib/cli/command_test.v b/v_windows/v/vlib/cli/command_test.v
new file mode 100644
index 0000000..e3c69b1
--- /dev/null
+++ b/v_windows/v/vlib/cli/command_test.v
@@ -0,0 +1,222 @@
+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: if_subcommands_parse_args_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 flag_should_be_set(cmd cli.Command) ? {
+ flag := cmd.flags.get_string('flag') ?
+ assert flag == 'value'
+}
+
+fn test_if_flag_gets_set() {
+ mut cmd := cli.Command{
+ name: 'command'
+ execute: flag_should_be_set
+ }
+ cmd.add_flag(cli.Flag{
+ flag: .string
+ name: 'flag'
+ })
+ cmd.parse(['command', '-flag', 'value'])
+}
+
+fn test_if_flag_gets_set_with_abbrev() {
+ mut cmd := cli.Command{
+ name: 'command'
+ execute: flag_should_be_set
+ }
+ cmd.add_flag(cli.Flag{
+ flag: .string
+ name: 'flag'
+ abbrev: 'f'
+ })
+ cmd.parse(['command', '-f', 'value'])
+}
+
+fn test_if_flag_gets_set_with_long_arg() {
+ mut cmd := cli.Command{
+ name: 'command'
+ execute: flag_should_be_set
+ posix_mode: true
+ }
+ cmd.add_flag(cli.Flag{
+ flag: .string
+ name: 'flag'
+ abbrev: 'f'
+ })
+ cmd.parse(['command', '--flag', 'value'])
+}
+
+fn flag_should_have_value_of_42(cmd cli.Command) ? {
+ flag := cmd.flags.get_string('flag') ?
+ assert flag == 'value'
+ value := cmd.flags.get_int('value') ?
+ assert value == 42
+}
+
+fn test_if_multiple_flags_get_set() {
+ mut cmd := cli.Command{
+ name: 'command'
+ execute: flag_should_have_value_of_42
+ }
+ 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 test_if_required_flags_get_set() {
+ mut cmd := cli.Command{
+ name: 'command'
+ execute: flag_should_have_value_of_42
+ }
+ cmd.add_flag(cli.Flag{
+ flag: .string
+ name: 'flag'
+ })
+ cmd.add_flag(cli.Flag{
+ flag: .int
+ name: 'value'
+ required: true
+ })
+ cmd.parse(['command', '-flag', 'value', '-value', '42'])
+}
+
+fn flag_is_set_in_subcommand(cmd cli.Command) ? {
+ flag := cmd.flags.get_string('flag') or { panic(err) }
+ assert flag == 'value'
+}
+
+fn test_if_flag_gets_set_in_subcommand() {
+ mut cmd := cli.Command{
+ name: 'command'
+ execute: empty_func
+ }
+ mut subcmd := cli.Command{
+ name: 'subcommand'
+ execute: flag_is_set_in_subcommand
+ }
+ subcmd.add_flag(cli.Flag{
+ flag: .string
+ name: 'flag'
+ })
+ cmd.add_command(subcmd)
+ cmd.parse(['command', 'subcommand', '-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: flag_is_set_in_subcommand
+ }
+ cmd.add_command(subcmd)
+ cmd.parse(['command', '-flag', 'value', 'subcommand'])
+}
+
+fn test_command_setup() {
+ mut cmd := cli.Command{
+ name: 'root'
+ commands: [
+ cli.Command{
+ name: 'child'
+ commands: [
+ cli.Command{
+ name: 'child-child'
+ },
+ ]
+ },
+ ]
+ }
+ assert isnil(cmd.commands[0].parent)
+ assert isnil(cmd.commands[0].commands[0].parent)
+ cmd.setup()
+ assert cmd.commands[0].parent.name == 'root'
+ assert cmd.commands[0].commands[0].parent.name == 'child'
+}
+
+// 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 in 0 .. array0.len {
+ if array0[i] != array1[i] {
+ return false
+ }
+ }
+ return true
+}
diff --git a/v_windows/v/vlib/cli/flag.v b/v_windows/v/vlib/cli/flag.v
new file mode 100644
index 0000000..fb8fa09
--- /dev/null
+++ b/v_windows/v/vlib/cli/flag.v
@@ -0,0 +1,296 @@
+module cli
+
+pub enum FlagType {
+ bool
+ int
+ float
+ string
+ // If flag can set multiple time, use array type
+ int_array
+ float_array
+ string_array
+}
+
+// Flag holds information for a command line flag.
+// (flags are also commonly referred to as "options" or "switches")
+// These are typically denoted in the shell by a short form `-f` and/or a long form `--flag`
+pub struct Flag {
+pub mut:
+ flag FlagType
+ // Name of flag
+ name string
+ // Like short option
+ abbrev string
+ // Desciption of flag
+ description string
+ global bool
+ // If flag is requierd
+ required bool
+ // Default value if no value provide by command line
+ default_value []string = []
+mut:
+ // Set true if flag found.
+ found bool
+ // Value of flag
+ value []string = []
+}
+
+// get_all_found returns an array of all `Flag`s found in the command parameters
+pub fn (flags []Flag) get_all_found() []Flag {
+ return flags.filter(it.found)
+}
+
+// get_bool returns `true` if the flag is set.
+// get_bool returns an error if the `FlagType` is not boolean.
+pub fn (flag Flag) get_bool() ?bool {
+ if flag.flag != .bool {
+ return error('$flag.name: Invalid flag type `$flag.flag`, expected `bool`')
+ }
+
+ val := flag.get_value_or_default_value()
+
+ return val.len > 0 && val[0] == 'true'
+}
+
+// get_bool returns `true` if the flag specified in `name` is set.
+// get_bool returns an error if the `FlagType` is not boolean.
+pub fn (flags []Flag) get_bool(name string) ?bool {
+ flag := flags.get(name) ?
+ return flag.get_bool()
+}
+
+// get_int returns the `int` value argument of the flag.
+// get_int returns an error if the `FlagType` is not integer.
+pub fn (flag Flag) get_int() ?int {
+ if flag.flag != .int {
+ return error('$flag.name: Invalid flag type `$flag.flag`, expected `int`')
+ }
+
+ val := flag.get_value_or_default_value()
+
+ if val.len == 0 {
+ return 0
+ } else {
+ return val[0].int()
+ }
+}
+
+// get_ints returns the array of `int` value argument of the flag specified in `name`.
+// get_ints returns an error if the `FlagType` is not integer.
+pub fn (flag Flag) get_ints() ?[]int {
+ if flag.flag != .int_array {
+ return error('$flag.name: Invalid flag type `$flag.flag`, expected `int_array`')
+ }
+
+ val := flag.get_value_or_default_value()
+
+ if val.len == 0 {
+ return []int{}
+ } else {
+ mut values := []int{}
+
+ for f in val {
+ values << f.int()
+ }
+
+ return values
+ }
+}
+
+// get_int returns the `int` value argument of the flag specified in `name`.
+// get_int returns an error if the `FlagType` is not integer.
+pub fn (flags []Flag) get_int(name string) ?int {
+ flag := flags.get(name) ?
+ return flag.get_int()
+}
+
+// get_ints returns the array of `int` value argument of the flag specified in `name`.
+// get_ints returns an error if the `FlagType` is not integer.
+pub fn (flags []Flag) get_ints(name string) ?[]int {
+ flag := flags.get(name) ?
+ return flag.get_ints()
+}
+
+// get_float returns the `f64` value argument of the flag.
+// get_float returns an error if the `FlagType` is not floating point.
+pub fn (flag Flag) get_float() ?f64 {
+ if flag.flag != .float {
+ return error('$flag.name: Invalid flag type `$flag.flag`, expected `float`')
+ }
+
+ val := flag.get_value_or_default_value()
+
+ if val.len == 0 {
+ return 0.0
+ } else {
+ return val[0].f64()
+ }
+}
+
+// get_floats returns the `f64` value argument of the flag.
+// get_floats returns an error if the `FlagType` is not floating point.
+pub fn (flag Flag) get_floats() ?[]f64 {
+ if flag.flag != .float_array {
+ return error('$flag.name: Invalid flag type `$flag.flag`, expected `float_array`')
+ }
+
+ val := flag.get_value_or_default_value()
+
+ if val.len == 0 {
+ return []f64{}
+ } else {
+ mut values := []f64{}
+
+ for f in val {
+ values << f.f64()
+ }
+
+ return values
+ }
+}
+
+// get_float returns the `f64` value argument of the flag specified in `name`.
+// get_float returns an error if the `FlagType` is not floating point.
+pub fn (flags []Flag) get_float(name string) ?f64 {
+ flag := flags.get(name) ?
+ return flag.get_float()
+}
+
+// get_floats returns the array of `f64` value argument of the flag specified in `name`.
+// get_floats returns an error if the `FlagType` is not floating point.
+pub fn (flags []Flag) get_floats(name string) ?[]f64 {
+ flag := flags.get(name) ?
+ return flag.get_floats()
+}
+
+// get_string returns the `string` value argument of the flag.
+// get_string returns an error if the `FlagType` is not string.
+pub fn (flag Flag) get_string() ?string {
+ if flag.flag != .string {
+ return error('$flag.name: Invalid flag type `$flag.flag`, expected `string`')
+ }
+
+ val := flag.get_value_or_default_value()
+
+ if val.len == 0 {
+ return ''
+ } else {
+ return val[0]
+ }
+}
+
+// get_strings returns the array of `string` value argument of the flag.
+// get_strings returns an error if the `FlagType` is not string.
+pub fn (flag Flag) get_strings() ?[]string {
+ if flag.flag != .string_array {
+ return error('$flag.name: Invalid flag type `$flag.flag`, expected `string_array`')
+ }
+
+ val := flag.get_value_or_default_value()
+
+ if val.len == 0 {
+ return []string{}
+ } else {
+ return val
+ }
+}
+
+// get_string returns the `string` value argument of the flag specified in `name`.
+// get_string returns an error if the `FlagType` is not string.
+pub fn (flags []Flag) get_string(name string) ?string {
+ flag := flags.get(name) ?
+ return flag.get_string()
+}
+
+// get_strings returns the `string` value argument of the flag specified in `name`.
+// get_strings returns an error if the `FlagType` is not string.
+pub fn (flags []Flag) get_strings(name string) ?[]string {
+ flag := flags.get(name) ?
+ return flag.get_strings()
+}
+
+// parse parses flag values from arguments and return
+// an array of arguments with all consumed elements removed.
+fn (mut flag Flag) parse(args []string, posix_mode bool) ?[]string {
+ if flag.matches(args, posix_mode) {
+ if flag.flag == .bool {
+ new_args := flag.parse_bool(args) ?
+ return new_args
+ } else {
+ if flag.value.len > 0 && flag.flag != .int_array && flag.flag != .float_array
+ && flag.flag != .string_array {
+ return error('The argument `$flag.name` accept only one value!')
+ }
+
+ new_args := flag.parse_raw(args) ?
+ return new_args
+ }
+ } else {
+ return args
+ }
+}
+
+// matches returns `true` if first arg in `args` matches this flag.
+fn (mut flag Flag) matches(args []string, posix_mode bool) bool {
+ prefix := if posix_mode { '--' } else { '-' }
+ return (flag.name != '' && args[0] == '$prefix$flag.name')
+ || (flag.name != '' && args[0].starts_with('$prefix$flag.name='))
+ || (flag.abbrev != '' && args[0] == '-$flag.abbrev')
+ || (flag.abbrev != '' && args[0].starts_with('-$flag.abbrev='))
+}
+
+fn (mut flag Flag) parse_raw(args []string) ?[]string {
+ if args[0].len > flag.name.len && args[0].contains('=') {
+ flag.value << args[0].split('=')[1]
+ return args[1..]
+ } else if args.len >= 2 {
+ flag.value << args[1]
+ return args[2..]
+ }
+ return error('Missing argument for `$flag.name`')
+}
+
+fn (mut flag Flag) parse_bool(args []string) ?[]string {
+ if args[0].len > flag.name.len && args[0].contains('=') {
+ flag.value = [args[0].split('=')[1]]
+ return args[1..]
+ } else if args.len >= 2 {
+ if args[1] in ['true', 'false'] {
+ flag.value = [args[1]]
+ return args[2..]
+ }
+ }
+ // In fact bool cannot be multiple
+ flag.value = ['true']
+ return args[1..]
+}
+
+// get returns the `Flag` matching `name` or an error
+// if it can't be found.
+fn (flags []Flag) get(name string) ?Flag {
+ for flag in flags {
+ if flag.name == name {
+ return flag
+ }
+ }
+ return error('Flag `$name` not found in $flags')
+}
+
+fn (flags []Flag) contains(name string) bool {
+ for flag in flags {
+ if flag.name == name || flag.abbrev == name {
+ return true
+ }
+ }
+ return false
+}
+
+// Check if value is set by command line option. If not, return default value.
+fn (flag Flag) get_value_or_default_value() []string {
+ if flag.value.len == 0 && flag.default_value.len > 0 {
+ // If default value is set and no value provide, use default value.
+ return flag.default_value
+ } else {
+ return flag.value
+ }
+}
diff --git a/v_windows/v/vlib/cli/flag_test.v b/v_windows/v/vlib/cli/flag_test.v
new file mode 100644
index 0000000..a40a7c0
--- /dev/null
+++ b/v_windows/v/vlib/cli/flag_test.v
@@ -0,0 +1,216 @@
+import cli
+
+fn test_if_string_flag_parses() {
+ mut flag := cli.Flag{
+ flag: .string
+ name: 'flag'
+ }
+ flag.parse(['-flag', 'value1'], false) or { panic(err) }
+ mut value := flag.get_string() or { panic(err) }
+ assert value == 'value1'
+
+ flag = cli.Flag{
+ flag: .string
+ name: 'flag'
+ }
+ flag.parse(['-flag=value2'], false) or { panic(err) }
+ value = flag.get_string() or { panic(err) }
+ assert value == 'value2'
+
+ flag = cli.Flag{
+ flag: .string_array
+ name: 'flag'
+ }
+ flag.parse(['-flag=value1'], false) or { panic(err) }
+ flag.parse(['-flag=value2'], false) or { panic(err) }
+ mut values := flag.get_strings() or { panic(err) }
+ assert values == ['value1', 'value2']
+
+ flags := [
+ cli.Flag{
+ flag: .string_array
+ name: 'flag'
+ value: ['a', 'b', 'c']
+ },
+ cli.Flag{
+ flag: .string
+ name: 'flag2'
+ },
+ ]
+
+ values = flags.get_strings('flag') or { panic(err) }
+ assert values == ['a', 'b', 'c']
+}
+
+fn test_if_bool_flag_parses() {
+ mut flag := cli.Flag{
+ flag: .bool
+ name: 'flag'
+ }
+ mut value := false
+ flag.parse(['-flag'], false) or { panic(err) }
+ value = flag.get_bool() or { panic(err) }
+ assert value == true
+ flag.parse(['-flag', 'false'], false) or { panic(err) }
+ value = flag.get_bool() or { panic(err) }
+ assert value == false
+ flag.parse(['-flag', 'true'], false) or { panic(err) }
+ value = flag.get_bool() or { panic(err) }
+ assert value == true
+ flag.parse(['-flag=false'], false) or { panic(err) }
+ value = flag.get_bool() or { panic(err) }
+ assert value == false
+ flag.parse(['-flag=true'], false) or { panic(err) }
+ value = flag.get_bool() or { panic(err) }
+ assert value == true
+}
+
+fn test_if_int_flag_parses() {
+ mut flag := cli.Flag{
+ flag: .int
+ name: 'flag'
+ }
+
+ mut value := 0
+ flag.parse(['-flag', '42'], false) or { panic(err) }
+ value = flag.get_int() or { panic(err) }
+ assert value == 42
+
+ flag = cli.Flag{
+ flag: .int
+ name: 'flag'
+ }
+
+ flag.parse(['-flag=45'], false) or { panic(err) }
+ value = flag.get_int() or { panic(err) }
+ assert value == 45
+
+ flag = cli.Flag{
+ flag: .int_array
+ name: 'flag'
+ }
+
+ flag.parse(['-flag=42'], false) or { panic(err) }
+ flag.parse(['-flag=45'], false) or { panic(err) }
+ mut values := flag.get_ints() or { panic(err) }
+ assert values == [42, 45]
+
+ flags := [
+ cli.Flag{
+ flag: .int_array
+ name: 'flag'
+ value: ['1', '2', '3']
+ },
+ cli.Flag{
+ flag: .int
+ name: 'flag2'
+ },
+ ]
+
+ values = flags.get_ints('flag') or { panic(err) }
+ assert values == [1, 2, 3]
+}
+
+fn test_if_float_flag_parses() {
+ mut flag := cli.Flag{
+ flag: .float
+ name: 'flag'
+ }
+ mut value := f64(0)
+ flag.parse(['-flag', '3.14158'], false) or { panic(err) }
+ value = flag.get_float() or { panic(err) }
+ assert value == 3.14158
+
+ flag = cli.Flag{
+ flag: .float
+ name: 'flag'
+ }
+
+ flag.parse(['-flag=3.14159'], false) or { panic(err) }
+ value = flag.get_float() or { panic(err) }
+ assert value == 3.14159
+
+ flag = cli.Flag{
+ flag: .float_array
+ name: 'flag'
+ }
+
+ flag.parse(['-flag=3.1'], false) or { panic(err) }
+ flag.parse(['-flag=1.3'], false) or { panic(err) }
+ mut values := flag.get_floats() or { panic(err) }
+ assert values == [3.1, 1.3]
+
+ flags := [
+ cli.Flag{
+ flag: .float_array
+ name: 'flag'
+ value: ['1.1', '2.2', '3.3']
+ },
+ cli.Flag{
+ flag: .float
+ name: 'flag2'
+ },
+ ]
+
+ values = flags.get_floats('flag') or { panic(err) }
+ assert values == [1.1, 2.2, 3.3]
+}
+
+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
+
+ value = false
+ flag = cli.Flag{
+ flag: .bool
+ name: 'flag'
+ abbrev: 'f'
+ }
+ flag.parse(['-f'], true) or { panic(err) }
+ value = flag.get_bool() or { panic(err) }
+ assert value == true
+}
+
+fn test_if_multiple_value_on_single_value() {
+ mut flag := cli.Flag{
+ flag: .float
+ name: 'flag'
+ }
+
+ flag.parse(['-flag', '3.14158'], false) or { panic(err) }
+
+ if _ := flag.parse(['-flag', '3.222'], false) {
+ panic("No multiple value flag don't raise an error!")
+ } else {
+ assert true
+ }
+}
+
+fn test_default_value() {
+ mut flag := cli.Flag{
+ flag: .float
+ name: 'flag'
+ default_value: ['1.234']
+ }
+
+ flag.parse(['-flag', '3.14158'], false) or { panic(err) }
+ mut value := flag.get_float() or { panic(err) }
+ assert value == 3.14158
+
+ flag = cli.Flag{
+ flag: .float
+ name: 'flag'
+ default_value: ['1.234']
+ }
+
+ flag.parse([''], false) or { panic(err) }
+ value = flag.get_float() or { panic(err) }
+ assert value == 1.234
+}
diff --git a/v_windows/v/vlib/cli/help.v b/v_windows/v/vlib/cli/help.v
new file mode 100644
index 0000000..f47f90e
--- /dev/null
+++ b/v_windows/v/vlib/cli/help.v
@@ -0,0 +1,176 @@
+module cli
+
+import term
+import strings
+
+const (
+ base_indent_len = 2
+ min_description_indent_len = 20
+ spacing = 2
+)
+
+fn help_flag(with_abbrev bool) Flag {
+ sabbrev := if with_abbrev { 'h' } else { '' }
+ return Flag{
+ flag: .bool
+ name: 'help'
+ abbrev: sabbrev
+ description: 'Prints help information.'
+ }
+}
+
+fn help_cmd() Command {
+ return Command{
+ name: 'help'
+ usage: '<command>'
+ description: 'Prints help information.'
+ execute: print_help_for_command
+ }
+}
+
+fn print_help_for_command(help_cmd Command) ? {
+ if help_cmd.args.len > 0 {
+ mut cmd := help_cmd.parent
+ for arg in help_cmd.args {
+ mut found := false
+ for sub_cmd in cmd.commands {
+ if sub_cmd.name == arg {
+ cmd = unsafe { &sub_cmd }
+ found = true
+ break
+ }
+ }
+ if !found {
+ args := help_cmd.args.join(' ')
+ println('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'
+ } else {
+ for i in 0 .. cmd.required_args {
+ help += ' <arg$i>'
+ }
+ }
+ help += '\n'
+ if cmd.description != '' {
+ help += '\n$cmd.description\n'
+ }
+ mut abbrev_len := 0
+ mut name_len := cli.min_description_indent_len
+ if cmd.posix_mode {
+ for flag in cmd.flags {
+ if flag.abbrev != '' {
+ abbrev_len = max(abbrev_len, flag.abbrev.len + cli.spacing + 1) // + 1 for '-' in front
+ }
+ name_len = max(name_len, abbrev_len + flag.name.len + cli.spacing + 2) // + 2 for '--' in front
+ }
+ for command in cmd.commands {
+ name_len = max(name_len, command.name.len + cli.spacing)
+ }
+ } else {
+ for flag in cmd.flags {
+ if flag.abbrev != '' {
+ abbrev_len = max(abbrev_len, flag.abbrev.len + cli.spacing + 1) // + 1 for '-' in front
+ }
+ name_len = max(name_len, abbrev_len + flag.name.len + cli.spacing + 1) // + 1 for '-' in front
+ }
+ for command in cmd.commands {
+ name_len = max(name_len, command.name.len + cli.spacing)
+ }
+ }
+ if cmd.flags.len > 0 {
+ help += '\nFlags:\n'
+ for flag in cmd.flags {
+ mut flag_name := ''
+ prefix := if cmd.posix_mode { '--' } else { '-' }
+ if flag.abbrev != '' {
+ abbrev_indent := ' '.repeat(abbrev_len - flag.abbrev.len - 1) // - 1 for '-' in front
+ flag_name = '-$flag.abbrev$abbrev_indent$prefix$flag.name'
+ } else {
+ abbrev_indent := ' '.repeat(abbrev_len)
+ flag_name = '$abbrev_indent$prefix$flag.name'
+ }
+ mut required := ''
+ if flag.required {
+ required = ' (required)'
+ }
+ base_indent := ' '.repeat(cli.base_indent_len)
+ description_indent := ' '.repeat(name_len - flag_name.len)
+ help += '$base_indent$flag_name$description_indent' +
+ pretty_description(flag.description + required, cli.base_indent_len + name_len) +
+ '\n'
+ }
+ }
+ if cmd.commands.len > 0 {
+ help += '\nCommands:\n'
+ for command in cmd.commands {
+ base_indent := ' '.repeat(cli.base_indent_len)
+ description_indent := ' '.repeat(name_len - command.name.len)
+ help += '$base_indent$command.name$description_indent' +
+ pretty_description(command.description, name_len) + '\n'
+ }
+ }
+ return help
+}
+
+// pretty_description resizes description text depending on terminal width.
+// Essentially, smart wrap-around
+fn pretty_description(s string, indent_len int) string {
+ width, _ := term.get_terminal_size()
+ // Don't prettify if the terminal is that small, it won't be pretty anyway.
+ if indent_len > width {
+ return s
+ }
+ indent := ' '.repeat(indent_len)
+ chars_per_line := width - indent_len
+ // Give us enough room, better a little bigger than smaller
+ mut acc := strings.new_builder(((s.len / chars_per_line) + 1) * (width + 1))
+ for k, line in s.split('\n') {
+ if k != 0 {
+ acc.write_string('\n$indent')
+ }
+ mut i := chars_per_line - 2
+ mut j := 0
+ for ; i < line.len; i += chars_per_line - 2 {
+ for line[i] != ` ` {
+ i--
+ }
+ // indent was already done the first iteration
+ if j != 0 {
+ acc.write_string(indent)
+ }
+ acc.writeln(line[j..i].trim_space())
+ j = i
+ }
+ // We need this even though it should never happen
+ if j != 0 {
+ acc.write_string(indent)
+ }
+ acc.write_string(line[j..].trim_space())
+ }
+ return acc.str()
+}
+
+fn max(a int, b int) int {
+ res := if a > b { a } else { b }
+ return res
+}
diff --git a/v_windows/v/vlib/cli/help_test.v b/v_windows/v/vlib/cli/help_test.v
new file mode 100644
index 0000000..62cb37e
--- /dev/null
+++ b/v_windows/v/vlib/cli/help_test.v
@@ -0,0 +1,65 @@
+module cli
+
+fn test_help_message() {
+ mut cmd := Command{
+ name: 'command'
+ description: 'description'
+ commands: [
+ Command{
+ name: 'sub'
+ description: 'subcommand'
+ },
+ Command{
+ name: 'sub2'
+ description: 'another subcommand'
+ },
+ ]
+ flags: [
+ Flag{
+ flag: .string
+ name: 'str'
+ description: 'str flag'
+ },
+ Flag{
+ flag: .bool
+ name: 'bool'
+ description: 'bool flag'
+ abbrev: 'b'
+ },
+ Flag{
+ flag: .string
+ name: 'required'
+ abbrev: 'r'
+ required: true
+ },
+ ]
+ }
+ assert cmd.help_message() == r'Usage: command [flags] [commands]
+
+description
+
+Flags:
+ -str str flag
+ -b -bool bool flag
+ -r -required (required)
+
+Commands:
+ sub subcommand
+ sub2 another subcommand
+'
+
+ cmd.posix_mode = true
+ assert cmd.help_message() == r'Usage: command [flags] [commands]
+
+description
+
+Flags:
+ --str str flag
+ -b --bool bool flag
+ -r --required (required)
+
+Commands:
+ sub subcommand
+ sub2 another subcommand
+'
+}
diff --git a/v_windows/v/vlib/cli/version.v b/v_windows/v/vlib/cli/version.v
new file mode 100644
index 0000000..0f3f583
--- /dev/null
+++ b/v_windows/v/vlib/cli/version.v
@@ -0,0 +1,25 @@
+module cli
+
+fn version_flag(with_abbrev bool) Flag {
+ sabbrev := if with_abbrev { 'v' } else { '' }
+ return Flag{
+ flag: .bool
+ name: 'version'
+ abbrev: sabbrev
+ description: 'Prints version information.'
+ }
+}
+
+fn version_cmd() Command {
+ return Command{
+ name: 'version'
+ description: 'Prints version information.'
+ execute: version_func
+ }
+}
+
+fn version_func(version_cmd Command) ? {
+ cmd := version_cmd.parent
+ version := '$cmd.name version $cmd.version'
+ println(version)
+}