Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9abbe37e02 | |||
| 52ff844ce1 | |||
| 20b6b961fb | |||
| 00b84278d8 | |||
| 9e28ee6545 | |||
| 92267aec50 | |||
| 95fae40d5f | |||
| b5f8d9eaab | |||
| ac8d5fa3a9 | |||
| f55e9cea82 | |||
| 4111864916 | |||
| f830851e58 | |||
| b9f5f92948 | |||
| 69f550884a | |||
| 9e2e5c1f37 | |||
| 548b816d4b |
@@ -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.
|
||||
|
||||
@@ -11,6 +11,12 @@ func (cli *CliParser) Usage() string {
|
||||
var sb strings.Builder
|
||||
|
||||
program, _ := cli.GetVersionSection("program")
|
||||
fmt.Fprint(&sb, "NAME ", program)
|
||||
if cli.description != "" {
|
||||
fmt.Fprint(&sb, " - ", cli.description)
|
||||
}
|
||||
sb.WriteByte('\n')
|
||||
|
||||
publicCount := cli.publicOptionCount()
|
||||
if publicCount > 0 {
|
||||
fmt.Fprintf(&sb, "USAGE: %s [<options>] %s\n", program, cli.getArgsTemplate())
|
||||
|
||||
@@ -2,6 +2,7 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type CliOptionTracer interface {
|
||||
@@ -13,10 +14,11 @@ type cliParser interface {
|
||||
getCliArgs(startIndex, endIndex int) (args []string)
|
||||
PrintVersion(specs []string)
|
||||
PrintUsage()
|
||||
FlagIsSet(flag int16) bool
|
||||
}
|
||||
|
||||
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
|
||||
@@ -24,6 +26,7 @@ type cliOptionParser interface {
|
||||
isSet() bool
|
||||
isHidden() bool
|
||||
requiresValue() bool
|
||||
finalCheck() (err error)
|
||||
}
|
||||
|
||||
type OptManager interface {
|
||||
@@ -31,22 +34,50 @@ type OptManager interface {
|
||||
}
|
||||
|
||||
type SpecialValueFunc func(manager OptManager, cliValue string, currentValue any) (value any, err error)
|
||||
type FinalCheckFunc func(currentValue any) (err error)
|
||||
|
||||
type OptReference interface {
|
||||
AddSpecialValue(cliValue string, specialFunc SpecialValueFunc)
|
||||
OnFinalCheck(checkFunc FinalCheckFunc)
|
||||
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 {
|
||||
description string
|
||||
version string
|
||||
options []cliOptionParser
|
||||
argSpecs []argSpec
|
||||
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.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 {
|
||||
@@ -138,16 +169,12 @@ func (cli *CliParser) addHelpAndVersion() {
|
||||
|
||||
}
|
||||
|
||||
func (cli *CliParser) Parse() (err error) {
|
||||
var arg string
|
||||
var i int
|
||||
var args []string
|
||||
func (cli *CliParser) parseOptions() (commandArgs []string, err error) {
|
||||
// var commandArgs []string
|
||||
var optionsAllowed bool = true
|
||||
|
||||
cli.addHelpAndVersion()
|
||||
|
||||
skipNext := false
|
||||
for i, arg = range cli.cliArgs[1:] {
|
||||
for i, arg := range cli.cliArgs[1:] {
|
||||
if skipNext {
|
||||
skipNext = false
|
||||
} else {
|
||||
@@ -161,40 +188,81 @@ func (cli *CliParser) Parse() (err error) {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
args = append(args, arg)
|
||||
commandArgs = append(commandArgs, arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
var argSpec argSpec
|
||||
var n, specIndex int
|
||||
i = 0
|
||||
return
|
||||
}
|
||||
|
||||
for specIndex, argSpec = range cli.argSpecs {
|
||||
if n, err = argSpec.parse(cli, specIndex, args, i); err != nil {
|
||||
func (cli *CliParser) checkOptionValues() (err error) {
|
||||
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
|
||||
}
|
||||
i += n
|
||||
if i >= len(args) {
|
||||
if i >= len(commandArgs) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// check if there are remaining arg-specs that require a value
|
||||
if err == nil {
|
||||
if i < len(args) {
|
||||
if i < len(commandArgs) {
|
||||
err = fmt.Errorf("too many arguments: %d allowed", i)
|
||||
} else {
|
||||
specIndex++
|
||||
if specIndex < len(cli.argSpecs) {
|
||||
// skip all non required args
|
||||
for _, spec := range cli.argSpecs[specIndex:] {
|
||||
if !spec.getBase().required {
|
||||
specIndex++
|
||||
}
|
||||
}
|
||||
}
|
||||
// return error if there are remaining arg-specs that require a value
|
||||
if specIndex < 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
|
||||
}
|
||||
|
||||
|
||||
+351
-47
@@ -6,76 +6,380 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type GlobaData struct {
|
||||
const version = `$VER:ddt-ocr,2.0.0,2026-03-19,celestino.amoroso@gmail.com:$`
|
||||
|
||||
type GlobalData struct {
|
||||
config string
|
||||
log []string
|
||||
printOcr bool
|
||||
saveClips bool
|
||||
trace bool
|
||||
page []int
|
||||
cliVars map[string]string
|
||||
inputName string
|
||||
workDir string
|
||||
attempts int
|
||||
sources []string
|
||||
dest string
|
||||
facoltativo string
|
||||
report string
|
||||
verbose int
|
||||
}
|
||||
|
||||
func (gd *GlobaData) String() string {
|
||||
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) {
|
||||
func TestVerbose(t *testing.T) {
|
||||
var cli CliParser
|
||||
var version = `$VER:mini-ddt-ocr,1.0.1,2025-11-23,celestino.amoroso@gmail.com:$`
|
||||
var gd GlobaData
|
||||
args := []string{
|
||||
"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 {
|
||||
var gd GlobalData
|
||||
|
||||
if err := initCli(&cli, &gd); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
usage := cli.Usage()
|
||||
fmt.Println(usage)
|
||||
if err := cli.Parse(); err == nil {
|
||||
fmt.Println(&gd)
|
||||
if err := cli.Parse(commonArgs()); err != nil {
|
||||
t.Error(err)
|
||||
} else if gd.verbose != 3 {
|
||||
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 {
|
||||
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() {
|
||||
if r := recover(); r != nil {
|
||||
err = r.(error)
|
||||
}
|
||||
}()
|
||||
cli.AddBoolOpt("print-ocr", "o", &gd.printOcr, "Stampa l'output del programma OCR")
|
||||
cli.AddBoolOpt("save-clip", "s", &gd.saveClips, "Registra le immagini delle aree ritagliata", "save-clips")
|
||||
cli.AddBoolOpt("trace", "t", &gd.trace, "Attiva la modalità di tracciamento delle operazioni")
|
||||
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")
|
||||
cli.AddStringArrayOpt("log", "l", &gd.log, gd.log, "Maschera di livelli di log")
|
||||
|
||||
cli.AddStringArrayArg("sorgenti", true, &gd.sources, "file da elaborare")
|
||||
cli.AddStringArg("dest", true, &gd.dest, "file di outout")
|
||||
cli.AddStringArg("facoltativo", false, &gd.facoltativo, "file facoltativo")
|
||||
// This will raise error because we can't declare two args array
|
||||
cli.AddStringArrayArg("other", true, nil, "other args")
|
||||
cli.AddStringArrayArg("other2", true, nil, "other args 2")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestOptHidden(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
|
||||
-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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+20
-2
@@ -13,7 +13,9 @@ type cliOptionBase struct {
|
||||
description string
|
||||
isArray bool
|
||||
hidden bool
|
||||
alreadySeen bool
|
||||
specialValues map[string]SpecialValueFunc
|
||||
finalCheckFunc FinalCheckFunc
|
||||
incompatibleWith []string
|
||||
}
|
||||
|
||||
@@ -40,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)
|
||||
@@ -66,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 {
|
||||
@@ -145,3 +152,14 @@ func (opt *cliOptionBase) fetchOptionValue(parser cliParser, argIndex int, value
|
||||
}
|
||||
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
@@ -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() (err error) {
|
||||
currentValue, _ := opt.getTargetVar()
|
||||
return opt.getBase().checkValue(currentValue)
|
||||
}
|
||||
|
||||
func (cli *CliParser) AddBoolOpt(name, short string, targetVar *bool, description string, aliases ...string) OptReference {
|
||||
if cli.optionExists(name, short, aliases) {
|
||||
panic(errOptionAlreadyDefined(name))
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+12
-3
@@ -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 {
|
||||
@@ -73,9 +72,13 @@ func (opt *cliOptionIntArray) parse(parser cliParser, argIndex int, valuePtr *st
|
||||
if val, ok := boxedValue.([]int); ok {
|
||||
*opt.targetVar = val
|
||||
} else {
|
||||
err = errInvalidOptionValue(opt.name, boxedValue, "array of int")
|
||||
err = errInvalidOptionValue(opt.name, boxedValue, "num-array")
|
||||
}
|
||||
} else {
|
||||
if !opt.alreadySeen || (valuePtr != nil && parser.FlagIsSet(ResetOnEqualSign)) {
|
||||
*opt.targetVar = []int{}
|
||||
opt.alreadySeen = true
|
||||
}
|
||||
for value := range strings.SplitSeq(optValue, ",") {
|
||||
var minRange, maxRange int
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
err = errInvalidOptionValue(opt.name, value, "num-array")
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -93,6 +97,11 @@ func (opt *cliOptionIntArray) parse(parser cliParser, argIndex int, valuePtr *st
|
||||
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 {
|
||||
aliases = cli.checkAlreadyUsedNames(name, short, aliases)
|
||||
opt := &cliOptionIntArray{
|
||||
|
||||
+8
-5
@@ -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() (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 {
|
||||
if cli.optionExists(name, short, aliases) {
|
||||
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
@@ -55,6 +54,8 @@ func (opt *cliOptionStringArray) parse(parser cliParser, argIndex int, valuePtr
|
||||
} else {
|
||||
err = errInvalidOptionValue(opt.name, boxedValue, "array of string")
|
||||
}
|
||||
} else if opt.alreadySeen {
|
||||
*opt.targetVar = append(*opt.targetVar, strings.Split(value, ",")...)
|
||||
} else {
|
||||
*opt.targetVar = strings.Split(value, ",")
|
||||
}
|
||||
@@ -83,3 +84,8 @@ func (cli *CliParser) AddStringArrayOpt(name, short string, targetVar *[]string,
|
||||
cli.options = append(cli.options, opt)
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt *cliOptionStringArray) finalCheck() (err error) {
|
||||
currentValue, _ := opt.getTargetVar()
|
||||
return opt.getBase().checkValue(currentValue)
|
||||
}
|
||||
|
||||
+7
-3
@@ -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 {
|
||||
@@ -71,7 +70,7 @@ func (opt *cliOptionStringMap) parse(parser cliParser, argIndex int, valuePtr *s
|
||||
err = errInvalidOptionValue(opt.name, boxedValue, "map of string")
|
||||
}
|
||||
} else {
|
||||
if dict == nil {
|
||||
if dict == nil || !opt.alreadySeen {
|
||||
dict = make(map[string]string)
|
||||
}
|
||||
for value := range strings.SplitSeq(value, ",") {
|
||||
@@ -90,6 +89,11 @@ func (opt *cliOptionStringMap) parse(parser cliParser, argIndex int, valuePtr *s
|
||||
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 {
|
||||
aliases = cli.checkAlreadyUsedNames(name, short, aliases)
|
||||
opt := &cliOptionStringMap{
|
||||
|
||||
+5
-2
@@ -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() (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 {
|
||||
if cli.optionExists(name, short, aliases) {
|
||||
|
||||
+1
-1
@@ -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}
|
||||
|
||||
+143
@@ -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)
|
||||
}
|
||||
@@ -105,11 +105,11 @@ 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 {
|
||||
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() {
|
||||
err = errMissingOptionValue(dashes + optName)
|
||||
} else {
|
||||
skipNextArg, err = opt.parse(cli, index, nil)
|
||||
skipNextArg, _, err = opt.parse(cli, index, nil)
|
||||
}
|
||||
} else {
|
||||
err = errUnknownOption(dashes + optName)
|
||||
|
||||
Reference in New Issue
Block a user