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