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:
Mislav Marohnić 2019-01-12 03:13:39 +01:00
Родитель ddf0d82510
Коммит 9eae131eef
2 изменённых файлов: 362 добавлений и 0 удалений

190
utils/args_parser.go Normal file
Просмотреть файл

@ -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),
}
}

172
utils/args_parser_test.go Normal file
Просмотреть файл

@ -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"))
}