package wand import ( "bytes" "code.squareroundforest.org/arpio/textedit" "errors" "fmt" "github.com/iancoleman/strcase" "io" "io/ioutil" "os" ) type file struct { filename string file io.ReadCloser } type config struct { values map[string][]string discard []string originalNames map[string]string } func fileReader(filename string) *file { return &file{ filename: filename, } } func (f *file) wrapErr(err error) error { return fmt.Errorf("%s: %w", f.filename, err) } func (f *file) Read(p []byte) (int, error) { if f.file == nil { file, err := os.Open(f.filename) if err != nil { return 0, f.wrapErr(err) } f.file = file } n, err := f.file.Read(p) if err != nil { return n, f.wrapErr(err) } return n, nil } func (f *file) Close() error { if f.file == nil { return nil } if err := f.file.Close(); err != nil { return f.wrapErr(err) } return nil } func unescapeConfig(s string) string { var b bytes.Buffer w := textedit.New( &b, textedit.Func( func(r rune, escaped bool) ([]rune, bool) { if escaped { return []rune{r}, false } if r == '\\' { return nil, true } return []rune{r}, false }, func(escaped bool) []rune { if escaped { return []rune{'\\'} } return nil }, ), ) w.Write([]byte(s)) return b.String() } func unquoteConfig(s string) string { if len(s) < 2 { return s } s = s[1 : len(s)-1] return unescapeConfig(s) } func readConfigFile(cmd Cmd, conf Config) (config, error) { var f io.ReadCloser if conf.test == "" { f = conf.file(cmd) defer f.Close() } else { f = ioutil.NopCloser(bytes.NewBufferString(conf.test)) } doc, err := parse(f) if err != nil { if conf.optional && (errors.Is(err, os.ErrPermission) || errors.Is(err, os.ErrNotExist)) { return config{}, nil } return config{}, fmt.Errorf("failed to read config file: %w", err) } var c config for _, entry := range doc.Nodes { if entry.Name != "key-val" { continue } var ( key string value string hasValue bool ) for _, token := range entry.Nodes { if token.Name == "key" { key = token.Text() continue } if token.Name != "value" { continue } hasValue = true for _, valueToken := range token.Nodes { if valueToken.Name == "value-chars" { value = unescapeConfig(valueToken.Text()) break } if valueToken.Name == "quoted" { value = unquoteConfig(valueToken.Text()) break } } } if c.originalNames == nil { c.originalNames = make(map[string]string) } name := strcase.ToKebab(key) c.originalNames[name] = key if !hasValue { delete(c.values, name) c.discard = append(c.discard, name) continue } if c.values == nil { c.values = make(map[string][]string) } c.values[name] = append(c.values[name], value) } return c, nil } func readConfigFromOption(cmd Cmd, cl commandLine, conf Config) (config, error) { f := mapFields(cmd.impl) if _, ok := f["config"]; ok { return config{}, nil } var c []Config for _, o := range cl.options { if o.name != "config" { continue } c = append(c, ConfigFile(o.value.str)) } return readConfig(cmd, cl, MergeConfig(c...)) } func readMergeConfig(cmd Cmd, cl commandLine, conf Config) (config, error) { var c []config for _, ci := range conf.merge { cci, err := readConfig(cmd, cl, ci) if err != nil { return config{}, err } c = append(c, cci) } var mc config for _, ci := range c { for _, d := range ci.discard { delete(mc.values, d) } for name, values := range ci.values { if mc.values == nil { mc.values = make(map[string][]string) } mc.values[name] = values } } return mc, nil } func readConfig(cmd Cmd, cl commandLine, conf Config) (config, error) { if conf.file != nil || conf.test != "" { return readConfigFile(cmd, conf) } if conf.fromOption { return readConfigFromOption(cmd, cl, conf) } return readMergeConfig(cmd, cl, conf) } func hasConfigFromOption(conf Config) bool { if conf.fromOption { return true } for _, c := range conf.merge { if hasConfigFromOption(c) { return true } } return false }