1package flags 2 3import ( 4 "bufio" 5 "fmt" 6 "io" 7 "os" 8 "reflect" 9 "sort" 10 "strconv" 11 "strings" 12) 13 14// IniError contains location information on where an error occurred. 15type IniError struct { 16 // The error message. 17 Message string 18 19 // The filename of the file in which the error occurred. 20 File string 21 22 // The line number at which the error occurred. 23 LineNumber uint 24} 25 26// Error provides a "file:line: message" formatted message of the ini error. 27func (x *IniError) Error() string { 28 return fmt.Sprintf( 29 "%s:%d: %s", 30 x.File, 31 x.LineNumber, 32 x.Message, 33 ) 34} 35 36// IniOptions for writing 37type IniOptions uint 38 39const ( 40 // IniNone indicates no options. 41 IniNone IniOptions = 0 42 43 // IniIncludeDefaults indicates that default values should be written. 44 IniIncludeDefaults = 1 << iota 45 46 // IniCommentDefaults indicates that if IniIncludeDefaults is used 47 // options with default values are written but commented out. 48 IniCommentDefaults 49 50 // IniIncludeComments indicates that comments containing the description 51 // of an option should be written. 52 IniIncludeComments 53 54 // IniDefault provides a default set of options. 55 IniDefault = IniIncludeComments 56) 57 58// IniParser is a utility to read and write flags options from and to ini 59// formatted strings. 60type IniParser struct { 61 ParseAsDefaults bool // override default flags 62 63 parser *Parser 64} 65 66type iniValue struct { 67 Name string 68 Value string 69 Quoted bool 70 LineNumber uint 71} 72 73type iniSection []iniValue 74 75type ini struct { 76 File string 77 Sections map[string]iniSection 78} 79 80// NewIniParser creates a new ini parser for a given Parser. 81func NewIniParser(p *Parser) *IniParser { 82 return &IniParser{ 83 parser: p, 84 } 85} 86 87// IniParse is a convenience function to parse command line options with default 88// settings from an ini formatted file. The provided data is a pointer to a struct 89// representing the default option group (named "Application Options"). For 90// more control, use flags.NewParser. 91func IniParse(filename string, data interface{}) error { 92 p := NewParser(data, Default) 93 94 return NewIniParser(p).ParseFile(filename) 95} 96 97// ParseFile parses flags from an ini formatted file. See Parse for more 98// information on the ini file format. The returned errors can be of the type 99// flags.Error or flags.IniError. 100func (i *IniParser) ParseFile(filename string) error { 101 ini, err := readIniFromFile(filename) 102 103 if err != nil { 104 return err 105 } 106 107 return i.parse(ini) 108} 109 110// Parse parses flags from an ini format. You can use ParseFile as a 111// convenience function to parse from a filename instead of a general 112// io.Reader. 113// 114// The format of the ini file is as follows: 115// 116// [Option group name] 117// option = value 118// 119// Each section in the ini file represents an option group or command in the 120// flags parser. The default flags parser option group (i.e. when using 121// flags.Parse) is named 'Application Options'. The ini option name is matched 122// in the following order: 123// 124// 1. Compared to the ini-name tag on the option struct field (if present) 125// 2. Compared to the struct field name 126// 3. Compared to the option long name (if present) 127// 4. Compared to the option short name (if present) 128// 129// Sections for nested groups and commands can be addressed using a dot `.' 130// namespacing notation (i.e [subcommand.Options]). Group section names are 131// matched case insensitive. 132// 133// The returned errors can be of the type flags.Error or flags.IniError. 134func (i *IniParser) Parse(reader io.Reader) error { 135 ini, err := readIni(reader, "") 136 137 if err != nil { 138 return err 139 } 140 141 return i.parse(ini) 142} 143 144// WriteFile writes the flags as ini format into a file. See Write 145// for more information. The returned error occurs when the specified file 146// could not be opened for writing. 147func (i *IniParser) WriteFile(filename string, options IniOptions) error { 148 return writeIniToFile(i, filename, options) 149} 150 151// Write writes the current values of all the flags to an ini format. 152// See Parse for more information on the ini file format. You typically 153// call this only after settings have been parsed since the default values of each 154// option are stored just before parsing the flags (this is only relevant when 155// IniIncludeDefaults is _not_ set in options). 156func (i *IniParser) Write(writer io.Writer, options IniOptions) { 157 writeIni(i, writer, options) 158} 159 160func readFullLine(reader *bufio.Reader) (string, error) { 161 var line []byte 162 163 for { 164 l, more, err := reader.ReadLine() 165 166 if err != nil { 167 return "", err 168 } 169 170 if line == nil && !more { 171 return string(l), nil 172 } 173 174 line = append(line, l...) 175 176 if !more { 177 break 178 } 179 } 180 181 return string(line), nil 182} 183 184func optionIniName(option *Option) string { 185 name := option.tag.Get("_read-ini-name") 186 187 if len(name) != 0 { 188 return name 189 } 190 191 name = option.tag.Get("ini-name") 192 193 if len(name) != 0 { 194 return name 195 } 196 197 return option.field.Name 198} 199 200func writeGroupIni(cmd *Command, group *Group, namespace string, writer io.Writer, options IniOptions) { 201 var sname string 202 203 if len(namespace) != 0 { 204 sname = namespace 205 } 206 207 if cmd.Group != group && len(group.ShortDescription) != 0 { 208 if len(sname) != 0 { 209 sname += "." 210 } 211 212 sname += group.ShortDescription 213 } 214 215 sectionwritten := false 216 comments := (options & IniIncludeComments) != IniNone 217 218 for _, option := range group.options { 219 if option.isFunc() || option.Hidden { 220 continue 221 } 222 223 if len(option.tag.Get("no-ini")) != 0 { 224 continue 225 } 226 227 val := option.value 228 229 if (options&IniIncludeDefaults) == IniNone && option.valueIsDefault() { 230 continue 231 } 232 233 if !sectionwritten { 234 fmt.Fprintf(writer, "[%s]\n", sname) 235 sectionwritten = true 236 } 237 238 if comments && len(option.Description) != 0 { 239 fmt.Fprintf(writer, "; %s\n", option.Description) 240 } 241 242 oname := optionIniName(option) 243 244 commentOption := (options&(IniIncludeDefaults|IniCommentDefaults)) == IniIncludeDefaults|IniCommentDefaults && option.valueIsDefault() 245 246 kind := val.Type().Kind() 247 switch kind { 248 case reflect.Slice: 249 kind = val.Type().Elem().Kind() 250 251 if val.Len() == 0 { 252 writeOption(writer, oname, kind, "", "", true, option.iniQuote) 253 } else { 254 for idx := 0; idx < val.Len(); idx++ { 255 v, _ := convertToString(val.Index(idx), option.tag) 256 257 writeOption(writer, oname, kind, "", v, commentOption, option.iniQuote) 258 } 259 } 260 case reflect.Map: 261 kind = val.Type().Elem().Kind() 262 263 if val.Len() == 0 { 264 writeOption(writer, oname, kind, "", "", true, option.iniQuote) 265 } else { 266 mkeys := val.MapKeys() 267 keys := make([]string, len(val.MapKeys())) 268 kkmap := make(map[string]reflect.Value) 269 270 for i, k := range mkeys { 271 keys[i], _ = convertToString(k, option.tag) 272 kkmap[keys[i]] = k 273 } 274 275 sort.Strings(keys) 276 277 for _, k := range keys { 278 v, _ := convertToString(val.MapIndex(kkmap[k]), option.tag) 279 280 writeOption(writer, oname, kind, k, v, commentOption, option.iniQuote) 281 } 282 } 283 default: 284 v, _ := convertToString(val, option.tag) 285 286 writeOption(writer, oname, kind, "", v, commentOption, option.iniQuote) 287 } 288 289 if comments { 290 fmt.Fprintln(writer) 291 } 292 } 293 294 if sectionwritten && !comments { 295 fmt.Fprintln(writer) 296 } 297} 298 299func writeOption(writer io.Writer, optionName string, optionType reflect.Kind, optionKey string, optionValue string, commentOption bool, forceQuote bool) { 300 if forceQuote || (optionType == reflect.String && !isPrint(optionValue)) { 301 optionValue = strconv.Quote(optionValue) 302 } 303 304 comment := "" 305 if commentOption { 306 comment = "; " 307 } 308 309 fmt.Fprintf(writer, "%s%s =", comment, optionName) 310 311 if optionKey != "" { 312 fmt.Fprintf(writer, " %s:%s", optionKey, optionValue) 313 } else if optionValue != "" { 314 fmt.Fprintf(writer, " %s", optionValue) 315 } 316 317 fmt.Fprintln(writer) 318} 319 320func writeCommandIni(command *Command, namespace string, writer io.Writer, options IniOptions) { 321 command.eachGroup(func(group *Group) { 322 if !group.Hidden { 323 writeGroupIni(command, group, namespace, writer, options) 324 } 325 }) 326 327 for _, c := range command.commands { 328 var nns string 329 330 if c.Hidden { 331 continue 332 } 333 334 if len(namespace) != 0 { 335 nns = c.Name + "." + nns 336 } else { 337 nns = c.Name 338 } 339 340 writeCommandIni(c, nns, writer, options) 341 } 342} 343 344func writeIni(parser *IniParser, writer io.Writer, options IniOptions) { 345 writeCommandIni(parser.parser.Command, "", writer, options) 346} 347 348func writeIniToFile(parser *IniParser, filename string, options IniOptions) error { 349 file, err := os.Create(filename) 350 351 if err != nil { 352 return err 353 } 354 355 defer file.Close() 356 357 writeIni(parser, file, options) 358 359 return nil 360} 361 362func readIniFromFile(filename string) (*ini, error) { 363 file, err := os.Open(filename) 364 365 if err != nil { 366 return nil, err 367 } 368 369 defer file.Close() 370 371 return readIni(file, filename) 372} 373 374func readIni(contents io.Reader, filename string) (*ini, error) { 375 ret := &ini{ 376 File: filename, 377 Sections: make(map[string]iniSection), 378 } 379 380 reader := bufio.NewReader(contents) 381 382 // Empty global section 383 section := make(iniSection, 0, 10) 384 sectionname := "" 385 386 ret.Sections[sectionname] = section 387 388 var lineno uint 389 390 for { 391 line, err := readFullLine(reader) 392 393 if err == io.EOF { 394 break 395 } else if err != nil { 396 return nil, err 397 } 398 399 lineno++ 400 line = strings.TrimSpace(line) 401 402 // Skip empty lines and lines starting with ; (comments) 403 if len(line) == 0 || line[0] == ';' || line[0] == '#' { 404 continue 405 } 406 407 if line[0] == '[' { 408 if line[0] != '[' || line[len(line)-1] != ']' { 409 return nil, &IniError{ 410 Message: "malformed section header", 411 File: filename, 412 LineNumber: lineno, 413 } 414 } 415 416 name := strings.TrimSpace(line[1 : len(line)-1]) 417 418 if len(name) == 0 { 419 return nil, &IniError{ 420 Message: "empty section name", 421 File: filename, 422 LineNumber: lineno, 423 } 424 } 425 426 sectionname = name 427 section = ret.Sections[name] 428 429 if section == nil { 430 section = make(iniSection, 0, 10) 431 ret.Sections[name] = section 432 } 433 434 continue 435 } 436 437 // Parse option here 438 keyval := strings.SplitN(line, "=", 2) 439 440 if len(keyval) != 2 { 441 return nil, &IniError{ 442 Message: fmt.Sprintf("malformed key=value (%s)", line), 443 File: filename, 444 LineNumber: lineno, 445 } 446 } 447 448 name := strings.TrimSpace(keyval[0]) 449 value := strings.TrimSpace(keyval[1]) 450 quoted := false 451 452 if len(value) != 0 && value[0] == '"' { 453 if v, err := strconv.Unquote(value); err == nil { 454 value = v 455 456 quoted = true 457 } else { 458 return nil, &IniError{ 459 Message: err.Error(), 460 File: filename, 461 LineNumber: lineno, 462 } 463 } 464 } 465 466 section = append(section, iniValue{ 467 Name: name, 468 Value: value, 469 Quoted: quoted, 470 LineNumber: lineno, 471 }) 472 473 ret.Sections[sectionname] = section 474 } 475 476 return ret, nil 477} 478 479func (i *IniParser) matchingGroups(name string) []*Group { 480 if len(name) == 0 { 481 var ret []*Group 482 483 i.parser.eachGroup(func(g *Group) { 484 ret = append(ret, g) 485 }) 486 487 return ret 488 } 489 490 g := i.parser.groupByName(name) 491 492 if g != nil { 493 return []*Group{g} 494 } 495 496 return nil 497} 498 499func (i *IniParser) parse(ini *ini) error { 500 p := i.parser 501 502 var quotesLookup = make(map[*Option]bool) 503 504 for name, section := range ini.Sections { 505 groups := i.matchingGroups(name) 506 507 if len(groups) == 0 { 508 return newErrorf(ErrUnknownGroup, "could not find option group `%s'", name) 509 } 510 511 for _, inival := range section { 512 var opt *Option 513 514 for _, group := range groups { 515 opt = group.optionByName(inival.Name, func(o *Option, n string) bool { 516 return strings.ToLower(o.tag.Get("ini-name")) == strings.ToLower(n) 517 }) 518 519 if opt != nil && len(opt.tag.Get("no-ini")) != 0 { 520 opt = nil 521 } 522 523 if opt != nil { 524 break 525 } 526 } 527 528 if opt == nil { 529 if (p.Options & IgnoreUnknown) == None { 530 return &IniError{ 531 Message: fmt.Sprintf("unknown option: %s", inival.Name), 532 File: ini.File, 533 LineNumber: inival.LineNumber, 534 } 535 } 536 537 continue 538 } 539 540 // ini value is ignored if override is set and 541 // value was previously set from non default 542 if i.ParseAsDefaults && !opt.isSetDefault { 543 continue 544 } 545 546 pval := &inival.Value 547 548 if !opt.canArgument() && len(inival.Value) == 0 { 549 pval = nil 550 } else { 551 if opt.value.Type().Kind() == reflect.Map { 552 parts := strings.SplitN(inival.Value, ":", 2) 553 554 // only handle unquoting 555 if len(parts) == 2 && parts[1][0] == '"' { 556 if v, err := strconv.Unquote(parts[1]); err == nil { 557 parts[1] = v 558 559 inival.Quoted = true 560 } else { 561 return &IniError{ 562 Message: err.Error(), 563 File: ini.File, 564 LineNumber: inival.LineNumber, 565 } 566 } 567 568 s := parts[0] + ":" + parts[1] 569 570 pval = &s 571 } 572 } 573 } 574 575 if err := opt.set(pval); err != nil { 576 return &IniError{ 577 Message: err.Error(), 578 File: ini.File, 579 LineNumber: inival.LineNumber, 580 } 581 } 582 583 // either all INI values are quoted or only values who need quoting 584 if _, ok := quotesLookup[opt]; !inival.Quoted || !ok { 585 quotesLookup[opt] = inival.Quoted 586 } 587 588 opt.tag.Set("_read-ini-name", inival.Name) 589 } 590 } 591 592 for opt, quoted := range quotesLookup { 593 opt.iniQuote = quoted 594 } 595 596 return nil 597} 598