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