mediocre-go-lib/mcfg/cli.go
Brian Picciano 4b446a0efc mctx: refactor so that contexts no longer carry mutable data
This change required refactoring nearly every package in this project,
but it does a lot to simplify mctx and make other code using it easier
to think about.

Other code, such as mlog and mcfg, had to be slightly modified for this
change to work as well.
2019-02-07 19:42:12 -05:00

190 lines
4.5 KiB
Go

package mcfg
import (
"fmt"
"io"
"os"
"reflect"
"sort"
"strings"
"github.com/mediocregopher/mediocre-go-lib/mctx"
"github.com/mediocregopher/mediocre-go-lib/merr"
)
// SourceCLI is a Source which will parse configuration from the CLI.
//
// Possible CLI options are generated by joining a Param's Path and Name with
// dashes. For example:
//
// ctx := mctx.New()
// ctx = mctx.ChildOf(ctx, "foo")
// ctx = mctx.ChildOf(ctx, "bar")
// addr := mcfg.String(ctx, "addr", "", "Some address")
// // 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.
//
// 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.
type SourceCLI struct {
Args []string // if nil then os.Args[1:] is used
DisableHelpPage bool
}
const (
cliKeyJoin = "-"
cliKeyPrefix = "--"
cliValSep = "="
cliHelpArg = "-h"
)
// Parse implements the method for the Source interface
func (cli SourceCLI) Parse(params []Param) ([]ParamValue, error) {
args := cli.Args
if cli.Args == nil {
args = os.Args[1:]
}
pM, err := cli.cliParams(params)
if err != nil {
return nil, err
}
pvs := make([]ParamValue, 0, len(args))
var (
key string
p Param
pvOk bool
pvStrVal string
pvStrValOk bool
)
for _, arg := range args {
if pvOk {
pvStrVal = arg
pvStrValOk = true
} else if !cli.DisableHelpPage && arg == cliHelpArg {
cli.printHelp(os.Stdout, pM)
os.Stdout.Sync()
os.Exit(1)
} else {
for key, p = range pM {
if arg == key {
pvOk = true
break
}
prefix := key + cliValSep
if !strings.HasPrefix(arg, prefix) {
continue
}
pvOk = true
pvStrVal = strings.TrimPrefix(arg, prefix)
pvStrValOk = true
break
}
if !pvOk {
return nil, merr.New("unexpected config parameter", "param", arg)
}
}
// pvOk is always true at this point, and so pv is filled in
// As a special case for CLI, if a boolean has no value set it means it
// is true.
if p.IsBool && !pvStrValOk {
pvStrVal = "true"
pvStrValOk = true
} 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
}
pvs = append(pvs, ParamValue{
Name: p.Name,
Path: mctx.Path(p.Context),
Value: p.fuzzyParse(pvStrVal),
})
key = ""
p = Param{}
pvOk = false
pvStrVal = ""
pvStrValOk = false
}
if pvOk && !pvStrValOk {
return nil, merr.New("param expected a value", "param", key)
}
return pvs, nil
}
func (cli SourceCLI) cliParams(params []Param) (map[string]Param, error) {
m := map[string]Param{}
for _, p := range params {
key := strings.Join(append(mctx.Path(p.Context), p.Name), cliKeyJoin)
m[cliKeyPrefix+key] = p
}
return m, nil
}
func (cli SourceCLI) printHelp(w io.Writer, pM map[string]Param) {
type pEntry struct {
arg string
Param
}
pA := make([]pEntry, 0, len(pM))
for arg, p := range pM {
pA = append(pA, pEntry{arg: arg, Param: p})
}
sort.Slice(pA, func(i, j int) bool {
if pA[i].Required != pA[j].Required {
return pA[i].Required
}
return pA[i].arg < pA[j].arg
})
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())
}
for _, p := range pA {
fmt.Fprintf(w, "\n%s", p.arg)
if p.IsBool {
fmt.Fprintf(w, " (Flag)")
} else if p.Required {
fmt.Fprintf(w, " (Required)")
} else if defVal := fmtDefaultVal(p.Into); defVal != "" {
fmt.Fprintf(w, " (Default: %s)", defVal)
}
fmt.Fprintf(w, "\n")
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)
}
}
fmt.Fprintf(w, "\n")
}