option.go 5.83 KB
Newer Older
Steven Allen's avatar
Steven Allen committed
1
package cmds
2 3 4 5

import (
	"fmt"
	"reflect"
6
	"strconv"
7 8 9 10 11 12 13 14 15
	"strings"
)

// Types of Command options
const (
	Invalid = reflect.Invalid
	Bool    = reflect.Bool
	Int     = reflect.Int
	Uint    = reflect.Uint
Overbool's avatar
Overbool committed
16 17
	Int64   = reflect.Int64
	Uint64  = reflect.Uint64
18 19
	Float   = reflect.Float64
	String  = reflect.String
20
	Strings = reflect.Array
21 22 23 24 25 26
)

type OptMap map[string]interface{}

// Option is used to specify a field that will be provided by a consumer
type Option interface {
27 28 29
	Name() string    // the main name of the option
	Names() []string // a list of unique names matched with user-provided flags

30 31 32 33 34
	Type() reflect.Kind  // value must be this type
	Description() string // a short string that describes this option

	WithDefault(interface{}) Option // sets the default value of the option
	Default() interface{}
keks's avatar
keks committed
35

36
	Parse(str string) (interface{}, error)
37 38 39 40 41 42 43
}

type option struct {
	names       []string
	kind        reflect.Kind
	description string
	defaultVal  interface{}
44
}
keks's avatar
keks committed
45

46 47
func (o *option) Name() string {
	return o.names[0]
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
}

func (o *option) Names() []string {
	return o.names
}

func (o *option) Type() reflect.Kind {
	return o.kind
}

func (o *option) Description() string {
	if len(o.description) == 0 {
		return ""
	}
	if !strings.HasSuffix(o.description, ".") {
		o.description += "."
	}
	if o.defaultVal != nil {
		if strings.Contains(o.description, "<<default>>") {
			return strings.Replace(o.description, "<<default>>",
				fmt.Sprintf("Default: %v.", o.defaultVal), -1)
		} else {
			return fmt.Sprintf("%s Default: %v.", o.description, o.defaultVal)
		}
	}
	return o.description
}

76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
type converter func(string) (interface{}, error)

var converters = map[reflect.Kind]converter{
	Bool: func(v string) (interface{}, error) {
		if v == "" {
			return true, nil
		}
		v = strings.ToLower(v)

		return strconv.ParseBool(v)
	},
	Int: func(v string) (interface{}, error) {
		val, err := strconv.ParseInt(v, 0, 32)
		if err != nil {
			return nil, err
		}
		return int(val), err
	},
	Uint: func(v string) (interface{}, error) {
		val, err := strconv.ParseUint(v, 0, 32)
		if err != nil {
			return nil, err
		}
Dirk McCormick's avatar
Dirk McCormick committed
99
		return uint(val), err
100
	},
Overbool's avatar
Overbool committed
101 102 103 104 105 106 107 108 109 110 111 112 113 114
	Int64: func(v string) (interface{}, error) {
		val, err := strconv.ParseInt(v, 0, 64)
		if err != nil {
			return nil, err
		}
		return val, err
	},
	Uint64: func(v string) (interface{}, error) {
		val, err := strconv.ParseUint(v, 0, 64)
		if err != nil {
			return nil, err
		}
		return val, err
	},
115 116 117 118 119 120
	Float: func(v string) (interface{}, error) {
		return strconv.ParseFloat(v, 64)
	},
	String: func(v string) (interface{}, error) {
		return v, nil
	},
121 122 123
	Strings: func(v string) (interface{}, error) {
		return v, nil
	},
124 125 126 127 128
}

func (o *option) Parse(v string) (interface{}, error) {
	conv, ok := converters[o.Type()]
	if !ok {
Steven Allen's avatar
Steven Allen committed
129
		return nil, fmt.Errorf("option %q takes %s arguments, but was passed %q", o.Name(), o.Type(), v)
130 131 132 133 134
	}

	return conv(v)
}

135 136
// constructor helper functions
func NewOption(kind reflect.Kind, names ...string) Option {
keks's avatar
keks committed
137
	var desc string
138

keks's avatar
keks committed
139 140 141 142
	if len(names) >= 2 {
		desc = names[len(names)-1]
		names = names[:len(names)-1]
	}
143 144 145 146 147 148 149 150

	return &option{
		names:       names,
		kind:        kind,
		description: desc,
	}
}

151
func (o *option) WithDefault(v interface{}) Option {
152 153 154 155 156 157 158 159 160 161 162 163
	if v == nil {
		panic(fmt.Errorf("cannot use nil as a default"))
	}

	// if type of value does not match the option type
	if vKind, oKind := reflect.TypeOf(v).Kind(), o.Type(); vKind != oKind {
		// if the reason they do not match is not because of Slice vs Array equivalence
		// Note: Figuring out if the type of Slice/Array matches is not done in this function
		if !((vKind == reflect.Array || vKind == reflect.Slice) && (oKind == reflect.Array || oKind == reflect.Slice)) {
			panic(fmt.Errorf("invalid default for the given type, expected %s got %s", o.Type(), vKind))
		}
	}
164 165 166 167
	o.defaultVal = v
	return o
}

168
func (o *option) Default() interface{} {
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
	return o.defaultVal
}

// TODO handle description separately. this will take care of the panic case in
// NewOption

// For all func {Type}Option(...string) functions, the last variadic argument
// is treated as the description field.

func BoolOption(names ...string) Option {
	return NewOption(Bool, names...)
}
func IntOption(names ...string) Option {
	return NewOption(Int, names...)
}
func UintOption(names ...string) Option {
	return NewOption(Uint, names...)
}
Overbool's avatar
Overbool committed
187 188 189 190 191 192
func Int64Option(names ...string) Option {
	return NewOption(Int64, names...)
}
func Uint64Option(names ...string) Option {
	return NewOption(Uint64, names...)
}
193 194 195 196 197 198
func FloatOption(names ...string) Option {
	return NewOption(Float, names...)
}
func StringOption(names ...string) Option {
	return NewOption(String, names...)
}
199 200

// StringsOption is a command option that can handle a slice of strings
201
func StringsOption(names ...string) Option {
202 203 204 205
	return &stringsOption{
		Option:    NewOption(Strings, names...),
		delimiter: "",
	}
206 207 208 209 210 211 212
}

// DelimitedStringsOption like StringsOption is a command option that can handle a slice of strings.
// However, DelimitedStringsOption will automatically break up the associated CLI inputs based on the delimiter.
// For example, instead of passing `command --option=val1 --option=val2` you can pass `command --option=val1,val2` or
// even `command --option=val1,val2 --option=val3,val4`.
//
213
// A delimiter of "" is invalid
214
func DelimitedStringsOption(delimiter string, names ...string) Option {
215 216 217
	if delimiter == "" {
		panic("cannot create a DelimitedStringsOption with no delimiter")
	}
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
	return &stringsOption{
		Option:    NewOption(Strings, names...),
		delimiter: delimiter,
	}
}

type stringsOption struct {
	Option
	delimiter string
}

func (s *stringsOption) WithDefault(v interface{}) Option {
	if v == nil {
		return s.Option.WithDefault(v)
	}

	defVal := v.([]string)
	s.Option = s.Option.WithDefault(defVal)
	return s
}

func (s *stringsOption) Parse(v string) (interface{}, error) {
	if s.delimiter == "" {
		return []string{v}, nil
	}

	return strings.Split(v, s.delimiter), nil
245
}