1// Copyright 2012 Jesse van den Kieboom. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5package flags 6 7import ( 8 "errors" 9 "reflect" 10 "strings" 11 "unicode/utf8" 12) 13 14// ErrNotPointerToStruct indicates that a provided data container is not 15// a pointer to a struct. Only pointers to structs are valid data containers 16// for options. 17var ErrNotPointerToStruct = errors.New("provided data is not a pointer to struct") 18 19// Group represents an option group. Option groups can be used to logically 20// group options together under a description. Groups are only used to provide 21// more structure to options both for the user (as displayed in the help message) 22// and for you, since groups can be nested. 23type Group struct { 24 // A short description of the group. The 25 // short description is primarily used in the built-in generated help 26 // message 27 ShortDescription string 28 29 // A long description of the group. The long 30 // description is primarily used to present information on commands 31 // (Command embeds Group) in the built-in generated help and man pages. 32 LongDescription string 33 34 // The namespace of the group 35 Namespace string 36 37 // The environment namespace of the group 38 EnvNamespace string 39 40 // If true, the group is not displayed in the help or man page 41 Hidden bool 42 43 // The parent of the group or nil if it has no parent 44 parent interface{} 45 46 // All the options in the group 47 options []*Option 48 49 // All the subgroups 50 groups []*Group 51 52 // Whether the group represents the built-in help group 53 isBuiltinHelp bool 54 55 data interface{} 56} 57 58type scanHandler func(reflect.Value, *reflect.StructField) (bool, error) 59 60// AddGroup adds a new group to the command with the given name and data. The 61// data needs to be a pointer to a struct from which the fields indicate which 62// options are in the group. 63func (g *Group) AddGroup(shortDescription string, longDescription string, data interface{}) (*Group, error) { 64 group := newGroup(shortDescription, longDescription, data) 65 66 group.parent = g 67 68 if err := group.scan(); err != nil { 69 return nil, err 70 } 71 72 g.groups = append(g.groups, group) 73 return group, nil 74} 75 76// Groups returns the list of groups embedded in this group. 77func (g *Group) Groups() []*Group { 78 return g.groups 79} 80 81// Options returns the list of options in this group. 82func (g *Group) Options() []*Option { 83 return g.options 84} 85 86// Find locates the subgroup with the given short description and returns it. 87// If no such group can be found Find will return nil. Note that the description 88// is matched case insensitively. 89func (g *Group) Find(shortDescription string) *Group { 90 lshortDescription := strings.ToLower(shortDescription) 91 92 var ret *Group 93 94 g.eachGroup(func(gg *Group) { 95 if gg != g && strings.ToLower(gg.ShortDescription) == lshortDescription { 96 ret = gg 97 } 98 }) 99 100 return ret 101} 102 103func (g *Group) findOption(matcher func(*Option) bool) (option *Option) { 104 g.eachGroup(func(g *Group) { 105 for _, opt := range g.options { 106 if option == nil && matcher(opt) { 107 option = opt 108 } 109 } 110 }) 111 112 return option 113} 114 115// FindOptionByLongName finds an option that is part of the group, or any of its 116// subgroups, by matching its long name (including the option namespace). 117func (g *Group) FindOptionByLongName(longName string) *Option { 118 return g.findOption(func(option *Option) bool { 119 return option.LongNameWithNamespace() == longName 120 }) 121} 122 123// FindOptionByShortName finds an option that is part of the group, or any of 124// its subgroups, by matching its short name. 125func (g *Group) FindOptionByShortName(shortName rune) *Option { 126 return g.findOption(func(option *Option) bool { 127 return option.ShortName == shortName 128 }) 129} 130 131func newGroup(shortDescription string, longDescription string, data interface{}) *Group { 132 return &Group{ 133 ShortDescription: shortDescription, 134 LongDescription: longDescription, 135 136 data: data, 137 } 138} 139 140func (g *Group) optionByName(name string, namematch func(*Option, string) bool) *Option { 141 prio := 0 142 var retopt *Option 143 144 g.eachGroup(func(g *Group) { 145 for _, opt := range g.options { 146 if namematch != nil && namematch(opt, name) && prio < 4 { 147 retopt = opt 148 prio = 4 149 } 150 151 if name == opt.field.Name && prio < 3 { 152 retopt = opt 153 prio = 3 154 } 155 156 if name == opt.LongNameWithNamespace() && prio < 2 { 157 retopt = opt 158 prio = 2 159 } 160 161 if opt.ShortName != 0 && name == string(opt.ShortName) && prio < 1 { 162 retopt = opt 163 prio = 1 164 } 165 } 166 }) 167 168 return retopt 169} 170 171func (g *Group) showInHelp() bool { 172 if g.Hidden { 173 return false 174 } 175 for _, opt := range g.options { 176 if opt.showInHelp() { 177 return true 178 } 179 } 180 return false 181} 182 183func (g *Group) eachGroup(f func(*Group)) { 184 f(g) 185 186 for _, gg := range g.groups { 187 gg.eachGroup(f) 188 } 189} 190 191func isStringFalsy(s string) bool { 192 return s == "" || s == "false" || s == "no" || s == "0" 193} 194 195func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, handler scanHandler) error { 196 stype := realval.Type() 197 198 if sfield != nil { 199 if ok, err := handler(realval, sfield); err != nil { 200 return err 201 } else if ok { 202 return nil 203 } 204 } 205 206 for i := 0; i < stype.NumField(); i++ { 207 field := stype.Field(i) 208 209 // PkgName is set only for non-exported fields, which we ignore 210 if field.PkgPath != "" && !field.Anonymous { 211 continue 212 } 213 214 mtag := newMultiTag(string(field.Tag)) 215 216 if err := mtag.Parse(); err != nil { 217 return err 218 } 219 220 // Skip fields with the no-flag tag 221 if mtag.Get("no-flag") != "" { 222 continue 223 } 224 225 // Dive deep into structs or pointers to structs 226 kind := field.Type.Kind() 227 fld := realval.Field(i) 228 229 if kind == reflect.Struct { 230 if err := g.scanStruct(fld, &field, handler); err != nil { 231 return err 232 } 233 } else if kind == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct { 234 flagCountBefore := len(g.options) + len(g.groups) 235 236 if fld.IsNil() { 237 fld = reflect.New(fld.Type().Elem()) 238 } 239 240 if err := g.scanStruct(reflect.Indirect(fld), &field, handler); err != nil { 241 return err 242 } 243 244 if len(g.options)+len(g.groups) != flagCountBefore { 245 realval.Field(i).Set(fld) 246 } 247 } 248 249 longname := mtag.Get("long") 250 shortname := mtag.Get("short") 251 252 // Need at least either a short or long name 253 if longname == "" && shortname == "" && mtag.Get("ini-name") == "" { 254 continue 255 } 256 257 short := rune(0) 258 rc := utf8.RuneCountInString(shortname) 259 260 if rc > 1 { 261 return newErrorf(ErrShortNameTooLong, 262 "short names can only be 1 character long, not `%s'", 263 shortname) 264 265 } else if rc == 1 { 266 short, _ = utf8.DecodeRuneInString(shortname) 267 } 268 269 description := mtag.Get("description") 270 def := mtag.GetMany("default") 271 272 optionalValue := mtag.GetMany("optional-value") 273 valueName := mtag.Get("value-name") 274 defaultMask := mtag.Get("default-mask") 275 276 optional := !isStringFalsy(mtag.Get("optional")) 277 required := !isStringFalsy(mtag.Get("required")) 278 choices := mtag.GetMany("choice") 279 hidden := !isStringFalsy(mtag.Get("hidden")) 280 281 option := &Option{ 282 Description: description, 283 ShortName: short, 284 LongName: longname, 285 Default: def, 286 EnvDefaultKey: mtag.Get("env"), 287 EnvDefaultDelim: mtag.Get("env-delim"), 288 OptionalArgument: optional, 289 OptionalValue: optionalValue, 290 Required: required, 291 ValueName: valueName, 292 DefaultMask: defaultMask, 293 Choices: choices, 294 Hidden: hidden, 295 296 group: g, 297 298 field: field, 299 value: realval.Field(i), 300 tag: mtag, 301 } 302 303 if option.isBool() && option.Default != nil { 304 return newErrorf(ErrInvalidTag, 305 "boolean flag `%s' may not have default values, they always default to `false' and can only be turned on", 306 option.shortAndLongName()) 307 } 308 309 g.options = append(g.options, option) 310 } 311 312 return nil 313} 314 315func (g *Group) checkForDuplicateFlags() *Error { 316 shortNames := make(map[rune]*Option) 317 longNames := make(map[string]*Option) 318 319 var duplicateError *Error 320 321 g.eachGroup(func(g *Group) { 322 for _, option := range g.options { 323 if option.LongName != "" { 324 longName := option.LongNameWithNamespace() 325 326 if otherOption, ok := longNames[longName]; ok { 327 duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same long name as option `%s'", option, otherOption) 328 return 329 } 330 longNames[longName] = option 331 } 332 if option.ShortName != 0 { 333 if otherOption, ok := shortNames[option.ShortName]; ok { 334 duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same short name as option `%s'", option, otherOption) 335 return 336 } 337 shortNames[option.ShortName] = option 338 } 339 } 340 }) 341 342 return duplicateError 343} 344 345func (g *Group) scanSubGroupHandler(realval reflect.Value, sfield *reflect.StructField) (bool, error) { 346 mtag := newMultiTag(string(sfield.Tag)) 347 348 if err := mtag.Parse(); err != nil { 349 return true, err 350 } 351 352 subgroup := mtag.Get("group") 353 354 if len(subgroup) != 0 { 355 var ptrval reflect.Value 356 357 if realval.Kind() == reflect.Ptr { 358 ptrval = realval 359 360 if ptrval.IsNil() { 361 ptrval.Set(reflect.New(ptrval.Type())) 362 } 363 } else { 364 ptrval = realval.Addr() 365 } 366 367 description := mtag.Get("description") 368 369 group, err := g.AddGroup(subgroup, description, ptrval.Interface()) 370 371 if err != nil { 372 return true, err 373 } 374 375 group.Namespace = mtag.Get("namespace") 376 group.EnvNamespace = mtag.Get("env-namespace") 377 group.Hidden = mtag.Get("hidden") != "" 378 379 return true, nil 380 } 381 382 return false, nil 383} 384 385func (g *Group) scanType(handler scanHandler) error { 386 // Get all the public fields in the data struct 387 ptrval := reflect.ValueOf(g.data) 388 389 if ptrval.Type().Kind() != reflect.Ptr { 390 panic(ErrNotPointerToStruct) 391 } 392 393 stype := ptrval.Type().Elem() 394 395 if stype.Kind() != reflect.Struct { 396 panic(ErrNotPointerToStruct) 397 } 398 399 realval := reflect.Indirect(ptrval) 400 401 if err := g.scanStruct(realval, nil, handler); err != nil { 402 return err 403 } 404 405 if err := g.checkForDuplicateFlags(); err != nil { 406 return err 407 } 408 409 return nil 410} 411 412func (g *Group) scan() error { 413 return g.scanType(g.scanSubGroupHandler) 414} 415 416func (g *Group) groupByName(name string) *Group { 417 if len(name) == 0 { 418 return g 419 } 420 421 return g.Find(name) 422} 423