cli/cli.go
2025-12-11 07:57:48 +01:00

217 lines
4.9 KiB
Go

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
}