package mcfg import ( "context" "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 // 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) } 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 pOk bool pvStrVal string pvStrValOk bool ) for i, arg := range args { if pOk { 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 { pOk = true break } prefix := key + cliValSep if !strings.HasPrefix(arg, prefix) { continue } pOk = true pvStrVal = strings.TrimPrefix(arg, prefix) pvStrValOk = true break } if !pOk { if cli.TailCallback != nil { cli.TailCallback(args[i:]) return pvs, nil } ctx := mctx.Annotate(context.Background(), "param", arg) return nil, merr.New("unexpected config parameter", ctx) } } // pOk is always true at this point, and so p 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" } 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{} pOk = false pvStrVal = "" pvStrValOk = false } if pOk && !pvStrValOk { ctx := mctx.Annotate(p.Context, "param", key) return nil, merr.New("param expected a value", ctx) } 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") }