Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

flag: add support for parsing flag.FlagParser style flags in to_struct[T] #22152

Merged
merged 3 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions vlib/flag/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ The module supports several flag "styles" like:
* GNU long style (`--long` / `--long=value`
* Go `flag` module style (`-flag`, `-flag-name` and GNU long)
* V style (`-v`,`-version`)
* V long style (`--v`,`--version`) as supported by `flag.FlagParser`

Its main features are:

- simplicity of usage.
- parses flags like `-f` or '--flag' or '--stuff=things' or '--things stuff'.
- parses flags like `-f` or `--flag` or `--stuff=things` or `--things stuff`.
- handles bool, int, float and string args.
- can flexibly generate usage information, listing all the declared flags.

Expand Down Expand Up @@ -209,4 +210,4 @@ fn main() {
println('an_int: ${an_int} | a_bool: ${a_bool} | a_float: ${a_float} | a_string: "${a_string}" ')
println(additional_args.join_lines())
}
```
```
74 changes: 71 additions & 3 deletions vlib/flag/flag_to.v
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub enum Style {
long // GNU style long option *only*. E.g.: `--name` or `--name=value`
short_long // extends `posix` style shorts with GNU style long options: `--flag` or `--name=value`
v // V style flags as found in flags for the `v` compiler. Single flag denote `-` followed by string identifier e.g.: `-verbose`, `-name value`, `-v`, `-n value` or `-d ident=value`
v_long // V long style flags as found in `flag.FlagParser`. Long flag denote `--` followed by string identifier e.g.: `--verbose`, `--name value`, `--v` or `--n value`.
go_flag // GO `flag` module style. Single flag denote `-` followed by string identifier e.g.: `-verbose`, `-name value`, `-v` or `-n value` and both long `--name value` and GNU long `--name=value`
cmd_exe // `cmd.exe` style flags. Single flag denote `/` followed by lower- or upper-case character
}
Expand Down Expand Up @@ -425,7 +426,7 @@ pub fn (mut fm FlagMapper) parse[T]() ! {
}
if is_long_delimiter {
if style == .v {
return error('long delimiter `${used_delimiter}` encountered in flag `${arg}` in ${style} (V) style parsing mode')
return error('long delimiter `${used_delimiter}` encountered in flag `${arg}` in ${style} (V) style parsing mode. Maybe you meant `.v_long`?')
}
if style == .short {
return error('long delimiter `${used_delimiter}` encountered in flag `${arg}` in ${style} (POSIX) style parsing mode')
Expand All @@ -436,6 +437,9 @@ pub fn (mut fm FlagMapper) parse[T]() ! {
if style == .long {
return error('short delimiter `${used_delimiter}` encountered in flag `${arg}` in ${style} (GNU) style parsing mode')
}
if style == .v_long {
return error('short delimiter `${used_delimiter}` encountered in flag `${arg}` in ${style} (V long) style parsing mode. Maybe you meant `.v`?')
}
if style == .short_long && flag_name.len > 1 && flag_name.contains('-') {
return error('long name `${flag_name}` used with short delimiter `${used_delimiter}` in flag `${arg}` in ${style} (POSIX/GNU) style parsing mode')
}
Expand Down Expand Up @@ -515,6 +519,10 @@ pub fn (mut fm FlagMapper) parse[T]() ! {
if fm.map_gnu_long(flag_ctx, field)! {
continue
}
} else if style == .v_long {
if fm.map_v_long(flag_ctx, field)! {
continue
}
} else if style == .go_flag {
if fm.map_go_flag_long(flag_ctx, field)! {
continue
Expand Down Expand Up @@ -686,11 +694,11 @@ pub fn (fm FlagMapper) to_doc(dc DocConfig) !string {
pub fn (fm FlagMapper) fields_docs(dc DocConfig) ![]string {
short_delimiter := match dc.style {
.short, .short_long, .v, .go_flag, .cmd_exe { dc.delimiter }
.long { dc.delimiter.repeat(2) }
.long, .v_long { dc.delimiter.repeat(2) }
}
long_delimiter := match dc.style {
.short, .v, .go_flag, .cmd_exe { dc.delimiter }
.long, .short_long { dc.delimiter.repeat(2) }
.long, .v_long, .short_long { dc.delimiter.repeat(2) }
}

pad_desc := if dc.layout.description_padding < 0 { 0 } else { dc.layout.description_padding }
Expand Down Expand Up @@ -1048,6 +1056,66 @@ fn (mut fm FlagMapper) map_v(flag_ctx FlagContext, field StructField) !bool {
return false
}

// map_v_long returns `true` if the V long style flag in `flag_ctx` can be mapped to `field`.
// map_v_long adds data of the match in the internal structures for further processing if applicable
fn (mut fm FlagMapper) map_v_long(flag_ctx FlagContext, field StructField) !bool {
flag_raw := flag_ctx.raw
flag_name := flag_ctx.name
pos := flag_ctx.pos
used_delimiter := flag_ctx.delimiter
next := flag_ctx.next

if flag_raw.contains('=') {
return error('flag `${flag_raw}` does not use `=` for assignment in V long style parsing mode')
}

if field.hints.has(.is_bool) {
if flag_name == field.match_name {
trace_println('${@FN}: found match for (bool) ${fm.dbg_match(flag_ctx, field,
'true', '')}')
fm.field_map_flag[field.name] = FlagData{
raw: flag_raw
field_name: field.name
delimiter: used_delimiter
name: flag_name
pos: pos
}
fm.handled_pos << pos
return true
}
}

if flag_name == field.match_name || flag_name == field.short {
if field.hints.has(.is_array) {
trace_println('${@FN}: found match for (V long style multiple occurences) ${fm.dbg_match(flag_ctx,
field, next, '')}')
fm.array_field_map_flag[field.name] << FlagData{
raw: flag_raw
field_name: field.name
delimiter: used_delimiter
name: flag_name
arg: ?string(next)
pos: pos
}
} else {
trace_println('${@FN}: found match for (V long style) ${fm.dbg_match(flag_ctx,
field, next, '')}')
fm.field_map_flag[field.name] = FlagData{
raw: flag_raw
field_name: field.name
delimiter: used_delimiter
name: flag_name
arg: ?string(next)
pos: pos
}
}
fm.handled_pos << pos
fm.handled_pos << pos + 1 // arg
return true
}
return false
}

// map_go_flag_short returns `true` if the GO short style flag in `flag_ctx` can be mapped to `field`.
// map_go_flag_short adds data of the match in the internal structures for further processing if applicable
fn (mut fm FlagMapper) map_go_flag_short(flag_ctx FlagContext, field StructField) !bool {
Expand Down
88 changes: 88 additions & 0 deletions vlib/flag/v_long_style_flags_test.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Test .v (V) parse style
import flag

const exe_and_v_long_args = ['/path/to/exe', '--version', '--d', 'ident=val', '--o', '/path/to',
'--test', 'abc', '--done', '--define', 'two', '--live']
const exe_and_v_long_args_with_tail = ['/path/to/exe', '--version', '--d', 'ident=val', '--test',
larpon marked this conversation as resolved.
Show resolved Hide resolved
'abc', '--done', '--d', 'two', '--live', 'run', '/path/to']

struct Prefs {
version bool @[short: v]
is_live bool @[long: live]
is_done bool @[long: done]
test string
defines []string @[long: define; short: d]
tail []string @[tail]
out string @[only: o]
not_mapped string = 'not changed'
}

fn test_long_v_style() {
prefs, _ := flag.to_struct[Prefs](exe_and_v_long_args, skip: 1, style: .v_long)!
assert prefs.version
assert prefs.is_live
assert prefs.is_done
assert prefs.test == 'abc'
assert prefs.defines.len == 2
assert prefs.defines[0] == 'ident=val'
assert prefs.defines[1] == 'two'
assert prefs.tail.len == 0
assert prefs.out == '/path/to'
assert prefs.not_mapped == 'not changed'
}

fn test_long_v_style_no_exe() {
prefs, _ := flag.to_struct[Prefs](exe_and_v_long_args[1..], style: .v_long)!
assert prefs.version
assert prefs.is_live
assert prefs.is_done
assert prefs.test == 'abc'
assert prefs.defines.len == 2
assert prefs.defines[0] == 'ident=val'
assert prefs.defines[1] == 'two'
assert prefs.tail.len == 0
assert prefs.out == '/path/to'
assert prefs.not_mapped == 'not changed'
}

fn test_long_v_style_with_tail() {
prefs, _ := flag.to_struct[Prefs](exe_and_v_long_args_with_tail, skip: 1, style: .v_long)!
assert prefs.version
assert prefs.is_live
assert prefs.is_done
assert prefs.test == 'abc'
assert prefs.not_mapped == 'not changed'
assert prefs.defines.len == 2
assert prefs.defines[0] == 'ident=val'
assert prefs.defines[1] == 'two'
assert prefs.out == ''
assert prefs.not_mapped == 'not changed'
assert prefs.tail.len == 2
assert prefs.tail[0] == 'run'
assert prefs.tail[1] == '/path/to'
}

fn test_long_v_style_with_tail_no_exe() {
prefs, _ := flag.to_struct[Prefs](exe_and_v_long_args_with_tail[1..], style: .v_long)!
assert prefs.version
assert prefs.is_live
assert prefs.is_done
assert prefs.test == 'abc'
assert prefs.not_mapped == 'not changed'
assert prefs.defines.len == 2
assert prefs.defines[0] == 'ident=val'
assert prefs.defines[1] == 'two'
assert prefs.out == ''
assert prefs.not_mapped == 'not changed'
assert prefs.tail.len == 2
assert prefs.tail[0] == 'run'
assert prefs.tail[1] == '/path/to'
}

fn test_long_v_style_error_message() {
if _, _ := flag.to_struct[Prefs](exe_and_v_long_args[1..], style: .v) {
assert false, 'flags should not have reached this assert'
} else {
assert err.msg() == 'long delimiter `--` encountered in flag `--version` in v (V) style parsing mode. Maybe you meant `.v_long`?'
}
}
Loading