247 lines
4.0 KiB
Go
247 lines
4.0 KiB
Go
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
|
|
},
|
|
nil,
|
|
),
|
|
)
|
|
|
|
w.Write([]byte(s))
|
|
w.Flush()
|
|
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
|
|
}
|