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