Skip to content

Commit

Permalink
flag: add support for parsing flag.FlagParser style flags in `to_st…
Browse files Browse the repository at this point in the history
…ruct[T]` (#22152)
  • Loading branch information
larpon authored Sep 3, 2024
1 parent f61a4f1 commit 04a3ecf
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 5 deletions.
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('`=` in flag `${flag_raw}` is not supported in V long style parsing mode. Use `--flag value` instead')
}

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
94 changes: 94 additions & 0 deletions vlib/flag/v_long_style_flags_test.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Test .v (V) parse style
import flag

const exe_and_v_long_args = ['/path/to/exe', '--version', '--p', 'ident=val', '--o', '/path/to',
'--test', 'abc', '--done', '--pop', 'two', '--live']
const exe_and_v_long_args_with_tail = ['/path/to/exe', '--version', '--p', 'ident=val', '--test',
'abc', '--done', '--p', 'two', '--live', 'run', '/path/to']
const error_wrong_assignment_flags = ['--o=error']

struct Prefs {
version bool @[short: v]
is_live bool @[long: live]
is_done bool @[long: done]
test string
pop_flags []string @[long: pop; short: p]
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.pop_flags.len == 2
assert prefs.pop_flags[0] == 'ident=val'
assert prefs.pop_flags[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.pop_flags.len == 2
assert prefs.pop_flags[0] == 'ident=val'
assert prefs.pop_flags[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.pop_flags.len == 2
assert prefs.pop_flags[0] == 'ident=val'
assert prefs.pop_flags[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.pop_flags.len == 2
assert prefs.pop_flags[0] == 'ident=val'
assert prefs.pop_flags[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`?'
}
if _, _ := flag.to_struct[Prefs](error_wrong_assignment_flags, style: .v_long) {
assert false, 'flags should not have reached this assert'
} else {
assert err.msg() == '`=` in flag `--o=error` is not supported in V long style parsing mode. Use `--flag value` instead'
}
}

0 comments on commit 04a3ecf

Please sign in to comment.