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