208 lines
5.3 KiB
Go
208 lines
5.3 KiB
Go
package multiconfig
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/fatih/camelcase"
|
|
"github.com/fatih/structs"
|
|
)
|
|
|
|
// FlagLoader satisfies the loader interface. It creates on the fly flags based
|
|
// on the field names and parses them to load into the given pointer of struct
|
|
// s.
|
|
type FlagLoader struct {
|
|
// Prefix prepends the prefix to each flag name i.e:
|
|
// --foo is converted to --prefix-foo.
|
|
// --foo-bar is converted to --prefix-foo-bar.
|
|
Prefix string
|
|
|
|
// Flatten doesn't add prefixes for nested structs. So previously if we had
|
|
// a nested struct `type T struct{Name struct{ ...}}`, this would generate
|
|
// --name-foo, --name-bar, etc. When Flatten is enabled, the flags will be
|
|
// flattend to the form: --foo, --bar, etc.. Panics if the nested structs
|
|
// has a duplicate field name in the root level of the struct (outer
|
|
// struct). Use this option only if you know what you do.
|
|
Flatten bool
|
|
|
|
// CamelCase adds a separator for field names in camelcase form. A
|
|
// fieldname of "AccessKey" would generate a flag name "--accesskey". If
|
|
// CamelCase is enabled, the flag name will be generated in the form of
|
|
// "--access-key"
|
|
CamelCase bool
|
|
|
|
// EnvPrefix is just a placeholder to print the correct usages when an
|
|
// EnvLoader is used
|
|
EnvPrefix string
|
|
|
|
// ErrorHandling is used to configure error handling used by
|
|
// *flag.FlagSet.
|
|
//
|
|
// By default it's flag.ContinueOnError.
|
|
ErrorHandling flag.ErrorHandling
|
|
|
|
// Args defines a custom argument list. If nil, os.Args[1:] is used.
|
|
Args []string
|
|
|
|
// FlagUsageFunc an optional function that is called to set a flag.Usage value
|
|
// The input is the raw flag name, and the output should be a string
|
|
// that will used in passed into the flag for Usage.
|
|
FlagUsageFunc func(name string) string
|
|
|
|
// only exists for testing. This is the raw flagset that is to parse
|
|
flagSet *flag.FlagSet
|
|
}
|
|
|
|
// Load loads the source into the config defined by struct s
|
|
func (f *FlagLoader) Load(s interface{}) error {
|
|
strct := structs.New(s)
|
|
structName := strct.Name()
|
|
|
|
flagSet := flag.NewFlagSet(structName, f.ErrorHandling)
|
|
f.flagSet = flagSet
|
|
|
|
for _, field := range strct.Fields() {
|
|
f.processField(field.Name(), field)
|
|
}
|
|
|
|
flagSet.Usage = func() {
|
|
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
|
|
flagSet.PrintDefaults()
|
|
fmt.Fprintf(os.Stderr, "\nGenerated environment variables:\n")
|
|
e := &EnvironmentLoader{
|
|
Prefix: f.EnvPrefix,
|
|
CamelCase: f.CamelCase,
|
|
}
|
|
e.PrintEnvs(s)
|
|
fmt.Println("")
|
|
}
|
|
|
|
args := filterArgs(os.Args[1:])
|
|
if f.Args != nil {
|
|
args = f.Args
|
|
}
|
|
|
|
return flagSet.Parse(args)
|
|
}
|
|
|
|
func filterArgs(args []string) []string {
|
|
r := []string{}
|
|
for i := 0; i < len(args); i++ {
|
|
if strings.Index(args[i], "test.") >= 0 {
|
|
if i + 1 < len(args) && strings.Index(args[i + 1], "-") == -1 {
|
|
i++
|
|
}
|
|
i++
|
|
} else {
|
|
r = append(r, args[i])
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
|
|
// processField generates a flag based on the given field and fieldName. If a
|
|
// nested struct is detected, a flag for each field of that nested struct is
|
|
// generated too.
|
|
func (f *FlagLoader) processField(fieldName string, field *structs.Field) error {
|
|
if f.CamelCase {
|
|
fieldName = strings.Join(camelcase.Split(fieldName), "-")
|
|
fieldName = strings.Replace(fieldName, "---", "-", -1)
|
|
}
|
|
|
|
switch field.Kind() {
|
|
case reflect.Struct:
|
|
for _, ff := range field.Fields() {
|
|
flagName := field.Name() + "-" + ff.Name()
|
|
|
|
if f.Flatten {
|
|
// first check if it's set or not, because if we have duplicate
|
|
// we don't want to break the flag. Panic by giving a readable
|
|
// output
|
|
f.flagSet.VisitAll(func(fl *flag.Flag) {
|
|
if strings.ToLower(ff.Name()) == fl.Name {
|
|
// already defined
|
|
panic(fmt.Sprintf("flag '%s' is already defined in outer struct", fl.Name))
|
|
}
|
|
})
|
|
|
|
flagName = ff.Name()
|
|
}
|
|
|
|
if err := f.processField(flagName, ff); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
default:
|
|
// Add custom prefix to the flag if it's set
|
|
if f.Prefix != "" {
|
|
fieldName = f.Prefix + "-" + fieldName
|
|
}
|
|
|
|
// we only can get the value from expored fields, unexported fields panics
|
|
if field.IsExported() {
|
|
f.flagSet.Var(newFieldValue(field), flagName(fieldName), f.flagUsage(fieldName, field))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *FlagLoader) flagUsage(fieldName string, field *structs.Field) string {
|
|
if f.FlagUsageFunc != nil {
|
|
return f.FlagUsageFunc(fieldName)
|
|
}
|
|
|
|
usage := field.Tag("flagUsage")
|
|
if usage != "" {
|
|
return usage
|
|
}
|
|
|
|
return fmt.Sprintf("Change value of %s.", fieldName)
|
|
}
|
|
|
|
// fieldValue satisfies the flag.Value and flag.Getter interfaces
|
|
type fieldValue struct {
|
|
field *structs.Field
|
|
}
|
|
|
|
func newFieldValue(f *structs.Field) *fieldValue {
|
|
return &fieldValue{
|
|
field: f,
|
|
}
|
|
}
|
|
|
|
func (f *fieldValue) Set(val string) error {
|
|
return fieldSet(f.field, val)
|
|
}
|
|
|
|
func (f *fieldValue) String() string {
|
|
if f.IsZero() {
|
|
return ""
|
|
}
|
|
|
|
return fmt.Sprintf("%v", f.field.Value())
|
|
}
|
|
|
|
func (f *fieldValue) Get() interface{} {
|
|
if f.IsZero() {
|
|
return nil
|
|
}
|
|
|
|
return f.field.Value()
|
|
}
|
|
|
|
func (f *fieldValue) IsZero() bool {
|
|
return f.field == nil
|
|
}
|
|
|
|
// This is an unexported interface, be careful about it.
|
|
// https://code.google.com/p/go/source/browse/src/pkg/flag/flag.go?name=release#101
|
|
func (f *fieldValue) IsBoolFlag() bool {
|
|
return f.field.Kind() == reflect.Bool
|
|
}
|
|
|
|
func flagName(name string) string { return strings.ToLower(name) }
|