add final checks on option values

This commit is contained in:
Celestino Amoroso 2026-03-05 22:30:07 +01:00
parent b5f8d9eaab
commit 95fae40d5f
15 changed files with 219 additions and 37 deletions

5
cli.go
View File

@ -17,7 +17,7 @@ type cliParser interface {
} }
type cliOptionParser 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 getTemplate() string
getDefaultValue() string getDefaultValue() string
getBase() *cliOptionBase getBase() *cliOptionBase
@ -25,6 +25,7 @@ type cliOptionParser interface {
isSet() bool isSet() bool
isHidden() bool isHidden() bool
requiresValue() bool requiresValue() bool
finalCheck(cliValue string) (err error)
} }
type OptManager interface { type OptManager interface {
@ -32,9 +33,11 @@ type OptManager interface {
} }
type SpecialValueFunc func(manager OptManager, cliValue string, currentValue any) (value any, err error) 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 { type OptReference interface {
AddSpecialValue(cliValue string, specialFunc SpecialValueFunc) AddSpecialValue(cliValue string, specialFunc SpecialValueFunc)
OnFinalCheck(checkFunc FinalCheckFunc)
SetHidden(hidden bool) SetHidden(hidden bool)
} }

View File

@ -110,7 +110,8 @@ where:
} }
} }
func TestParser(t *testing.T) { 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: save-clip, Type: bool, Value: false
Option: trace, Type: bool, Value: true Option: trace, Type: bool, Value: true
Option: page, Type: num-array, Value: [17 18] Option: page, Type: num-array, Value: [17 18]
@ -136,7 +137,7 @@ Option: version, Type: n/a, Value: <nil>
if err := cli.Parse(); err == nil { if err := cli.Parse(); err == nil {
cli.TraceOptions(tracer) cli.TraceOptions(tracer)
if sb.String() != expectedOutput { 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 { } else {
t.Error(err) t.Error(err)
@ -323,15 +324,16 @@ where:
<dest> Output destination file <dest> Output destination file
<report> Optional report file <report> Optional report file
<options> <options>
-o, --print-ocr Print the OCR output to stderr -V, --verbose Print verbose output (default: "0")
-t, --trace Enable trace mode for detailed logging -o, --print-ocr Print the OCR output to stderr
-p, --page(s) <num>["," ...] Process only the specified pages (comma-separated list) -t, --trace Enable trace mode for detailed logging
-c, --config <file> Alternate configuration file -p, --page(s) <num>["," ...] Process only the specified pages (comma-separated list)
-l, --log(s) <string>["," ...] Logging options (comma-separated list) -c, --config <file> Alternate configuration file
-V, --var(s) <key=value>["," ...] Define one or more comma separated variables for the actions context (multiple allowed) -l, --log(s) <string>["," ...] Logging options (comma-separated list)
-n, --input-name <string> Input file name when source comes from stdin --var(s) <key=value>["," ...] Define one or more comma separated variables for the actions context (multiple allowed)
-d, --work-dir <dir> Work directory -n, --input-name <string> Input file name when source comes from stdin
--attempts <num> Attempts for retrying failed operations (default: "1") -d, --work-dir <dir> Work directory
--attempts <num> Attempts for retrying failed operations (default: "1")
` `
var cli CliParser var cli CliParser
var gd GlobalData var gd GlobalData
@ -342,7 +344,7 @@ where:
} }
usage := cli.Usage() usage := cli.Usage()
if usage != expectedUsage { 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)
} }
} }

View File

@ -15,6 +15,7 @@ type cliOptionBase struct {
hidden bool hidden bool
alreadySeen bool alreadySeen bool
specialValues map[string]SpecialValueFunc specialValues map[string]SpecialValueFunc
finalCheckFunc FinalCheckFunc
incompatibleWith []string incompatibleWith []string
} }
@ -41,6 +42,10 @@ func (opt *cliOptionBase) SetHidden(hidden bool) {
opt.hidden = hidden opt.hidden = hidden
} }
func (opt *cliOptionBase) OnFinalCheck(checkFunc FinalCheckFunc) {
opt.finalCheckFunc = checkFunc
}
func (opt *cliOptionBase) AddSpecialValue(cliValue string, specialFunc SpecialValueFunc) { func (opt *cliOptionBase) AddSpecialValue(cliValue string, specialFunc SpecialValueFunc) {
if opt.specialValues == nil { if opt.specialValues == nil {
opt.specialValues = make(map[string]SpecialValueFunc) opt.specialValues = make(map[string]SpecialValueFunc)
@ -67,8 +72,9 @@ func (opt *cliOptionBase) addIncompatibleOption(names ...string) {
opt.incompatibleWith = append(opt.incompatibleWith, names...) opt.incompatibleWith = append(opt.incompatibleWith, names...)
} }
func (opt *cliOptionBase) parse(parser cliParser, valuePtr *string) (err error) { func (opt *cliOptionBase) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, value string, err error) {
return fmt.Errorf("unhandled option %q", opt.name) err = fmt.Errorf("unhandled option %q", opt.name)
return
} }
func (opt *cliOptionBase) getDefaultValue() string { func (opt *cliOptionBase) getDefaultValue() string {
@ -146,3 +152,14 @@ func (opt *cliOptionBase) fetchOptionValue(parser cliParser, argIndex int, value
} }
return 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
}

View File

@ -47,11 +47,11 @@ func (opt *cliOptionBool) getTemplate() (templ string) {
return 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 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 opt.targetVar != nil {
if boxedValue != nil { if boxedValue != nil {
if val, ok := boxedValue.(bool); ok { if val, ok := boxedValue.(bool); ok {
@ -67,6 +67,11 @@ func (opt *cliOptionBool) parse(parser cliParser, argIndex int, valuePtr *string
return 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 { func (cli *CliParser) AddBoolOpt(name, short string, targetVar *bool, description string, aliases ...string) OptReference {
if cli.optionExists(name, short, aliases) { if cli.optionExists(name, short, aliases) {
panic(errOptionAlreadyDefined(name)) panic(errOptionAlreadyDefined(name))

View File

@ -23,7 +23,7 @@ func (opt *cliOptionHelp) getTemplate() string {
return opt.makeOptSimpleTemplate(false, false, "") 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() parser.PrintUsage()
err = io.EOF err = io.EOF
return return

View File

@ -63,8 +63,7 @@ func parseIntRange(value string) (min int, max int, err error) {
return return
} }
func (opt *cliOptionIntArray) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, err error) { func (opt *cliOptionIntArray) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, optValue string, err error) {
var optValue string
if optValue, skipNextArg, err = opt.fetchOptionValue(parser, argIndex, valuePtr); err == nil { if optValue, skipNextArg, err = opt.fetchOptionValue(parser, argIndex, valuePtr); err == nil {
var boxedValue any var boxedValue any
if boxedValue, err = opt.getSpecialValue(parser, optValue, opt.targetVar); err == nil { 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 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 { func (cli *CliParser) AddIntArrayOpt(name, short string, targetVar *[]int, defaultValue []int, description string, aliases ...string) OptReference {
aliases = cli.checkAlreadyUsedNames(name, short, aliases) aliases = cli.checkAlreadyUsedNames(name, short, aliases)
opt := &cliOptionIntArray{ opt := &cliOptionIntArray{

View File

@ -38,11 +38,10 @@ func (opt *cliOptionInt) getTemplate() string {
return opt.makeOptTemplate(false, intTypeName) return opt.makeOptTemplate(false, intTypeName)
} }
func (opt *cliOptionInt) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, err error) { func (opt *cliOptionInt) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, optValue string, err error) {
var value string if optValue, skipNextArg, err = opt.fetchOptionValue(parser, argIndex, valuePtr); err == nil {
if value, skipNextArg, err = opt.fetchOptionValue(parser, argIndex, valuePtr); err == nil {
var boxedValue any 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 opt.targetVar != nil {
if boxedValue != nil { if boxedValue != nil {
if val, ok := boxedValue.(string); ok { 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") err = errInvalidOptionValue(opt.name, boxedValue, "int")
} }
} else { } else {
*opt.targetVar, err = strconv.Atoi(value) *opt.targetVar, err = strconv.Atoi(optValue)
} }
} }
} }
} }
return 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 { func (cli *CliParser) AddIntOpt(name, short string, targetVar *int, defaultValue int, description string, aliases ...string) OptReference {
if cli.optionExists(name, short, aliases) { if cli.optionExists(name, short, aliases) {

View File

@ -2,7 +2,7 @@ package cli
func (cli *CliParser) SetOptionValue(name string, value string) (err error) { func (cli *CliParser) SetOptionValue(name string, value string) (err error) {
if opt := cli.findOptionByArg(name); opt != nil { if opt := cli.findOptionByArg(name); opt != nil {
_, err = opt.parse(cli, -1, &value) _, _, err = opt.parse(cli, -1, &value)
} else { } else {
err = errOptionNotFound(name) err = errOptionNotFound(name)
} }

View File

@ -46,12 +46,17 @@ func (opt *cliOptionMulti) getTemplate() (templ string) {
return 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 { if opt.targetVar != nil {
*opt.targetVar++ *opt.targetVar++
} }
value = "true"
return 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 { func (cli *CliParser) AddMultiOpt(name, short string, targetVar *int, defaultValue int, description string, aliases ...string) OptReference {
if cli.optionExists(name, short, aliases) { if cli.optionExists(name, short, aliases) {

View File

@ -43,8 +43,7 @@ func (opt *cliOptionStringArray) getTemplate() string {
// parse retrieves the option value from the parser and updates the target variable. // parse retrieves the option value from the parser and updates the target variable.
// It handles comma-separated values and special values if configured. // It handles comma-separated values and special values if configured.
func (opt *cliOptionStringArray) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, err error) { func (opt *cliOptionStringArray) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, value string, err error) {
var value string
if value, skipNextArg, err = opt.fetchOptionValue(parser, argIndex, valuePtr); err == nil { if value, skipNextArg, err = opt.fetchOptionValue(parser, argIndex, valuePtr); err == nil {
var boxedValue any var boxedValue any
if boxedValue, err = opt.getSpecialValue(parser, value, opt.targetVar); err == nil { 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) cli.options = append(cli.options, opt)
return opt return opt
} }
func (opt *cliOptionStringArray) finalCheck(cliValue string) (err error) {
currentValue, _ := opt.getTargetVar()
return opt.getBase().checkValue(cliValue, currentValue)
}

View File

@ -57,8 +57,7 @@ func (opt *cliOptionStringMap) getTemplate() string {
return opt.makeOptTemplate(true, "key=value") return opt.makeOptTemplate(true, "key=value")
} }
func (opt *cliOptionStringMap) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, err error) { func (opt *cliOptionStringMap) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, value string, err error) {
var value string
if value, skipNextArg, err = opt.fetchOptionValue(parser, argIndex, valuePtr); err == nil { if value, skipNextArg, err = opt.fetchOptionValue(parser, argIndex, valuePtr); err == nil {
var boxedValue any var boxedValue any
if boxedValue, err = opt.getSpecialValue(parser, value, opt.targetVar); err == nil { 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 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 { 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) aliases = cli.checkAlreadyUsedNames(name, short, aliases)
opt := &cliOptionStringMap{ opt := &cliOptionStringMap{

View File

@ -36,8 +36,7 @@ func (opt *cliOptionString) getTemplate() string {
return opt.makeOptTemplate(false, stringTypeName) return opt.makeOptTemplate(false, stringTypeName)
} }
func (opt *cliOptionString) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, err error) { func (opt *cliOptionString) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, value string, err error) {
var value string
if value, skipNextArg, err = opt.fetchOptionValue(parser, argIndex, valuePtr); err == nil { if value, skipNextArg, err = opt.fetchOptionValue(parser, argIndex, valuePtr); err == nil {
var boxedValue any var boxedValue any
if boxedValue, err = opt.getSpecialValue(parser, value, opt.targetVar); err == nil { 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 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 { func (cli *CliParser) AddStringOpt(name, short string, targetVar *string, defaultValue string, description string, aliases ...string) OptReference {
if cli.optionExists(name, short, aliases) { if cli.optionExists(name, short, aliases) {

View File

@ -23,7 +23,7 @@ func (opt *cliOptionVersion) getTemplate() string {
return opt.makeOptSimpleTemplate(false, true, "section") 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 var args []string
if valuePtr != nil { if valuePtr != nil {
args = []string{*valuePtr} args = []string{*valuePtr}

128
opt_test.go Normal file
View File

@ -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())
}
}
}

View File

@ -104,12 +104,16 @@ func (cli *CliParser) parseArg(arg string, index int) (skipNextArg bool, err err
} }
for i, optName := range opts { for i, optName := range opts {
if opt := cli.findOptionByArg(dashes + optName); opt != nil { if opt := cli.findOptionByArg(dashes + optName); opt != nil {
var optValue string
if equalPresent && i == len(opts)-1 { 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() { } else if i < len(opts)-1 && opt.requiresValue() {
err = errMissingOptionValue(dashes + optName) err = errMissingOptionValue(dashes + optName)
} else { } 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 { } else {
err = errUnknownOption(dashes + optName) err = errUnknownOption(dashes + optName)