blob: e714d3d38d1c78050b37754f6aa719b769873c21 [file] [log] [blame]
Scott Bakerbdb962b2020-04-03 10:53:36 -07001package 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}