зеркало из https://github.com/mislav/hub.git
Create a lightweight CLI flag parser
Design considerations: - treats `--flag=VALUE` as equivalent of `--flag VALUE` - treats `-abcVALUE` as equivalent to `-a -b -c VALUE` - allows `--opt=false` as opposite of `--opt` or `--opt=true` - retrieve values as string, slice of strings, bool, or int
This commit is contained in:
Родитель
ddf0d82510
Коммит
9eae131eef
|
@ -0,0 +1,190 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type argsFlag struct {
|
||||
expectsValue bool
|
||||
values []string
|
||||
}
|
||||
|
||||
func (f *argsFlag) addValue(v string) {
|
||||
f.values = append(f.values, v)
|
||||
}
|
||||
|
||||
func (f *argsFlag) lastValue() string {
|
||||
l := len(f.values)
|
||||
if l > 0 {
|
||||
return f.values[l-1]
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (f *argsFlag) reset() {
|
||||
if len(f.values) > 0 {
|
||||
f.values = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
type ArgsParser struct {
|
||||
flagMap map[string]*argsFlag
|
||||
flagAliases map[string]string
|
||||
PositionalIndices []int
|
||||
terminated bool
|
||||
}
|
||||
|
||||
func (p *ArgsParser) Parse(args []string) ([]string, error) {
|
||||
var flagName string
|
||||
var flagValue string
|
||||
var hasFlagValue bool
|
||||
var i int
|
||||
var arg string
|
||||
|
||||
p.terminated = false
|
||||
for _, f := range p.flagMap {
|
||||
f.reset()
|
||||
}
|
||||
if len(p.PositionalIndices) > 0 {
|
||||
p.PositionalIndices = []int{}
|
||||
}
|
||||
|
||||
positional := []string{}
|
||||
var parseError error
|
||||
logError := func(f string, p ...interface{}) {
|
||||
if parseError == nil {
|
||||
parseError = fmt.Errorf(f, p...)
|
||||
}
|
||||
}
|
||||
|
||||
acknowledgeFlag := func() bool {
|
||||
canonicalFlagName := flagName
|
||||
if n, found := p.flagAliases[flagName]; found {
|
||||
canonicalFlagName = n
|
||||
}
|
||||
f := p.flagMap[canonicalFlagName]
|
||||
if f == nil {
|
||||
if len(flagName) == 2 {
|
||||
logError("unknown shorthand flag: '%s' in %s", flagName[1:], arg)
|
||||
} else {
|
||||
logError("unknown flag: '%s'", flagName)
|
||||
}
|
||||
return true
|
||||
}
|
||||
if f.expectsValue {
|
||||
if !hasFlagValue {
|
||||
i++
|
||||
if i < len(args) {
|
||||
flagValue = args[i]
|
||||
} else {
|
||||
logError("no value given for '%s'", flagName)
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else if hasFlagValue && len(flagName) <= 2 {
|
||||
flagValue = ""
|
||||
}
|
||||
f.addValue(flagValue)
|
||||
return f.expectsValue
|
||||
}
|
||||
|
||||
for i = 0; i < len(args); i++ {
|
||||
arg = args[i]
|
||||
|
||||
if p.terminated || arg == "-" {
|
||||
} else if arg == "--" {
|
||||
if !p.terminated {
|
||||
p.terminated = true
|
||||
continue
|
||||
}
|
||||
} else if strings.HasPrefix(arg, "--") {
|
||||
flagName = arg
|
||||
eq := strings.IndexByte(arg, '=')
|
||||
hasFlagValue = eq >= 0
|
||||
if hasFlagValue {
|
||||
flagName = arg[:eq]
|
||||
flagValue = arg[eq+1:]
|
||||
}
|
||||
acknowledgeFlag()
|
||||
continue
|
||||
} else if arg[0] == '-' {
|
||||
for j := 1; j < len(arg); j++ {
|
||||
flagName = "-" + arg[j:j+1]
|
||||
flagValue = ""
|
||||
hasFlagValue = j+1 < len(arg)
|
||||
if hasFlagValue {
|
||||
flagValue = arg[j+1:]
|
||||
}
|
||||
if acknowledgeFlag() {
|
||||
break
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
p.PositionalIndices = append(p.PositionalIndices, i)
|
||||
positional = append(positional, arg)
|
||||
}
|
||||
|
||||
return positional, parseError
|
||||
}
|
||||
|
||||
func (p *ArgsParser) RegisterValue(name string, aliases ...string) {
|
||||
f := &argsFlag{expectsValue: true}
|
||||
p.flagMap[name] = f
|
||||
for _, alias := range aliases {
|
||||
p.flagAliases[alias] = name
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ArgsParser) RegisterBool(name string, aliases ...string) {
|
||||
f := &argsFlag{expectsValue: false}
|
||||
p.flagMap[name] = f
|
||||
for _, alias := range aliases {
|
||||
p.flagAliases[alias] = name
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ArgsParser) Value(name string) string {
|
||||
if f, found := p.flagMap[name]; found {
|
||||
return f.lastValue()
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ArgsParser) AllValues(name string) []string {
|
||||
if f, found := p.flagMap[name]; found {
|
||||
return f.values
|
||||
} else {
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ArgsParser) Bool(name string) bool {
|
||||
if f, found := p.flagMap[name]; found {
|
||||
return len(f.values) > 0 && f.lastValue() != "false"
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ArgsParser) Int(name string) int {
|
||||
i, _ := strconv.Atoi(p.Value(name))
|
||||
return i
|
||||
}
|
||||
|
||||
func (p *ArgsParser) HasReceived(name string) bool {
|
||||
f, found := p.flagMap[name]
|
||||
return found && len(f.values) > 0
|
||||
}
|
||||
|
||||
func NewArgsParser() *ArgsParser {
|
||||
return &ArgsParser{
|
||||
flagMap: make(map[string]*argsFlag),
|
||||
flagAliases: make(map[string]string),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func equal(t *testing.T, expected, got interface{}) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(expected, got) {
|
||||
t.Errorf("expected: %#v, got: %#v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgsParser(t *testing.T) {
|
||||
p := NewArgsParser()
|
||||
p.RegisterValue("--hello", "-e")
|
||||
p.RegisterValue("--origin", "-o")
|
||||
args := []string{"--hello", "world", "one", "--", "--two"}
|
||||
rest, err := p.Parse(args)
|
||||
equal(t, nil, err)
|
||||
equal(t, []string{"one", "--two"}, rest)
|
||||
equal(t, "world", p.Value("--hello"))
|
||||
equal(t, true, p.HasReceived("--hello"))
|
||||
equal(t, "", p.Value("-e"))
|
||||
equal(t, false, p.HasReceived("-e"))
|
||||
equal(t, "", p.Value("--origin"))
|
||||
equal(t, false, p.HasReceived("--origin"))
|
||||
equal(t, []int{2, 4}, p.PositionalIndices)
|
||||
}
|
||||
|
||||
func TestArgsParser_RepeatedInvocation(t *testing.T) {
|
||||
p := NewArgsParser()
|
||||
p.RegisterValue("--hello", "-e")
|
||||
p.RegisterValue("--origin", "-o")
|
||||
|
||||
rest, err := p.Parse([]string{"--hello", "world", "--", "one"})
|
||||
equal(t, nil, err)
|
||||
equal(t, []string{"one"}, rest)
|
||||
equal(t, []int{3}, p.PositionalIndices)
|
||||
equal(t, true, p.HasReceived("--hello"))
|
||||
equal(t, "world", p.Value("--hello"))
|
||||
equal(t, false, p.HasReceived("--origin"))
|
||||
equal(t, true, p.terminated)
|
||||
|
||||
rest, err = p.Parse([]string{"two", "-oupstream"})
|
||||
equal(t, nil, err)
|
||||
equal(t, []string{"two"}, rest)
|
||||
equal(t, []int{0}, p.PositionalIndices)
|
||||
equal(t, false, p.HasReceived("--hello"))
|
||||
equal(t, true, p.HasReceived("--origin"))
|
||||
equal(t, "upstream", p.Value("--origin"))
|
||||
equal(t, false, p.terminated)
|
||||
}
|
||||
|
||||
func TestArgsParser_UnknownFlag(t *testing.T) {
|
||||
p := NewArgsParser()
|
||||
p.RegisterValue("--hello")
|
||||
p.RegisterBool("--yes", "-y")
|
||||
|
||||
args := []string{"--hello", "world", "--nonexist", "one", "--", "--two"}
|
||||
rest, err := p.Parse(args)
|
||||
equal(t, errors.New("unknown flag: '--nonexist'"), err)
|
||||
equal(t, []string{"one", "--two"}, rest)
|
||||
|
||||
rest, err = p.Parse([]string{"one", "-yelp"})
|
||||
equal(t, errors.New("unknown shorthand flag: 'e' in -yelp"), err)
|
||||
equal(t, []string{"one"}, rest)
|
||||
equal(t, true, p.Bool("--yes"))
|
||||
}
|
||||
|
||||
func TestArgsParser_Values(t *testing.T) {
|
||||
p := NewArgsParser()
|
||||
p.RegisterValue("--origin", "-o")
|
||||
args := []string{"--origin=a=b", "--origin=", "--origin", "c", "-o"}
|
||||
rest, err := p.Parse(args)
|
||||
equal(t, errors.New("no value given for '-o'"), err)
|
||||
equal(t, []string{}, rest)
|
||||
equal(t, []string{"a=b", "", "c"}, p.AllValues("--origin"))
|
||||
}
|
||||
|
||||
func TestArgsParser_Bool(t *testing.T) {
|
||||
p := NewArgsParser()
|
||||
p.RegisterBool("--noop")
|
||||
p.RegisterBool("--draft", "-d")
|
||||
args := []string{"-d", "--draft=false"}
|
||||
rest, err := p.Parse(args)
|
||||
equal(t, nil, err)
|
||||
equal(t, []string{}, rest)
|
||||
equal(t, false, p.Bool("--draft"))
|
||||
equal(t, true, p.HasReceived("--draft"))
|
||||
equal(t, false, p.HasReceived("-d"))
|
||||
equal(t, false, p.HasReceived("--noop"))
|
||||
equal(t, false, p.Bool("--noop"))
|
||||
}
|
||||
|
||||
func TestArgsParser_BoolValue(t *testing.T) {
|
||||
p := NewArgsParser()
|
||||
p.RegisterBool("--draft")
|
||||
args := []string{"--draft=yes pls"}
|
||||
rest, err := p.Parse(args)
|
||||
equal(t, nil, err)
|
||||
equal(t, []string{}, rest)
|
||||
equal(t, true, p.HasReceived("--draft"))
|
||||
equal(t, true, p.Bool("--draft"))
|
||||
equal(t, "yes pls", p.Value("--draft"))
|
||||
}
|
||||
|
||||
func TestArgsParser_Shorthand(t *testing.T) {
|
||||
p := NewArgsParser()
|
||||
p.RegisterValue("--origin", "-o")
|
||||
p.RegisterBool("--draft", "-d")
|
||||
p.RegisterBool("--copy", "-c")
|
||||
args := []string{"-co", "one", "-dotwo"}
|
||||
rest, err := p.Parse(args)
|
||||
equal(t, nil, err)
|
||||
equal(t, []string{}, rest)
|
||||
equal(t, []string{"one", "two"}, p.AllValues("--origin"))
|
||||
equal(t, true, p.Bool("--draft"))
|
||||
equal(t, true, p.Bool("--copy"))
|
||||
}
|
||||
|
||||
func TestArgsParser_ShorthandEdgeCase(t *testing.T) {
|
||||
p := NewArgsParser()
|
||||
p.RegisterBool("--draft", "-d")
|
||||
p.RegisterBool("-f")
|
||||
p.RegisterBool("-a")
|
||||
p.RegisterBool("-l")
|
||||
p.RegisterBool("-s")
|
||||
p.RegisterBool("-e")
|
||||
args := []string{"-dfalse"}
|
||||
rest, err := p.Parse(args)
|
||||
equal(t, nil, err)
|
||||
equal(t, []string{}, rest)
|
||||
equal(t, true, p.Bool("--draft"))
|
||||
}
|
||||
|
||||
func TestArgsParser_Dashes(t *testing.T) {
|
||||
p := NewArgsParser()
|
||||
p.RegisterValue("--file", "-F")
|
||||
args := []string{"-F-", "-", "--", "-F", "--"}
|
||||
rest, err := p.Parse(args)
|
||||
equal(t, nil, err)
|
||||
equal(t, []string{"-", "-F", "--"}, rest)
|
||||
equal(t, "-", p.Value("--file"))
|
||||
}
|
||||
|
||||
func TestArgsParser_RepeatedArg(t *testing.T) {
|
||||
p := NewArgsParser()
|
||||
p.RegisterValue("--msg", "-m")
|
||||
args := []string{"--msg=hello", "-m", "world", "--msg", "how", "-mare you?"}
|
||||
rest, err := p.Parse(args)
|
||||
equal(t, nil, err)
|
||||
equal(t, []string{}, rest)
|
||||
equal(t, "are you?", p.Value("--msg"))
|
||||
equal(t, []string{"hello", "world", "how", "are you?"}, p.AllValues("--msg"))
|
||||
}
|
||||
|
||||
func TestArgsParser_Int(t *testing.T) {
|
||||
p := NewArgsParser()
|
||||
p.RegisterValue("--limit", "-L")
|
||||
p.RegisterValue("--depth", "-d")
|
||||
args := []string{"-L24", "-d", "-3"}
|
||||
rest, err := p.Parse(args)
|
||||
equal(t, nil, err)
|
||||
equal(t, []string{}, rest)
|
||||
equal(t, true, p.HasReceived("--limit"))
|
||||
equal(t, 24, p.Int("--limit"))
|
||||
equal(t, true, p.HasReceived("--depth"))
|
||||
equal(t, -3, p.Int("--depth"))
|
||||
}
|
Загрузка…
Ссылка в новой задаче