first commit status: option and argument parsing, short aliases grouping, special values, hidden options

Option types: bool int, int-array, string, string-array, string-map, file, dir
This commit is contained in:
Celestino Amoroso 2025-12-11 07:57:48 +01:00
parent b9a4efc956
commit 38e36839f8
22 changed files with 1604 additions and 0 deletions

29
LICENSE Normal file
View File

@ -0,0 +1,29 @@
Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Celestino Amoroso, nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

40
arg-base.go Normal file
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
arg-string-array.go Normal file
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
arg-string.go Normal file
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,
})
}

86
cli-usage.go Normal file
View File

@ -0,0 +1,86 @@
package cli
import (
"fmt"
"os"
"strings"
)
// Usage()
func (cli *CliParser) Usage() string {
var sb strings.Builder
program, _ := cli.GetVersionSection("program")
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
cli-version.go Normal file
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")
}
}
}
}

216
cli.go Normal file
View File

@ -0,0 +1,216 @@
package cli
import (
"fmt"
)
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()
}
type cliOptionParser interface {
parse(parser cliParser, argIndex int, valuePtr *string) (skipNextArg bool, err error)
getTemplate() string
getDefaultValue() string
getBase() *cliOptionBase
init()
isSet() bool
isHidden() bool
requiresValue() bool
}
type OptManager interface {
SetOptionValue(name string, value string) (err error)
}
type SpecialValueFunc func(manager OptManager, cliValue string, currentValue any) (value any, err error)
type OptReference interface {
AddSpecialValue(cliValue string, specialFunc SpecialValueFunc)
SetHidden(hidden bool)
}
type CliParser struct {
version string
options []cliOptionParser
argSpecs []argSpec
cliArgs []string
}
func (cli *CliParser) Init(argv []string, version string) {
cli.version = version
cli.cliArgs = argv
}
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) Parse() (err error) {
var arg string
var i int
var args []string
var optionsAllowed bool = true
cli.addHelpAndVersion()
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 {
args = append(args, arg)
}
}
}
if err == nil {
var argSpec argSpec
var n, specIndex int
i = 0
for specIndex, argSpec = range cli.argSpecs {
if n, err = argSpec.parse(cli, specIndex, args, i); err != nil {
break
}
i += n
if i >= len(args) {
break
}
}
if err == nil {
if i < len(args) {
err = fmt.Errorf("too many arguments: %d allowed", i)
} else {
specIndex++
for _, spec := range cli.argSpecs[specIndex:] {
if !spec.getBase().required {
specIndex++
}
}
if specIndex < len(cli.argSpecs) {
err = errTooFewArguments(len(cli.argSpecs))
}
}
}
}
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
}

81
cli_test.go Normal file
View File

@ -0,0 +1,81 @@
package cli
import (
"fmt"
"strings"
"testing"
)
type GlobaData struct {
config string
log []string
printOcr bool
saveClips bool
trace bool
page []int
sources []string
dest string
facoltativo string
}
func (gd *GlobaData) String() string {
var sb strings.Builder
fmt.Fprintf(&sb, "Options:\n")
fmt.Fprintf(&sb, " Config.....: %q\n", gd.config)
fmt.Fprintf(&sb, " Log........: %v\n", gd.log)
fmt.Fprintf(&sb, " Print-Ocr..: %v\n", gd.printOcr)
fmt.Fprintf(&sb, " Save-Clips.: %v\n", gd.saveClips)
fmt.Fprintf(&sb, " Page.......: %v\n", gd.page)
fmt.Fprintf(&sb, " Trace......: %v\n", gd.trace)
fmt.Fprintf(&sb, "Argumentes:\n")
fmt.Fprintf(&sb, " Sources....: %s\n", strings.Join(gd.sources, ", "))
fmt.Fprintf(&sb, " Destination: %s\n", gd.dest)
fmt.Fprintf(&sb, " Facoltativo: %s\n", gd.facoltativo)
return sb.String()
}
func TestUsage(t *testing.T) {
var cli CliParser
var version = `$VER:mini-ddt-ocr,1.0.1,2025-11-23,celestino.amoroso@gmail.com:$`
var gd GlobaData
args := []string{
"mini-ddt-ocr",
"--log", "all",
"--config", "devel-config.yaml",
// "--var", "deploy_env=devel",
"--print-ocr",
"--pages", "17,18",
"../../dev-stuff/ocis-upload.log", "ciccio", "bello", "pippo",
}
cli.Init(args, version)
if err := gd.addOptions(&cli); err != nil {
t.Error(err)
return
}
usage := cli.Usage()
fmt.Println(usage)
if err := cli.Parse(); err == nil {
fmt.Println(&gd)
} else {
t.Error(err)
}
}
func (gd *GlobaData) addOptions(cli *CliParser) (err error) {
defer func() {
if r := recover(); r != nil {
err = r.(error)
}
}()
cli.AddBoolOpt("print-ocr", "o", &gd.printOcr, "Stampa l'output del programma OCR")
cli.AddBoolOpt("save-clip", "s", &gd.saveClips, "Registra le immagini delle aree ritagliata", "save-clips")
cli.AddBoolOpt("trace", "t", &gd.trace, "Attiva la modalità di tracciamento delle operazioni")
cli.AddIntArrayOpt("page", "p", &gd.page, gd.page, "Elabora la pagina specificata")
cli.AddStringOpt("config", "c", &gd.config, gd.config, "Specifica un percorso alternativo per il file di configurazione")
cli.AddStringArrayOpt("log", "l", &gd.log, gd.log, "Maschera di livelli di log")
cli.AddStringArrayArg("sorgenti", true, &gd.sources, "file da elaborare")
cli.AddStringArg("dest", true, &gd.dest, "file di outout")
cli.AddStringArg("facoltativo", false, &gd.facoltativo, "file facoltativo")
return
}

39
common.go Normal file
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)
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.portale-stac.it/go-pkg/cli
go 1.25.4

147
opt-base.go Normal file
View File

@ -0,0 +1,147 @@
package cli
import (
"fmt"
"slices"
"strings"
)
type cliOptionBase struct {
name string
shortAlias string
aliases []string
description string
isArray bool
hidden bool
specialValues map[string]SpecialValueFunc
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) 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, valuePtr *string) (err error) {
return fmt.Errorf("unhandled option %q", opt.name)
}
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
}

85
opt-bool.go Normal file
View File

@ -0,0 +1,85 @@
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, err error) {
var boxedValue any
value := "true"
if boxedValue, err = opt.getSpecialValue(parser, value, 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 (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
opt-help.go Normal file
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, err error) {
parser.PrintUsage()
err = io.EOF
return
}

111
opt-int-array.go Normal file
View File

@ -0,0 +1,111 @@
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, err error) {
var optValue string
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, "array of int")
}
} else {
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 {
break
}
}
}
}
}
}
return
}
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
}

78
opt-int.go Normal file
View File

@ -0,0 +1,78 @@
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, err error) {
var value string
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, err = strconv.Atoi(val)
} else {
err = errInvalidOptionValue(opt.name, boxedValue, "int")
}
} else {
*opt.targetVar, err = strconv.Atoi(value)
}
}
}
}
return
}
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
opt-manager.go Normal file
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
}

85
opt-string-array.go Normal file
View File

@ -0,0 +1,85 @@
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, err error) {
var value string
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 {
*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
}

108
opt-string-map.go Normal file
View File

@ -0,0 +1,108 @@
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, err error) {
var value string
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 {
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 (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
}

136
opt-string.go Normal file
View File

@ -0,0 +1,136 @@
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, err error) {
var value string
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 (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
opt-version.go Normal file
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, 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
}

122
parser.go Normal file
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
simple-opt-tracer.go Normal file
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)
}