structured flags as alternative to config #76

This commit is contained in:
Umputun
2018-06-10 02:27:37 -05:00
parent 864cdf3a1d
commit c3cd7877ef
19 changed files with 181 additions and 82 deletions
+9 -3
View File
@@ -1,8 +1,15 @@
language: go
os:
- linux
- osx
go:
- 1.6.x
- 1.x
- 1.7.x
- 1.8.x
- 1.9.x
- 1.10.x
install:
# go-flags
@@ -10,8 +17,7 @@ install:
- go build -v ./...
# linting
- go get github.com/golang/lint
- go install github.com/golang/lint/golint
- go get github.com/golang/lint/golint
# code coverage
- go get golang.org/x/tools/cmd/cover
-1
View File
@@ -110,7 +110,6 @@ args, err := flags.ParseArgs(&opts, args)
if err != nil {
panic(err)
os.Exit(1)
}
fmt.Printf("Verbosity: %v\n", opts.Verbose)
+12 -2
View File
@@ -5,7 +5,6 @@ import (
"sort"
"strconv"
"strings"
"unsafe"
)
// Command represents an application command. Commands can be added to the
@@ -229,7 +228,17 @@ func (c *Command) scanSubcommandHandler(parentg *Group) scanHandler {
subcommand := mtag.Get("command")
if len(subcommand) != 0 {
ptrval := reflect.NewAt(realval.Type(), unsafe.Pointer(realval.UnsafeAddr()))
var ptrval reflect.Value
if realval.Kind() == reflect.Ptr {
ptrval = realval
if ptrval.IsNil() {
ptrval.Set(reflect.New(ptrval.Type().Elem()))
}
} else {
ptrval = realval.Addr()
}
shortDescription := mtag.Get("description")
longDescription := mtag.Get("long-description")
@@ -237,6 +246,7 @@ func (c *Command) scanSubcommandHandler(parentg *Group) scanHandler {
aliases := mtag.GetMany("alias")
subc, err := c.AddCommand(subcommand, shortDescription, longDescription, ptrval.Interface())
if err != nil {
return true, err
}
+4
View File
@@ -125,6 +125,10 @@ The following is a list of tags for struct fields supported by go-flags:
gets prepended to every option's long name and
subgroup's namespace of this group, separated by
the parser's namespace delimiter (optional)
env-namespace: when specified on a group struct field, the env-namespace
gets prepended to every option's env key and
subgroup's env-namespace of this group, separated by
the parser's env-namespace delimiter (optional)
command: when specified on a struct field, makes the struct
field a (sub)command with the given name (optional)
subcommands-optional: when specified on a command struct field, makes
+17 -2
View File
@@ -9,7 +9,6 @@ import (
"reflect"
"strings"
"unicode/utf8"
"unsafe"
)
// ErrNotPointerToStruct indicates that a provided data container is not
@@ -35,6 +34,9 @@ type Group struct {
// The namespace of the group
Namespace string
// The environment namespace of the group
EnvNamespace string
// If true, the group is not displayed in the help or man page
Hidden bool
@@ -338,15 +340,28 @@ func (g *Group) scanSubGroupHandler(realval reflect.Value, sfield *reflect.Struc
subgroup := mtag.Get("group")
if len(subgroup) != 0 {
ptrval := reflect.NewAt(realval.Type(), unsafe.Pointer(realval.UnsafeAddr()))
var ptrval reflect.Value
if realval.Kind() == reflect.Ptr {
ptrval = realval
if ptrval.IsNil() {
ptrval.Set(reflect.New(ptrval.Type()))
}
} else {
ptrval = realval.Addr()
}
description := mtag.Get("description")
group, err := g.AddGroup(subgroup, description, ptrval.Interface())
if err != nil {
return true, err
}
group.Namespace = mtag.Get("namespace")
group.EnvNamespace = mtag.Get("env-namespace")
group.Hidden = mtag.Get("hidden") != ""
return true, nil
+3 -3
View File
@@ -225,12 +225,12 @@ func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alig
}
var envDef string
if option.EnvDefaultKey != "" {
if option.EnvKeyWithNamespace() != "" {
var envPrintable string
if runtime.GOOS == "windows" {
envPrintable = "%" + option.EnvDefaultKey + "%"
envPrintable = "%" + option.EnvKeyWithNamespace() + "%"
} else {
envPrintable = "$" + option.EnvDefaultKey
envPrintable = "$" + option.EnvKeyWithNamespace()
}
envDef = fmt.Sprintf(" [%s]", envPrintable)
}
+3 -3
View File
@@ -83,11 +83,11 @@ func writeManPageOptions(wr io.Writer, grp *Group) {
if len(opt.Default) != 0 {
fmt.Fprintf(wr, " <default: \\fI%s\\fR>", manQuote(strings.Join(quoteV(opt.Default), ", ")))
} else if len(opt.EnvDefaultKey) != 0 {
} else if len(opt.EnvKeyWithNamespace()) != 0 {
if runtime.GOOS == "windows" {
fmt.Fprintf(wr, " <default: \\fI%%%s%%\\fR>", manQuote(opt.EnvDefaultKey))
fmt.Fprintf(wr, " <default: \\fI%%%s%%\\fR>", manQuote(opt.EnvKeyWithNamespace()))
} else {
fmt.Fprintf(wr, " <default: \\fI$%s\\fR>", manQuote(opt.EnvDefaultKey))
fmt.Fprintf(wr, " <default: \\fI$%s\\fR>", manQuote(opt.EnvKeyWithNamespace()))
}
}
+55 -7
View File
@@ -3,9 +3,9 @@ package flags
import (
"bytes"
"fmt"
"os"
"reflect"
"strings"
"syscall"
"unicode/utf8"
)
@@ -139,6 +139,57 @@ func (option *Option) LongNameWithNamespace() string {
return longName
}
// EnvKeyWithNamespace returns the option's env key with the group namespaces
// prepended by walking up the option's group tree. Namespaces and the env key
// itself are separated by the parser's namespace delimiter. If the env key is
// empty an empty string is returned.
func (option *Option) EnvKeyWithNamespace() string {
if len(option.EnvDefaultKey) == 0 {
return ""
}
// fetch the namespace delimiter from the parser which is always at the
// end of the group hierarchy
namespaceDelimiter := ""
g := option.group
for {
if p, ok := g.parent.(*Parser); ok {
namespaceDelimiter = p.EnvNamespaceDelimiter
break
}
switch i := g.parent.(type) {
case *Command:
g = i.Group
case *Group:
g = i
}
}
// concatenate long name with namespace
key := option.EnvDefaultKey
g = option.group
for g != nil {
if g.EnvNamespace != "" {
key = g.EnvNamespace + namespaceDelimiter + key
}
switch i := g.parent.(type) {
case *Command:
g = i.Group
case *Group:
g = i
case *Parser:
g = nil
}
}
return key
}
// String converts an option to a human friendly readable string describing the
// option.
func (option *Option) String() string {
@@ -260,13 +311,10 @@ func (option *Option) empty() {
func (option *Option) clearDefault() {
usedDefault := option.Default
if envKey := option.EnvDefaultKey; envKey != "" {
// os.Getenv() makes no distinction between undefined and
// empty values, so we use syscall.Getenv()
if value, ok := syscall.Getenv(envKey); ok {
if envKey := option.EnvKeyWithNamespace(); envKey != "" {
if value, ok := os.LookupEnv(envKey); ok {
if option.EnvDefaultDelim != "" {
usedDefault = strings.Split(value,
option.EnvDefaultDelim)
usedDefault = strings.Split(value, option.EnvDefaultDelim)
} else {
usedDefault = []string{value}
}
+7 -3
View File
@@ -29,6 +29,9 @@ type Parser struct {
// NamespaceDelimiter separates group namespaces and option long names
NamespaceDelimiter string
// EnvNamespaceDelimiter separates group env namespaces and env keys
EnvNamespaceDelimiter string
// UnknownOptionsHandler is a function which gets called when the parser
// encounters an unknown option. The function receives the unknown option
// name, a SplitArgument which specifies its value if set with an argument
@@ -170,9 +173,10 @@ func NewParser(data interface{}, options Options) *Parser {
// be added to this parser by using AddGroup and AddCommand.
func NewNamedParser(appname string, options Options) *Parser {
p := &Parser{
Command: newCommand(appname, "", "", nil),
Options: options,
NamespaceDelimiter: ".",
Command: newCommand(appname, "", "", nil),
Options: options,
NamespaceDelimiter: ".",
EnvNamespaceDelimiter: "_",
}
p.Command.parent = p
+1 -1
View File
@@ -1,4 +1,4 @@
// +build !windows,!plan9,!solaris
// +build !windows,!plan9,!solaris,!appengine
package flags
+1 -1
View File
@@ -1,4 +1,4 @@
// +build windows plan9 solaris
// +build windows plan9 solaris appengine
package flags