package wand import ( "errors" "fmt" "github.com/iancoleman/strcase" "io" "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) io.ReadCloser { 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 readConfigFile(cmd Cmd, conf Config) (config, error) { f := conf.file(cmd) defer f.Close() 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" { value = token.Text() hasValue = true continue } } if c.originalNames == nil { c.originalNames = make(map[string]string) } name := strcase.ToKebab(key) c.originalNames[name] = key if !hasValue { 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, Config{file: func(Cmd) io.ReadCloser { return fileReader(o.value.str) }}) } return readConfig(cmd, cl, Config{merge: 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 { 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 }