Skip to content

Commit

Permalink
feat: Allow disabling negative options by no_negative_options
Browse files Browse the repository at this point in the history
Also make negative option handling consistent.
Make sure that stcut fields are positive only.
  • Loading branch information
prantlf committed Oct 15, 2023
1 parent df0b0b8 commit a53f710
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 26 deletions.
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ Names of fields in the options structure are inferred from the command-line opti
* The long variant of an option will be mapped to its field name. The short variant will be used only if the long variant is missing.
* Upper-case letters will converted to lower-case.
* Dashes (`-`) in an option name will be converted to underscores (`_`) in its field name.
* No negative names (starting with `no_`). If you want to specify a negative option (the short one using a capital letter and the long one starting with `--no-`), you can, but the field has to be positive and assigned the default value `true`. Then you can detect the presence of the negative option by a comparison to `false`.

If you write a short (single-letter) option for a boolean flag in upper-case, it will set the value `false` to the boolean field instead of `true`. If you write a long option for a boolean flag, you can negate its value by prefixing the option with `no-`:

Expand Down Expand Up @@ -303,6 +304,7 @@ The following input fields are available:
| `disable_short_negative` | `bool` | `false` | disables handling uppercase letters as negated options |
| `ignore_number_overflow` | `bool` | `false` | ignores an overflow when converting numbers to option fields |
| `options_anywhere` | `bool` | `false` | do not look for options only after the line with `Options:` |
| `no_negative_options` | `bool` | `false` | do not recognise options starting with `no-` as negations |

### Advanced

Expand Down Expand Up @@ -381,11 +383,15 @@ struct Opts {
}
```

## TODO
## Contributing

This is a work in progress.
In lieu of a formal styleguide, take care to maintain the existing coding style. Lint and test your code.

* Add a string map as the parsing target.
## License

Copyright (c) 2023 Ferdinand Prantl

Licensed under the MIT license.

[VPM]: https://vpm.vlang.io/packages/prantlf.cargs
[getopt and getopt_long]: https://en.wikipedia.org/wiki/Getopt
Expand Down
4 changes: 2 additions & 2 deletions src/analyse.v
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ mut:
val string
}

fn analyse_usage(text string, anywhere bool) []Opt {
fn analyse_usage(text string, anywhere bool, no_negative bool) []Opt {
mut re_def := regex_opt('^\\s*-([^\\-])?(?:[|,]\\s*-)?(?:-([^ ]+))?(?:\\s+[<\\[]([^>\\]]+)[>\\]])?') or {
panic(err)
}
Expand All @@ -37,7 +37,7 @@ fn analyse_usage(text string, anywhere bool) []Opt {
if grp_opt[2].start >= 0 {
opt.val = line[grp_opt[2].start..grp_opt[2].end]
}
if opt.long.starts_with('no-') {
if !no_negative && opt.long.starts_with('no-') {
orig_name := opt.long
opt.long = opt.long[3..]
cargs.d.log('option short: "%s", long: "%s" (originally "%s"), value: "%s"',
Expand Down
30 changes: 22 additions & 8 deletions src/analyse_test.v
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module cargs

fn test_no_opts() {
opts := analyse_usage('', false)
opts := analyse_usage('', false, false)
assert opts.len == 0
}

Expand All @@ -10,7 +10,7 @@ fn test_no_options_line() {
Options:
-l|--line-break append a line break to the JSON output
',
true)
true, false)
assert opts.len == 1
assert opts[0].short == 'l'
assert opts[0].long == 'line-break'
Expand All @@ -22,7 +22,7 @@ fn test_flag() {
Options:
-l|--line-break append a line break to the JSON output
',
false)
false, false)
assert opts.len == 1
assert opts[0].short == 'l'
assert opts[0].long == 'line-break'
Expand All @@ -33,7 +33,8 @@ fn test_short_flag() {
opts := analyse_usage('
Options:
-l append a line break to the JSON output
', false)
', false,
false)
assert opts.len == 1
assert opts[0].short == 'l'
assert opts[0].long == ''
Expand All @@ -45,19 +46,31 @@ fn test_long_flag() {
Options:
--line-break append a line break to the JSON output
',
false)
false, false)
assert opts.len == 1
assert opts[0].short == ''
assert opts[0].long == 'line-break'
assert opts[0].val == ''
}

fn test_no_negative() {
opts := analyse_usage('
Options:
--no-line-break do not append a line break to the JSON output
',
false, true)
assert opts.len == 1
assert opts[0].short == ''
assert opts[0].long == 'no-line-break'
assert opts[0].val == ''
}

fn test_val() {
opts := analyse_usage('
Options:
-o|--output <file> write the JSON output to a file
',
false)
false, false)
assert opts.len == 1
assert opts[0].short == 'o'
assert opts[0].long == 'output'
Expand All @@ -68,7 +81,8 @@ fn test_short_val() {
opts := analyse_usage('
Options:
-o <file> write the JSON output to a file
', false)
', false,
false)
assert opts.len == 1
assert opts[0].short == 'o'
assert opts[0].long == ''
Expand All @@ -80,7 +94,7 @@ fn test_long_val() {
Options:
--output [file] write the JSON output to a file
',
false)
false, false)
assert opts.len == 1
assert opts[0].short == ''
assert opts[0].long == 'output'
Expand Down
38 changes: 25 additions & 13 deletions src/parse.v
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mut:
disable_short_negative bool
ignore_number_overflow bool
options_anywhere bool
no_negative_options bool
}

pub struct Scanned {
Expand Down Expand Up @@ -122,7 +123,7 @@ pub fn parse_scanned_to[T](scanned &Scanned, input &Input, mut cfg T) ![]string
if d.is_enabled() {
d.log_str('flag "${flag}" used')
}
set_flag(mut cfg, opt, flag)!
set_flag(mut cfg, opt, flag, input.no_negative_options)!
}
applied << opt
} else {
Expand All @@ -133,7 +134,7 @@ pub fn parse_scanned_to[T](scanned &Scanned, input &Input, mut cfg T) ![]string
}
}

check_applied(cfg, applied)!
check_applied(cfg, applied, input.no_negative_options)!

for i < l {
cmds << args[i]
Expand All @@ -144,7 +145,7 @@ pub fn parse_scanned_to[T](scanned &Scanned, input &Input, mut cfg T) ![]string

pub fn scan(usage string, input &Input) !Scanned {
d.log_str('scan command-line usage and fill options')
opts := analyse_usage(usage, input.options_anywhere)
opts := analyse_usage(usage, input.options_anywhere, input.no_negative_options)
raw_args := input.args or { os.args[1..] }
args := split_short_opts(opts, raw_args)!
return Scanned{opts, args, usage}
Expand Down Expand Up @@ -242,7 +243,7 @@ fn split_short_opts(opts []Opt, raw_args []string) ![]string {

fn (opts []Opt) find_opt_and_flag(arg string, input Input) ?(Opt, bool) {
mut flag := true
name := if arg.starts_with('no-') {
name := if !input.no_negative_options && arg.starts_with('no-') {
flag = false
arg[3..]
} else {
Expand All @@ -252,10 +253,21 @@ fn (opts []Opt) find_opt_and_flag(arg string, input Input) ?(Opt, bool) {
for opt in opts {
if name.len == 1 {
if name == opt.short {
return opt, true
} else if !input.disable_short_negative && name[0] < u8(`a`)
&& name[0] == opt.short[0] & ~32 {
return opt, false
flag = if !input.disable_short_negative && !input.no_negative_options
&& name[0] < u8(`a`) {
false
} else {
true
}
return opt, flag
} else if !input.disable_short_negative && !input.no_negative_options
&& opt.short.len == 1 && name[0] & ~32 == opt.short[0] & ~32 {
flag = if name[0] < u8(`a`) {
false
} else {
true
}
return opt, flag
}
} else {
if name == opt.long {
Expand Down Expand Up @@ -283,7 +295,7 @@ fn (opt &Opt) has_name(name string) bool {
}
}

fn check_applied[T](cfg T, applied []Opt) ! {
fn check_applied[T](cfg T, applied []Opt, no_negative bool) ! {
mut valid := []Opt{}

$for field in T.fields {
Expand All @@ -309,7 +321,7 @@ fn check_applied[T](cfg T, applied []Opt) ! {
mut found := false
for opt in applied {
name := opt.field_name()
if name == arg_name {
if name == field.name || opt.long == arg_name {
valid << opt
found = true
break
Expand Down Expand Up @@ -347,7 +359,7 @@ fn set_val[T](mut cfg T, opt Opt, val string, input Input) ! {
}
}

if name == arg_name {
if name == field.name || opt.long == arg_name {
ino := nooverflow || input.ignore_number_overflow

if d.is_enabled() {
Expand Down Expand Up @@ -452,7 +464,7 @@ fn convert_val[T](val string, ignore_overflow bool) !T {
}
}

fn set_flag[T](mut cfg T, opt Opt, flag bool) ! {
fn set_flag[T](mut cfg T, opt Opt, flag bool, no_negative bool) ! {
name := opt.field_name()
$for field in T.fields {
mut arg_name := field.name
Expand All @@ -462,7 +474,7 @@ fn set_flag[T](mut cfg T, opt Opt, flag bool) ! {
}
}

if name == arg_name {
if name == field.name || opt.long == arg_name {
if d.is_enabled() {
d.log_str('setting flag "${name}" using argument "${arg_name}" to "${flag}"')
}
Expand Down
39 changes: 39 additions & 0 deletions src/parse_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ struct TwoTrue {
output string
}

struct TwoNegative {
no_line_break bool
}

struct TwoPositiveTrue {
line_break bool = true
}

struct TwoPositiveFalse {
line_break bool
}

fn test_no_opts_no_args() {
opts, args := parse[Two]('', Input{ args: [] })!
assert opts.line_break == false
Expand Down Expand Up @@ -62,6 +74,33 @@ fn test_no_options_line() {
assert opts.line_break == true
}

fn test_no_negative_options() {
opts, args := parse[TwoNegative]('
Options:
-n|--no-line-break do not append a line break to the JSON output
',
Input{ args: ['-n'], no_negative_options: true })!
assert opts.no_line_break == true
}

fn test_negative_option() {
opts, args := parse[TwoPositiveTrue]('
Options:
-N|--no-line-break do not append a line break to the JSON output
',
Input{ args: ['-N'] })!
assert opts.line_break == false
}

fn test_positive_option() {
opts, args := parse[TwoPositiveFalse]('
Options:
-N|--no-line-break do not append a line break to the JSON output
',
Input{ args: ['-n'] })!
assert opts.line_break == true
}

fn test_neg_short_flag() {
opts, args := parse[Two]('
Options:
Expand Down

0 comments on commit a53f710

Please sign in to comment.