2025-08-24 01:45:25 +02:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-24 04:46:54 +02:00
|
|
|
func fileReader(filename string) *file {
|
2025-08-24 01:45:25 +02:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-24 04:46:54 +02:00
|
|
|
c = append(c, Config{file: func(Cmd) *file { return fileReader(o.value.str) }})
|
2025-08-24 01:45:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|