2025-08-24 01:45:25 +02:00
|
|
|
package wand
|
|
|
|
|
|
|
|
|
|
import (
|
2025-08-26 03:21:35 +02:00
|
|
|
"bytes"
|
2025-08-24 01:45:25 +02:00
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"github.com/iancoleman/strcase"
|
|
|
|
|
"io"
|
2025-08-26 03:21:35 +02:00
|
|
|
"io/ioutil"
|
2025-08-24 01:45:25 +02:00
|
|
|
"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
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-05 03:19:00 +02:00
|
|
|
func unescapeConfig(s string) string {
|
|
|
|
|
var (
|
|
|
|
|
u []rune
|
|
|
|
|
escaped bool
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
r := []rune(s)
|
|
|
|
|
for _, ri := range r {
|
|
|
|
|
if escaped {
|
|
|
|
|
u = append(u, ri)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ri == '\\' {
|
|
|
|
|
escaped = true
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
u = append(u, ri)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return string(u)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func unquoteConfig(s string) string {
|
|
|
|
|
if len(s) < 2 {
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s = s[1 : len(s)-1]
|
|
|
|
|
return unescapeConfig(s)
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-24 01:45:25 +02:00
|
|
|
func readConfigFile(cmd Cmd, conf Config) (config, error) {
|
2025-08-26 03:21:35 +02:00
|
|
|
var f io.ReadCloser
|
|
|
|
|
if conf.test == "" {
|
|
|
|
|
f = conf.file(cmd)
|
|
|
|
|
defer f.Close()
|
|
|
|
|
} else {
|
|
|
|
|
f = ioutil.NopCloser(bytes.NewBufferString(conf.test))
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-24 01:45:25 +02:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-05 03:19:00 +02:00
|
|
|
if token.Name != "value" {
|
2025-08-24 01:45:25 +02:00
|
|
|
continue
|
|
|
|
|
}
|
2025-09-05 03:19:00 +02:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-24 01:45:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if c.originalNames == nil {
|
|
|
|
|
c.originalNames = make(map[string]string)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
name := strcase.ToKebab(key)
|
|
|
|
|
c.originalNames[name] = key
|
|
|
|
|
if !hasValue {
|
2025-09-01 02:07:48 +02:00
|
|
|
delete(c.values, name)
|
2025-08-24 01:45:25 +02:00
|
|
|
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) {
|
2025-08-26 03:21:35 +02:00
|
|
|
if conf.file != nil || conf.test != "" {
|
2025-08-24 01:45:25 +02:00
|
|
|
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
|
|
|
|
|
}
|