598 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			598 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
| package flags
 | |
| 
 | |
| import (
 | |
| 	"bufio"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"os"
 | |
| 	"reflect"
 | |
| 	"sort"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| )
 | |
| 
 | |
| // IniError contains location information on where an error occurred.
 | |
| type IniError struct {
 | |
| 	// The error message.
 | |
| 	Message string
 | |
| 
 | |
| 	// The filename of the file in which the error occurred.
 | |
| 	File string
 | |
| 
 | |
| 	// The line number at which the error occurred.
 | |
| 	LineNumber uint
 | |
| }
 | |
| 
 | |
| // Error provides a "file:line: message" formatted message of the ini error.
 | |
| func (x *IniError) Error() string {
 | |
| 	return fmt.Sprintf(
 | |
| 		"%s:%d: %s",
 | |
| 		x.File,
 | |
| 		x.LineNumber,
 | |
| 		x.Message,
 | |
| 	)
 | |
| }
 | |
| 
 | |
| // IniOptions for writing
 | |
| type IniOptions uint
 | |
| 
 | |
| const (
 | |
| 	// IniNone indicates no options.
 | |
| 	IniNone IniOptions = 0
 | |
| 
 | |
| 	// IniIncludeDefaults indicates that default values should be written.
 | |
| 	IniIncludeDefaults = 1 << iota
 | |
| 
 | |
| 	// IniCommentDefaults indicates that if IniIncludeDefaults is used
 | |
| 	// options with default values are written but commented out.
 | |
| 	IniCommentDefaults
 | |
| 
 | |
| 	// IniIncludeComments indicates that comments containing the description
 | |
| 	// of an option should be written.
 | |
| 	IniIncludeComments
 | |
| 
 | |
| 	// IniDefault provides a default set of options.
 | |
| 	IniDefault = IniIncludeComments
 | |
| )
 | |
| 
 | |
| // IniParser is a utility to read and write flags options from and to ini
 | |
| // formatted strings.
 | |
| type IniParser struct {
 | |
| 	ParseAsDefaults bool // override default flags
 | |
| 
 | |
| 	parser *Parser
 | |
| }
 | |
| 
 | |
| type iniValue struct {
 | |
| 	Name       string
 | |
| 	Value      string
 | |
| 	Quoted     bool
 | |
| 	LineNumber uint
 | |
| }
 | |
| 
 | |
| type iniSection []iniValue
 | |
| 
 | |
| type ini struct {
 | |
| 	File     string
 | |
| 	Sections map[string]iniSection
 | |
| }
 | |
| 
 | |
| // NewIniParser creates a new ini parser for a given Parser.
 | |
| func NewIniParser(p *Parser) *IniParser {
 | |
| 	return &IniParser{
 | |
| 		parser: p,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // IniParse is a convenience function to parse command line options with default
 | |
| // settings from an ini formatted file. The provided data is a pointer to a struct
 | |
| // representing the default option group (named "Application Options"). For
 | |
| // more control, use flags.NewParser.
 | |
| func IniParse(filename string, data interface{}) error {
 | |
| 	p := NewParser(data, Default)
 | |
| 
 | |
| 	return NewIniParser(p).ParseFile(filename)
 | |
| }
 | |
| 
 | |
| // ParseFile parses flags from an ini formatted file. See Parse for more
 | |
| // information on the ini file format. The returned errors can be of the type
 | |
| // flags.Error or flags.IniError.
 | |
| func (i *IniParser) ParseFile(filename string) error {
 | |
| 	ini, err := readIniFromFile(filename)
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return i.parse(ini)
 | |
| }
 | |
| 
 | |
| // Parse parses flags from an ini format. You can use ParseFile as a
 | |
| // convenience function to parse from a filename instead of a general
 | |
| // io.Reader.
 | |
| //
 | |
| // The format of the ini file is as follows:
 | |
| //
 | |
| //     [Option group name]
 | |
| //     option = value
 | |
| //
 | |
| // Each section in the ini file represents an option group or command in the
 | |
| // flags parser. The default flags parser option group (i.e. when using
 | |
| // flags.Parse) is named 'Application Options'. The ini option name is matched
 | |
| // in the following order:
 | |
| //
 | |
| //     1. Compared to the ini-name tag on the option struct field (if present)
 | |
| //     2. Compared to the struct field name
 | |
| //     3. Compared to the option long name (if present)
 | |
| //     4. Compared to the option short name (if present)
 | |
| //
 | |
| // Sections for nested groups and commands can be addressed using a dot `.'
 | |
| // namespacing notation (i.e [subcommand.Options]). Group section names are
 | |
| // matched case insensitive.
 | |
| //
 | |
| // The returned errors can be of the type flags.Error or flags.IniError.
 | |
| func (i *IniParser) Parse(reader io.Reader) error {
 | |
| 	ini, err := readIni(reader, "")
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return i.parse(ini)
 | |
| }
 | |
| 
 | |
| // WriteFile writes the flags as ini format into a file. See Write
 | |
| // for more information. The returned error occurs when the specified file
 | |
| // could not be opened for writing.
 | |
| func (i *IniParser) WriteFile(filename string, options IniOptions) error {
 | |
| 	return writeIniToFile(i, filename, options)
 | |
| }
 | |
| 
 | |
| // Write writes the current values of all the flags to an ini format.
 | |
| // See Parse for more information on the ini file format. You typically
 | |
| // call this only after settings have been parsed since the default values of each
 | |
| // option are stored just before parsing the flags (this is only relevant when
 | |
| // IniIncludeDefaults is _not_ set in options).
 | |
| func (i *IniParser) Write(writer io.Writer, options IniOptions) {
 | |
| 	writeIni(i, writer, options)
 | |
| }
 | |
| 
 | |
| func readFullLine(reader *bufio.Reader) (string, error) {
 | |
| 	var line []byte
 | |
| 
 | |
| 	for {
 | |
| 		l, more, err := reader.ReadLine()
 | |
| 
 | |
| 		if err != nil {
 | |
| 			return "", err
 | |
| 		}
 | |
| 
 | |
| 		if line == nil && !more {
 | |
| 			return string(l), nil
 | |
| 		}
 | |
| 
 | |
| 		line = append(line, l...)
 | |
| 
 | |
| 		if !more {
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return string(line), nil
 | |
| }
 | |
| 
 | |
| func optionIniName(option *Option) string {
 | |
| 	name := option.tag.Get("_read-ini-name")
 | |
| 
 | |
| 	if len(name) != 0 {
 | |
| 		return name
 | |
| 	}
 | |
| 
 | |
| 	name = option.tag.Get("ini-name")
 | |
| 
 | |
| 	if len(name) != 0 {
 | |
| 		return name
 | |
| 	}
 | |
| 
 | |
| 	return option.field.Name
 | |
| }
 | |
| 
 | |
| func writeGroupIni(cmd *Command, group *Group, namespace string, writer io.Writer, options IniOptions) {
 | |
| 	var sname string
 | |
| 
 | |
| 	if len(namespace) != 0 {
 | |
| 		sname = namespace
 | |
| 	}
 | |
| 
 | |
| 	if cmd.Group != group && len(group.ShortDescription) != 0 {
 | |
| 		if len(sname) != 0 {
 | |
| 			sname += "."
 | |
| 		}
 | |
| 
 | |
| 		sname += group.ShortDescription
 | |
| 	}
 | |
| 
 | |
| 	sectionwritten := false
 | |
| 	comments := (options & IniIncludeComments) != IniNone
 | |
| 
 | |
| 	for _, option := range group.options {
 | |
| 		if option.isFunc() || option.Hidden {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if len(option.tag.Get("no-ini")) != 0 {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		val := option.value
 | |
| 
 | |
| 		if (options&IniIncludeDefaults) == IniNone && option.valueIsDefault() {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if !sectionwritten {
 | |
| 			fmt.Fprintf(writer, "[%s]\n", sname)
 | |
| 			sectionwritten = true
 | |
| 		}
 | |
| 
 | |
| 		if comments && len(option.Description) != 0 {
 | |
| 			fmt.Fprintf(writer, "; %s\n", option.Description)
 | |
| 		}
 | |
| 
 | |
| 		oname := optionIniName(option)
 | |
| 
 | |
| 		commentOption := (options&(IniIncludeDefaults|IniCommentDefaults)) == IniIncludeDefaults|IniCommentDefaults && option.valueIsDefault()
 | |
| 
 | |
| 		kind := val.Type().Kind()
 | |
| 		switch kind {
 | |
| 		case reflect.Slice:
 | |
| 			kind = val.Type().Elem().Kind()
 | |
| 
 | |
| 			if val.Len() == 0 {
 | |
| 				writeOption(writer, oname, kind, "", "", true, option.iniQuote)
 | |
| 			} else {
 | |
| 				for idx := 0; idx < val.Len(); idx++ {
 | |
| 					v, _ := convertToString(val.Index(idx), option.tag)
 | |
| 
 | |
| 					writeOption(writer, oname, kind, "", v, commentOption, option.iniQuote)
 | |
| 				}
 | |
| 			}
 | |
| 		case reflect.Map:
 | |
| 			kind = val.Type().Elem().Kind()
 | |
| 
 | |
| 			if val.Len() == 0 {
 | |
| 				writeOption(writer, oname, kind, "", "", true, option.iniQuote)
 | |
| 			} else {
 | |
| 				mkeys := val.MapKeys()
 | |
| 				keys := make([]string, len(val.MapKeys()))
 | |
| 				kkmap := make(map[string]reflect.Value)
 | |
| 
 | |
| 				for i, k := range mkeys {
 | |
| 					keys[i], _ = convertToString(k, option.tag)
 | |
| 					kkmap[keys[i]] = k
 | |
| 				}
 | |
| 
 | |
| 				sort.Strings(keys)
 | |
| 
 | |
| 				for _, k := range keys {
 | |
| 					v, _ := convertToString(val.MapIndex(kkmap[k]), option.tag)
 | |
| 
 | |
| 					writeOption(writer, oname, kind, k, v, commentOption, option.iniQuote)
 | |
| 				}
 | |
| 			}
 | |
| 		default:
 | |
| 			v, _ := convertToString(val, option.tag)
 | |
| 
 | |
| 			writeOption(writer, oname, kind, "", v, commentOption, option.iniQuote)
 | |
| 		}
 | |
| 
 | |
| 		if comments {
 | |
| 			fmt.Fprintln(writer)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if sectionwritten && !comments {
 | |
| 		fmt.Fprintln(writer)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func writeOption(writer io.Writer, optionName string, optionType reflect.Kind, optionKey string, optionValue string, commentOption bool, forceQuote bool) {
 | |
| 	if forceQuote || (optionType == reflect.String && !isPrint(optionValue)) {
 | |
| 		optionValue = strconv.Quote(optionValue)
 | |
| 	}
 | |
| 
 | |
| 	comment := ""
 | |
| 	if commentOption {
 | |
| 		comment = "; "
 | |
| 	}
 | |
| 
 | |
| 	fmt.Fprintf(writer, "%s%s =", comment, optionName)
 | |
| 
 | |
| 	if optionKey != "" {
 | |
| 		fmt.Fprintf(writer, " %s:%s", optionKey, optionValue)
 | |
| 	} else if optionValue != "" {
 | |
| 		fmt.Fprintf(writer, " %s", optionValue)
 | |
| 	}
 | |
| 
 | |
| 	fmt.Fprintln(writer)
 | |
| }
 | |
| 
 | |
| func writeCommandIni(command *Command, namespace string, writer io.Writer, options IniOptions) {
 | |
| 	command.eachGroup(func(group *Group) {
 | |
| 		if !group.Hidden {
 | |
| 			writeGroupIni(command, group, namespace, writer, options)
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	for _, c := range command.commands {
 | |
| 		var nns string
 | |
| 
 | |
| 		if c.Hidden {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if len(namespace) != 0 {
 | |
| 			nns = c.Name + "." + nns
 | |
| 		} else {
 | |
| 			nns = c.Name
 | |
| 		}
 | |
| 
 | |
| 		writeCommandIni(c, nns, writer, options)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func writeIni(parser *IniParser, writer io.Writer, options IniOptions) {
 | |
| 	writeCommandIni(parser.parser.Command, "", writer, options)
 | |
| }
 | |
| 
 | |
| func writeIniToFile(parser *IniParser, filename string, options IniOptions) error {
 | |
| 	file, err := os.Create(filename)
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	defer file.Close()
 | |
| 
 | |
| 	writeIni(parser, file, options)
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func readIniFromFile(filename string) (*ini, error) {
 | |
| 	file, err := os.Open(filename)
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	defer file.Close()
 | |
| 
 | |
| 	return readIni(file, filename)
 | |
| }
 | |
| 
 | |
| func readIni(contents io.Reader, filename string) (*ini, error) {
 | |
| 	ret := &ini{
 | |
| 		File:     filename,
 | |
| 		Sections: make(map[string]iniSection),
 | |
| 	}
 | |
| 
 | |
| 	reader := bufio.NewReader(contents)
 | |
| 
 | |
| 	// Empty global section
 | |
| 	section := make(iniSection, 0, 10)
 | |
| 	sectionname := ""
 | |
| 
 | |
| 	ret.Sections[sectionname] = section
 | |
| 
 | |
| 	var lineno uint
 | |
| 
 | |
| 	for {
 | |
| 		line, err := readFullLine(reader)
 | |
| 
 | |
| 		if err == io.EOF {
 | |
| 			break
 | |
| 		} else if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		lineno++
 | |
| 		line = strings.TrimSpace(line)
 | |
| 
 | |
| 		// Skip empty lines and lines starting with ; (comments)
 | |
| 		if len(line) == 0 || line[0] == ';' || line[0] == '#' {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if line[0] == '[' {
 | |
| 			if line[0] != '[' || line[len(line)-1] != ']' {
 | |
| 				return nil, &IniError{
 | |
| 					Message:    "malformed section header",
 | |
| 					File:       filename,
 | |
| 					LineNumber: lineno,
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			name := strings.TrimSpace(line[1 : len(line)-1])
 | |
| 
 | |
| 			if len(name) == 0 {
 | |
| 				return nil, &IniError{
 | |
| 					Message:    "empty section name",
 | |
| 					File:       filename,
 | |
| 					LineNumber: lineno,
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			sectionname = name
 | |
| 			section = ret.Sections[name]
 | |
| 
 | |
| 			if section == nil {
 | |
| 				section = make(iniSection, 0, 10)
 | |
| 				ret.Sections[name] = section
 | |
| 			}
 | |
| 
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// Parse option here
 | |
| 		keyval := strings.SplitN(line, "=", 2)
 | |
| 
 | |
| 		if len(keyval) != 2 {
 | |
| 			return nil, &IniError{
 | |
| 				Message:    fmt.Sprintf("malformed key=value (%s)", line),
 | |
| 				File:       filename,
 | |
| 				LineNumber: lineno,
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		name := strings.TrimSpace(keyval[0])
 | |
| 		value := strings.TrimSpace(keyval[1])
 | |
| 		quoted := false
 | |
| 
 | |
| 		if len(value) != 0 && value[0] == '"' {
 | |
| 			if v, err := strconv.Unquote(value); err == nil {
 | |
| 				value = v
 | |
| 
 | |
| 				quoted = true
 | |
| 			} else {
 | |
| 				return nil, &IniError{
 | |
| 					Message:    err.Error(),
 | |
| 					File:       filename,
 | |
| 					LineNumber: lineno,
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		section = append(section, iniValue{
 | |
| 			Name:       name,
 | |
| 			Value:      value,
 | |
| 			Quoted:     quoted,
 | |
| 			LineNumber: lineno,
 | |
| 		})
 | |
| 
 | |
| 		ret.Sections[sectionname] = section
 | |
| 	}
 | |
| 
 | |
| 	return ret, nil
 | |
| }
 | |
| 
 | |
| func (i *IniParser) matchingGroups(name string) []*Group {
 | |
| 	if len(name) == 0 {
 | |
| 		var ret []*Group
 | |
| 
 | |
| 		i.parser.eachGroup(func(g *Group) {
 | |
| 			ret = append(ret, g)
 | |
| 		})
 | |
| 
 | |
| 		return ret
 | |
| 	}
 | |
| 
 | |
| 	g := i.parser.groupByName(name)
 | |
| 
 | |
| 	if g != nil {
 | |
| 		return []*Group{g}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (i *IniParser) parse(ini *ini) error {
 | |
| 	p := i.parser
 | |
| 
 | |
| 	var quotesLookup = make(map[*Option]bool)
 | |
| 
 | |
| 	for name, section := range ini.Sections {
 | |
| 		groups := i.matchingGroups(name)
 | |
| 
 | |
| 		if len(groups) == 0 {
 | |
| 			return newErrorf(ErrUnknownGroup, "could not find option group `%s'", name)
 | |
| 		}
 | |
| 
 | |
| 		for _, inival := range section {
 | |
| 			var opt *Option
 | |
| 
 | |
| 			for _, group := range groups {
 | |
| 				opt = group.optionByName(inival.Name, func(o *Option, n string) bool {
 | |
| 					return strings.ToLower(o.tag.Get("ini-name")) == strings.ToLower(n)
 | |
| 				})
 | |
| 
 | |
| 				if opt != nil && len(opt.tag.Get("no-ini")) != 0 {
 | |
| 					opt = nil
 | |
| 				}
 | |
| 
 | |
| 				if opt != nil {
 | |
| 					break
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			if opt == nil {
 | |
| 				if (p.Options & IgnoreUnknown) == None {
 | |
| 					return &IniError{
 | |
| 						Message:    fmt.Sprintf("unknown option: %s", inival.Name),
 | |
| 						File:       ini.File,
 | |
| 						LineNumber: inival.LineNumber,
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			// ini value is ignored if override is set and
 | |
| 			// value was previously set from non default
 | |
| 			if i.ParseAsDefaults && !opt.isSetDefault {
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			pval := &inival.Value
 | |
| 
 | |
| 			if !opt.canArgument() && len(inival.Value) == 0 {
 | |
| 				pval = nil
 | |
| 			} else {
 | |
| 				if opt.value.Type().Kind() == reflect.Map {
 | |
| 					parts := strings.SplitN(inival.Value, ":", 2)
 | |
| 
 | |
| 					// only handle unquoting
 | |
| 					if len(parts) == 2 && parts[1][0] == '"' {
 | |
| 						if v, err := strconv.Unquote(parts[1]); err == nil {
 | |
| 							parts[1] = v
 | |
| 
 | |
| 							inival.Quoted = true
 | |
| 						} else {
 | |
| 							return &IniError{
 | |
| 								Message:    err.Error(),
 | |
| 								File:       ini.File,
 | |
| 								LineNumber: inival.LineNumber,
 | |
| 							}
 | |
| 						}
 | |
| 
 | |
| 						s := parts[0] + ":" + parts[1]
 | |
| 
 | |
| 						pval = &s
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			if err := opt.set(pval); err != nil {
 | |
| 				return &IniError{
 | |
| 					Message:    err.Error(),
 | |
| 					File:       ini.File,
 | |
| 					LineNumber: inival.LineNumber,
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			// either all INI values are quoted or only values who need quoting
 | |
| 			if _, ok := quotesLookup[opt]; !inival.Quoted || !ok {
 | |
| 				quotesLookup[opt] = inival.Quoted
 | |
| 			}
 | |
| 
 | |
| 			opt.tag.Set("_read-ini-name", inival.Name)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	for opt, quoted := range quotesLookup {
 | |
| 		opt.iniQuote = quoted
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 |