new /v2 subdirectory
This commit is contained in:
@@ -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.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type argSpec interface {
|
||||||
|
getBase() *argBase
|
||||||
|
parse(cli *CliParser, specIndex int, args []string, argIndex int) (consumedArgs int, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type argBase struct {
|
||||||
|
name string
|
||||||
|
description string
|
||||||
|
required bool
|
||||||
|
repeat bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------- Argument Template functions ----------------
|
||||||
|
func (cli *CliParser) getArgTemplate(spec argSpec) (templ string) {
|
||||||
|
arg := spec.getBase()
|
||||||
|
templ = "<" + arg.name + ">"
|
||||||
|
if arg.repeat {
|
||||||
|
templ += " ..."
|
||||||
|
}
|
||||||
|
if !arg.required {
|
||||||
|
templ = "[" + templ + "]"
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CliParser) getArgsTemplate() (argsTemplate string) {
|
||||||
|
templates := make([]string, len(cli.argSpecs))
|
||||||
|
for i, argSpec := range cli.argSpecs {
|
||||||
|
templates[i] = cli.getArgTemplate(argSpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
argsTemplate = strings.Join(templates, " ")
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
// -------------- argStringArray ----------------
|
||||||
|
type argStringArray struct {
|
||||||
|
base argBase
|
||||||
|
targetVar *[]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (arg *argStringArray) getBase() *argBase {
|
||||||
|
return &arg.base
|
||||||
|
}
|
||||||
|
|
||||||
|
func (spec *argStringArray) parse(cli *CliParser, specIndex int, args []string, argIndex int) (consumedArgs int, err error) {
|
||||||
|
consumedArgs = 1
|
||||||
|
if spec.targetVar != nil {
|
||||||
|
if argIndex < len(args) {
|
||||||
|
remainingSpecs := len(cli.argSpecs) - specIndex - 1
|
||||||
|
availableArgs := len(args) - remainingSpecs
|
||||||
|
if availableArgs > 0 {
|
||||||
|
*spec.targetVar = args[argIndex : argIndex+availableArgs]
|
||||||
|
consumedArgs = availableArgs
|
||||||
|
} else {
|
||||||
|
err = errTooFewArguments(len(cli.argSpecs))
|
||||||
|
}
|
||||||
|
} else if spec.base.required {
|
||||||
|
err = errMissingRequiredArg(spec.base.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CliParser) AddStringArrayArg(name string, required bool, targetVar *[]string, description string) {
|
||||||
|
// todo: check if arg already exists
|
||||||
|
if len(cli.argSpecs) > 0 {
|
||||||
|
lastArg := cli.argSpecs[len(cli.argSpecs)-1].getBase()
|
||||||
|
if lastArg.repeat {
|
||||||
|
panic(errRepeatArgAlreadyDefined(lastArg.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cli.argSpecs = append(cli.argSpecs, &argStringArray{
|
||||||
|
base: argBase{
|
||||||
|
name: name,
|
||||||
|
description: description,
|
||||||
|
required: required,
|
||||||
|
repeat: true,
|
||||||
|
},
|
||||||
|
targetVar: targetVar,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
// -------------- argString ----------------
|
||||||
|
type argString struct {
|
||||||
|
base argBase
|
||||||
|
targetVar *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (arg *argString) getBase() *argBase {
|
||||||
|
return &arg.base
|
||||||
|
}
|
||||||
|
|
||||||
|
func (spec *argString) parse(cli *CliParser, specIndex int, args []string, argIndex int) (consumedArgs int, err error) {
|
||||||
|
consumedArgs = 1
|
||||||
|
if spec.targetVar != nil {
|
||||||
|
if argIndex < len(args) {
|
||||||
|
*spec.targetVar = args[argIndex]
|
||||||
|
} else if spec.base.required {
|
||||||
|
err = errMissingRequiredArg(spec.base.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CliParser) AddStringArg(name string, required bool, targetVar *string, description string) {
|
||||||
|
// todo: check if arg already exists
|
||||||
|
cli.argSpecs = append(cli.argSpecs, &argString{
|
||||||
|
base: argBase{
|
||||||
|
name: name,
|
||||||
|
description: description,
|
||||||
|
required: required,
|
||||||
|
repeat: false,
|
||||||
|
},
|
||||||
|
targetVar: targetVar,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Usage()
|
||||||
|
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())
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(&sb, "USAGE: %s %s\n", program, cli.getArgsTemplate())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cli.argSpecs) > 0 || len(cli.options) > 0 {
|
||||||
|
fmt.Fprintf(&sb, "where:\n")
|
||||||
|
if len(cli.argSpecs) > 0 {
|
||||||
|
for _, argSpec := range cli.argSpecs {
|
||||||
|
arg := argSpec.getBase()
|
||||||
|
fmt.Fprintf(&sb, " <%s> %s\n", arg.name, arg.description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if publicCount > 0 {
|
||||||
|
fmt.Fprintf(&sb, " <options>\n")
|
||||||
|
templates, maxWidth := cli.makeOptionTemplateList()
|
||||||
|
for i, opti := range cli.options {
|
||||||
|
if opti.isHidden() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
opt := opti.getBase()
|
||||||
|
aliases := opt.aliases
|
||||||
|
if len(aliases) > 0 && opt.isArray {
|
||||||
|
// Skip the last alias because it is the implicit plural alias
|
||||||
|
aliases = aliases[0 : len(aliases)-1]
|
||||||
|
}
|
||||||
|
if len(aliases) > 0 {
|
||||||
|
fmt.Fprintf(&sb, " %-*s %s (alias: %s)", maxWidth, templates[i], opt.description, strings.Join(aliases, ", "))
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(&sb, " %-*s %s", maxWidth, templates[i], opt.description)
|
||||||
|
}
|
||||||
|
if value := opti.getDefaultValue(); value != "" {
|
||||||
|
fmt.Fprintf(&sb, " (default: %q)", value)
|
||||||
|
}
|
||||||
|
sb.WriteByte('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CliParser) publicOptionCount() (count int) {
|
||||||
|
for _, opti := range cli.options {
|
||||||
|
opt := opti.getBase()
|
||||||
|
if !opt.hidden {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CliParser) makeOptionTemplateList() (templates []string, maxWidth int) {
|
||||||
|
maxWidth = 0
|
||||||
|
templates = make([]string, len(cli.options))
|
||||||
|
for i, opti := range cli.options {
|
||||||
|
if opti.isHidden() {
|
||||||
|
templates[i] = ""
|
||||||
|
} else {
|
||||||
|
templates[i] = opti.getTemplate()
|
||||||
|
if len(templates[i]) > maxWidth {
|
||||||
|
maxWidth = len(templates[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return templates, maxWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintUsage()
|
||||||
|
func (cli *CliParser) PrintUsage() {
|
||||||
|
os.Stdout.WriteString(cli.Usage())
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// const (
|
||||||
|
// VER_PROGRAM = iota
|
||||||
|
// VER_VERSION
|
||||||
|
// VER_DATE
|
||||||
|
// VER_EMAIL
|
||||||
|
// )
|
||||||
|
|
||||||
|
// GetVersionSection()
|
||||||
|
func (cli *CliParser) GetVersionSection(sectionName string) (secValue string, err error) {
|
||||||
|
var sectionId int
|
||||||
|
if sectionName == "" || sectionName == "all" || sectionName == "full" {
|
||||||
|
secValue = cli.version[5 : len(cli.version)-2]
|
||||||
|
} else {
|
||||||
|
sections := strings.Split(cli.version[5:len(cli.version)-2], ",")
|
||||||
|
switch sectionName {
|
||||||
|
case "program":
|
||||||
|
sectionId = 0
|
||||||
|
case "version", "number":
|
||||||
|
sectionId = 1
|
||||||
|
case "date":
|
||||||
|
sectionId = 2
|
||||||
|
case "email":
|
||||||
|
sectionId = 3
|
||||||
|
case "full":
|
||||||
|
sectionId = -1
|
||||||
|
default:
|
||||||
|
sectionId = 1
|
||||||
|
}
|
||||||
|
if sectionId >= 0 && sectionId < len(sections) {
|
||||||
|
secValue = sections[sectionId]
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("unknown version section %q", sectionName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintVersion()
|
||||||
|
func (cli *CliParser) PrintVersion(specs []string) {
|
||||||
|
if len(specs) == 0 {
|
||||||
|
if specValue, err := cli.GetVersionSection("number"); err == nil {
|
||||||
|
os.Stdout.WriteString(specValue + "\n")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, spec := range specs {
|
||||||
|
if specValue, err := cli.GetVersionSection(spec); err == nil {
|
||||||
|
os.Stdout.WriteString(specValue + "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CliOptionTracer interface {
|
||||||
|
TraceCliOption(name string, valueType string, value any)
|
||||||
|
}
|
||||||
|
|
||||||
|
type cliParser interface {
|
||||||
|
getOptionValue(argIndex int) (string, bool)
|
||||||
|
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, optValue string, err error)
|
||||||
|
getTemplate() string
|
||||||
|
getDefaultValue() string
|
||||||
|
getBase() *cliOptionBase
|
||||||
|
init()
|
||||||
|
isSet() bool
|
||||||
|
isHidden() bool
|
||||||
|
requiresValue() bool
|
||||||
|
finalCheck() (err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type OptManager interface {
|
||||||
|
SetOptionValue(name string, value string) (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 {
|
||||||
|
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(version string, description string, flags ...int16) {
|
||||||
|
cli.version = version
|
||||||
|
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 {
|
||||||
|
var opti cliOptionParser
|
||||||
|
if opti = cli.findOptionByName(optName); opti == nil {
|
||||||
|
return errOptionNotFound(optName)
|
||||||
|
}
|
||||||
|
for _, incompatibleName := range incompatibleOptNames {
|
||||||
|
var incompatibleOpti cliOptionParser
|
||||||
|
if incompatibleOpti = cli.findOptionByName(incompatibleName); incompatibleOpti == nil {
|
||||||
|
return errOptionNotFound(incompatibleName)
|
||||||
|
}
|
||||||
|
incompatibleOpti.getBase().addIncompatibleOption(optName)
|
||||||
|
}
|
||||||
|
opti.getBase().addIncompatibleOption(incompatibleOptNames...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CliParser) TraceOptions(tracer CliOptionTracer) {
|
||||||
|
for _, opti := range cli.options {
|
||||||
|
opt := opti.getBase()
|
||||||
|
var valueType string
|
||||||
|
var value any
|
||||||
|
if optiWithTargetVar, ok := opti.(interface {
|
||||||
|
getTargetVar() (any, string)
|
||||||
|
}); ok {
|
||||||
|
value, valueType = optiWithTargetVar.getTargetVar()
|
||||||
|
} else {
|
||||||
|
value = nil
|
||||||
|
valueType = "n/a"
|
||||||
|
}
|
||||||
|
tracer.TraceCliOption(opt.name, valueType, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CliParser) AddVersion() {
|
||||||
|
name := "version"
|
||||||
|
short := "v"
|
||||||
|
aliases := []string(nil)
|
||||||
|
description := `Print program info; allowed sections: "all", "program", "number" or "version", "date", "email"`
|
||||||
|
if cli.optionExists(name, short, aliases) {
|
||||||
|
panic(errOptionAlreadyDefined(name))
|
||||||
|
}
|
||||||
|
cli.options = append(cli.options, &cliOptionVersion{
|
||||||
|
cliOptionBase: cliOptionBase{
|
||||||
|
name: name,
|
||||||
|
shortAlias: short,
|
||||||
|
aliases: aliases,
|
||||||
|
description: description,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CliParser) AddHelp() {
|
||||||
|
name := "help"
|
||||||
|
short := "h"
|
||||||
|
aliases := []string(nil)
|
||||||
|
description := "Print this help text"
|
||||||
|
if cli.optionExists(name, short, aliases) {
|
||||||
|
panic(errOptionAlreadyDefined(name))
|
||||||
|
}
|
||||||
|
cli.options = append(cli.options, &cliOptionHelp{
|
||||||
|
cliOptionBase: cliOptionBase{
|
||||||
|
name: name,
|
||||||
|
shortAlias: short,
|
||||||
|
aliases: aliases,
|
||||||
|
description: description,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CliParser) addHelpAndVersion() {
|
||||||
|
var hasHelp, hasVersion bool
|
||||||
|
for _, opti := range cli.options {
|
||||||
|
opti.init()
|
||||||
|
opt := opti.getBase()
|
||||||
|
if opt.Is("help") {
|
||||||
|
hasHelp = true
|
||||||
|
} else if opt.Is("version") {
|
||||||
|
hasVersion = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasHelp {
|
||||||
|
cli.AddHelp()
|
||||||
|
}
|
||||||
|
if !hasVersion {
|
||||||
|
cli.AddVersion()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CliParser) parseOptions() (commandArgs []string, err error) {
|
||||||
|
// var commandArgs []string
|
||||||
|
var optionsAllowed bool = true
|
||||||
|
|
||||||
|
skipNext := false
|
||||||
|
for i, arg := range cli.cliArgs[1:] {
|
||||||
|
if skipNext {
|
||||||
|
skipNext = false
|
||||||
|
} else {
|
||||||
|
if optionsAllowed && arg[0] == '-' {
|
||||||
|
if arg == "--" {
|
||||||
|
optionsAllowed = false
|
||||||
|
} else if skipNext, err = cli.parseArg(arg, i); err == nil {
|
||||||
|
err = cli.checkCompatibility(arg)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
commandArgs = append(commandArgs, arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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(commandArgs) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if there are remaining arg-specs that require a value
|
||||||
|
if err == nil {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CliParser) checkCompatibility(arg string) (err error) {
|
||||||
|
var opti cliOptionParser
|
||||||
|
if opti = cli.findOptionByArg(arg); opti != nil {
|
||||||
|
opt := opti.getBase()
|
||||||
|
for _, incompatibleName := range opt.incompatibleWith {
|
||||||
|
var incompatibleOpti cliOptionParser
|
||||||
|
if incompatibleOpti = cli.findOptionByName(incompatibleName); incompatibleOpti != nil {
|
||||||
|
if incompatibleOpti.isSet() {
|
||||||
|
err = errIncompatibleOptions(opt.name, incompatibleOpti.getBase().name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
+385
@@ -0,0 +1,385 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
report string
|
||||||
|
verbose int
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerbose(t *testing.T) {
|
||||||
|
var cli CliParser
|
||||||
|
var gd GlobalData
|
||||||
|
|
||||||
|
if err := initCli(&cli, &gd); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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 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)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func errMissingOptionValue(opt string) error {
|
||||||
|
return fmt.Errorf("option %q requires a value", opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func errOptionAlreadyDefined(opt string) error {
|
||||||
|
return fmt.Errorf("option name %q already used", opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func errRepeatArgAlreadyDefined(argName string) error {
|
||||||
|
return fmt.Errorf("repeat property already set for arg <%s>", argName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func errMissingRequiredArg(argName string) error {
|
||||||
|
return fmt.Errorf("missing required arg <%s>", argName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func errTooFewArguments(numRequired int) error {
|
||||||
|
return fmt.Errorf("too few arguments: ad least %d required", numRequired)
|
||||||
|
}
|
||||||
|
|
||||||
|
func errOptionNotFound(name string) error {
|
||||||
|
return fmt.Errorf("option %q not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func errInvalidOptionValue(name string, value any, optType string) error {
|
||||||
|
return fmt.Errorf("invalid value %v (%T) for option %q (%s)", value, value, name, optType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func errIncompatibleOptions(currentOptName string, incompatibleOptName string) error {
|
||||||
|
return fmt.Errorf("option %q cannot specified together with option %q", currentOptName, incompatibleOptName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func errUnknownOption(name string) error {
|
||||||
|
return fmt.Errorf("unknown option %q", name)
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{"version":3,"term":{"cols":124,"rows":29,"type":"xterm-256color","theme":{"fg":"#000000","bg":"#ffffff","palette":"#000000:#990000:#00a600:#999900:#0000b3:#b300b3:#00a6b3:#bfbfbf:#666666:#e60000:#00d900:#e6e600:#0000ff:#e600e6:#00e6e6:#e6e6e6"}},"timestamp":1772788104,"env":{"SHELL":"/bin/bash"}}
|
||||||
|
[0.377778, "o", "\u001b]0;camoroso@minis:~/go/src/git.portale-stac.it/go-pkg/cli\u0007"]
|
||||||
|
[0.002778, "o", "\u001b]3008;start=d3bcbbeb-42c6-4c51-80ba-f39e06f42083;machineid=526a9f09b4ab47fe82b923c17d37ab65;user=camoroso;hostname=minis.portale-stac.it;bootid=eb177a24-5411-4a8d-8c1f-794a53af4509;pid=00000000000000275634;type=shell;cwd=/home/camoroso/go/src/git.portale-stac.it/go-pkg/cli\u001b\\\u001b[?2004h"]
|
||||||
|
[0.000004, "o", "\u001b[0m\u001b[32mcamoroso@minis\u001b[0m:\u001b[32m~/go/src/git.portale-stac.it/go-pkg/cli\u001b[0m$ "]
|
||||||
|
[8.403131, "o", "l"]
|
||||||
|
[0.169049, "o", "l"]
|
||||||
|
[0.325703, "o", "\r\n\u001b[?2004l\r"]
|
||||||
|
[0.009626, "o", "\u001b]3008;start=953815a1-a645-440b-8eff-0f9c11592d0a;machineid=526a9f09b4ab47fe82b923c17d37ab65;user=camoroso;hostname=minis.portale-stac.it;bootid=eb177a24-5411-4a8d-8c1f-794a53af4509;pid=00000000000000275634;type=command;cwd=/home/camoroso/go/src/git.portale-stac.it/go-pkg/cli\u001b\\"]
|
||||||
|
[0.005808, "o", "totale 124\r\n-rw-r--r--. 1 camoroso camoroso 823 11 dic 07.49 arg-base.go\r\n-rw-r--r--. 1 camoroso camoroso 1309 11 dic 07.49 arg-string-array.go\r\n-rw-r--r--. 1 camoroso camoroso 859 11 dic 07.49 arg-string.go\r\n-rw-r--r--. 1 camoroso camoroso 6397 6 mar 09.30 cli.go\r\n-rw-r--r--. 1 camoroso camoroso 12102 5 mar 22.30 cli_test.go\r\n-rw-r--r--. 1 camoroso camoroso 2241 23 gen 11.20 cli-usage.go\r\n-rw-r--r--. 1 camoroso camoroso 1249 11 dic 07.49 cli-version.go\r\n-rw-r--r--. 1 camoroso camoroso 1163 11 dic 07.49 common.go\r\n-rw-r--r--. 1 camoroso camoroso 1221 6 mar 10.08 demo.cast\r\n-rw-r--r--. 1 camoroso camoroso 534 14 dic 13.06 expected.txt\r\n-rw-r--r--. 1 camoroso camoroso 49 11 dic 07.53 go.mod\r\n-rw-r--r--. 1 camoroso camoroso 535 14 dic 13.06 got.txt\r\n-rw-r--r--. 1 camoroso camoroso 1520 11 dic 07.52 LICENSE\r\n-rw-r--r--. 1 camoroso camoroso 4009 6 mar 09.32 opt-base.go\r\n-rw-r--r--. 1 camoroso camoroso 1893 6 mar 09.32 opt-bool.go\r\n-rw-r--r--. 1 camoroso camoroso 525 5 mar 22.30 opt-help.go\r\n-rw-r--r--. 1 camoroso camoroso 3036 6 mar 09.32 opt-int-array.go\r\n-rw-r--r--. 1 camoroso camoroso 1977 6 mar 09.32 opt-int.go\r\n-rw-r--r--. 1 camoroso camoroso 238 5 mar 22.30 opt-manager.go\r\n-rw-r--r--. 1 camoroso camoroso 1650 6 mar 09.32 opt-multi.go\r\n-rw-r--r--. 1 camoroso camoroso 2597 6 mar 09.33 opt-string-array.go\r\n-rw-r--r--. 1 camoroso camoroso 3509 6 mar 09.33 opt-string.go\r\n-rw-r--r--. 1 camoroso camoroso 2761 6 mar 09.33 opt-string-map.go\r\n-rw-r--r--. 1 camoroso camoroso 2938 6 mar 09.31 opt_test.go\r\n-rw-r--r--. 1 camoroso camoroso 681 5 mar 22.30 opt-version.go\r\n-rw-r--r--. 1 camoroso camoroso 2748 6 mar 06.40 parser.go\r\n-rw-r--r--. 1 camoroso camoroso 3179 14 dic 05.49 README.md\r\n-rw-r--r--. 1 camoroso camoroso 357 11 dic 07.49 simple-opt-tracer.go\r\n"]
|
||||||
|
[0.000679, "o", "\u001b]0;camoroso@minis:~/go/src/git.portale-stac.it/go-pkg/cli\u0007\u001b]3008;end=953815a1-a645-440b-8eff-0f9c11592d0a;exit=success\u001b\\"]
|
||||||
|
[0.005912, "o", "\u001b]3008;start=d3bcbbeb-42c6-4c51-80ba-f39e06f42083;machineid=526a9f09b4ab47fe82b923c17d37ab65;user=camoroso;hostname=minis.portale-stac.it;bootid=eb177a24-5411-4a8d-8c1f-794a53af4509;pid=00000000000000275634;type=shell;cwd=/home/camoroso/go/src/git.portale-stac.it/go-pkg/cli\u001b\\"]
|
||||||
|
[0.000155, "o", "\u001b[?2004h\u001b[0m\u001b[32mcamoroso@minis\u001b[0m:\u001b[32m~/go/src/git.portale-stac.it/go-pkg/cli\u001b[0m$ "]
|
||||||
|
[5.384265, "o", "vi "]
|
||||||
|
[3.604471, "o", "R"]
|
||||||
|
[0.476469, "o", "E"]
|
||||||
|
[0.52018, "o", "ADME.md "]
|
||||||
|
[1.009636, "o", "\r\n\u001b[?2004l\r"]
|
||||||
|
[0.021129, "o", "\u001b]3008;start=0ab19f1b-fb87-4b0b-9e3d-56eaf214f0c7;machineid=526a9f09b4ab47fe82b923c17d37ab65;user=camoroso;hostname=minis.portale-stac.it;bootid=eb177a24-5411-4a8d-8c1f-794a53af4509;pid=00000000000000275634;type=command;cwd=/home/camoroso/go/src/git.portale-stac.it/go-pkg/cli\u001b\\"]
|
||||||
|
[0.066055, "o", "\u001b[?1049h\u001b[22;0;0t\u001b[>4;2m\u001b[?1h\u001b=\u001b[?2004h\u001b[?1004h\u001b[1;29r\u001b[?12h\u001b[?12l\u001b[22;2t\u001b[22;1t"]
|
||||||
|
[0.000017, "o", "\u001b[27m\u001b[23m\u001b[29m\u001b[m\u001b[H\u001b[2J\u001b[?25l\u001b[29;1H\"README.md\""]
|
||||||
|
[0.000161, "o", " 84R, 3179B"]
|
||||||
|
[0.019593, "o", "\u001b[2;1H▽\u001b[6n\u001b[2;1H \u001b[3;1H\u001bPzz\u001b\\\u001b[0%m\u001b[6n\u001b[3;1H \u001b[1;1H\u001b[>c\u001b]10;?\u0007\u001b]11;?\u0007"]
|
||||||
|
[0.004625, "o", "\u001b[1;1H\u001b[35m# \u001b[m\u001b[35mgo-pkg/cli\u001b[m\u001b[2;1H\u001b[K\u001b[3;1HLightweight, dependency-free command-line argument and option parsing library for Go.\u001b[3;86H\u001b[K\u001b[5;1H\u001b[35m## \u001b[m\u001b[35mOverview\u001b[m\r\n\r\nThis package provides a small, composable CLI parser with support for:\r\n\u001b[38;5;130m-\u001b[m boolean, string, int, and array/map options\r\n\u001b[38;5;130m-\u001b[m positional arguments (single and repeating)\r\n\u001b[38;5;130m-\u001b[m aliases, short flags, defaults and hidden options\r\n\u001b[38;5;130m-\u001b[m validation for incompatible options and custom \"special\" values\r\n\u001b[38;5;130m-\u001b[m automatic usage and version \u001b[103mprint\u001b[ming\r\n\r\nSee the core parser implementation in [\u001b[35m`\u001b[mparser.go\u001b[35m`\u001b[m](\u001b[31mparser.go\u001b[m) and the high-level API in [\u001b[35m`\u001b[mcli.go\u001b[35m`\u001b[m](\u001b[31mcli.go\u001b[m).\r\n\r\n\u001b[35m## \u001b[m\u001b[35mFeatures\u001b[m\r\n\r\n\u001b[38;5;130m-\u001b[m Option types: [\u001b[35m`\u001b[mAddBoolOpt\u001b[35m`\u001b[m](\u001b[31mopt-bool.go\u001b[m), [\u001b[35m`\u001b[mAddStringOpt\u001b[35m`\u001b[m](\u001b[31mopt-string.go\u001b[m), [\u001b[35m`\u001b[mAddIntOpt\u001b[35m`\u001b[m](\u001b[31mopt-int.go\u001b[m), [\u001b[35m`\u001b[mAddStringArrayOptt\u001b[19;1H\u001b[35m`\u001b[m](\u001b[31mopt-string-array.go\u001b[m), [\u001b[35m`\u001b[mAddIntArrayOpt\u001b[35m`\u001b[m](\u001b[31mopt-int-array.go\u001b[m), [\u001b[35m`\u001b[mAddStringMapOpt\u001b[35m`\u001b[m](\u001b[31mopt-string-map.go\u001b[m)\r\n\u001b[38;5;130m-\u001b[m Positional args: [\u001b[35m`\u001b[mAddStringArg\u001b[35m`\u001b[m](\u001b[31marg-string.go\u001b[m), [\u001b[35m`\u001b[mAddStringArrayArg\u001b[35m`\u001b[m](\u001b[31marg-string-array.go\u001b[m)\r\n\u001b[38;5;130m-\u001b[m Usage & version output: [\u001b[35m`\u001b[mCliParser.Usage\u001b[35m`\u001b[m](\u001b[31mcli-usage.go\u001b[m), [\u001b[35m`\u001b[mCliParser.PrintVersion\u001b[35m`\u001b[m](\u001b[31mcli-version.go\u001b[m)\r\n\u001b[38;5;130m-\u001b[m Option tracing via [\u001b[35m`\u001b[mCliParser.TraceOptions\u001b[35m`\u001b[m](\u001b[31mcli.go\u001b[m) and [\u001b[35m`\u001b[mSimpleOptionTracer\u001b[35m`\u001b[m](\u001b[31msimple-opt-tracer.go\u001b[m)\r\n\u001b[38;5;130m-\u001b[m Programmatic option setting: [\u001b[35m`\u001b[mSetOptionValue\u001b[35m`\u001b[m](\u001b[31mopt-manager.go\u001b[m)\r\n\r\n\u001b[35m## \u001b[m\u001b[35mQuick start\u001b[m\r\n\r\nExample: define options and parse argv\u001b[29;107H1,1\u001b[11CCim\u001b[1;1H\u001b[?25h\u001b[?4m"]
|
||||||
|
[2.98534, "o", "\u001b[?25l\u001b[29;97H:\u001b[1;1H\u001b[29;1H\u001b[K\u001b[29;1H:\u001b[?25h"]
|
||||||
|
[0.478525, "o", "q"]
|
||||||
|
[0.506599, "o", "\r"]
|
||||||
|
[0.00491, "o", "\u001b[?25l\u001b[?2004l\u001b[>4;m\u001b[23;2t\u001b[23;1t\u001b[29;1H\u001b[K\u001b[29;1H\u001b[?1004l\u001b[?2004l\u001b[?1l\u001b>\u001b[?1049l\u001b[23;0;0t\u001b[?25h\u001b[>4;m"]
|
||||||
|
[0.004173, "o", "\u001b]0;camoroso@minis:~/go/src/git.portale-stac.it/go-pkg/cli\u0007\u001b]3008;end=0ab19f1b-fb87-4b0b-9e3d-56eaf214f0c7;exit=success\u001b\\"]
|
||||||
|
[0.01003, "o", "\u001b]3008;start=d3bcbbeb-42c6-4c51-80ba-f39e06f42083;machineid=526a9f09b4ab47fe82b923c17d37ab65;user=camoroso;hostname=minis.portale-stac.it;bootid=eb177a24-5411-4a8d-8c1f-794a53af4509;pid=00000000000000275634;type=shell;cwd=/home/camoroso/go/src/git.portale-stac.it/go-pkg/cli\u001b\\"]
|
||||||
|
[0.000293, "o", "\u001b[?2004h\u001b[0m\u001b[32mcamoroso@minis\u001b[0m:\u001b[32m~/go/src/git.portale-stac.it/go-pkg/cli\u001b[0m$ "]
|
||||||
|
[4.057766, "o", "\u001b[?2004l\r\r\nexit\r\n"]
|
||||||
|
[0.003248, "x", "0"]
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
Option: print-ocr, Type: bool, Value: true
|
||||||
|
Option: save-clip, Type: bool, Value: false
|
||||||
|
Option: trace, Type: bool, Value: false
|
||||||
|
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:
|
||||||
|
Option: help, Type: n/a, Value: <nil>
|
||||||
|
Option: version, Type: n/a, Value: <nil>
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
Option: print-ocr, Type: bool, Value: true
|
||||||
|
Option: save-clip, Type: bool, Value: false
|
||||||
|
Option: trace, Type: bool, Value: false
|
||||||
|
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:
|
||||||
|
Option: help, Type: n/a, Value: <nil>
|
||||||
|
Option: version, Type: n/a, Value: <nil>
|
||||||
+165
@@ -0,0 +1,165 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cliOptionBase struct {
|
||||||
|
name string
|
||||||
|
shortAlias string
|
||||||
|
aliases []string
|
||||||
|
description string
|
||||||
|
isArray bool
|
||||||
|
hidden bool
|
||||||
|
alreadySeen bool
|
||||||
|
specialValues map[string]SpecialValueFunc
|
||||||
|
finalCheckFunc FinalCheckFunc
|
||||||
|
incompatibleWith []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionBase) getBase() *cliOptionBase {
|
||||||
|
return opt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionBase) getTargetVar() (any, string) {
|
||||||
|
return nil, "n/a"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionBase) Is(name string) (result bool) {
|
||||||
|
if result = opt.name == name; !result && len(opt.aliases) > 0 {
|
||||||
|
result = slices.Contains(opt.aliases, name)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionBase) isHidden() bool {
|
||||||
|
return opt.hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
opt.specialValues[cliValue] = specialFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionBase) getSpecialFunc(cliValue string) (specialFunc SpecialValueFunc, exists bool) {
|
||||||
|
if len(opt.specialValues) > 0 {
|
||||||
|
specialFunc, exists = opt.specialValues[cliValue]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionBase) isSet() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionBase) requiresValue() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionBase) addIncompatibleOption(names ...string) {
|
||||||
|
opt.incompatibleWith = append(opt.incompatibleWith, names...)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func pluralSuffix(word string) (suffix string) {
|
||||||
|
if len(word) > 0 {
|
||||||
|
if strings.HasSuffix(word, "x") || strings.HasSuffix(word, "s") || strings.HasSuffix(word, "ch") {
|
||||||
|
suffix = "es"
|
||||||
|
} else {
|
||||||
|
suffix = "s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func makePlural(word string) (plural string) {
|
||||||
|
return word + pluralSuffix(word)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionBase) makeOptTemplate(isArray bool, valueType string) (templ string) {
|
||||||
|
var suffix, multiValue string
|
||||||
|
if isArray {
|
||||||
|
suffix = "(" + pluralSuffix(opt.name) + ")"
|
||||||
|
multiValue = `["," ...]`
|
||||||
|
}
|
||||||
|
if opt.shortAlias != "" {
|
||||||
|
templ = fmt.Sprintf(`-%s, --%s%s <%s>%s`, opt.shortAlias, opt.name, suffix, valueType, multiValue)
|
||||||
|
} else {
|
||||||
|
templ = fmt.Sprintf(`--%s%s <%s>%s`, opt.name, suffix, valueType, multiValue)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionBase) makeOptSimpleTemplate(valueRequired, multiValue bool, valueType string) (templ string) {
|
||||||
|
var valueSpec, dots string
|
||||||
|
|
||||||
|
if multiValue {
|
||||||
|
dots = " ..."
|
||||||
|
}
|
||||||
|
if len(valueType) > 0 {
|
||||||
|
valueSpec = " <" + valueType + ">" + dots
|
||||||
|
if !valueRequired {
|
||||||
|
valueSpec = " [" + valueSpec[1:] + "]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opt.shortAlias != "" {
|
||||||
|
templ = fmt.Sprintf(`-%s, --%s%s`, opt.shortAlias, opt.name, valueSpec)
|
||||||
|
} else {
|
||||||
|
templ = fmt.Sprintf(`--%s%s`, opt.name, valueSpec)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionBase) getSpecialValue(parser cliParser, value string, targetVar any) (boxedValue any, err error) {
|
||||||
|
if specialFunc, exists := opt.getSpecialFunc(value); exists {
|
||||||
|
manager := parser.(OptManager)
|
||||||
|
boxedValue, err = specialFunc(manager, value, targetVar)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionBase) fetchOptionValue(parser cliParser, argIndex int, valuePtr *string) (value string, skipNextArg bool, err error) {
|
||||||
|
if valuePtr != nil {
|
||||||
|
value = *valuePtr
|
||||||
|
} else {
|
||||||
|
if source, optionPresent := parser.getOptionValue(argIndex); optionPresent {
|
||||||
|
skipNextArg = true
|
||||||
|
value = source
|
||||||
|
} else {
|
||||||
|
err = errMissingOptionValue(opt.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
boolTypeName = "bool"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cliOptionBool struct {
|
||||||
|
cliOptionBase
|
||||||
|
targetVar *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionBool) init() {
|
||||||
|
if opt.targetVar != nil {
|
||||||
|
*opt.targetVar = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionBool) getTargetVar() (any, string) {
|
||||||
|
var value bool
|
||||||
|
if opt.targetVar != nil {
|
||||||
|
value = *opt.targetVar
|
||||||
|
}
|
||||||
|
return value, boolTypeName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionBool) isSet() bool {
|
||||||
|
if opt.targetVar != nil {
|
||||||
|
return *(opt.targetVar)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionBool) requiresValue() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionBool) 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 *cliOptionBool) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, optValue string, err error) {
|
||||||
|
var boxedValue any
|
||||||
|
optValue = "true"
|
||||||
|
|
||||||
|
if boxedValue, err = opt.getSpecialValue(parser, optValue, opt.targetVar); err == nil {
|
||||||
|
if opt.targetVar != nil {
|
||||||
|
if boxedValue != nil {
|
||||||
|
if val, ok := boxedValue.(bool); ok {
|
||||||
|
*(opt.targetVar) = val
|
||||||
|
} else {
|
||||||
|
err = errInvalidOptionValue(opt.name, boxedValue, "bool")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
*(opt.targetVar) = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
opt := &cliOptionBool{
|
||||||
|
cliOptionBase: cliOptionBase{
|
||||||
|
name: name,
|
||||||
|
shortAlias: short,
|
||||||
|
aliases: aliases,
|
||||||
|
description: description,
|
||||||
|
},
|
||||||
|
targetVar: targetVar,
|
||||||
|
}
|
||||||
|
cli.options = append(cli.options, opt)
|
||||||
|
return opt
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cliOptionHelp struct {
|
||||||
|
cliOptionBase
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionHelp) init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionHelp) requiresValue() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionHelp) getDefaultValue() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionHelp) getTemplate() string {
|
||||||
|
return opt.makeOptSimpleTemplate(false, false, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionHelp) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, value string, err error) {
|
||||||
|
parser.PrintUsage()
|
||||||
|
err = io.EOF
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
intArrayTypeName = "num-array"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cliOptionIntArray struct {
|
||||||
|
cliOptionBase
|
||||||
|
defaultValue []int
|
||||||
|
targetVar *[]int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionIntArray) init() {
|
||||||
|
if opt.targetVar != nil {
|
||||||
|
*opt.targetVar = opt.defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionIntArray) getTargetVar() (any, string) {
|
||||||
|
var value []int
|
||||||
|
if opt.targetVar != nil {
|
||||||
|
value = *opt.targetVar
|
||||||
|
}
|
||||||
|
return value, intArrayTypeName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionIntArray) requiresValue() bool {
|
||||||
|
return opt.targetVar != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionIntArray) getDefaultValue() string {
|
||||||
|
def := make([]string, len(opt.defaultValue))
|
||||||
|
for i, v := range opt.defaultValue {
|
||||||
|
def[i] = strconv.Itoa(v)
|
||||||
|
}
|
||||||
|
return strings.Join(def, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionIntArray) getTemplate() string {
|
||||||
|
return opt.makeOptTemplate(true, "num")
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseIntRange(value string) (min int, max int, err error) {
|
||||||
|
var dashPresent bool
|
||||||
|
var minStr, maxStr string
|
||||||
|
minStr, maxStr, dashPresent = strings.Cut(value, "-")
|
||||||
|
if dashPresent {
|
||||||
|
if min, err = strconv.Atoi(minStr); err == nil {
|
||||||
|
if max, err = strconv.Atoi(maxStr); err == nil {
|
||||||
|
if min > max {
|
||||||
|
err = errInvalidOptionValue("", value, "invalid range")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if min, err = strconv.Atoi(value); err == nil {
|
||||||
|
max = min
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if opt.targetVar != nil {
|
||||||
|
if boxedValue != nil {
|
||||||
|
if val, ok := boxedValue.([]int); ok {
|
||||||
|
*opt.targetVar = val
|
||||||
|
} else {
|
||||||
|
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 {
|
||||||
|
for i := minRange; i <= maxRange; i++ {
|
||||||
|
*opt.targetVar = append(*opt.targetVar, i)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = errInvalidOptionValue(opt.name, value, "num-array")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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{
|
||||||
|
cliOptionBase: cliOptionBase{
|
||||||
|
name: name,
|
||||||
|
shortAlias: short,
|
||||||
|
aliases: aliases,
|
||||||
|
description: description,
|
||||||
|
isArray: true,
|
||||||
|
},
|
||||||
|
targetVar: targetVar,
|
||||||
|
defaultValue: defaultValue,
|
||||||
|
}
|
||||||
|
cli.options = append(cli.options, opt)
|
||||||
|
return opt
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
const (
|
||||||
|
intTypeName = "num"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cliOptionInt struct {
|
||||||
|
cliOptionBase
|
||||||
|
defaultValue int
|
||||||
|
targetVar *int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionInt) init() {
|
||||||
|
if opt.targetVar != nil {
|
||||||
|
*opt.targetVar = opt.defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionInt) getTargetVar() (any, string) {
|
||||||
|
var value int
|
||||||
|
if opt.targetVar != nil {
|
||||||
|
value = *opt.targetVar
|
||||||
|
}
|
||||||
|
return value, intTypeName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionInt) requiresValue() bool {
|
||||||
|
return opt.targetVar != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionInt) getDefaultValue() string {
|
||||||
|
return strconv.Itoa(opt.defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionInt) getTemplate() string {
|
||||||
|
return opt.makeOptTemplate(false, intTypeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, optValue, opt.targetVar); err == nil {
|
||||||
|
if opt.targetVar != nil {
|
||||||
|
if boxedValue != nil {
|
||||||
|
if val, ok := boxedValue.(string); ok {
|
||||||
|
*opt.targetVar, err = strconv.Atoi(val)
|
||||||
|
} else {
|
||||||
|
err = errInvalidOptionValue(opt.name, boxedValue, "int")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
*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) {
|
||||||
|
panic(errOptionAlreadyDefined(name))
|
||||||
|
}
|
||||||
|
opt := &cliOptionInt{
|
||||||
|
cliOptionBase: cliOptionBase{
|
||||||
|
name: name,
|
||||||
|
shortAlias: short,
|
||||||
|
aliases: aliases,
|
||||||
|
description: description,
|
||||||
|
},
|
||||||
|
targetVar: targetVar,
|
||||||
|
defaultValue: defaultValue,
|
||||||
|
}
|
||||||
|
cli.options = append(cli.options, opt)
|
||||||
|
return opt
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
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)
|
||||||
|
} else {
|
||||||
|
err = errOptionNotFound(name)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
stringArrayTypeName = "string-array"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cliOptionStringArray struct {
|
||||||
|
cliOptionBase
|
||||||
|
defaultValue []string
|
||||||
|
targetVar *[]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionStringArray) init() {
|
||||||
|
opt.isArray = true
|
||||||
|
if opt.targetVar != nil {
|
||||||
|
*opt.targetVar = opt.defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionStringArray) getTargetVar() (any, string) {
|
||||||
|
var value []string
|
||||||
|
if opt.targetVar != nil {
|
||||||
|
value = *opt.targetVar
|
||||||
|
}
|
||||||
|
return value, stringArrayTypeName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionStringArray) requiresValue() bool {
|
||||||
|
return opt.targetVar != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionStringArray) getDefaultValue() string {
|
||||||
|
return strings.Join(opt.defaultValue, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionStringArray) getTemplate() string {
|
||||||
|
return opt.makeOptTemplate(true, "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, 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 {
|
||||||
|
if opt.targetVar != nil {
|
||||||
|
if boxedValue != nil {
|
||||||
|
if val, ok := boxedValue.([]string); ok {
|
||||||
|
*opt.targetVar = val
|
||||||
|
} 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, ",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddStringArrayOpt adds a new string array option to the CLI parser.
|
||||||
|
// It takes the name, short alias, target variable, default value, description, and optional aliases.
|
||||||
|
// It returns a reference to the created option.
|
||||||
|
func (cli *CliParser) AddStringArrayOpt(name, short string, targetVar *[]string, defaultValue []string, description string, aliases ...string) OptReference {
|
||||||
|
aliases = cli.checkAlreadyUsedNames(name, short, aliases)
|
||||||
|
opt := &cliOptionStringArray{
|
||||||
|
cliOptionBase: cliOptionBase{
|
||||||
|
name: name,
|
||||||
|
shortAlias: short,
|
||||||
|
aliases: aliases,
|
||||||
|
description: description,
|
||||||
|
isArray: true,
|
||||||
|
},
|
||||||
|
targetVar: targetVar,
|
||||||
|
defaultValue: defaultValue,
|
||||||
|
}
|
||||||
|
cli.options = append(cli.options, opt)
|
||||||
|
return opt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionStringArray) finalCheck() (err error) {
|
||||||
|
currentValue, _ := opt.getTargetVar()
|
||||||
|
return opt.getBase().checkValue(currentValue)
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
stringMapTypeName = "map-string"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cliOptionStringMap struct {
|
||||||
|
cliOptionBase
|
||||||
|
defaultValue map[string]string
|
||||||
|
targetVar *map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionStringMap) init() {
|
||||||
|
opt.isArray = true
|
||||||
|
if opt.targetVar != nil {
|
||||||
|
*opt.targetVar = opt.defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionStringMap) getTargetVar() (any, string) {
|
||||||
|
var value map[string]string
|
||||||
|
if opt.targetVar != nil {
|
||||||
|
value = *opt.targetVar
|
||||||
|
}
|
||||||
|
return value, stringMapTypeName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionStringMap) getBase() *cliOptionBase {
|
||||||
|
return &opt.cliOptionBase
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapJoin[T any](m map[string]T, kvSep, itemSep string) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
for key, value := range m {
|
||||||
|
if sb.Len() > 0 {
|
||||||
|
sb.WriteString(itemSep)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&sb, "%q%s%v", key, kvSep, value)
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionStringMap) requiresValue() bool {
|
||||||
|
return opt.targetVar != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionStringMap) getDefaultValue() string {
|
||||||
|
return MapJoin(opt.defaultValue, ",", "=")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionStringMap) getTemplate() string {
|
||||||
|
return opt.makeOptTemplate(true, "key=value")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if opt.targetVar != nil {
|
||||||
|
dict := *opt.targetVar
|
||||||
|
if boxedValue != nil {
|
||||||
|
if val, ok := boxedValue.(map[string]string); ok {
|
||||||
|
dict = val
|
||||||
|
} else {
|
||||||
|
err = errInvalidOptionValue(opt.name, boxedValue, "map of string")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if dict == nil || !opt.alreadySeen {
|
||||||
|
dict = make(map[string]string)
|
||||||
|
}
|
||||||
|
for value := range strings.SplitSeq(value, ",") {
|
||||||
|
if k, v, sepExists := strings.Cut(value, "="); sepExists {
|
||||||
|
dict[k] = v
|
||||||
|
} else {
|
||||||
|
dict[k] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*opt.targetVar = dict
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
aliases = cli.checkAlreadyUsedNames(name, short, aliases)
|
||||||
|
opt := &cliOptionStringMap{
|
||||||
|
cliOptionBase: cliOptionBase{
|
||||||
|
name: name,
|
||||||
|
shortAlias: short,
|
||||||
|
aliases: aliases,
|
||||||
|
description: description,
|
||||||
|
isArray: true,
|
||||||
|
},
|
||||||
|
targetVar: targetVar,
|
||||||
|
defaultValue: defaultValue,
|
||||||
|
}
|
||||||
|
cli.options = append(cli.options, opt)
|
||||||
|
return opt
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
const (
|
||||||
|
stringTypeName = "string"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cliOptionString struct {
|
||||||
|
cliOptionBase
|
||||||
|
defaultValue string
|
||||||
|
targetVar *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionString) init() {
|
||||||
|
if opt.targetVar != nil {
|
||||||
|
*opt.targetVar = opt.defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionString) getTargetVar() (any, string) {
|
||||||
|
var value string
|
||||||
|
if opt.targetVar != nil {
|
||||||
|
value = *opt.targetVar
|
||||||
|
}
|
||||||
|
return value, stringTypeName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionString) requiresValue() bool {
|
||||||
|
return opt.targetVar != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionString) getDefaultValue() string {
|
||||||
|
return opt.defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionString) getTemplate() string {
|
||||||
|
return opt.makeOptTemplate(false, stringTypeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if opt.targetVar != nil {
|
||||||
|
if boxedValue != nil {
|
||||||
|
if val, ok := boxedValue.(string); ok {
|
||||||
|
*(opt.targetVar) = val
|
||||||
|
} else {
|
||||||
|
err = errInvalidOptionValue(opt.name, boxedValue, "string")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
*(opt.targetVar) = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
panic(errOptionAlreadyDefined(name))
|
||||||
|
}
|
||||||
|
opt := &cliOptionString{
|
||||||
|
cliOptionBase: cliOptionBase{
|
||||||
|
name: name,
|
||||||
|
shortAlias: short,
|
||||||
|
aliases: aliases,
|
||||||
|
description: description,
|
||||||
|
},
|
||||||
|
targetVar: targetVar,
|
||||||
|
defaultValue: defaultValue,
|
||||||
|
}
|
||||||
|
cli.options = append(cli.options, opt)
|
||||||
|
return opt
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------- cliOptionDir ----------------------
|
||||||
|
|
||||||
|
type cliOptionDir struct {
|
||||||
|
cliOptionString
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionDir) getTemplate() string {
|
||||||
|
return opt.cliOptionBase.makeOptTemplate(false, "dir")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CliParser) AddDirOpt(name, short string, targetVar *string, defaultValue string, description string, aliases ...string) OptReference {
|
||||||
|
if cli.optionExists(name, short, aliases) {
|
||||||
|
panic(errOptionAlreadyDefined(name))
|
||||||
|
}
|
||||||
|
opt := &cliOptionDir{
|
||||||
|
cliOptionString: cliOptionString{
|
||||||
|
cliOptionBase: cliOptionBase{
|
||||||
|
name: name,
|
||||||
|
shortAlias: short,
|
||||||
|
aliases: aliases,
|
||||||
|
description: description,
|
||||||
|
},
|
||||||
|
targetVar: targetVar,
|
||||||
|
defaultValue: defaultValue,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cli.options = append(cli.options, opt)
|
||||||
|
return opt
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------- cliOptionFile ----------------------
|
||||||
|
|
||||||
|
type cliOptionFile struct {
|
||||||
|
cliOptionString
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionFile) getTemplate() string {
|
||||||
|
return opt.cliOptionBase.makeOptTemplate(false, "file")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CliParser) AddFileOpt(name, short string, targetVar *string, defaultValue string, description string, aliases ...string) OptReference {
|
||||||
|
if cli.optionExists(name, short, aliases) {
|
||||||
|
panic(errOptionAlreadyDefined(name))
|
||||||
|
}
|
||||||
|
opt := &cliOptionFile{
|
||||||
|
cliOptionString: cliOptionString{
|
||||||
|
cliOptionBase: cliOptionBase{
|
||||||
|
name: name,
|
||||||
|
shortAlias: short,
|
||||||
|
aliases: aliases,
|
||||||
|
description: description,
|
||||||
|
},
|
||||||
|
targetVar: targetVar,
|
||||||
|
defaultValue: defaultValue,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cli.options = append(cli.options, opt)
|
||||||
|
return opt
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cliOptionVersion struct {
|
||||||
|
cliOptionBase
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionVersion) init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionVersion) requiresValue() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionVersion) getDefaultValue() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opt *cliOptionVersion) getTemplate() string {
|
||||||
|
return opt.makeOptSimpleTemplate(false, true, "section")
|
||||||
|
}
|
||||||
|
|
||||||
|
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}
|
||||||
|
} else {
|
||||||
|
args = parser.getCliArgs(argIndex+2, -1)
|
||||||
|
}
|
||||||
|
parser.PrintVersion(args)
|
||||||
|
err = io.EOF
|
||||||
|
return
|
||||||
|
}
|
||||||
+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)
|
||||||
|
}
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Private functions implementation
|
||||||
|
|
||||||
|
func (cli *CliParser) optionExists(name, short string, aliases []string) (exists bool) {
|
||||||
|
for _, opti := range cli.options {
|
||||||
|
opt := opti.getBase()
|
||||||
|
if len(opt.shortAlias) > 0 && opt.shortAlias == short {
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
} else if opt.Is(name) {
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
if slices.ContainsFunc(aliases, opt.Is) {
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CliParser) checkAlreadyUsedNames(name, short string, aliases []string) []string {
|
||||||
|
if plural := makePlural(name); !slices.Contains(aliases, plural) {
|
||||||
|
aliases = append(aliases, plural)
|
||||||
|
}
|
||||||
|
if cli.optionExists(name, short, aliases) {
|
||||||
|
panic(errOptionAlreadyDefined(name))
|
||||||
|
}
|
||||||
|
return aliases
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CliParser) getOptionValue(argIndex int) (value string, present bool) {
|
||||||
|
if argIndex < len(cli.cliArgs)-2 {
|
||||||
|
value = cli.cliArgs[1+argIndex+1]
|
||||||
|
present = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CliParser) getCliArgs(startIndex, endIndex int) (args []string) {
|
||||||
|
if endIndex < 0 || endIndex > len(cli.cliArgs) {
|
||||||
|
endIndex = len(cli.cliArgs)
|
||||||
|
}
|
||||||
|
if startIndex < 0 {
|
||||||
|
startIndex = 0
|
||||||
|
}
|
||||||
|
if startIndex > endIndex {
|
||||||
|
startIndex = endIndex
|
||||||
|
}
|
||||||
|
args = cli.cliArgs[startIndex:endIndex]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CliParser) findOptionByArg(arg string) (matchingOpt cliOptionParser) {
|
||||||
|
if strings.HasPrefix(arg, "--") {
|
||||||
|
for _, opti := range cli.options {
|
||||||
|
opt := opti.getBase()
|
||||||
|
if opt.Is(arg[2:]) {
|
||||||
|
matchingOpt = opti
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(arg, "-") {
|
||||||
|
for _, opti := range cli.options {
|
||||||
|
opt := opti.getBase()
|
||||||
|
if opt.shortAlias == arg[1:] {
|
||||||
|
matchingOpt = opti
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CliParser) findOptionByName(optName string) (matchingOpt cliOptionParser) {
|
||||||
|
for _, opti := range cli.options {
|
||||||
|
opt := opti.getBase()
|
||||||
|
if opt.Is(optName) {
|
||||||
|
matchingOpt = opti
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CliParser) parseArg(arg string, index int) (skipNextArg bool, err error) {
|
||||||
|
var opts []string
|
||||||
|
var name, value, dashes string
|
||||||
|
var equalPresent bool
|
||||||
|
name, value, equalPresent = strings.Cut(arg, "=")
|
||||||
|
if strings.HasPrefix(name, "--") {
|
||||||
|
opts = []string{name[2:]}
|
||||||
|
dashes = "--"
|
||||||
|
} else {
|
||||||
|
opts = strings.Split(name[1:], "")
|
||||||
|
dashes = "-"
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
} else if i < len(opts)-1 && opt.requiresValue() {
|
||||||
|
err = errMissingOptionValue(dashes + optName)
|
||||||
|
} else {
|
||||||
|
skipNextArg, _, err = opt.parse(cli, index, nil)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = errUnknownOption(dashes + optName)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SimpleOptionTracer struct {
|
||||||
|
w io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSimpleOptionTracer(w io.Writer) *SimpleOptionTracer {
|
||||||
|
return &SimpleOptionTracer{w: w}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *SimpleOptionTracer) TraceCliOption(name string, valueType string, value any) {
|
||||||
|
fmt.Fprintf(tr.w, "Option: %s, Type: %s, Value: %v\n", name, valueType, value)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user