gregoryv/cmdline

View on GitHub
option.go

Summary

Maintainability
B
4 hrs
Test Coverage
package cmdline

import (
    "fmt"
    "net/url"
    "os"
    "strconv"
    "strings"
    "time"
)

// Option defines a command line option, ie. --username
type Option struct {
    args         []string // without command
    names        string
    defaultValue string
    enumerated   []string
    quoteValue   bool // in usage output
    doc          []string
    err          error

    argIndex int // position in args for e.g. --username
    valIndex int // position for option value, same as argIndex if e.g. --i=1

    envMap func(string) string

    // usage does not show value
    hidden bool
}

// NewOption returns an option defined by a comma separated list of
// names and arguments to match against. Usually you would call
// Parser.Option(names) over this.
func NewOption(names string, args ...string) *Option {
    return &Option{names: names, args: args, argIndex: -1, valIndex: -1}
}

func (opt *Option) setDefault(def interface{}) {
    opt.defaultValue = fmt.Sprintf("%v", def)
}

// Doc sets the documentation lines for this option.
func (opt *Option) Doc(lines ...string) {
    opt.doc = lines
}

// Int same as IntOpt but does not return the Option.
func (opt *Option) Int(def int) int {
    v, _ := opt.IntOpt(def)
    return v
}

// IntOpt returns int value from the arguments or the given default value.
func (opt *Option) IntOpt(def int) (int, *Option) {
    opt.setDefault(def)
    v, err := opt.stringArg()
    if err != nil {
        opt.fail()
        return def, opt
    }
    if v == "" {
        return def, opt
    }
    iv, err := strconv.Atoi(v)
    if err != nil {
        opt.fail()
    }
    return iv, opt
}

// Uint8 same as uint8(Uint(...))
func (opt *Option) Uint8(def uint8) uint8 {
    return uint8(opt.Uint(uint64(def)))
}

// Uint16 same as uint16(Uint(...))
func (opt *Option) Uint16(def uint16) uint16 {
    return uint16(opt.Uint(uint64(def)))
}

// Uint32 same as uint32(Uint(...))
func (opt *Option) Uint32(def uint32) uint32 {
    return uint32(opt.Uint(uint64(def)))
}

// Uint same as UintOpt but does not return the Option
func (opt *Option) Uint(def uint64) uint64 {
    v, _ := opt.UintOpt(def)
    return v
}

// UintOpt returns an unsigned int option
func (opt *Option) UintOpt(def uint64) (uint64, *Option) {
    opt.setDefault(def)
    v, err := opt.stringArg()
    if err != nil {
        opt.fail()
        return def, opt
    }
    if v == "" {
        return def, opt
    }
    iv, err := strconv.ParseUint(v, 0, 64)
    if err != nil {
        opt.fail()
    }
    return iv, opt

}

func (opt *Option) Duration(def string) time.Duration {
    u, _ := opt.DurationOpt(def)
    return u
}

func (opt *Option) DurationOpt(def string) (time.Duration, *Option) {
    opt.setDefault(def)
    defDur, err := time.ParseDuration(def)
    if err != nil {
        opt.fail()
        opt.err = err
        return 0, opt
    }
    v, err := opt.stringArg()
    if err != nil {
        opt.fail()
        return defDur, opt
    }
    if v == "" {
        return defDur, opt
    }
    dur, err := time.ParseDuration(v)
    if err != nil {
        opt.fail()
        opt.err = err
        return defDur, opt
    }
    return dur, opt
}

func (opt *Option) Url(def string) *url.URL {
    u, _ := opt.UrlOpt(def)
    return u
}

func (opt *Option) UrlOpt(def string) (*url.URL, *Option) {
    opt.setDefault(def)
    defUrl, err := url.Parse(def)
    if err != nil {
        opt.fail()
        opt.err = err
        return nil, opt
    }
    v, err := opt.stringArg()
    if err != nil {
        opt.fail()
        return defUrl, opt
    }
    if v == "" {
        return defUrl, opt
    }
    u, err := url.Parse(v)
    if err != nil {
        opt.fail()
        opt.err = err
        return defUrl, opt
    }
    return u, opt
}

// Enum same as EnumOpt but does not return the Option
func (opt *Option) Enum(def string, possible ...string) string {
    val, _ := opt.EnumOpt(def, possible...)
    return val
}

// Enum returns an enumerated string. It's ok to only have one.
func (opt *Option) EnumOpt(def string, possible ...string) (string, *Option) {
    if len(possible) == 0 {
        possible = []string{def}
    }
    val, opt := opt.StringOpt(def)

    if val != def {
        index := make(map[string]interface{})
        for _, e := range possible {
            index[e] = nil
        }
        if _, found := index[val]; !found {
            opt.err = fmt.Errorf("incorrect %s %q", opt.names, val)
        }
    }
    opt.enumerated = possible
    return val, opt
}

// String same as StringOpt but does not return the Option.
func (opt *Option) String(def string) string {
    val, _ := opt.StringOpt(def)
    return val
}

// StringOpt returns string value from the arguments or the given default value.
func (opt *Option) StringOpt(def string) (string, *Option) {
    opt.setDefault(def)
    opt.quoteValue = true
    // todo , have to distinquish between option not found and value not found
    v, err := opt.stringArg()
    if err != nil {
        opt.fail()
        return def, opt
    }
    if isOption(v) {
        opt.fail()
    }
    if v == "" {
        return def, opt
    }
    return unquote(v), opt
}

func unquote(v string) string {
    if len(v) < 2 {
        return v
    }

    first := v[0]
    last := v[len(v)-1]
    if isQuoteChar(first) && first == last {
        return v[1 : len(v)-1]
    }
    return v
}

func isQuoteChar(v byte) bool {
    switch v {
    case '`', '"', '\'':
        return true
    default:
        return false
    }
}

func (opt *Option) stringArg() (string, error) {
    i, found := opt.find()
    if found {
        arg := opt.args[i]
        opt.valIndex = i
        // NamedArg is -i=value
        eqIndex := strings.Index(arg, "=")
        if eqIndex > 0 {
            return arg[eqIndex+1:], nil
        }
        isLast := len(opt.args)-1 == i
        if isLast {
            opt.fail()
            return "", fmt.Errorf("missing value")
        }
        opt.valIndex = i + 1
        // NamedArg is -i
        return opt.args[i+1], nil
    }
    return opt.envValue(), nil
}

// If last element in option names starts with $ expand it
func (opt *Option) envValue() string {
    names := opt.argNames()
    env := names[len(names)-1] // last element
    if env[0] != '$' {
        return opt.defaultValue
    }
    return os.Expand(env, opt.envMap)
}

func (opt *Option) argNames() []string {
    return strings.Split(strings.ReplaceAll(opt.names, " ", ""), ",")
}

func (opt *Option) match(arg string) bool {
    if !isOption(arg) {
        return false
    }
    names := opt.argNames()
    argName, _ := nameAndValue(arg)
    for _, name := range names {
        if name == argName {
            return true
        }
    }
    return false
}

func nameAndValue(arg string) (string, string) {
    parts := strings.Split(arg, "=")
    if len(parts) > 1 {
        return parts[0], parts[1]
    }
    return parts[0], ""
}

func isOption(arg string) bool {
    return len(arg) > 0 && arg[0] == '-'
}

// Bool returns bool value from the arguments or the given default value.
func (opt *Option) Bool(def bool) bool {
    if def == true {
        opt.setDefault("true")
    } else {
        opt.setDefault("false")
    }

    v := opt.boolArg()
    return v
}

// BoolOpt returns bool value from the arguments.
// The Option is returned for more configuration.
func (opt *Option) BoolOpt() (bool, *Option) {
    opt.setDefault("")
    v := opt.boolArg()
    return v, opt
}

func (opt *Option) boolArg() bool {
    value := opt.envValue()

    i, found := opt.find()
    if found {
        // also check if any value is given
        val, isOption := opt.get(i + 1)
        if isOption || val == "" {
            value = "true"
        } else {
            value = val
        }
    }

    v, err := ParseBool(value)
    if err != nil {
        opt.err = fmt.Errorf("Invalid bool: %w", err)
    }
    return v
}

// find returns the index of the given option and sets internal arg.Index
// returns 0, false if not found
func (opt *Option) find() (i int, found bool) {
    for i, arg := range opt.args {
        if opt.match(arg) {
            opt.argIndex = i
            return i, true
        }
    }
    return 0, false
}

// get returns the argument and true if it starts with '-'
func (opt *Option) get(i int) (string, bool) {
    if i >= len(opt.args) {
        return "", false
    }
    next := opt.args[i]
    if len(next) == 0 {
        return "", false
    }
    isOption := next[0:1] == "-"
    return next, isOption
}

// ParseBool returns true if the string evaluates to a true
// expression. See example for possible values.
func ParseBool(v string) (bool, error) {
    switch v {
    case "1", "y", "yes", "Yes", "YES", "true", "True", "TRUE":
        return true, nil
    case "", "0", "n", "no", "No", "NO", "false", "False", "FALSE":
        return false, nil
    }
    return false, fmt.Errorf("parse bool %q", v)
}

func (opt *Option) fail() {
    opt.err = fmt.Errorf("Invalid option: %s", opt.names)
}

// Float64 returns float64
// value from the arguments or the given default value.
func (opt *Option) Float64(def float64) float64 {
    v, _ := opt.Float64Opt(def)
    return v
}

// Float64Opt returns float64 value from the arguments or the given
// default value.
func (opt *Option) Float64Opt(def float64) (float64, *Option) {
    opt.setDefault(def)
    v, err := opt.stringArg()
    if err != nil {
        opt.fail()
        return def, opt
    }
    if v == "" {
        return def, opt
    }
    iv, err := strconv.ParseFloat(v, 64)
    if err != nil {
        opt.fail()
    }
    return iv, opt
}