| 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 |
| } |