new /v2 subdirectory

This commit is contained in:
2026-03-19 18:28:11 +01:00
parent 52ff844ce1
commit dae8be92e3
27 changed files with 2359 additions and 0 deletions
+86
View File
@@ -0,0 +1,86 @@
# go-pkg/cli
Lightweight, dependency-free command-line argument and option parsing library for Go.
## Overview
This package provides a small, composable CLI parser with support for:
- boolean, string, int, and array/map options
- positional arguments (single and repeating)
- aliases, short flags, defaults and hidden options
- validation for incompatible options and custom "special" values
- automatic usage and version printing
See the core parser implementation in [`parser.go`](parser.go) and the high-level API in [`cli.go`](cli.go).
## Features
- Option types: [`AddBoolOpt`](opt-bool.go), [`AddStringOpt`](opt-string.go), [`AddIntOpt`](opt-int.go), [`AddStringArrayOpt`](opt-string-array.go), [`AddIntArrayOpt`](opt-int-array.go), [`AddStringMapOpt`](opt-string-map.go)
- Positional args: [`AddStringArg`](arg-string.go), [`AddStringArrayArg`](arg-string-array.go)
- Usage & version output: [`CliParser.Usage`](cli-usage.go), [`CliParser.PrintVersion`](cli-version.go)
- Option tracing via [`CliParser.TraceOptions`](cli.go) and [`SimpleOptionTracer`](simple-opt-tracer.go)
- Programmatic option setting: [`SetOptionValue`](opt-manager.go)
## Quick start
Example: define options and parse argv
```go
package main
import (
"fmt"
"git.portale-stac.it/go-pkg/cli"
)
func main() {
var parser cli.CliParser
parser.Init([]string{"prog", "--debug", "--config", "app.yaml", "input.txt"}, "$VER:prog,0.1.0,2025,email:$")
// define target variables and options
var debug bool
var config string
var inputs []string
parser.AddBoolOpt("debug", "d", &debug, "Enable debug")
parser.AddStringOpt("config", "c", &config, "app.yaml", "Config file")
parser.AddStringArrayArg("files", true, &inputs, "Input files")
// print usage: parser.Usage() - see [`CliParser.Usage`](cli-usage.go)
if err := parser.Parse(); err != nil {
fmt.Println("parse error:", err)
return
}
fmt.Println("debug:", debug)
fmt.Println("config:", config)
fmt.Println("inputs:", inputs)
}
```
Refer to the unit test for a realistic example: [`cli_test.go`](cli_test.go).
## Usage
## API Reference
Key types and methods:
- [`CliParser`](cli.go) — main parser type
- [`CliParser.Init`](cli.go) — initialize with argv and version
- [`CliParser.Parse`](cli.go) — run parsing
- Option constructors: [`AddBoolOpt`](opt-bool.go), [`AddStringOpt`](opt-string.go), [`AddIntOpt`](opt-int.go), [`AddStringArrayOpt`](opt-string-array.go), [`AddIntArrayOpt`](opt-int-array.go), [`AddStringMapOpt`](opt-string-map.go)
- Arg constructors: [`AddStringArg`](arg-string.go), [`AddStringArrayArg`](arg-string-array.go)
- Utilities: [`TraceOptions`](cli.go), [`SetOptionValue`](opt-manager.go), [`SimpleOptionTracer`](simple-opt-tracer.go)
For implementation details, consult:
- parser internals: [`parser.go`](parser.go)
- option base helpers: [`opt-base.go`](opt-base.go)
- usage/version printing: [`cli-usage.go`](cli-usage.go), [`cli-version.go`](cli-version.go)
## License
This project is licensed under the terms in [LICENSE](LICENSE).
## Contributing
Open issues or pull requests are welcome. Run and extend the tests in [`cli_test.go`](cli_test.go) when making changes.
+40
View File
@@ -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
}
+49
View File
@@ -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,
})
}
+36
View File
@@ -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,
})
}
+92
View File
@@ -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())
}
+59
View File
@@ -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")
}
}
}
}
+284
View File
@@ -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
View File
@@ -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
}
+39
View File
@@ -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)
}
+32
View File
@@ -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"]
+10
View File
@@ -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>
+3
View File
@@ -0,0 +1,3 @@
module git.portale-stac.it/go-pkg/cli/v2
go 1.25.4
+10
View File
@@ -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
View File
@@ -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
}
+90
View File
@@ -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
}
+30
View File
@@ -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
}
+120
View File
@@ -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
}
+81
View File
@@ -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
}
+10
View File
@@ -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
}
+77
View File
@@ -0,0 +1,77 @@
package cli
import (
"fmt"
"strconv"
)
const (
multiTypeName = "num"
)
type cliOptionMulti struct {
cliOptionBase
defaultValue int
targetVar *int
}
func (opt *cliOptionMulti) init() {
if opt.targetVar != nil {
*opt.targetVar = opt.defaultValue
}
}
func (opt *cliOptionMulti) getTargetVar() (any, string) {
var value int
if opt.targetVar != nil {
value = *opt.targetVar
}
return value, multiTypeName
}
func (opt *cliOptionMulti) requiresValue() bool {
return false
}
func (opt *cliOptionMulti) getDefaultValue() string {
return strconv.Itoa(opt.defaultValue)
}
func (opt *cliOptionMulti) getTemplate() (templ string) {
if opt.shortAlias != "" {
templ = fmt.Sprintf(`-%s, --%s`, opt.shortAlias, opt.name)
} else {
templ = fmt.Sprintf(`--%s`, opt.name)
}
return
}
func (opt *cliOptionMulti) parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, value string, err error) {
if opt.targetVar != nil {
*opt.targetVar++
}
value = "true"
return
}
func (opt *cliOptionMulti) finalCheck() (err error) {
currentValue, _ := opt.getTargetVar()
return opt.getBase().checkValue(currentValue)
}
func (cli *CliParser) AddMultiOpt(name, short string, targetVar *int, defaultValue int, description string, aliases ...string) OptReference {
if cli.optionExists(name, short, aliases) {
panic(errOptionAlreadyDefined(name))
}
opt := &cliOptionMulti{
cliOptionBase: cliOptionBase{
name: name,
shortAlias: short,
aliases: aliases,
description: description,
},
targetVar: targetVar,
defaultValue: defaultValue,
}
cli.options = append(cli.options, opt)
return opt
}
+91
View File
@@ -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)
}
+112
View File
@@ -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
}
+139
View File
@@ -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
}
+36
View File
@@ -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
View File
@@ -0,0 +1,143 @@
package cli
import (
"fmt"
"testing"
)
func TestOneOptWithEqual(t *testing.T) {
var cli CliParser
var color string
// Always recover from panic to return error before adding options
defer func() {
if r := recover(); r != nil {
if err := r.(error); err != nil {
t.Error(err)
}
}
}()
// Define options
cli.AddStringOpt("color", "c", &color, "white", "Set color")
args := []string{
"TestOneOptWithEqual",
"--color=blue",
}
cli.Init("1.0.0", "TestOneOptWithEqual description")
if err := cli.Parse(args); err != nil {
t.Error(err)
} else if color != "blue" {
t.Errorf("Expected color blue, got %q", color)
}
}
func addBoxOption(cli *CliParser, boxPtr *[]int) {
// Define options
optRef := cli.AddIntArrayOpt("box", "b", boxPtr, []int{1, 2, 3, 4}, "Box spec: Left,Width,Top,Height; provide 4 int values")
optRef.OnFinalCheck(func(currentValue any) (err error) {
if array, ok := currentValue.([]int); ok {
if len(array) != 4 {
err = fmt.Errorf("--box option requires exactly 4 items, %d provided", len(array))
}
} else {
err = fmt.Errorf("wrong datatype for --box option value")
}
return
})
}
func testIntArrayOptOk(t *testing.T, n int, args []string, flags ...int16) {
var box []int
var cli CliParser
t.Logf("Arg set n. %d", n)
addBoxOption(&cli, &box)
cli.Init("1.0.0", "TestIntArrayOpt description", flags...)
if err := cli.Parse(args); err != nil {
t.Error(err)
} else if len(box) != 4 {
t.Errorf(`Expected 4 items, got %d`, len(box))
}
}
func testIntArrayOptKo(t *testing.T, n int, args []string, msg string, flags ...int16) {
var box []int
var cli CliParser
t.Logf("Arg set n. %d", n)
addBoxOption(&cli, &box)
cli.Init("1.0.0", "TestIntArrayOpt description", flags...)
if err := cli.Parse(args); err == nil {
t.Errorf("Expected error, got nil")
} else if err.Error() != msg {
t.Errorf(`Invalid error: %q; expected: %q`, err.Error(), msg)
}
}
func TestIntArrayOpt(t *testing.T) {
var args []string
// Always recover from panic to return error before adding options
defer func() {
if r := recover(); r != nil {
if err := r.(error); err != nil {
t.Error(err)
}
}
}()
n := 0
n++
args = []string{
"TestIntArrayOpt",
"--box=100,200,150,450",
}
testIntArrayOptOk(t, n, args)
n++
args = []string{
"TestIntArrayOpt",
"--box=100,200,150",
}
testIntArrayOptKo(t, n, args, "--box option requires exactly 4 items, 3 provided")
n++
args = []string{
"TestIntArrayOpt",
"--box", "100,200,150,450",
}
testIntArrayOptOk(t, n, args)
n++
args = []string{
"TestIntArrayOpt",
"--box",
}
testIntArrayOptKo(t, n, args, `option "box" requires a value`)
n++
args = []string{
"TestIntArrayOpt",
"--box", "100,200,150",
"--box", "450",
}
testIntArrayOptOk(t, n, args)
n++
args = []string{
"TestIntArrayOpt",
"--box", "100,200,150",
"--box", "450,12",
}
testIntArrayOptKo(t, n, args, "--box option requires exactly 4 items, 5 provided")
n++
args = []string{
"TestIntArrayOpt",
"--box", "100,200,150",
"--box=100,200,150,450",
}
testIntArrayOptOk(t, n, args, ResetOnEqualSign)
}
+122
View File
@@ -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
}
+18
View File
@@ -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)
}