Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
597 lines
13 KiB
597 lines
13 KiB
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 |
|
}
|
|
|