2018-01-11 22:19:25 +00:00
|
|
|
package mcfg
|
|
|
|
|
|
|
|
import (
|
2019-02-09 19:08:30 +00:00
|
|
|
"context"
|
2018-01-11 22:19:25 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"reflect"
|
|
|
|
"sort"
|
|
|
|
"strings"
|
2019-01-25 03:02:04 +00:00
|
|
|
|
2019-02-05 20:18:17 +00:00
|
|
|
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
2019-01-25 03:02:04 +00:00
|
|
|
"github.com/mediocregopher/mediocre-go-lib/merr"
|
2018-01-11 22:19:25 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// SourceCLI is a Source which will parse configuration from the CLI.
|
|
|
|
//
|
2019-01-08 19:21:55 +00:00
|
|
|
// Possible CLI options are generated by joining a Param's Path and Name with
|
|
|
|
// dashes. For example:
|
2018-01-11 22:19:25 +00:00
|
|
|
//
|
2019-01-08 19:21:55 +00:00
|
|
|
// ctx := mctx.New()
|
|
|
|
// ctx = mctx.ChildOf(ctx, "foo")
|
|
|
|
// ctx = mctx.ChildOf(ctx, "bar")
|
|
|
|
// addr := mcfg.String(ctx, "addr", "", "Some address")
|
2018-01-11 22:19:25 +00:00
|
|
|
// // the CLI option to fill addr will be "--foo-bar-addr"
|
|
|
|
//
|
|
|
|
// If the "-h" option is seen then a help page will be printed to
|
|
|
|
// stdout and the process will exit. Since all normally-defined parameters must
|
|
|
|
// being with double-dash ("--") they won't ever conflict with the help option.
|
|
|
|
//
|
2018-08-14 00:44:03 +00:00
|
|
|
// SourceCLI behaves a little differently with boolean parameters. Setting the
|
|
|
|
// value of a boolean parameter directly _must_ be done with an equals, for
|
|
|
|
// example: `--boolean-flag=1` or `--boolean-flag=false`. Using the
|
|
|
|
// space-separated format will not work. If a boolean has no equal-separated
|
|
|
|
// value it is assumed to be setting the value to `true`, as would be expected.
|
2018-01-11 22:19:25 +00:00
|
|
|
type SourceCLI struct {
|
|
|
|
Args []string // if nil then os.Args[1:] is used
|
|
|
|
|
|
|
|
DisableHelpPage bool
|
2019-04-03 03:21:16 +00:00
|
|
|
|
|
|
|
// Normally if any unexpected Arg value is encountered Parse will error out.
|
|
|
|
// If instead TailCallback is set then it will be called whenever the first
|
|
|
|
// unexpected Arg is encountered, and will not error out. TailCallback will
|
|
|
|
// be given a slice of Args starting at the first unexpected element.
|
|
|
|
TailCallback func([]string)
|
2018-01-11 22:19:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
cliKeyJoin = "-"
|
|
|
|
cliKeyPrefix = "--"
|
|
|
|
cliValSep = "="
|
|
|
|
cliHelpArg = "-h"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Parse implements the method for the Source interface
|
2019-01-08 19:21:55 +00:00
|
|
|
func (cli SourceCLI) Parse(params []Param) ([]ParamValue, error) {
|
2018-01-11 22:19:25 +00:00
|
|
|
args := cli.Args
|
|
|
|
if cli.Args == nil {
|
|
|
|
args = os.Args[1:]
|
|
|
|
}
|
|
|
|
|
2019-01-08 19:21:55 +00:00
|
|
|
pM, err := cli.cliParams(params)
|
2018-01-11 22:19:25 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
pvs := make([]ParamValue, 0, len(args))
|
|
|
|
var (
|
|
|
|
key string
|
2019-01-25 22:33:36 +00:00
|
|
|
p Param
|
2019-04-03 03:21:16 +00:00
|
|
|
pOk bool
|
2018-01-11 22:19:25 +00:00
|
|
|
pvStrVal string
|
|
|
|
pvStrValOk bool
|
|
|
|
)
|
2019-04-03 03:21:16 +00:00
|
|
|
for i, arg := range args {
|
|
|
|
if pOk {
|
2018-01-11 22:19:25 +00:00
|
|
|
pvStrVal = arg
|
|
|
|
pvStrValOk = true
|
|
|
|
} else if !cli.DisableHelpPage && arg == cliHelpArg {
|
2018-08-14 01:02:06 +00:00
|
|
|
cli.printHelp(os.Stdout, pM)
|
2018-01-11 22:19:25 +00:00
|
|
|
os.Stdout.Sync()
|
|
|
|
os.Exit(1)
|
|
|
|
} else {
|
2019-01-25 22:33:36 +00:00
|
|
|
for key, p = range pM {
|
2018-01-11 22:19:25 +00:00
|
|
|
if arg == key {
|
2019-04-03 03:21:16 +00:00
|
|
|
pOk = true
|
2018-01-11 22:19:25 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
prefix := key + cliValSep
|
|
|
|
if !strings.HasPrefix(arg, prefix) {
|
|
|
|
continue
|
|
|
|
}
|
2019-04-03 03:21:16 +00:00
|
|
|
pOk = true
|
2018-01-11 22:19:25 +00:00
|
|
|
pvStrVal = strings.TrimPrefix(arg, prefix)
|
|
|
|
pvStrValOk = true
|
|
|
|
break
|
|
|
|
}
|
2019-04-03 03:21:16 +00:00
|
|
|
if !pOk {
|
|
|
|
if cli.TailCallback != nil {
|
|
|
|
cli.TailCallback(args[i:])
|
|
|
|
return pvs, nil
|
|
|
|
}
|
2019-02-27 18:05:51 +00:00
|
|
|
ctx := mctx.Annotate(context.Background(), "param", arg)
|
|
|
|
return nil, merr.New("unexpected config parameter", ctx)
|
2018-01-11 22:19:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-03 03:21:16 +00:00
|
|
|
// pOk is always true at this point, and so p is filled in
|
2018-01-11 22:19:25 +00:00
|
|
|
|
2018-08-14 00:44:03 +00:00
|
|
|
// As a special case for CLI, if a boolean has no value set it means it
|
|
|
|
// is true.
|
2019-01-25 22:33:36 +00:00
|
|
|
if p.IsBool && !pvStrValOk {
|
2018-08-14 00:44:03 +00:00
|
|
|
pvStrVal = "true"
|
2018-01-11 22:19:25 +00:00
|
|
|
} else if !pvStrValOk {
|
|
|
|
// everything else should have a value. if pvStrVal isn't filled it
|
|
|
|
// means the next arg should be one. Continue the loop, it'll get
|
|
|
|
// filled with the next one (hopefully)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2019-01-25 22:33:36 +00:00
|
|
|
pvs = append(pvs, ParamValue{
|
|
|
|
Name: p.Name,
|
2019-02-05 20:18:17 +00:00
|
|
|
Path: mctx.Path(p.Context),
|
2019-01-25 22:33:36 +00:00
|
|
|
Value: p.fuzzyParse(pvStrVal),
|
|
|
|
})
|
2018-08-14 00:44:03 +00:00
|
|
|
|
2018-01-11 22:19:25 +00:00
|
|
|
key = ""
|
2019-01-25 22:33:36 +00:00
|
|
|
p = Param{}
|
2019-04-03 03:21:16 +00:00
|
|
|
pOk = false
|
2018-01-11 22:19:25 +00:00
|
|
|
pvStrVal = ""
|
|
|
|
pvStrValOk = false
|
|
|
|
}
|
2019-04-03 03:21:16 +00:00
|
|
|
if pOk && !pvStrValOk {
|
2019-02-27 18:05:51 +00:00
|
|
|
ctx := mctx.Annotate(p.Context, "param", key)
|
|
|
|
return nil, merr.New("param expected a value", ctx)
|
2018-01-11 22:19:25 +00:00
|
|
|
}
|
|
|
|
return pvs, nil
|
|
|
|
}
|
|
|
|
|
2019-01-08 19:21:55 +00:00
|
|
|
func (cli SourceCLI) cliParams(params []Param) (map[string]Param, error) {
|
2018-08-14 01:02:06 +00:00
|
|
|
m := map[string]Param{}
|
2019-01-08 19:21:55 +00:00
|
|
|
for _, p := range params {
|
2019-02-05 20:18:17 +00:00
|
|
|
key := strings.Join(append(mctx.Path(p.Context), p.Name), cliKeyJoin)
|
2018-08-14 01:02:06 +00:00
|
|
|
m[cliKeyPrefix+key] = p
|
2018-01-11 22:19:25 +00:00
|
|
|
}
|
|
|
|
return m, nil
|
|
|
|
}
|
|
|
|
|
2018-08-14 01:02:06 +00:00
|
|
|
func (cli SourceCLI) printHelp(w io.Writer, pM map[string]Param) {
|
|
|
|
type pEntry struct {
|
2018-01-11 22:19:25 +00:00
|
|
|
arg string
|
2018-08-14 01:02:06 +00:00
|
|
|
Param
|
2018-01-11 22:19:25 +00:00
|
|
|
}
|
|
|
|
|
2018-08-14 01:02:06 +00:00
|
|
|
pA := make([]pEntry, 0, len(pM))
|
|
|
|
for arg, p := range pM {
|
|
|
|
pA = append(pA, pEntry{arg: arg, Param: p})
|
2018-01-11 22:19:25 +00:00
|
|
|
}
|
|
|
|
|
2018-08-14 01:02:06 +00:00
|
|
|
sort.Slice(pA, func(i, j int) bool {
|
2019-01-25 03:02:04 +00:00
|
|
|
if pA[i].Required != pA[j].Required {
|
|
|
|
return pA[i].Required
|
|
|
|
}
|
2018-08-14 01:02:06 +00:00
|
|
|
return pA[i].arg < pA[j].arg
|
2018-01-11 22:19:25 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
fmtDefaultVal := func(ptr interface{}) string {
|
|
|
|
if ptr == nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
val := reflect.Indirect(reflect.ValueOf(ptr))
|
|
|
|
zero := reflect.Zero(val.Type())
|
|
|
|
if reflect.DeepEqual(val.Interface(), zero.Interface()) {
|
|
|
|
return ""
|
|
|
|
} else if val.Type().Kind() == reflect.String {
|
|
|
|
return fmt.Sprintf("%q", val.Interface())
|
|
|
|
}
|
|
|
|
return fmt.Sprint(val.Interface())
|
|
|
|
}
|
|
|
|
|
2018-08-14 01:02:06 +00:00
|
|
|
for _, p := range pA {
|
|
|
|
fmt.Fprintf(w, "\n%s", p.arg)
|
|
|
|
if p.IsBool {
|
2018-01-11 22:19:25 +00:00
|
|
|
fmt.Fprintf(w, " (Flag)")
|
2019-01-25 03:02:04 +00:00
|
|
|
} else if p.Required {
|
|
|
|
fmt.Fprintf(w, " (Required)")
|
2018-08-14 01:02:06 +00:00
|
|
|
} else if defVal := fmtDefaultVal(p.Into); defVal != "" {
|
2018-01-11 22:19:25 +00:00
|
|
|
fmt.Fprintf(w, " (Default: %s)", defVal)
|
|
|
|
}
|
|
|
|
fmt.Fprintf(w, "\n")
|
2019-02-03 00:35:30 +00:00
|
|
|
if usage := p.Usage; usage != "" {
|
|
|
|
// make all usages end with a period, because I say so
|
|
|
|
usage = strings.TrimSpace(usage)
|
|
|
|
if !strings.HasSuffix(usage, ".") {
|
|
|
|
usage += "."
|
|
|
|
}
|
|
|
|
fmt.Fprintln(w, "\t"+usage)
|
2018-01-11 22:19:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
fmt.Fprintf(w, "\n")
|
|
|
|
}
|