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