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 }