Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+65
View File
@@ -0,0 +1,65 @@
## Description
`cli` is a command line option parser, that supports
declarative subcommands, each having a separate set of options.
See also the `flag` module, for a simpler command line option parser,
that supports only options.
## 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'
alias: 's'
execute: fn (cmd cli.Command) ! {
println('hello subcommand')
return
}
},
]
}
app.setup()
app.parse(os.args)
}
```
Subcommands can set `alias` to accept a shorter invocation token, for example
`example-app s`.
## Help layout
`--help` is generated automatically. Beyond `Usage:`, `Flags:` and `Commands:`
the output picks up extra sections when the relevant `Command` fields are
populated:
- **`Inherited flags:`** — flags inherited from any ancestor through
`Flag.global`. Renders separately from local flags so a sub-command's
own surface stays readable.
- **Grouped commands** — set `group` on a sub-command to lift it out of
the default `Commands:` block into a section named after the group.
The group string is rendered verbatim followed by `:`, so capitalisation
is the caller's choice. Sub-commands sharing a group keep their
declaration order.
- **`Examples:`** — set `examples []string` on a `Command`; each entry
becomes one indented line.
- **`Learn more:`** — set `learn_more string`; newlines split the block
into separate lines.
Sections that have nothing to render are simply omitted, so existing apps
see no change unless they opt in by setting these fields. See
`examples/cli_groups.v` for a runnable program that exercises all of them.
+35
View File
@@ -0,0 +1,35 @@
// vtest build: tinyc
module main
import v.util.diff
import term
import os
const vexe = @VEXE
const vroot = os.dir(vexe)
fn test_cli_programs() {
testdata := os.join_path(vroot, 'vlib/cli/testdata')
mut has_err := false
for test in os.walk_ext(testdata, '.vv') {
print(test + ' ')
out_path := test.all_before_last('.vv') + '.out'
if !os.exists(out_path) {
println(term.red('FAIL'))
eprintln('failed to find output file for `${test}`')
has_err = true
continue
}
expected_out := os.read_file(out_path)!.replace('\r\n', '\n')
test_out := os.execute('${vexe} run ${test}').output.replace('\r\n', '\n')
diff_ := diff.compare_text(expected_out, test_out)!
if diff_ != '' {
println(term.red('FAIL'))
eprintln(diff_)
has_err = true
} else {
println(term.green('OK'))
}
}
assert !has_err
}
+450
View File
@@ -0,0 +1,450 @@
module cli
import term
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
alias string
usage string
description string
man_description string
version string
// group is the section title under which `--help` lists this sub-command
// in its parent's command listing. Empty falls back to the default
// `Commands:` section. The string is rendered verbatim followed by `:`
// — capitalisation is the caller's responsibility. Groups appear in the
// order their first member is declared, which can interact with
// `sort_commands` (sorting may change which member of each group comes
// first, hence which group is listed first).
group string
// examples lists invocations rendered under `Examples:` by `--help`.
// Each entry is one line; a leading `$` is conventional but not required.
examples []string
// learn_more is rendered under the `Learn more:` section by `--help`.
// Newlines split the block into separate lines.
learn_more string
pre_execute FnCommandCallback = unsafe { nil }
execute FnCommandCallback = unsafe { nil }
post_execute FnCommandCallback = unsafe { nil }
disable_flags bool
sort_flags bool
sort_commands bool
parent &Command = unsafe { nil }
commands []Command
flags []Flag
required_args int
args []string
posix_mode bool
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
}
type Defaults = CommandFlag | 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 << ' alias: "${cmd.alias}"'
res << ' usage: "${cmd.usage}"'
res << ' version: "${cmd.version}"'
res << ' description: "${cmd.description}"'
res << ' man_description: "${cmd.man_description}"'
res << ' group: "${cmd.group}"'
res << ' examples: ${cmd.examples}'
res << ' learn_more: "${cmd.learn_more}"'
res << ' disable_flags: ${cmd.disable_flags}'
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 unsafe { 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 << ' posix_mode: ${cmd.posix_mode}'
match cmd.defaults.help {
bool {
res << ' defaults.help: ${cmd.defaults.help}'
}
CommandFlag {
res << ' defaults.help.command: ${cmd.defaults.help.command}'
res << ' defaults.help.flag: ${cmd.defaults.help.flag}'
}
}
match cmd.defaults.man {
bool {
res << ' defaults.man: ${cmd.defaults.man}'
}
CommandFlag {
res << ' defaults.man.command: ${cmd.defaults.man.command}'
res << ' defaults.man.flag: ${cmd.defaults.man.flag}'
}
}
match cmd.defaults.version {
bool {
res << ' defaults.version: ${cmd.defaults.version}'
}
CommandFlag {
res << ' defaults.version.command: ${cmd.defaults.version.command}'
res << ' defaults.version.flag: ${cmd.defaults.version.flag}'
}
}
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
for existing in cmd.commands {
if existing.name == subcmd.name {
eprintln_exit('Command with the name `${subcmd.name}` already exists')
}
if existing.alias != '' && existing.alias == subcmd.name {
eprintln_exit('Command with the name `${subcmd.name}` already exists as an alias')
}
if subcmd.alias != '' && existing.matches(subcmd.alias) {
eprintln_exit('Command alias `${subcmd.alias}` already exists')
}
}
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) {
eprintln_exit('Flag with the name `${flag.name}` already exists')
}
cmd.flags << flag
}
// TODO: remove deprecated `disable_<>` switches after deprecation period.
fn (mut cmd Command) parse_defaults() {
// Help
if cmd.defaults.help is bool {
cmd.defaults.parsed.help.flag = cmd.defaults.help
cmd.defaults.parsed.help.command = cmd.defaults.help
} 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 {
cmd.defaults.parsed.version.flag = cmd.defaults.version
cmd.defaults.parsed.version.command = cmd.defaults.version
} 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 {
cmd.defaults.parsed.man.flag = cmd.defaults.man
cmd.defaults.parsed.man.command = cmd.defaults.man
} 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
if !cmd.disable_flags {
cmd.add_default_flags()
}
// Add Commands
cmd.add_default_commands()
}
// parse parses the flags and commands from the given `args` into the `Command`.
pub fn (mut cmd Command) parse(args []string) {
cmd.parse_defaults()
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`, `--man`, and
// `-v`/`--version` flags to the `Command`.
fn (mut cmd Command) add_default_flags() {
if cmd.defaults.parsed.help.flag && !cmd.flags.contains('help') {
use_help_abbrev := !cmd.flags.contains('h') && cmd.posix_mode
cmd.add_flag(help_flag(use_help_abbrev))
}
if cmd.defaults.parsed.version.flag && cmd.version != '' && !cmd.flags.contains('version') {
use_version_abbrev := !cmd.flags.contains('v') && cmd.posix_mode
cmd.add_flag(version_flag(use_version_abbrev))
}
if cmd.defaults.parsed.man.flag && !cmd.flags.contains('man') {
cmd.add_flag(man_flag())
}
}
// add_default_commands adds the command functions of the commonly
// used `help`, `man`, and `version` subcommands to the `Command`.
fn (mut cmd Command) add_default_commands() {
if cmd.defaults.parsed.help.command && !cmd.commands.contains('help') && cmd.is_root() {
cmd.add_command(help_cmd())
}
if cmd.defaults.parsed.version.command && cmd.version != '' && !cmd.commands.contains('version') {
cmd.add_command(version_cmd())
}
if cmd.defaults.parsed.man.command && !cmd.commands.contains('man') && cmd.is_root() {
cmd.add_command(man_cmd())
}
}
fn (mut cmd Command) parse_flags() {
for cmd.args.len > 0 {
if !cmd.args[0].starts_with('-') {
return
}
mut found := false
for mut flag in cmd.flags {
if flag.matches(cmd.args[0], cmd.posix_mode) {
found = true
flag.found = true
// Eat flag and its values, continue with reduced args.
cmd.args = flag.parse(cmd.args, cmd.posix_mode) or {
eprintln_exit('Failed to parse flag `${cmd.args[0]}`: ${err}')
}
break
}
}
if !found {
eprintln_exit('Command `${cmd.name}` has no flag `${cmd.args[0]}`')
}
}
}
fn (mut cmd Command) parse_commands() {
global_flags := cmd.flags.filter(it.global)
cmd.check_help_flag()
cmd.check_version_flag()
cmd.check_man_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.matches(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.defaults.parsed.help.command {
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 {
descriptor := if cmd.required_args == 1 { 'argument' } else { 'arguments' }
eprintln_exit('Command `${cmd.name}` needs at least ${cmd.required_args} ${descriptor}')
}
}
cmd.check_required_flags()
cmd.handle_cb(cmd.pre_execute, 'preexecution')
cmd.handle_cb(cmd.execute, 'execution')
cmd.handle_cb(cmd.post_execute, 'postexecution')
}
fn (cmd &Command) display_name() string {
if cmd.alias == '' {
return cmd.name
}
return '${cmd.name} (${cmd.alias})'
}
fn (cmd &Command) matches(token string) bool {
return cmd.name == token || (cmd.alias != '' && cmd.alias == token)
}
fn (mut cmd Command) handle_cb(cb FnCommandCallback, label string) {
if !isnil(cb) {
cb(*cmd) or {
label_message := term.ecolorize(term.bright_red, 'cli ${label} error:')
eprintln_exit('${label_message} ${err}')
}
}
}
fn (cmd &Command) check_help_flag() {
if cmd.defaults.parsed.help.flag && cmd.flags.contains('help') {
help_enabled :=
cmd.flags.get_bool('help') or { return } // ignore error and handle command normally
if help_enabled {
cmd.execute_help()
exit(0)
}
}
}
fn (cmd &Command) check_man_flag() {
if cmd.defaults.parsed.man.flag && cmd.flags.contains('man') {
man_enabled :=
cmd.flags.get_bool('man') or { return } // ignore error and handle command normally
if man_enabled {
cmd.execute_man()
exit(0)
}
}
}
fn (cmd &Command) check_version_flag() {
if cmd.defaults.parsed.version.flag && cmd.version != '' && cmd.flags.contains('version') {
version_enabled :=
cmd.flags.get_bool('version') or { return } // ignore error and handle command normally
if version_enabled {
print_version_for_command(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()
eprintln_exit('Flag `${flag.name}` is required by `${full_name}`')
}
}
}
// execute_help executes the callback registered for the `-h`/`--help` flag option.
pub fn (cmd &Command) execute_help() {
if cmd.commands.contains('help') {
sub := cmd.commands.get('help') or { return } // ignore error and handle command normally
if !isnil(sub.execute) {
sub.execute(sub) or { panic(err) }
return
}
}
print(cmd.help_message())
}
// execute_man executes the callback registered for the `-man` flag option.
pub fn (cmd &Command) execute_man() {
if cmd.commands.contains('man') {
sub := cmd.commands.get('man') or { return }
sub.execute(sub) or { panic(err) }
} else {
print(cmd.manpage())
}
}
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
}
@[noreturn]
fn eprintln_exit(message string) {
eprintln(message)
exit(1)
}
+309
View File
@@ -0,0 +1,309 @@
@[has_globals]
module main
import cli
__global (
if_subcommands_parse_args_called bool
flag_should_be_set_called bool
assert_flags_called bool
flag_is_set_in_subcommand_called bool
)
fn test_if_command_parses_empty_args() {
mut cmd := cli.Command{
name: 'command'
execute: empty_func
}
cmd.parse(['command'])
assert cmd.name == 'command' && 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' && cmd.args == ['arg0', 'arg1']
}
fn test_if_subcommands_parse_args() {
mut cmd := cli.Command{
name: 'command'
}
if_subcommands_parse_args_called = false
subcmd := cli.Command{
name: 'subcommand'
execute: if_subcommands_parse_args_func
}
cmd.add_command(subcmd)
cmd.parse(['command', 'subcommand', 'arg0', 'arg1'])
assert if_subcommands_parse_args_called
}
fn test_if_subcommand_alias_parses_args() {
mut cmd := cli.Command{
name: 'command'
}
subcmd := cli.Command{
name: 'subcommand'
alias: 'sc'
execute: if_subcommands_parse_args_func
}
cmd.add_command(subcmd)
cmd.parse(['command', 'sc', 'arg0', 'arg1'])
}
fn if_subcommands_parse_args_func(cmd cli.Command) ! {
if_subcommands_parse_args_called = true
assert cmd.name == 'subcommand' && cmd.args == ['arg0', 'arg1']
}
fn test_default_subcommands() {
mut cmd := cli.Command{
name: 'command'
}
cmd.parse(['command'])
assert cmd.commands.any(it.name == 'help')
assert cmd.commands.any(it.name == 'man')
cmd = cli.Command{
name: 'command'
version: '1.0.0'
}
cmd.parse(['command'])
assert cmd.commands.any(it.name == 'version')
}
fn flag_should_be_set(cmd cli.Command) ! {
flag_should_be_set_called = true
flag := cmd.flags.get_string('flag')!
assert flag == 'value'
}
fn test_if_flag_gets_set() {
flag_should_be_set_called = false
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'])
assert flag_should_be_set_called
}
fn test_if_flag_gets_set_with_abbrev() {
flag_should_be_set_called = false
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'])
assert flag_should_be_set_called
}
fn test_if_flag_gets_set_with_long_arg() {
flag_should_be_set_called = false
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'])
assert flag_should_be_set_called
}
fn assert_flags(cmd cli.Command) ! {
assert_flags_called = true
flag := cmd.flags.get_string('flag')!
assert flag == 'value'
value := cmd.flags.get_int('value')!
assert value == 42
flag2 := cmd.flags.get_string('flag-2')!
assert flag2 == 'value-2'
}
fn test_if_multiple_flags_get_set() {
assert_flags_called = false
mut cmd := cli.Command{
name: 'command'
execute: assert_flags
}
cmd.add_flag(cli.Flag{
flag: .string
name: 'flag'
})
cmd.add_flag(cli.Flag{
flag: .string
name: 'flag-2'
})
cmd.add_flag(cli.Flag{
flag: .int
name: 'value'
})
cmd.parse(['command', '-flag=value', '-value', '42', '-flag-2', 'value-2'])
assert assert_flags_called
}
fn test_if_required_flags_get_set() {
assert_flags_called = false
mut cmd := cli.Command{
name: 'command'
execute: assert_flags
}
cmd.add_flag(cli.Flag{
flag: .string
name: 'flag'
})
cmd.add_flag(cli.Flag{
flag: .string
name: 'flag-2'
})
cmd.add_flag(cli.Flag{
flag: .int
name: 'value'
required: true
})
cmd.parse(['command', '-flag', 'value', '-value', '42', '-flag-2', 'value-2'])
assert assert_flags_called
}
fn flag_is_set_in_subcommand(cmd cli.Command) ! {
flag_is_set_in_subcommand_called = true
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
}
flag_is_set_in_subcommand_called = false
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'])
assert flag_is_set_in_subcommand_called
}
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
})
flag_is_set_in_subcommand_called = false
subcmd := cli.Command{
name: 'subcommand'
execute: flag_is_set_in_subcommand
}
cmd.add_command(subcmd)
cmd.parse(['command', '-flag', 'value', 'subcommand'])
assert flag_is_set_in_subcommand_called
}
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'
}
fn test_help_message_lists_command_aliases() {
cmd := cli.Command{
name: 'command'
description: 'description'
commands: [
cli.Command{
name: 'sub'
alias: 's'
description: 'subcommand'
},
]
}
assert cmd.help_message() == r'Usage: command [commands]
description
Commands:
sub (s) subcommand
'
}
fn test_manpage_lists_command_aliases() {
mut cmd := cli.Command{
name: 'command'
description: 'description'
commands: [
cli.Command{
name: 'sub'
alias: 's'
description: 'subcommand'
},
]
}
cmd.setup()
assert cmd.manpage().after_char(`\n`) == r'.Dt COMMAND 1
.Os
.Sh NAME
.Nm command
.Nd description
.Sh SYNOPSIS
.Nm command
.Nm command
.Ar subcommand
.Sh DESCRIPTION
description
.Pp
The subcommands are as follows:
.Bl -tag -width indent
.It Cm sub Pq Cm s
subcommand
.El
.Sh SEE ALSO
.Xr command-sub 1
'
}
// helper functions
fn empty_func(cmd cli.Command) ! {
}
+294
View File
@@ -0,0 +1,294 @@
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
// Description of flag
description string
// If the flag is added to this command and to all subcommands
global bool
// If flag is required
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.
pub fn (mut flag Flag) parse(args []string, posix_mode bool) ![]string {
return match true {
!flag.matches(args[0], posix_mode) {
args
}
flag.flag == .bool {
flag.parse_bool(args)!
}
flag.value.len > 0 && flag.flag !in [.int_array, .float_array, .string_array] {
error('The argument `${flag.name}` accept only one value!')
}
else {
flag.parse_raw(args)!
}
}
}
// matches returns `true` if first arg in `args` matches this flag.
fn (mut flag Flag) matches(arg string, posix_mode bool) bool {
prefix := if posix_mode { '--' } else { '-' }
return (flag.name != '' && arg == '${prefix}${flag.name}')
|| (flag.name != '' && arg.starts_with('${prefix}${flag.name}='))
|| (flag.abbrev != '' && arg == '-${flag.abbrev}')
|| (flag.abbrev != '' && arg.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
}
}
+171
View File
@@ -0,0 +1,171 @@
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']
}
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]
}
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]
}
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
}
+269
View File
@@ -0,0 +1,269 @@
module cli
import term
import strings
const base_indent_len = 2
const min_description_indent_len = 20
const spacing = 2
fn help_flag(with_abbrev bool) Flag {
sabbrev := if with_abbrev { 'h' } else { '' }
return Flag{
flag: .bool
name: 'help'
abbrev: sabbrev
description: 'Print help information'
}
}
fn help_cmd() Command {
return Command{
name: 'help'
usage: '<command>'
description: 'Print help information'
execute: print_help_for_command
}
}
// print_help_for_command outputs the help message of `help_cmd`.
pub fn print_help_for_command(cmd Command) ! {
if cmd.args.len > 0 {
for sub_cmd in cmd.commands {
if sub_cmd.matches(cmd.args[0]) {
cmd_ := unsafe { &sub_cmd }
print(cmd_.help_message())
return
}
}
print('Invalid command: ${cmd.args.join(' ')}')
} else if cmd.parent != unsafe { nil } {
print(cmd.parent.help_message())
}
}
// help_message returns a generated help message as a `string` for the `Command`.
//
// The output is composed of (each section is omitted when empty):
//
// 1. `Usage:` line
// 2. The description, if any
// 3. `Flags:` — flags defined on this command
// 4. `Inherited flags:` — flags propagated from any ancestor via `Flag.global`
// 5. One section per sub-command group; the group name is rendered as the
// user wrote it followed by `:`. Sub-commands without a group go under
// the default `Commands:` section.
// 6. `Examples:` — `Command.examples` entries, one per line
// 7. `Learn more:` — the multi-line `Command.learn_more` field
pub 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'
}
abbrev_len, name_len := cmd.help_columns()
local_flags, inherited_flags := cmd.partition_flags()
if local_flags.len > 0 {
help += render_flag_section('Flags', local_flags, abbrev_len, name_len, cmd.posix_mode)
}
if inherited_flags.len > 0 {
help += render_flag_section('Inherited flags', inherited_flags, abbrev_len, name_len,
cmd.posix_mode)
}
if cmd.commands.len > 0 {
groups, order := group_commands(cmd.commands)
for idx, key in order {
title := if key == '' { 'Commands' } else { key }
help += render_command_section(title, groups[idx], name_len)
}
}
if cmd.examples.len > 0 {
help += '\nExamples:\n'
base_indent := ' '.repeat(base_indent_len)
for line in cmd.examples {
help += '${base_indent}${line}\n'
}
}
if cmd.learn_more != '' {
help += '\nLearn more:\n'
base_indent := ' '.repeat(base_indent_len)
for line in cmd.learn_more.split_into_lines() {
help += '${base_indent}${line}\n'
}
}
return help
}
// help_columns computes the abbreviation column width and the name column
// width used by `Flags:`, `Inherited Flags:` and command sections so all
// rows align vertically.
fn (cmd &Command) help_columns() (int, int) {
mut abbrev_len := 0
mut name_len := min_description_indent_len
if cmd.posix_mode {
for flag in cmd.flags {
if flag.abbrev != '' {
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
}
} else {
for flag in cmd.flags {
if flag.abbrev != '' {
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 + 1) // + 1 for '-' in front
}
}
for command in cmd.commands {
name_len = max(name_len, command.display_name().len + spacing)
}
return abbrev_len, name_len
}
// partition_flags splits this command's `flags` into two arrays preserving
// declaration order: locally-defined flags first, then flags inherited from
// any ancestor via `Flag.global`.
fn (cmd &Command) partition_flags() ([]Flag, []Flag) {
inherited_names := cmd.inherited_global_flag_names()
mut local := []Flag{}
mut inherited := []Flag{}
for flag in cmd.flags {
if flag.name in inherited_names {
inherited << flag
} else {
local << flag
}
}
return local, inherited
}
// inherited_global_flag_names returns the names of flags that any ancestor
// declared with `global = true`. A flag is "inherited" only when an ancestor
// declared it global, never when this command itself defines it.
fn (cmd &Command) inherited_global_flag_names() []string {
mut names := []string{}
mut walker := cmd.parent
for unsafe { walker != nil } {
for flag in walker.flags {
if flag.global && flag.name !in names {
names << flag.name
}
}
walker = walker.parent
}
return names
}
// group_commands buckets `commands` by their `group` field, preserving the
// declaration order of both groups and members.
fn group_commands(commands []Command) ([][]Command, []string) {
mut order := []string{}
mut groups := [][]Command{}
for sub in commands {
key := sub.group
idx := order.index(key)
if idx == -1 {
order << key
groups << [sub]
} else {
groups[idx] << sub
}
}
return groups, order
}
fn render_flag_section(title string, flags []Flag, abbrev_len int, name_len int, posix_mode bool) string {
mut out := '\n${title}:\n'
prefix := if posix_mode { '--' } else { '-' }
base_indent := ' '.repeat(base_indent_len)
for flag in flags {
mut flag_name := ''
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)'
}
description_indent := ' '.repeat(name_len - flag_name.len)
out += '${base_indent}${flag_name}${description_indent}' +
pretty_description(flag.description + required, base_indent_len + name_len) + '\n'
}
return out
}
fn render_command_section(title string, commands []Command, name_len int) string {
mut out := '\n${title}:\n'
base_indent := ' '.repeat(base_indent_len)
for command in commands {
command_name := command.display_name()
description_indent := ' '.repeat(name_len - command_name.len)
out += '${base_indent}${command_name}${description_indent}' +
pretty_description(command.description, base_indent_len + name_len) + '\n'
}
return out
}
// 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 j > 0 && line[j] != ` ` {
j--
}
// 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
}
+200
View File
@@ -0,0 +1,200 @@
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
'
}
fn test_help_message_groups_commands_by_group_field() {
cmd := Command{
name: 'app'
description: 'app'
commands: [
Command{
name: 'auth'
description: 'Authenticate'
group: 'Core commands'
},
Command{
name: 'repo'
description: 'Work with repositories'
group: 'Core commands'
},
Command{
name: 'alias'
description: 'Define shortcuts'
group: 'General commands'
},
Command{
name: 'misc'
description: 'Other things'
},
]
}
assert cmd.help_message() == r'Usage: app [commands]
app
Core commands:
auth Authenticate
repo Work with repositories
General commands:
alias Define shortcuts
Commands:
misc Other things
'
}
fn test_help_message_renders_group_title_verbatim() {
// Group string is rendered as-is; capitalisation is the caller's
// choice. Non-ASCII first characters must work without mojibake.
cmd := Command{
name: 'app'
commands: [
Command{
name: 'edit'
description: 'Edit something'
group: 'éditeurs'
},
]
}
assert cmd.help_message() == r'Usage: app [commands]
éditeurs:
edit Edit something
'
}
fn test_help_message_separates_inherited_flags_from_locals() {
mut root := Command{
name: 'app'
flags: [
Flag{
flag: .string
name: 'config'
abbrev: 'c'
description: 'Path to config'
global: true
},
]
commands: [
Command{
name: 'run'
description: 'Run something'
flags: [
Flag{
flag: .bool
name: 'verbose'
abbrev: 'v'
description: 'Verbose output'
},
]
},
]
}
root.setup()
mut sub := root.commands[0]
sub.parent = unsafe { &root }
// Mirror what `parse_commands` does when a child is dispatched.
sub.flags << root.flags[0]
assert sub.help_message() == r'Usage: app run [flags]
Run something
Flags:
-v -verbose Verbose output
Inherited flags:
-c -config Path to config
'
}
fn test_help_message_renders_examples_and_learn_more() {
cmd := Command{
name: 'app'
description: 'app'
examples: ['\$ app run', '\$ app help run']
learn_more: 'Use `app help <command>` for details about a command.\nDocumentation lives at https://example.test'
}
assert cmd.help_message() == r'Usage: app
app
Examples:
$ app run
$ app help run
Learn more:
Use `app help <command>` for details about a command.
Documentation lives at https://example.test
'
}
fn test_help_message_omits_empty_optional_sections() {
cmd := Command{
name: 'app'
}
// No description, no flags, no commands, no examples, no learn_more.
assert cmd.help_message() == 'Usage: app\n'
}
+173
View File
@@ -0,0 +1,173 @@
module cli
import time
fn man_flag() Flag {
return Flag{
flag: .bool
name: 'man'
description: 'Print the auto-generated manpage'
}
}
fn man_cmd() Command {
return Command{
name: 'man'
usage: '<subcommand>'
description: 'Print the auto-generated manpage'
execute: print_manpage_for_command
}
}
// print_manpage_for_command prints the manpage for the command or subcommand in `man_cmd` to stdout.
pub fn print_manpage_for_command(cmd Command) ! {
if cmd.args.len > 0 {
for sub_cmd in cmd.commands {
if sub_cmd.matches(cmd.args[0]) {
target := unsafe { &sub_cmd }
print(target.manpage())
return
}
}
print('Invalid command: ${cmd.args.join(' ')}')
} else if cmd.parent != unsafe { nil } {
print(cmd.parent.manpage())
}
}
// manpage returns a `string` containing the mdoc(7) manpage for this `Command`.
pub fn (cmd &Command) manpage() string {
mut mdoc := '.Dd ${time.now().strftime('%B %d, %Y')}\n'
mdoc += '.Dt ${cmd.full_name().replace(' ', '-').to_upper()} 1\n'
mdoc += '.Os\n.Sh NAME\n.Nm ${cmd.full_name().replace(' ', '-')}\n.Nd ${cmd.description}\n'
mdoc += '.Sh SYNOPSIS\n'
mdoc += '.Nm ${cmd.root().name}\n'
if unsafe { cmd.parent != 0 } {
mut parents := []Command{}
if !cmd.parent.is_root() {
parents.prepend(cmd.parent)
for {
p := parents[0]
if p.parent.is_root() {
break
} else {
parents.prepend(p.parent)
}
}
for c in parents {
mdoc += '.Ar ${c.name}\n'
}
}
mdoc += '.Ar ${cmd.name}\n'
}
for flag in cmd.flags {
mdoc += '.Op'
if flag.abbrev != '' {
mdoc += ' Fl ${flag.abbrev}'
} else {
if cmd.posix_mode {
mdoc += ' Fl -${flag.name}'
} else {
mdoc += ' Fl ${flag.name}'
}
}
match flag.flag {
.int, .float, .int_array, .float_array { mdoc += ' Ar num' }
.string, .string_array { mdoc += ' Ar string' }
else {}
}
mdoc += '\n'
}
for i in 0 .. cmd.required_args {
mdoc += '.Ar arg${i}\n'
}
if cmd.commands.len > 0 {
mdoc += '.Nm ${cmd.root().name}\n'
if unsafe { cmd.parent != 0 } {
mut parents := []Command{}
if !cmd.parent.is_root() {
parents.prepend(cmd.parent)
for {
p := parents[0]
if p.parent.is_root() {
break
} else {
parents.prepend(p.parent)
}
}
for c in parents {
mdoc += '.Ar ${c.name}\n'
}
}
mdoc += '.Ar ${cmd.name}\n'
}
mdoc += '.Ar subcommand\n'
}
mdoc += '.Sh DESCRIPTION\n'
if cmd.man_description != '' {
mdoc += '${cmd.man_description}\n'
} else if cmd.description != '' {
mdoc += '${cmd.description}\n'
}
if cmd.flags.len > 0 {
mdoc += '.Pp\nThe options are as follows:\n'
mdoc += '.Bl -tag -width indent\n'
for flag in cmd.flags {
mdoc += '.It'
if flag.abbrev != '' {
mdoc += ' Fl ${flag.abbrev}'
}
if cmd.posix_mode {
mdoc += ' Fl -${flag.name}'
} else {
mdoc += ' Fl ${flag.name}'
}
mdoc += '\n'
if flag.description != '' {
mdoc += '${flag.description}\n'
}
}
mdoc += '.El\n'
}
if cmd.commands.len > 0 {
mdoc += '.Pp\nThe subcommands are as follows:\n'
mdoc += '.Bl -tag -width indent\n'
for c in cmd.commands {
mdoc += '.It Cm ${c.name}'
if c.alias != '' {
mdoc += ' Pq Cm ${c.alias}'
}
mdoc += '\n'
if c.description != '' {
mdoc += '${c.description}\n'
}
}
mdoc += '.El\n'
}
if cmd.commands.len > 0 {
mdoc += '.Sh SEE ALSO\n'
mut cmds := []string{}
if unsafe { cmd.parent != 0 } {
cmds << cmd.parent.full_name().replace(' ', '-')
}
for c in cmd.commands {
cmds << c.full_name().replace(' ', '-')
}
cmds.sort()
mut i := 1
for c in cmds {
mdoc += '.Xr ${c} 1'
if i == cmds.len {
mdoc += '\n'
} else {
mdoc += ' ,\n'
}
i++
}
}
return mdoc
}
+110
View File
@@ -0,0 +1,110 @@
module cli
fn test_manpage() {
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
},
]
}
cmd.setup()
assert cmd.manpage().after_char(`\n`) == r'.Dt COMMAND 1
.Os
.Sh NAME
.Nm command
.Nd description
.Sh SYNOPSIS
.Nm command
.Op Fl str Ar string
.Op Fl b
.Op Fl r Ar string
.Nm command
.Ar subcommand
.Sh DESCRIPTION
description
.Pp
The options are as follows:
.Bl -tag -width indent
.It Fl str
str flag
.It Fl b Fl bool
bool flag
.It Fl r Fl required
.El
.Pp
The subcommands are as follows:
.Bl -tag -width indent
.It Cm sub
subcommand
.It Cm sub2
another subcommand
.El
.Sh SEE ALSO
.Xr command-sub 1 ,
.Xr command-sub2 1
'
cmd.posix_mode = true
assert cmd.manpage().after_char(`\n`) == r'.Dt COMMAND 1
.Os
.Sh NAME
.Nm command
.Nd description
.Sh SYNOPSIS
.Nm command
.Op Fl -str Ar string
.Op Fl b
.Op Fl r Ar string
.Nm command
.Ar subcommand
.Sh DESCRIPTION
description
.Pp
The options are as follows:
.Bl -tag -width indent
.It Fl -str
str flag
.It Fl b Fl -bool
bool flag
.It Fl r Fl -required
.El
.Pp
The subcommands are as follows:
.Bl -tag -width indent
.It Cm sub
subcommand
.It Cm sub2
another subcommand
.El
.Sh SEE ALSO
.Xr command-sub 1 ,
.Xr command-sub2 1
'
}
+7
View File
@@ -0,0 +1,7 @@
Usage: [flags] [commands]
Flags:
-version Print version information
Commands:
help Print help information
+13
View File
@@ -0,0 +1,13 @@
import cli { Command, CommandFlag }
fn main() {
mut cmd := Command{
version: '0.1.0'
defaults: struct {
help: CommandFlag{true, false}
version: CommandFlag{false, true}
man: false
}
}
cmd.parse([''])
}
@@ -0,0 +1 @@
Command `foo` has no flag `-man`
@@ -0,0 +1,11 @@
import cli { Command, CommandFlag }
fn main() {
mut cmd := Command{
name: 'foo'
defaults: struct {
man: CommandFlag{true, false}
}
}
cmd.parse(['foo', '-man'])
}
+7
View File
@@ -0,0 +1,7 @@
Usage: [flags] [commands]
Flags:
-help Print help information
Commands:
help Print help information
+10
View File
@@ -0,0 +1,10 @@
import cli { Command }
import os
fn main() {
mut cmd := Command{}
cmd.defaults = struct {
man: false
}
cmd.parse(os.args)
}
@@ -0,0 +1,8 @@
Usage: foo [flags] [commands]
Flags:
-help Print help information
-man Print the auto-generated manpage
Commands:
man Print the auto-generated manpage
@@ -0,0 +1,11 @@
import cli { Command, CommandFlag }
fn main() {
mut cmd := Command{
name: 'foo'
defaults: struct {
help: CommandFlag{false, true}
}
}
cmd.parse(['foo', '-help'])
}
@@ -0,0 +1 @@
foo version 0.1.0
@@ -0,0 +1,13 @@
import cli { Command, CommandFlag }
fn main() {
mut cmd := Command{
name: 'foo'
version: '0.1.0'
posix_mode: true
defaults: struct {
version: CommandFlag{false, true}
}
}
cmd.parse(['foo', '--version'])
}
+10
View File
@@ -0,0 +1,10 @@
Usage: cli greet [flags] <name>
Prints greeting in different languages.
Flags:
-fun '{"uri":"mqtt://broker.emqx.io:1883","topic":"test_emq/1
'{"uri":"mqtt://broker.emqx.io:1883","topic":"test_emq/1","filters":[{"producer_id":0,"trace_group":2,"string_id
":3001},{"string_id":3002}]}'
-help Print help information
-man Print the auto-generated manpage
+21
View File
@@ -0,0 +1,21 @@
import cli { Command, Flag }
mut cmd := Command{
name: 'cli'
description: 'An example of the cli library.'
version: '1.0.0'
}
mut greet_cmd := Command{
name: 'greet'
description: 'Prints greeting in different languages.'
usage: '<name>'
required_args: 1
}
greet_cmd.add_flag(Flag{
flag: .string_array
name: 'fun'
description: '\'{"uri":"mqtt://broker.emqx.io:1883","topic":"test_emq/1","filters":[{"producer_id":0,"trace_group":2,"string_id":3001},{"string_id":3002}]}\''
})
cmd.add_command(greet_cmd)
cmd.setup()
cmd.parse(['cli', 'greet', '-help'])
View File
Whitespace-only changes.
+11
View File
@@ -0,0 +1,11 @@
import cli { Command }
fn main() {
mut cmd := Command{
name: 'cmd'
}
cmd.add_command(
name: 'foo'
)
cmd.parse(['', 'cmd', 'foo'])
}
+41
View File
@@ -0,0 +1,41 @@
module cli
fn version_flag(with_abbrev bool) Flag {
sabbrev := if with_abbrev { 'v' } else { '' }
return Flag{
flag: .bool
name: 'version'
abbrev: sabbrev
description: 'Print version information'
}
}
fn version_cmd() Command {
return Command{
name: 'version'
description: 'Print version information'
execute: print_version_for_command
}
}
fn print_version_for_command(cmd Command) ! {
if cmd.args.len > 0 {
for sub_cmd in cmd.commands {
if sub_cmd.name == cmd.args[0] {
target := unsafe { &sub_cmd }
print(target.version())
return
}
}
println('Invalid command: ${cmd.args.join(' ')}')
} else if cmd.parent != unsafe { nil } {
println(cmd.parent.version())
} else {
println(cmd.version())
}
}
// version returns a generated version `string` for the `Command`.
pub fn (cmd &Command) version() string {
return '${cmd.name} version ${cmd.version}'
}