1package docopt 2 3import ( 4 "fmt" 5 "reflect" 6 "strconv" 7 "strings" 8 "unicode" 9) 10 11func errKey(key string) error { 12 return fmt.Errorf("no such key: %q", key) 13} 14func errType(key string) error { 15 return fmt.Errorf("key: %q failed type conversion", key) 16} 17func errStrconv(key string, convErr error) error { 18 return fmt.Errorf("key: %q failed type conversion: %s", key, convErr) 19} 20 21// Opts is a map of command line options to their values, with some convenience 22// methods for value type conversion (bool, float64, int, string). For example, 23// to get an option value as an int: 24// 25// opts, _ := docopt.ParseDoc("Usage: sleep <seconds>") 26// secs, _ := opts.Int("<seconds>") 27// 28// Additionally, Opts.Bind allows you easily populate a struct's fields with the 29// values of each option value. See below for examples. 30// 31// Lastly, you can still treat Opts as a regular map, and do any type checking 32// and conversion that you want to yourself. For example: 33// 34// if s, ok := opts["<binary>"].(string); ok { 35// if val, err := strconv.ParseUint(s, 2, 64); err != nil { ... } 36// } 37// 38// Note that any non-boolean option / flag will have a string value in the 39// underlying map. 40type Opts map[string]interface{} 41 42func (o Opts) String(key string) (s string, err error) { 43 v, ok := o[key] 44 if !ok { 45 err = errKey(key) 46 return 47 } 48 s, ok = v.(string) 49 if !ok { 50 err = errType(key) 51 } 52 return 53} 54 55func (o Opts) Bool(key string) (b bool, err error) { 56 v, ok := o[key] 57 if !ok { 58 err = errKey(key) 59 return 60 } 61 b, ok = v.(bool) 62 if !ok { 63 err = errType(key) 64 } 65 return 66} 67 68func (o Opts) Int(key string) (i int, err error) { 69 s, err := o.String(key) 70 if err != nil { 71 return 72 } 73 i, err = strconv.Atoi(s) 74 if err != nil { 75 err = errStrconv(key, err) 76 } 77 return 78} 79 80func (o Opts) Float64(key string) (f float64, err error) { 81 s, err := o.String(key) 82 if err != nil { 83 return 84 } 85 f, err = strconv.ParseFloat(s, 64) 86 if err != nil { 87 err = errStrconv(key, err) 88 } 89 return 90} 91 92// Bind populates the fields of a given struct with matching option values. 93// Each key in Opts will be mapped to an exported field of the struct pointed 94// to by `v`, as follows: 95// 96// abc int // Unexported field, ignored 97// Abc string // Mapped from `--abc`, `<abc>`, or `abc` 98// // (case insensitive) 99// A string // Mapped from `-a`, `<a>` or `a` 100// // (case insensitive) 101// Abc int `docopt:"XYZ"` // Mapped from `XYZ` 102// Abc bool `docopt:"-"` // Mapped from `-` 103// Abc bool `docopt:"-x,--xyz"` // Mapped from `-x` or `--xyz` 104// // (first non-zero value found) 105// 106// Tagged (annotated) fields will always be mapped first. If no field is tagged 107// with an option's key, Bind will try to map the option to an appropriately 108// named field (as above). 109// 110// Bind also handles conversion to bool, float, int or string types. 111func (o Opts) Bind(v interface{}) error { 112 structVal := reflect.ValueOf(v) 113 if structVal.Kind() != reflect.Ptr { 114 return newError("'v' argument is not pointer to struct type") 115 } 116 for structVal.Kind() == reflect.Ptr { 117 structVal = structVal.Elem() 118 } 119 if structVal.Kind() != reflect.Struct { 120 return newError("'v' argument is not pointer to struct type") 121 } 122 structType := structVal.Type() 123 124 tagged := make(map[string]int) // Tagged field tags 125 untagged := make(map[string]int) // Untagged field names 126 127 for i := 0; i < structType.NumField(); i++ { 128 field := structType.Field(i) 129 if isUnexportedField(field) || field.Anonymous { 130 continue 131 } 132 tag := field.Tag.Get("docopt") 133 if tag == "" { 134 untagged[field.Name] = i 135 continue 136 } 137 for _, t := range strings.Split(tag, ",") { 138 tagged[t] = i 139 } 140 } 141 142 // Get the index of the struct field to use, based on the option key. 143 // Second argument is true/false on whether something was matched. 144 getFieldIndex := func(key string) (int, bool) { 145 if i, ok := tagged[key]; ok { 146 return i, true 147 } 148 if i, ok := untagged[guessUntaggedField(key)]; ok { 149 return i, true 150 } 151 return -1, false 152 } 153 154 indexMap := make(map[string]int) // Option keys to field index 155 156 // Pre-check that option keys are mapped to fields and fields are zero valued, before populating them. 157 for k := range o { 158 i, ok := getFieldIndex(k) 159 if !ok { 160 if k == "--help" || k == "--version" { // Don't require these to be mapped. 161 continue 162 } 163 return newError("mapping of %q is not found in given struct, or is an unexported field", k) 164 } 165 fieldVal := structVal.Field(i) 166 zeroVal := reflect.Zero(fieldVal.Type()) 167 if !reflect.DeepEqual(fieldVal.Interface(), zeroVal.Interface()) { 168 return newError("%q field is non-zero, will be overwritten by value of %q", structType.Field(i).Name, k) 169 } 170 indexMap[k] = i 171 } 172 173 // Populate fields with option values. 174 for k, v := range o { 175 i, ok := indexMap[k] 176 if !ok { 177 continue // Not mapped. 178 } 179 field := structVal.Field(i) 180 if !reflect.DeepEqual(field.Interface(), reflect.Zero(field.Type()).Interface()) { 181 // The struct's field is already non-zero (by our doing), so don't change it. 182 // This happens with comma separated tags, e.g. `docopt:"-h,--help"` which is a 183 // convenient way of checking if one of multiple boolean flags are set. 184 continue 185 } 186 optVal := reflect.ValueOf(v) 187 // Option value is the zero Value, so we can't get its .Type(). No need to assign anyway, so move along. 188 if !optVal.IsValid() { 189 continue 190 } 191 if !field.CanSet() { 192 return newError("%q field cannot be set", structType.Field(i).Name) 193 } 194 // Try to assign now if able. bool and string values should be assignable already. 195 if optVal.Type().AssignableTo(field.Type()) { 196 field.Set(optVal) 197 continue 198 } 199 // Try to convert the value and assign if able. 200 switch field.Kind() { 201 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 202 if x, err := o.Int(k); err == nil { 203 field.SetInt(int64(x)) 204 continue 205 } 206 case reflect.Float32, reflect.Float64: 207 if x, err := o.Float64(k); err == nil { 208 field.SetFloat(x) 209 continue 210 } 211 } 212 // TODO: Something clever (recursive?) with non-string slices. 213 // case reflect.Slice: 214 // if optVal.Kind() == reflect.Slice { 215 // for i := 0; i < optVal.Len(); i++ { 216 // sliceVal := optVal.Index(i) 217 // fmt.Printf("%v", sliceVal) 218 // } 219 // fmt.Printf("\n") 220 // } 221 return newError("value of %q is not assignable to %q field", k, structType.Field(i).Name) 222 } 223 224 return nil 225} 226 227// isUnexportedField returns whether the field is unexported. 228// isUnexportedField is to avoid the bug in versions older than Go1.3. 229// See following links: 230// https://code.google.com/p/go/issues/detail?id=7247 231// http://golang.org/ref/spec#Exported_identifiers 232func isUnexportedField(field reflect.StructField) bool { 233 return !(field.PkgPath == "" && unicode.IsUpper(rune(field.Name[0]))) 234} 235 236// Convert a string like "--my-special-flag" to "MySpecialFlag". 237func titleCaseDashes(key string) string { 238 nextToUpper := true 239 mapFn := func(r rune) rune { 240 if r == '-' { 241 nextToUpper = true 242 return -1 243 } 244 if nextToUpper { 245 nextToUpper = false 246 return unicode.ToUpper(r) 247 } 248 return r 249 } 250 return strings.Map(mapFn, key) 251} 252 253// Best guess which field.Name in a struct to assign for an option key. 254func guessUntaggedField(key string) string { 255 switch { 256 case strings.HasPrefix(key, "--") && len(key[2:]) > 1: 257 return titleCaseDashes(key[2:]) 258 case strings.HasPrefix(key, "-") && len(key[1:]) == 1: 259 return titleCaseDashes(key[1:]) 260 case strings.HasPrefix(key, "<") && strings.HasSuffix(key, ">"): 261 key = key[1 : len(key)-1] 262 } 263 return strings.Title(strings.ToLower(key)) 264} 265