From 95fae40d5f7eba051e7e3549ab1a12825152ea37 Mon Sep 17 00:00:00 2001 From: Celestino Amoroso Date: Thu, 5 Mar 2026 22:30:07 +0100 Subject: [PATCH] add final checks on option values --- cli.go | 5 +- cli_test.go | 26 ++++----- opt-base.go | 21 +++++++- opt-bool.go | 11 ++-- opt-help.go | 2 +- opt-int-array.go | 8 ++- opt-int.go | 13 +++-- opt-manager.go | 2 +- opt-multi.go | 7 ++- opt-string-array.go | 8 ++- opt-string-map.go | 8 ++- opt-string.go | 7 ++- opt-version.go | 2 +- opt_test.go | 128 ++++++++++++++++++++++++++++++++++++++++++++ parser.go | 8 ++- 15 files changed, 219 insertions(+), 37 deletions(-) create mode 100644 opt_test.go diff --git a/cli.go b/cli.go index 88d4321..ea1268d 100644 --- a/cli.go +++ b/cli.go @@ -17,7 +17,7 @@ type cliParser interface { } type cliOptionParser interface { - parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, err error) + parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, optValue string, err error) getTemplate() string getDefaultValue() string getBase() *cliOptionBase @@ -25,6 +25,7 @@ type cliOptionParser interface { isSet() bool isHidden() bool requiresValue() bool + finalCheck(cliValue string) (err error) } type OptManager interface { @@ -32,9 +33,11 @@ type OptManager interface { } type SpecialValueFunc func(manager OptManager, cliValue string, currentValue any) (value any, err error) +type FinalCheckFunc func(cliValue string, currentValue any) (err error) type OptReference interface { AddSpecialValue(cliValue string, specialFunc SpecialValueFunc) + OnFinalCheck(checkFunc FinalCheckFunc) SetHidden(hidden bool) } diff --git a/cli_test.go b/cli_test.go index 5bcca67..8738195 100644 --- a/cli_test.go +++ b/cli_test.go @@ -110,7 +110,8 @@ where: } } func TestParser(t *testing.T) { - const expectedOutput = `Option: print-ocr, Type: bool, Value: true + const expectedOutput = `Option: verbose, Type: num, Value: 3 +Option: print-ocr, Type: bool, Value: true Option: save-clip, Type: bool, Value: false Option: trace, Type: bool, Value: true Option: page, Type: num-array, Value: [17 18] @@ -136,7 +137,7 @@ Option: version, Type: n/a, Value: if err := cli.Parse(); err == nil { cli.TraceOptions(tracer) if sb.String() != expectedOutput { - t.Errorf("Parsed options do not match expected.\nGot:\n%q\nExpected:\n%q", sb.String(), expectedOutput) + t.Errorf("Parsed options do not match expected list.\nGot:\n%q\nExpected:\n%q", sb.String(), expectedOutput) } } else { t.Error(err) @@ -323,15 +324,16 @@ where: Output destination file Optional report file - -o, --print-ocr Print the OCR output to stderr - -t, --trace Enable trace mode for detailed logging - -p, --page(s) ["," ...] Process only the specified pages (comma-separated list) - -c, --config Alternate configuration file - -l, --log(s) ["," ...] Logging options (comma-separated list) - -V, --var(s) ["," ...] Define one or more comma separated variables for the actions context (multiple allowed) - -n, --input-name Input file name when source comes from stdin - -d, --work-dir Work directory - --attempts Attempts for retrying failed operations (default: "1") + -V, --verbose Print verbose output (default: "0") + -o, --print-ocr Print the OCR output to stderr + -t, --trace Enable trace mode for detailed logging + -p, --page(s) ["," ...] Process only the specified pages (comma-separated list) + -c, --config Alternate configuration file + -l, --log(s) ["," ...] Logging options (comma-separated list) + --var(s) ["," ...] Define one or more comma separated variables for the actions context (multiple allowed) + -n, --input-name Input file name when source comes from stdin + -d, --work-dir Work directory + --attempts Attempts for retrying failed operations (default: "1") ` var cli CliParser var gd GlobalData @@ -342,7 +344,7 @@ where: } usage := cli.Usage() if usage != expectedUsage { - t.Errorf("Usage output does not match expected.\nGot:\n%s\nExpected:\n%s", usage, expectedUsage) + t.Errorf("Usage output does not match expected text.\nGot:\n%s\nExpected:\n%s", usage, expectedUsage) } } diff --git a/opt-base.go b/opt-base.go index 90eabf3..3cf1b0a 100644 --- a/opt-base.go +++ b/opt-base.go @@ -15,6 +15,7 @@ type cliOptionBase struct { hidden bool alreadySeen bool specialValues map[string]SpecialValueFunc + finalCheckFunc FinalCheckFunc incompatibleWith []string } @@ -41,6 +42,10 @@ func (opt *cliOptionBase) SetHidden(hidden bool) { opt.hidden = hidden } +func (opt *cliOptionBase) OnFinalCheck(checkFunc FinalCheckFunc) { + opt.finalCheckFunc = checkFunc +} + func (opt *cliOptionBase) AddSpecialValue(cliValue string, specialFunc SpecialValueFunc) { if opt.specialValues == nil { opt.specialValues = make(map[string]SpecialValueFunc) @@ -67,8 +72,9 @@ func (opt *cliOptionBase) addIncompatibleOption(names ...string) { opt.incompatibleWith = append(opt.incompatibleWith, names...) } -func (opt *cliOptionBase) parse(parser cliParser, valuePtr *string) (err error) { - return fmt.Errorf("unhandled option %q", opt.name) +func (opt *cliOptionBase) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, value string, err error) { + err = fmt.Errorf("unhandled option %q", opt.name) + return } func (opt *cliOptionBase) getDefaultValue() string { @@ -146,3 +152,14 @@ func (opt *cliOptionBase) fetchOptionValue(parser cliParser, argIndex int, value } return } + +func (opt *cliOptionBase) finalCheck(cliValue string) (err error) { + return +} + +func (opt *cliOptionBase) checkValue(cliValue string, value any) (err error) { + if opt.finalCheckFunc != nil { + err = opt.finalCheckFunc(cliValue, value) + } + return +} diff --git a/opt-bool.go b/opt-bool.go index bdb4f18..66ba054 100644 --- a/opt-bool.go +++ b/opt-bool.go @@ -47,11 +47,11 @@ func (opt *cliOptionBool) getTemplate() (templ string) { return } -func (opt *cliOptionBool) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, err error) { +func (opt *cliOptionBool) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, optValue string, err error) { var boxedValue any - value := "true" + optValue = "true" - if boxedValue, err = opt.getSpecialValue(parser, value, opt.targetVar); err == nil { + if boxedValue, err = opt.getSpecialValue(parser, optValue, opt.targetVar); err == nil { if opt.targetVar != nil { if boxedValue != nil { if val, ok := boxedValue.(bool); ok { @@ -67,6 +67,11 @@ func (opt *cliOptionBool) parse(parser cliParser, argIndex int, valuePtr *string return } +func (opt *cliOptionBool) finalCheck(cliValue string) (err error) { + currentValue, _ := opt.getTargetVar() + return opt.getBase().checkValue(cliValue, currentValue) +} + func (cli *CliParser) AddBoolOpt(name, short string, targetVar *bool, description string, aliases ...string) OptReference { if cli.optionExists(name, short, aliases) { panic(errOptionAlreadyDefined(name)) diff --git a/opt-help.go b/opt-help.go index 0873b35..ab4521a 100644 --- a/opt-help.go +++ b/opt-help.go @@ -23,7 +23,7 @@ func (opt *cliOptionHelp) getTemplate() string { return opt.makeOptSimpleTemplate(false, false, "") } -func (opt *cliOptionHelp) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, err error) { +func (opt *cliOptionHelp) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, value string, err error) { parser.PrintUsage() err = io.EOF return diff --git a/opt-int-array.go b/opt-int-array.go index 162e75f..d039aaf 100644 --- a/opt-int-array.go +++ b/opt-int-array.go @@ -63,8 +63,7 @@ func parseIntRange(value string) (min int, max int, err error) { return } -func (opt *cliOptionIntArray) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, err error) { - var optValue string +func (opt *cliOptionIntArray) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, optValue string, err error) { if optValue, skipNextArg, err = opt.fetchOptionValue(parser, argIndex, valuePtr); err == nil { var boxedValue any if boxedValue, err = opt.getSpecialValue(parser, optValue, opt.targetVar); err == nil { @@ -98,6 +97,11 @@ func (opt *cliOptionIntArray) parse(parser cliParser, argIndex int, valuePtr *st return } +func (opt *cliOptionIntArray) finalCheck(cliValue string) (err error) { + currentValue, _ := opt.getTargetVar() + return opt.getBase().checkValue(cliValue, currentValue) +} + func (cli *CliParser) AddIntArrayOpt(name, short string, targetVar *[]int, defaultValue []int, description string, aliases ...string) OptReference { aliases = cli.checkAlreadyUsedNames(name, short, aliases) opt := &cliOptionIntArray{ diff --git a/opt-int.go b/opt-int.go index 2cca9da..7c28314 100644 --- a/opt-int.go +++ b/opt-int.go @@ -38,11 +38,10 @@ func (opt *cliOptionInt) getTemplate() string { return opt.makeOptTemplate(false, intTypeName) } -func (opt *cliOptionInt) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, err error) { - var value string - if value, skipNextArg, err = opt.fetchOptionValue(parser, argIndex, valuePtr); err == nil { +func (opt *cliOptionInt) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, optValue string, err error) { + if optValue, skipNextArg, err = opt.fetchOptionValue(parser, argIndex, valuePtr); err == nil { var boxedValue any - if boxedValue, err = opt.getSpecialValue(parser, value, opt.targetVar); err == nil { + if boxedValue, err = opt.getSpecialValue(parser, optValue, opt.targetVar); err == nil { if opt.targetVar != nil { if boxedValue != nil { if val, ok := boxedValue.(string); ok { @@ -51,13 +50,17 @@ func (opt *cliOptionInt) parse(parser cliParser, argIndex int, valuePtr *string) err = errInvalidOptionValue(opt.name, boxedValue, "int") } } else { - *opt.targetVar, err = strconv.Atoi(value) + *opt.targetVar, err = strconv.Atoi(optValue) } } } } return } +func (opt *cliOptionInt) finalCheck(cliValue string) (err error) { + currentValue, _ := opt.getTargetVar() + return opt.getBase().checkValue(cliValue, currentValue) +} func (cli *CliParser) AddIntOpt(name, short string, targetVar *int, defaultValue int, description string, aliases ...string) OptReference { if cli.optionExists(name, short, aliases) { diff --git a/opt-manager.go b/opt-manager.go index 960d773..215ea52 100644 --- a/opt-manager.go +++ b/opt-manager.go @@ -2,7 +2,7 @@ package cli func (cli *CliParser) SetOptionValue(name string, value string) (err error) { if opt := cli.findOptionByArg(name); opt != nil { - _, err = opt.parse(cli, -1, &value) + _, _, err = opt.parse(cli, -1, &value) } else { err = errOptionNotFound(name) } diff --git a/opt-multi.go b/opt-multi.go index 61b509e..01e5c54 100644 --- a/opt-multi.go +++ b/opt-multi.go @@ -46,12 +46,17 @@ func (opt *cliOptionMulti) getTemplate() (templ string) { return } -func (opt *cliOptionMulti) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, err error) { +func (opt *cliOptionMulti) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, value string, err error) { if opt.targetVar != nil { *opt.targetVar++ } + value = "true" return } +func (opt *cliOptionMulti) finalCheck(cliValue string) (err error) { + currentValue, _ := opt.getTargetVar() + return opt.getBase().checkValue(cliValue, currentValue) +} func (cli *CliParser) AddMultiOpt(name, short string, targetVar *int, defaultValue int, description string, aliases ...string) OptReference { if cli.optionExists(name, short, aliases) { diff --git a/opt-string-array.go b/opt-string-array.go index b66eba6..b914df9 100644 --- a/opt-string-array.go +++ b/opt-string-array.go @@ -43,8 +43,7 @@ func (opt *cliOptionStringArray) getTemplate() string { // parse retrieves the option value from the parser and updates the target variable. // It handles comma-separated values and special values if configured. -func (opt *cliOptionStringArray) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, err error) { - var value string +func (opt *cliOptionStringArray) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, value string, err error) { if value, skipNextArg, err = opt.fetchOptionValue(parser, argIndex, valuePtr); err == nil { var boxedValue any if boxedValue, err = opt.getSpecialValue(parser, value, opt.targetVar); err == nil { @@ -85,3 +84,8 @@ func (cli *CliParser) AddStringArrayOpt(name, short string, targetVar *[]string, cli.options = append(cli.options, opt) return opt } + +func (opt *cliOptionStringArray) finalCheck(cliValue string) (err error) { + currentValue, _ := opt.getTargetVar() + return opt.getBase().checkValue(cliValue, currentValue) +} diff --git a/opt-string-map.go b/opt-string-map.go index 014d11a..e19c475 100644 --- a/opt-string-map.go +++ b/opt-string-map.go @@ -57,8 +57,7 @@ func (opt *cliOptionStringMap) getTemplate() string { return opt.makeOptTemplate(true, "key=value") } -func (opt *cliOptionStringMap) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, err error) { - var value string +func (opt *cliOptionStringMap) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, value string, err error) { if value, skipNextArg, err = opt.fetchOptionValue(parser, argIndex, valuePtr); err == nil { var boxedValue any if boxedValue, err = opt.getSpecialValue(parser, value, opt.targetVar); err == nil { @@ -90,6 +89,11 @@ func (opt *cliOptionStringMap) parse(parser cliParser, argIndex int, valuePtr *s return } +func (opt *cliOptionStringMap) finalCheck(cliValue string) (err error) { + currentValue, _ := opt.getTargetVar() + return opt.getBase().checkValue(cliValue, currentValue) +} + func (cli *CliParser) AddStringMapOpt(name, short string, targetVar *map[string]string, defaultValue map[string]string, description string, aliases ...string) OptReference { aliases = cli.checkAlreadyUsedNames(name, short, aliases) opt := &cliOptionStringMap{ diff --git a/opt-string.go b/opt-string.go index 90bf7f0..fc723b9 100644 --- a/opt-string.go +++ b/opt-string.go @@ -36,8 +36,7 @@ func (opt *cliOptionString) getTemplate() string { return opt.makeOptTemplate(false, stringTypeName) } -func (opt *cliOptionString) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, err error) { - var value string +func (opt *cliOptionString) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, value string, err error) { if value, skipNextArg, err = opt.fetchOptionValue(parser, argIndex, valuePtr); err == nil { var boxedValue any if boxedValue, err = opt.getSpecialValue(parser, value, opt.targetVar); err == nil { @@ -56,6 +55,10 @@ func (opt *cliOptionString) parse(parser cliParser, argIndex int, valuePtr *stri } return } +func (opt *cliOptionString) finalCheck(cliValue string) (err error) { + currentValue, _ := opt.getTargetVar() + return opt.getBase().checkValue(cliValue, currentValue) +} func (cli *CliParser) AddStringOpt(name, short string, targetVar *string, defaultValue string, description string, aliases ...string) OptReference { if cli.optionExists(name, short, aliases) { diff --git a/opt-version.go b/opt-version.go index 4e671ab..b97c8f3 100644 --- a/opt-version.go +++ b/opt-version.go @@ -23,7 +23,7 @@ func (opt *cliOptionVersion) getTemplate() string { return opt.makeOptSimpleTemplate(false, true, "section") } -func (opt *cliOptionVersion) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, err error) { +func (opt *cliOptionVersion) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, value string, err error) { var args []string if valuePtr != nil { args = []string{*valuePtr} diff --git a/opt_test.go b/opt_test.go new file mode 100644 index 0000000..c5fba1f --- /dev/null +++ b/opt_test.go @@ -0,0 +1,128 @@ +package cli + +import ( + "fmt" + "testing" +) + +func TestOneOptWithEqual(t *testing.T) { + var cli CliParser + var color string + // Always recover from panic to return error before adding options + defer func() { + if r := recover(); r != nil { + if err := r.(error); err != nil { + t.Error(err) + } + + } + }() + + // Define options + cli.AddStringOpt("color", "c", &color, "white", "Set color") + + args := []string{ + "TestOneOptWithEqual", + "--color=blue", + } + + cli.Init(args, "1.0.0", "TestOneOptWithEqual description") + if err := cli.Parse(); err != nil { + t.Error(err) + } else if color != "blue" { + t.Errorf("Expected color blue, got %q", color) + } +} + +func addBoxOption(cli *CliParser, boxPtr *[]int) { + // Define options + optRef := cli.AddIntArrayOpt("box", "b", boxPtr, []int{1, 2, 3, 4}, "Box spec: Left,Width,Top,Height; provide 4 int values") + optRef.OnFinalCheck(func(cliValue string, currentValue any) (err error) { + if array, ok := currentValue.([]int); ok { + if len(array) != 4 { + err = fmt.Errorf("--box option requires exactly 4 items, %d provided", len(array)) + } + } else { + err = fmt.Errorf("wrong datatype for --box option value") + } + return + }) +} + +func TestIntArrayOpt(t *testing.T) { + var box []int + // Always recover from panic to return error before adding options + defer func() { + if r := recover(); r != nil { + if err := r.(error); err != nil { + t.Error(err) + } + + } + }() + + args_1 := []string{ + "TestIntArrayOpt", + "--box=100,200,150,450", + } + + if true { + var cli CliParser + t.Logf("Arg set n. 1") + addBoxOption(&cli, &box) + cli.Init(args_1, "1.0.0", "TestIntArrayOpt description") + if err := cli.Parse(); err != nil { + t.Error(err) + } else if len(box) != 4 { + t.Errorf(`Expected 4 items, got %d`, len(box)) + } + } + + args_2 := []string{ + "TestIntArrayOpt", + "--box=100,200,150", + } + if true { + var cli CliParser + t.Logf("Arg set n. 2") + addBoxOption(&cli, &box) + cli.Init(args_2, "1.0.0", "TestIntArrayOpt description") + if err := cli.Parse(); err == nil { + t.Errorf("Expected error, got nil") + } else if err.Error() != "--box option requires exactly 4 items, 3 provided" { + t.Errorf(`Invalid error: %q; expected: "--box option requires exactly 4 items, 3 provided"`, err.Error()) + } + } + + args_3 := []string{ + "TestIntArrayOpt", + "--box", "100,200,150,450", + } + if true { + var cli CliParser + t.Logf("Arg set n. 3") + addBoxOption(&cli, &box) + cli.Init(args_3, "1.0.0", "TestIntArrayOpt description") + if err := cli.Parse(); err != nil { + t.Error(err) + } else if len(box) != 4 { + t.Errorf(`Expected 4 items, got %d`, len(box)) + } + } + + args_4 := []string{ + "TestIntArrayOpt", + "--box", + } + if true { + var cli CliParser + t.Logf("Arg set n. 4") + addBoxOption(&cli, &box) + cli.Init(args_4, "1.0.0", "TestIntArrayOpt description") + if err := cli.Parse(); err == nil { + t.Errorf("Expected error, got nil") + } else if err.Error() != `option "box" requires a value` { + t.Errorf(`Invalid error - Expected: 'option "box" requires a value'; got '%s'`, err.Error()) + } + } +} diff --git a/parser.go b/parser.go index f69548f..51e2cc0 100644 --- a/parser.go +++ b/parser.go @@ -104,12 +104,16 @@ func (cli *CliParser) parseArg(arg string, index int) (skipNextArg bool, err err } for i, optName := range opts { if opt := cli.findOptionByArg(dashes + optName); opt != nil { + var optValue string if equalPresent && i == len(opts)-1 { - _, err = opt.parse(cli, index, &value) + _, optValue, err = opt.parse(cli, index, &value) } else if i < len(opts)-1 && opt.requiresValue() { err = errMissingOptionValue(dashes + optName) } else { - skipNextArg, err = opt.parse(cli, index, nil) + skipNextArg, optValue, err = opt.parse(cli, index, nil) + } + if err == nil && i == len(opts)-1 { + err = opt.finalCheck(optValue) } } else { err = errUnknownOption(dashes + optName)