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