16 Commits

Author SHA1 Message Date
camoroso 9abbe37e02 go.mod: appended /v2 to the module path 2026-03-19 18:54:16 +01:00
camoroso 52ff844ce1 last changes broke compatibility so let's increase major. New version v2.0.0 2026-03-19 09:15:12 +01:00
camoroso 20b6b961fb cli args moved from Init() to Parse() 2026-03-19 09:10:49 +01:00
camoroso 00b84278d8 CliParser.Init() accepts optional flags. Currently una flag is available: ResetOnEqualSign 2026-03-06 11:45:58 +01:00
camoroso 9e28ee6545 final checks on opt values moved after option parsing is complete 2026-03-06 10:36:07 +01:00
camoroso 92267aec50 cli.go: in CliParser.Parse() renamed args as commandArgs 2026-03-06 06:32:17 +01:00
camoroso 95fae40d5f add final checks on option values 2026-03-05 22:30:07 +01:00
camoroso b5f8d9eaab cli.go: fixed an out-of-bound error on cli.argSpecs array 2026-03-05 06:33:14 +01:00
camoroso ac8d5fa3a9 value of a container option (e.g. array) is reset to empty on first specification 2026-03-03 05:58:15 +01:00
camoroso f55e9cea82 opt-multi: changed the help template 2026-02-04 05:54:49 +01:00
camoroso 4111864916 New option type 'multi'. Its value is the number of occurences of an option 2026-02-04 05:22:43 +01:00
camoroso f830851e58 test: added NAME section to the help output 2026-01-23 11:20:27 +01:00
camoroso b9f5f92948 cli-usage.go: added black separator around NAME sections 2026-01-23 11:19:19 +01:00
camoroso 69f550884a added program name and description to the help output 2026-01-22 12:36:33 +01:00
camoroso 9e2e5c1f37 opt-int-array.go: changed data type in error messages 2026-01-22 12:34:22 +01:00
camoroso 548b816d4b README.md AI generated 2025-12-14 05:50:27 +01:00
18 changed files with 847 additions and 115 deletions
+86
View File
@@ -0,0 +1,86 @@
# go-pkg/cli
Lightweight, dependency-free command-line argument and option parsing library for Go.
## Overview
This package provides a small, composable CLI parser with support for:
- boolean, string, int, and array/map options
- positional arguments (single and repeating)
- aliases, short flags, defaults and hidden options
- validation for incompatible options and custom "special" values
- automatic usage and version printing
See the core parser implementation in [`parser.go`](parser.go) and the high-level API in [`cli.go`](cli.go).
## Features
- Option types: [`AddBoolOpt`](opt-bool.go), [`AddStringOpt`](opt-string.go), [`AddIntOpt`](opt-int.go), [`AddStringArrayOpt`](opt-string-array.go), [`AddIntArrayOpt`](opt-int-array.go), [`AddStringMapOpt`](opt-string-map.go)
- Positional args: [`AddStringArg`](arg-string.go), [`AddStringArrayArg`](arg-string-array.go)
- Usage & version output: [`CliParser.Usage`](cli-usage.go), [`CliParser.PrintVersion`](cli-version.go)
- Option tracing via [`CliParser.TraceOptions`](cli.go) and [`SimpleOptionTracer`](simple-opt-tracer.go)
- Programmatic option setting: [`SetOptionValue`](opt-manager.go)
## Quick start
Example: define options and parse argv
```go
package main
import (
"fmt"
"git.portale-stac.it/go-pkg/cli"
)
func main() {
var parser cli.CliParser
parser.Init([]string{"prog", "--debug", "--config", "app.yaml", "input.txt"}, "$VER:prog,0.1.0,2025,email:$")
// define target variables and options
var debug bool
var config string
var inputs []string
parser.AddBoolOpt("debug", "d", &debug, "Enable debug")
parser.AddStringOpt("config", "c", &config, "app.yaml", "Config file")
parser.AddStringArrayArg("files", true, &inputs, "Input files")
// print usage: parser.Usage() - see [`CliParser.Usage`](cli-usage.go)
if err := parser.Parse(); err != nil {
fmt.Println("parse error:", err)
return
}
fmt.Println("debug:", debug)
fmt.Println("config:", config)
fmt.Println("inputs:", inputs)
}
```
Refer to the unit test for a realistic example: [`cli_test.go`](cli_test.go).
## Usage
## API Reference
Key types and methods:
- [`CliParser`](cli.go) — main parser type
- [`CliParser.Init`](cli.go) — initialize with argv and version
- [`CliParser.Parse`](cli.go) — run parsing
- Option constructors: [`AddBoolOpt`](opt-bool.go), [`AddStringOpt`](opt-string.go), [`AddIntOpt`](opt-int.go), [`AddStringArrayOpt`](opt-string-array.go), [`AddIntArrayOpt`](opt-int-array.go), [`AddStringMapOpt`](opt-string-map.go)
- Arg constructors: [`AddStringArg`](arg-string.go), [`AddStringArrayArg`](arg-string-array.go)
- Utilities: [`TraceOptions`](cli.go), [`SetOptionValue`](opt-manager.go), [`SimpleOptionTracer`](simple-opt-tracer.go)
For implementation details, consult:
- parser internals: [`parser.go`](parser.go)
- option base helpers: [`opt-base.go`](opt-base.go)
- usage/version printing: [`cli-usage.go`](cli-usage.go), [`cli-version.go`](cli-version.go)
## License
This project is licensed under the terms in [LICENSE](LICENSE).
## Contributing
Open issues or pull requests are welcome. Run and extend the tests in [`cli_test.go`](cli_test.go) when making changes.
+6
View File
@@ -11,6 +11,12 @@ func (cli *CliParser) Usage() string {
var sb strings.Builder var sb strings.Builder
program, _ := cli.GetVersionSection("program") program, _ := cli.GetVersionSection("program")
fmt.Fprint(&sb, "NAME ", program)
if cli.description != "" {
fmt.Fprint(&sb, " - ", cli.description)
}
sb.WriteByte('\n')
publicCount := cli.publicOptionCount() publicCount := cli.publicOptionCount()
if publicCount > 0 { if publicCount > 0 {
fmt.Fprintf(&sb, "USAGE: %s [<options>] %s\n", program, cli.getArgsTemplate()) fmt.Fprintf(&sb, "USAGE: %s [<options>] %s\n", program, cli.getArgsTemplate())
+87 -19
View File
@@ -2,6 +2,7 @@ package cli
import ( import (
"fmt" "fmt"
"strings"
) )
type CliOptionTracer interface { type CliOptionTracer interface {
@@ -13,10 +14,11 @@ type cliParser interface {
getCliArgs(startIndex, endIndex int) (args []string) getCliArgs(startIndex, endIndex int) (args []string)
PrintVersion(specs []string) PrintVersion(specs []string)
PrintUsage() PrintUsage()
FlagIsSet(flag int16) bool
} }
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
@@ -24,6 +26,7 @@ type cliOptionParser interface {
isSet() bool isSet() bool
isHidden() bool isHidden() bool
requiresValue() bool requiresValue() bool
finalCheck() (err error)
} }
type OptManager interface { type OptManager interface {
@@ -31,22 +34,50 @@ 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(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)
} }
const (
ResetOnEqualSign = int16(1 << iota) // for some option types, like arrays, the equal signs reset to empty the accumulator of values
)
type CliParser struct { type CliParser struct {
description string
version string version string
options []cliOptionParser options []cliOptionParser
argSpecs []argSpec argSpecs []argSpec
cliArgs []string cliArgs []string
flags int16
} }
func (cli *CliParser) Init(argv []string, version string) { func (cli *CliParser) Init(version string, description string, flags ...int16) {
cli.version = version cli.version = version
cli.cliArgs = argv cli.description = description
for _, flag := range flags {
cli.flags |= flag
}
}
func (cli *CliParser) FlagIsSet(flag int16) bool {
return (cli.flags & flag) == flag
}
func (cli *CliParser) GetOption(name string) (ref OptReference) {
var opt cliOptionParser
if strings.HasPrefix(name, "-") {
opt = cli.findOptionByArg(name)
} else {
opt = cli.findOptionByName(name)
}
if opt != nil {
ref = opt.(OptReference)
}
return ref
} }
func (cli *CliParser) SetIncompatibleOption(optName string, incompatibleOptNames ...string) error { func (cli *CliParser) SetIncompatibleOption(optName string, incompatibleOptNames ...string) error {
@@ -138,16 +169,12 @@ func (cli *CliParser) addHelpAndVersion() {
} }
func (cli *CliParser) Parse() (err error) { func (cli *CliParser) parseOptions() (commandArgs []string, err error) {
var arg string // var commandArgs []string
var i int
var args []string
var optionsAllowed bool = true var optionsAllowed bool = true
cli.addHelpAndVersion()
skipNext := false skipNext := false
for i, arg = range cli.cliArgs[1:] { for i, arg := range cli.cliArgs[1:] {
if skipNext { if skipNext {
skipNext = false skipNext = false
} else { } else {
@@ -161,40 +188,81 @@ func (cli *CliParser) Parse() (err error) {
break break
} }
} else { } else {
args = append(args, arg) commandArgs = append(commandArgs, arg)
} }
} }
} }
if err == nil { return
var argSpec argSpec }
var n, specIndex int
i = 0
for specIndex, argSpec = range cli.argSpecs { func (cli *CliParser) checkOptionValues() (err error) {
if n, err = argSpec.parse(cli, specIndex, args, i); err != nil { for _, opt := range cli.options {
if err = opt.finalCheck(); err != nil {
break
}
}
return
}
func (cli *CliParser) parseCommandArgs(commandArgs []string) (err error) {
var n int
i := 0
// acquire arguments as required by the argSpecs
specIndex := -1
for index, argSpec := range cli.argSpecs {
specIndex = index
if n, err = argSpec.parse(cli, specIndex, commandArgs, i); err != nil {
break break
} }
i += n i += n
if i >= len(args) { if i >= len(commandArgs) {
break break
} }
} }
// check if there are remaining arg-specs that require a value
if err == nil { if err == nil {
if i < len(args) { if i < len(commandArgs) {
err = fmt.Errorf("too many arguments: %d allowed", i) err = fmt.Errorf("too many arguments: %d allowed", i)
} else { } else {
specIndex++ specIndex++
if specIndex < len(cli.argSpecs) {
// skip all non required args
for _, spec := range cli.argSpecs[specIndex:] { for _, spec := range cli.argSpecs[specIndex:] {
if !spec.getBase().required { if !spec.getBase().required {
specIndex++ specIndex++
} }
} }
}
// return error if there are remaining arg-specs that require a value
if specIndex < len(cli.argSpecs) { if specIndex < len(cli.argSpecs) {
err = errTooFewArguments(len(cli.argSpecs)) err = errTooFewArguments(len(cli.argSpecs))
} }
} }
} }
return
} }
func (cli *CliParser) Parse(argv []string) (err error) {
var commandArgs []string
cli.cliArgs = argv
cli.addHelpAndVersion()
// first parse options and collect command arguments in the args array
if commandArgs, err = cli.parseOptions(); err != nil {
return
}
// do final checks
if err = cli.checkOptionValues(); err != nil {
return
}
// then parse collected arguments
err = cli.parseCommandArgs(commandArgs)
return return
} }
+351 -47
View File
@@ -6,76 +6,380 @@ import (
"testing" "testing"
) )
type GlobaData struct { const version = `$VER:ddt-ocr,2.0.0,2026-03-19,celestino.amoroso@gmail.com:$`
type GlobalData struct {
config string config string
log []string log []string
printOcr bool printOcr bool
saveClips bool saveClips bool
trace bool trace bool
page []int page []int
cliVars map[string]string
inputName string
workDir string
attempts int
sources []string sources []string
dest string dest string
facoltativo string report string
verbose int
} }
func (gd *GlobaData) String() string { func TestVerbose(t *testing.T) {
var sb strings.Builder
fmt.Fprintf(&sb, "Options:\n")
fmt.Fprintf(&sb, " Config.....: %q\n", gd.config)
fmt.Fprintf(&sb, " Log........: %v\n", gd.log)
fmt.Fprintf(&sb, " Print-Ocr..: %v\n", gd.printOcr)
fmt.Fprintf(&sb, " Save-Clips.: %v\n", gd.saveClips)
fmt.Fprintf(&sb, " Page.......: %v\n", gd.page)
fmt.Fprintf(&sb, " Trace......: %v\n", gd.trace)
fmt.Fprintf(&sb, "Argumentes:\n")
fmt.Fprintf(&sb, " Sources....: %s\n", strings.Join(gd.sources, ", "))
fmt.Fprintf(&sb, " Destination: %s\n", gd.dest)
fmt.Fprintf(&sb, " Facoltativo: %s\n", gd.facoltativo)
return sb.String()
}
func TestUsage(t *testing.T) {
var cli CliParser var cli CliParser
var version = `$VER:mini-ddt-ocr,1.0.1,2025-11-23,celestino.amoroso@gmail.com:$` var gd GlobalData
var gd GlobaData
args := []string{ if err := initCli(&cli, &gd); err != nil {
"mini-ddt-ocr",
"--log", "all",
"--config", "devel-config.yaml",
// "--var", "deploy_env=devel",
"--print-ocr",
"--pages", "17,18",
"../../dev-stuff/ocis-upload.log", "ciccio", "bello", "pippo",
}
cli.Init(args, version)
if err := gd.addOptions(&cli); err != nil {
t.Error(err) t.Error(err)
return return
} }
usage := cli.Usage() if err := cli.Parse(commonArgs()); err != nil {
fmt.Println(usage) t.Error(err)
if err := cli.Parse(); err == nil { } else if gd.verbose != 3 {
fmt.Println(&gd) t.Errorf("Expected verbose level 3, got %d", gd.verbose)
}
}
func TestVersion(t *testing.T) {
var cli CliParser
var gd GlobalData
if err := initCli(&cli, &gd); err != nil {
t.Error(err)
return
}
if v, err := cli.GetVersionSection("all"); err == nil {
if v != version[5:len(version)-2] {
t.Errorf("Version string does not match expected.\nGot:\n%q\nExpected:\n%q", v, version)
}
} else { } else {
t.Error(err) t.Error(err)
} }
} }
func (gd *GlobaData) addOptions(cli *CliParser) (err error) { func TestVersionNumber(t *testing.T) {
var cli CliParser
var gd GlobalData
if err := initCli(&cli, &gd); err != nil {
t.Error(err)
return
}
secValues := []string{"ddt-ocr", "1.0.1", "2025-11-23", "celestino.amoroso@gmail.com"}
for i, sec := range []string{"program", "version", "date", "email"} {
if v, err := cli.GetVersionSection(sec); err == nil {
if v != secValues[i] {
t.Errorf("Version section %q does not match expected value.\nGot:\n%q\nExpected:\n%q", sec, v, version)
}
} else {
t.Error(err)
}
}
}
func TestUsage(t *testing.T) {
const expectedUsage = `NAME ddt-ocr - cli-test
USAGE: ddt-ocr [<options>] <image-sources> ... <dest> [<report>]
where:
<image-sources> Source image files
<dest> Output destination file
<report> Optional report file
<options>
-V, --verbose Print verbose output (default: "0")
-o, --print-ocr Print the OCR output to stderr
-s, --save-clip Save the image clips as PNG files (alias: save-clips)
-t, --trace Enable trace mode for detailed logging
-p, --page(s) <num>["," ...] Process only the specified pages (comma-separated list)
-c, --config <file> Alternate configuration file
-l, --log(s) <string>["," ...] Logging options (comma-separated list)
--var(s) <key=value>["," ...] Define one or more comma separated variables for the actions context (multiple allowed)
-n, --input-name <string> Input file name when source comes from stdin
-d, --work-dir <dir> Work directory
--attempts <num> Attempts for retrying failed operations (default: "1")
`
var cli CliParser
var gd GlobalData
if err := initCli(&cli, &gd); err != nil {
t.Error(err)
return
}
usage := cli.Usage()
if usage != expectedUsage {
t.Errorf("Usage output does not match expected.\nGot:\n%s\nExpected:\n%s", usage, expectedUsage)
}
}
func TestParser(t *testing.T) {
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]
Option: config, Type: string, Value: devel-config.yaml
Option: log, Type: string-array, Value: [all]
Option: var, Type: map-string, Value: map[deploy_env:devel]
Option: input-name, Type: string, Value: my-scan.pdf
Option: work-dir, Type: string, Value:
Option: attempts, Type: num, Value: 1
Option: help, Type: n/a, Value: <nil>
Option: version, Type: n/a, Value: <nil>
`
var cli CliParser
var gd GlobalData
var sb strings.Builder
if err := initCli(&cli, &gd); err != nil {
t.Error(err)
return
}
tracer := NewSimpleOptionTracer(&sb)
if err := cli.Parse(commonArgs()); err == nil {
cli.TraceOptions(tracer)
if 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)
}
}
func commonArgs() []string {
return []string{
"ddt-ocr",
"--log", "all",
"-t",
"-VVV",
"--config", "devel-config.yaml",
"--var", "deploy_env=devel",
"--print-ocr",
"--pages", "17,18",
"--input-name=my-scan.pdf",
"scan1.pdf", "scan2.pdf", "result.txt", "report.txt",
}
}
func initCli(cli *CliParser, gd *GlobalData) (err error) {
cli.Init(version, "cli-test")
err = gd.addOptions(cli)
return
}
func TestOptErrorUnknownOption(t *testing.T) {
const unknownOption = "--logging"
var expectedErr = errUnknownOption(unknownOption)
var cli CliParser
var gd GlobalData
if err := initCliUnknownOption(&cli, &gd); err == nil {
if err = cli.Parse(commonBadArgs(unknownOption)); err != nil {
if err.Error() != expectedErr.Error() {
t.Errorf("Invalid error message.\nGot:\n%v\nExpected:\n%v", err, expectedErr)
}
} else {
t.Errorf("Expected error for unknown option %q, but got none", unknownOption)
}
} else {
t.Error(err)
return
}
}
func commonBadArgs(option string) []string {
return []string{
"ddt-ocr",
option,
"scan1.pdf", "scan2.pdf", "result.txt", "report.txt",
}
}
func initCliUnknownOption(cli *CliParser, gd *GlobalData) (err error) {
cli.Init(version, "cli-test")
err = gd.addOptions(cli)
return
}
func TestOptErrorMissingOptionValue(t *testing.T) {
const missingValueOption = "--page"
var expectedErr = fmt.Errorf(`invalid value scan1.pdf (string) for option "page" (num-array)`)
var cli CliParser
var gd GlobalData
if err := initCliMissingOptionValue(&cli, &gd); err == nil {
if err = cli.Parse(commonBadArgs(missingValueOption)); err != nil {
if err.Error() != expectedErr.Error() {
t.Errorf("Invalid error message.\nGot:\n%v\nExpected:\n%v", err, expectedErr)
}
} else {
t.Errorf("Expected error for unknown option %q, but got none", missingValueOption)
}
} else {
t.Error(err)
return
}
}
func initCliMissingOptionValue(cli *CliParser, gd *GlobalData) (err error) {
cli.Init(version, "cli-test")
err = gd.addOptions(cli)
return
}
func TestOptErrorInvalidOptionValue(t *testing.T) {
const missingInvalidValueOption = "--page"
var expectedErr = errInvalidOptionValue("page", "scan1.pdf", "num-array")
var cli CliParser
var gd GlobalData
if err := initCliInvalidOptionValue(&cli, &gd); err == nil {
if err = cli.Parse(commonBadArgs(missingInvalidValueOption)); err != nil {
if err.Error() != expectedErr.Error() {
t.Errorf("Invalid error message.\nGot:\n%v\nExpected:\n%v", err, expectedErr)
}
} else {
t.Errorf("Expected error for unknown option %q, but got none", missingInvalidValueOption)
}
} else {
t.Error(err)
return
}
}
func initCliInvalidOptionValue(cli *CliParser, gd *GlobalData) (err error) {
cli.Init(version, "cli-test")
err = gd.addOptions(cli)
return
}
func TestArgErrorMissingRequired(t *testing.T) {
const missingRequiredArg = "--page"
var expectedErr = fmt.Errorf(`missing required arg <image-sources>`)
var cli CliParser
var gd GlobalData
if err := initCliMissingRequiredArg(&cli, &gd); err == nil {
if err = cli.Parse([]string{"ddt-ocr"}); err != nil {
if err.Error() != expectedErr.Error() {
t.Errorf("Invalid error message.\nGot:\n%v\nExpected:\n%v", err, expectedErr)
}
} else {
t.Errorf("Expected error for unknown option %q, but got none", missingRequiredArg)
}
} else {
t.Error(err)
return
}
}
func initCliMissingRequiredArg(cli *CliParser, gd *GlobalData) (err error) {
cli.Init(version, "cli-test")
err = gd.addOptions(cli)
return
}
func TestArgErrorRepeatProperty(t *testing.T) {
var expectedErr = fmt.Errorf(`repeat property already set for arg <other>`)
var cli CliParser
var gd GlobalData
if err := initCliRepeatArg(&cli, &gd); err != nil {
if err.Error() != expectedErr.Error() {
t.Errorf("Invalid error message.\nGot:\n%v\nExpected:\n%v", err, expectedErr)
}
} else {
t.Errorf("Expected error, but got none")
}
}
func initCliRepeatArg(cli *CliParser, gd *GlobalData) (err error) {
cli.Init(version, "cli-test")
if err = gd.addOptions(cli); err == nil {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
err = r.(error) err = r.(error)
} }
}() }()
cli.AddBoolOpt("print-ocr", "o", &gd.printOcr, "Stampa l'output del programma OCR") // This will raise error because we can't declare two args array
cli.AddBoolOpt("save-clip", "s", &gd.saveClips, "Registra le immagini delle aree ritagliata", "save-clips") cli.AddStringArrayArg("other", true, nil, "other args")
cli.AddBoolOpt("trace", "t", &gd.trace, "Attiva la modalità di tracciamento delle operazioni") cli.AddStringArrayArg("other2", true, nil, "other args 2")
cli.AddIntArrayOpt("page", "p", &gd.page, gd.page, "Elabora la pagina specificata") }
cli.AddStringOpt("config", "c", &gd.config, gd.config, "Specifica un percorso alternativo per il file di configurazione") return
cli.AddStringArrayOpt("log", "l", &gd.log, gd.log, "Maschera di livelli di log") }
cli.AddStringArrayArg("sorgenti", true, &gd.sources, "file da elaborare") func TestOptHidden(t *testing.T) {
cli.AddStringArg("dest", true, &gd.dest, "file di outout") const expectedUsage = `NAME ddt-ocr - cli-test
cli.AddStringArg("facoltativo", false, &gd.facoltativo, "file facoltativo") USAGE: ddt-ocr [<options>] <image-sources> ... <dest> [<report>]
where:
<image-sources> Source image files
<dest> Output destination file
<report> Optional report file
<options>
-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) <num>["," ...] Process only the specified pages (comma-separated list)
-c, --config <file> Alternate configuration file
-l, --log(s) <string>["," ...] Logging options (comma-separated list)
--var(s) <key=value>["," ...] Define one or more comma separated variables for the actions context (multiple allowed)
-n, --input-name <string> Input file name when source comes from stdin
-d, --work-dir <dir> Work directory
--attempts <num> Attempts for retrying failed operations (default: "1")
`
var cli CliParser
var gd GlobalData
if err := initCliHiddenOpt(&cli, &gd); err != nil {
t.Error(err)
return
}
usage := cli.Usage()
if usage != expectedUsage {
t.Errorf("Usage output does not match expected text.\nGot:\n%s\nExpected:\n%s", usage, expectedUsage)
}
}
func initCliHiddenOpt(cli *CliParser, gd *GlobalData) (err error) {
if err = initCli(cli, gd); err == nil {
if ref := cli.GetOption("save-clip"); ref != nil {
ref.SetHidden(true)
} else {
err = fmt.Errorf("option save-clip not found")
}
}
return
}
func (gd *GlobalData) addOptions(cli *CliParser) (err error) {
// Always recover from panic to return error before adding options
defer func() {
if r := recover(); r != nil {
err = r.(error)
}
}()
// Define options
cli.AddMultiOpt("verbose", "V", &gd.verbose, 0, "Print verbose output")
cli.AddBoolOpt("print-ocr", "o", &gd.printOcr, "Print the OCR output to stderr")
cli.AddBoolOpt("save-clip", "s", &gd.saveClips, "Save the image clips as PNG files", "save-clips")
cli.AddBoolOpt("trace", "t", &gd.trace, "Enable trace mode for detailed logging")
cli.AddIntArrayOpt("page", "p", &gd.page, gd.page, "Process only the specified pages (comma-separated list)")
cli.AddFileOpt("config", "c", &gd.config, gd.config, "Alternate configuration file")
cli.AddStringArrayOpt("log", "l", &gd.log, gd.log, "Logging options (comma-separated list)")
cli.AddStringMapOpt("var", "", &gd.cliVars, nil, "Define one or more comma separated variables for the actions context (multiple allowed)")
cli.AddStringOpt("input-name", "n", &gd.inputName, "", "Input file name when source comes from stdin")
cli.AddDirOpt("work-dir", "d", &gd.workDir, "", "Work directory")
if ref := cli.AddIntOpt("attempts", "", &gd.attempts, 1, "Attempts for retrying failed operations"); ref != nil {
ref.AddSpecialValue("many", func(manager OptManager, cliValue string, currentValue any) (any, error) {
return 1000, nil
})
ref.AddSpecialValue("few", func(manager OptManager, cliValue string, currentValue any) (any, error) {
manager.SetOptionValue("page", "1")
return nil, nil
})
}
// Define arguments
cli.AddStringArrayArg("image-sources", true, &gd.sources, "Source image files")
cli.AddStringArg("dest", true, &gd.dest, "Output destination file")
cli.AddStringArg("report", false, &gd.report, "Optional report file")
return return
} }
+1 -1
View File
@@ -1,3 +1,3 @@
module git.portale-stac.it/go-pkg/cli module git.portale-stac.it/go-pkg/cli/v2
go 1.25.4 go 1.25.4
+20 -2
View File
@@ -13,7 +13,9 @@ type cliOptionBase struct {
description string description string
isArray bool isArray bool
hidden bool hidden bool
alreadySeen bool
specialValues map[string]SpecialValueFunc specialValues map[string]SpecialValueFunc
finalCheckFunc FinalCheckFunc
incompatibleWith []string incompatibleWith []string
} }
@@ -40,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)
@@ -66,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 {
@@ -145,3 +152,14 @@ func (opt *cliOptionBase) fetchOptionValue(parser cliParser, argIndex int, value
} }
return return
} }
func (opt *cliOptionBase) finalCheck() (err error) {
return
}
func (opt *cliOptionBase) checkValue(value any) (err error) {
if opt.finalCheckFunc != nil {
err = opt.finalCheckFunc(value)
}
return
}
+8 -3
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() (err error) {
currentValue, _ := opt.getTargetVar()
return opt.getBase().checkValue(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))
+1 -1
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
+12 -3
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 {
@@ -73,9 +72,13 @@ func (opt *cliOptionIntArray) parse(parser cliParser, argIndex int, valuePtr *st
if val, ok := boxedValue.([]int); ok { if val, ok := boxedValue.([]int); ok {
*opt.targetVar = val *opt.targetVar = val
} else { } else {
err = errInvalidOptionValue(opt.name, boxedValue, "array of int") err = errInvalidOptionValue(opt.name, boxedValue, "num-array")
} }
} else { } else {
if !opt.alreadySeen || (valuePtr != nil && parser.FlagIsSet(ResetOnEqualSign)) {
*opt.targetVar = []int{}
opt.alreadySeen = true
}
for value := range strings.SplitSeq(optValue, ",") { for value := range strings.SplitSeq(optValue, ",") {
var minRange, maxRange int var minRange, maxRange int
if minRange, maxRange, err = parseIntRange(value); err == nil { if minRange, maxRange, err = parseIntRange(value); err == nil {
@@ -83,6 +86,7 @@ func (opt *cliOptionIntArray) parse(parser cliParser, argIndex int, valuePtr *st
*opt.targetVar = append(*opt.targetVar, i) *opt.targetVar = append(*opt.targetVar, i)
} }
} else { } else {
err = errInvalidOptionValue(opt.name, value, "num-array")
break break
} }
} }
@@ -93,6 +97,11 @@ func (opt *cliOptionIntArray) parse(parser cliParser, argIndex int, valuePtr *st
return return
} }
func (opt *cliOptionIntArray) finalCheck() (err error) {
currentValue, _ := opt.getTargetVar()
return opt.getBase().checkValue(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{
+8 -5
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() (err error) {
currentValue, _ := opt.getTargetVar()
return opt.getBase().checkValue(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) {
+1 -1
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)
} }
+77
View File
@@ -0,0 +1,77 @@
package cli
import (
"fmt"
"strconv"
)
const (
multiTypeName = "num"
)
type cliOptionMulti struct {
cliOptionBase
defaultValue int
targetVar *int
}
func (opt *cliOptionMulti) init() {
if opt.targetVar != nil {
*opt.targetVar = opt.defaultValue
}
}
func (opt *cliOptionMulti) getTargetVar() (any, string) {
var value int
if opt.targetVar != nil {
value = *opt.targetVar
}
return value, multiTypeName
}
func (opt *cliOptionMulti) requiresValue() bool {
return false
}
func (opt *cliOptionMulti) getDefaultValue() string {
return strconv.Itoa(opt.defaultValue)
}
func (opt *cliOptionMulti) getTemplate() (templ string) {
if opt.shortAlias != "" {
templ = fmt.Sprintf(`-%s, --%s`, opt.shortAlias, opt.name)
} else {
templ = fmt.Sprintf(`--%s`, opt.name)
}
return
}
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() (err error) {
currentValue, _ := opt.getTargetVar()
return opt.getBase().checkValue(currentValue)
}
func (cli *CliParser) AddMultiOpt(name, short string, targetVar *int, defaultValue int, description string, aliases ...string) OptReference {
if cli.optionExists(name, short, aliases) {
panic(errOptionAlreadyDefined(name))
}
opt := &cliOptionMulti{
cliOptionBase: cliOptionBase{
name: name,
shortAlias: short,
aliases: aliases,
description: description,
},
targetVar: targetVar,
defaultValue: defaultValue,
}
cli.options = append(cli.options, opt)
return opt
}
+8 -2
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 {
@@ -55,6 +54,8 @@ func (opt *cliOptionStringArray) parse(parser cliParser, argIndex int, valuePtr
} else { } else {
err = errInvalidOptionValue(opt.name, boxedValue, "array of string") err = errInvalidOptionValue(opt.name, boxedValue, "array of string")
} }
} else if opt.alreadySeen {
*opt.targetVar = append(*opt.targetVar, strings.Split(value, ",")...)
} else { } else {
*opt.targetVar = strings.Split(value, ",") *opt.targetVar = strings.Split(value, ",")
} }
@@ -83,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() (err error) {
currentValue, _ := opt.getTargetVar()
return opt.getBase().checkValue(currentValue)
}
+7 -3
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 {
@@ -71,7 +70,7 @@ func (opt *cliOptionStringMap) parse(parser cliParser, argIndex int, valuePtr *s
err = errInvalidOptionValue(opt.name, boxedValue, "map of string") err = errInvalidOptionValue(opt.name, boxedValue, "map of string")
} }
} else { } else {
if dict == nil { if dict == nil || !opt.alreadySeen {
dict = make(map[string]string) dict = make(map[string]string)
} }
for value := range strings.SplitSeq(value, ",") { for value := range strings.SplitSeq(value, ",") {
@@ -90,6 +89,11 @@ func (opt *cliOptionStringMap) parse(parser cliParser, argIndex int, valuePtr *s
return return
} }
func (opt *cliOptionStringMap) finalCheck() (err error) {
currentValue, _ := opt.getTargetVar()
return opt.getBase().checkValue(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{
+5 -2
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() (err error) {
currentValue, _ := opt.getTargetVar()
return opt.getBase().checkValue(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) {
+1 -1
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}
+143
View File
@@ -0,0 +1,143 @@
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("1.0.0", "TestOneOptWithEqual description")
if err := cli.Parse(args); 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(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 testIntArrayOptOk(t *testing.T, n int, args []string, flags ...int16) {
var box []int
var cli CliParser
t.Logf("Arg set n. %d", n)
addBoxOption(&cli, &box)
cli.Init("1.0.0", "TestIntArrayOpt description", flags...)
if err := cli.Parse(args); err != nil {
t.Error(err)
} else if len(box) != 4 {
t.Errorf(`Expected 4 items, got %d`, len(box))
}
}
func testIntArrayOptKo(t *testing.T, n int, args []string, msg string, flags ...int16) {
var box []int
var cli CliParser
t.Logf("Arg set n. %d", n)
addBoxOption(&cli, &box)
cli.Init("1.0.0", "TestIntArrayOpt description", flags...)
if err := cli.Parse(args); err == nil {
t.Errorf("Expected error, got nil")
} else if err.Error() != msg {
t.Errorf(`Invalid error: %q; expected: %q`, err.Error(), msg)
}
}
func TestIntArrayOpt(t *testing.T) {
var args []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)
}
}
}()
n := 0
n++
args = []string{
"TestIntArrayOpt",
"--box=100,200,150,450",
}
testIntArrayOptOk(t, n, args)
n++
args = []string{
"TestIntArrayOpt",
"--box=100,200,150",
}
testIntArrayOptKo(t, n, args, "--box option requires exactly 4 items, 3 provided")
n++
args = []string{
"TestIntArrayOpt",
"--box", "100,200,150,450",
}
testIntArrayOptOk(t, n, args)
n++
args = []string{
"TestIntArrayOpt",
"--box",
}
testIntArrayOptKo(t, n, args, `option "box" requires a value`)
n++
args = []string{
"TestIntArrayOpt",
"--box", "100,200,150",
"--box", "450",
}
testIntArrayOptOk(t, n, args)
n++
args = []string{
"TestIntArrayOpt",
"--box", "100,200,150",
"--box", "450,12",
}
testIntArrayOptKo(t, n, args, "--box option requires exactly 4 items, 5 provided")
n++
args = []string{
"TestIntArrayOpt",
"--box", "100,200,150",
"--box=100,200,150,450",
}
testIntArrayOptOk(t, n, args, ResetOnEqualSign)
}
+2 -2
View File
@@ -105,11 +105,11 @@ 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 {
if equalPresent && i == len(opts)-1 { if equalPresent && i == len(opts)-1 {
_, err = opt.parse(cli, index, &value) _, _, 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, _, err = opt.parse(cli, index, nil)
} }
} else { } else {
err = errUnknownOption(dashes + optName) err = errUnknownOption(dashes + optName)