From d27ab09cdfa702c8d548d4ccccf0361d1bb76380 Mon Sep 17 00:00:00 2001 From: larpon <768942+larpon@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:04:40 +0200 Subject: [PATCH] flag: add a relaxed parsing mode, that turn flag match errors into `no_match` entries instead (#22191) --- vlib/flag/flag_to.v | 34 ++++++++++++++++++++++-- vlib/flag/flag_to_misc_test.v | 35 ++++++++++++++++++++++++- vlib/flag/flag_to_relaxed_test.v | 45 ++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 vlib/flag/flag_to_relaxed_test.v diff --git a/vlib/flag/flag_to.v b/vlib/flag/flag_to.v index 2b3a23f86..614e5f1a8 100644 --- a/vlib/flag/flag_to.v +++ b/vlib/flag/flag_to.v @@ -18,6 +18,11 @@ struct FlagContext { pos int // position in arg array } +pub enum ParseMode { + strict // return errors for unknown or malformed flags per default + relaxed // relax flag match errors and add them to `no_match` list instead +} + pub enum Style { short // Posix short only, allows multiple shorts -def is `-d -e -f` and "sticky" arguments e.g.: `-ofoo` = `-o foo` long // GNU style long option *only*. E.g.: `--name` or `--name=value` @@ -73,8 +78,9 @@ fn (sf StructField) shortest_match_name() ?string { @[params] pub struct ParseConfig { pub: - delimiter string = '-' // delimiter used for flags - style Style = .short_long // expected flag style + delimiter string = '-' // delimiter used for flags + mode ParseMode = .strict // return errors for unknown or malformed flags per default + style Style = .short_long // expected flag style stop ?string // single, usually '--', string that stops parsing flags/options skip u16 // skip this amount in the input argument array, usually `1` for skipping executable or subcmd entry } @@ -422,27 +428,51 @@ pub fn (mut fm FlagMapper) parse[T]() ! { is_short_delimiter := used_delimiter.count(delimiter) == 1 is_invalid_delimiter := !is_long_delimiter && !is_short_delimiter if is_invalid_delimiter { + if config.mode == .relaxed { + fm.no_match << pos + continue + } return error('invalid delimiter `${used_delimiter}` for flag `${arg}`') } if is_long_delimiter { if style == .v { + if config.mode == .relaxed { + fm.no_match << pos + continue + } return error('long delimiter `${used_delimiter}` encountered in flag `${arg}` in ${style} (V) style parsing mode. Maybe you meant `.v_flag_parser`?') } if style == .short { + if config.mode == .relaxed { + fm.no_match << pos + continue + } return error('long delimiter `${used_delimiter}` encountered in flag `${arg}` in ${style} (POSIX) style parsing mode') } } if is_short_delimiter { if style == .long { + if config.mode == .relaxed { + fm.no_match << pos + continue + } return error('short delimiter `${used_delimiter}` encountered in flag `${arg}` in ${style} (GNU) style parsing mode') } if style == .short_long && flag_name.len > 1 && flag_name.contains('-') { + if config.mode == .relaxed { + fm.no_match << pos + continue + } return error('long name `${flag_name}` used with short delimiter `${used_delimiter}` in flag `${arg}` in ${style} (POSIX/GNU) style parsing mode') } } if flag_name == '' { + if config.mode == .relaxed { + fm.no_match << pos + continue + } return error('invalid delimiter-only flag `${arg}`') } diff --git a/vlib/flag/flag_to_misc_test.v b/vlib/flag/flag_to_misc_test.v index 5b1d521e1..3c56181ec 100644 --- a/vlib/flag/flag_to_misc_test.v +++ b/vlib/flag/flag_to_misc_test.v @@ -1,6 +1,6 @@ import flag -const all_style_enums = [flag.Style.short, .short_long, .long, .v] +const all_style_enums = [flag.Style.short, .short_long, .long, .v, .v_flag_parser] const posix_gnu_style_enums = [flag.Style.short, .short_long, .long] const mixed_args = ['/path/to/exe', '-vv', 'vvv', '-version', '--mix', '--mix-all=all', '-ldflags', '-m', '2', '-fgh', '["test", "test"]', '-m', '{map: 2, ml-q:"hello"}'] @@ -169,3 +169,36 @@ fn test_flag_error_messages() { assert no_matches == ['--some-test=ouch'] } } + +fn test_flag_no_error_messages_when_relaxed() { + // Test that there's no errors in `mode: .relaxed` + _, no_matches1 := flag.to_struct[Config](posix_and_gnu_args_with_subcmd, + skip: 1 + style: .short + mode: .relaxed + )! + assert no_matches1 == ['subcmd', '--device=two', '--amount=8'] + + _, no_matches2 := flag.to_struct[LongConfig](gnu_args_error, + style: .long + mode: .relaxed + )! + assert no_matches2 == ['oo'] + + if _, _ := flag.to_struct[LongConfig](['--version=1.2.3'], + style: .long + mode: .relaxed + ) + { + assert false, 'flags should not have reached this assert' + } else { + // Type errors should not be ignored, only non-matching flags/args + assert err.msg() == 'flag `--version=1.2.3` can not be assigned to bool field "show_version"' + } + + _, no_matches3 := flag.to_struct[IgnoreConfig](ignore_args_error, + style: .long + mode: .relaxed + )! + assert no_matches3 == ['--some-test=ouch'] +} diff --git a/vlib/flag/flag_to_relaxed_test.v b/vlib/flag/flag_to_relaxed_test.v new file mode 100644 index 000000000..029a0075f --- /dev/null +++ b/vlib/flag/flag_to_relaxed_test.v @@ -0,0 +1,45 @@ +import flag + +const one_ok_gnu_arg_no_tail = ['-flip', '--g', '/path/to', '--mix', '-ver'] +const two_ok_gnu_arg_no_tail = ['-flip', '--g', '--sound=blop', '/path/to', '--mix', '-ver'] + +const one_ok_gnu_arg_tail = ['-flip', '--g', '/path/to', '--mix', '-ver', 'tail'] +const two_ok_gnu_arg_tail = ['-flip', '--g', '--sound=blop', '/path/to', '--mix', '-ver', 'tail'] + +struct Config { + mix bool + sound string + beep bool + path string @[tail] +} + +fn test_flag_relaxed_mode() { + // Test `mode: .relaxed` + config1, no_matches1 := flag.to_struct[Config](one_ok_gnu_arg_no_tail, mode: .relaxed)! + assert config1.mix == true + assert config1.sound == '' + assert config1.beep == false + assert config1.path == '' + assert no_matches1 == ['-flip', '--g', '/path/to', '-ver'] + + config2, no_matches2 := flag.to_struct[Config](two_ok_gnu_arg_no_tail, mode: .relaxed)! + assert config2.mix == true + assert config2.sound == 'blop' + assert config2.beep == false + assert config2.path == '' + assert no_matches2 == ['-flip', '--g', '/path/to', '-ver'] + + config3, no_matches3 := flag.to_struct[Config](one_ok_gnu_arg_tail, mode: .relaxed)! + assert config3.mix == true + assert config3.sound == '' + assert config3.beep == false + assert config3.path == 'tail' + assert no_matches3 == ['-flip', '--g', '/path/to', '-ver'] + + config4, no_matches4 := flag.to_struct[Config](two_ok_gnu_arg_tail, mode: .relaxed)! + assert config4.mix == true + assert config4.sound == 'blop' + assert config4.beep == false + assert config4.path == 'tail' + assert no_matches4 == ['-flip', '--g', '/path/to', '-ver'] +} -- 2.39.5