1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
|
// Package ingo provides a drop-in replacement for flag.Parse() with flags
// persisted in a user editable configuration file.
package ingo
import (
"bufio"
"bytes"
"flag"
"fmt"
"io"
"os"
"os/user"
"path"
"strings"
"unicode/utf8"
)
const updateWarning = `!!!!!!!!!!
! WARNING: %s was probably updated,
! Check and update %s as necessary
! and remove the last "deprecated" paragraph to disable this message!
!!!!!!!!!!
`
const configHeader = `# %s configuration
#
# This config has https://github.com/schachmat/ingo syntax.
# Empty lines or lines starting with # will be ignored.
# All other lines must look like "KEY=VALUE" (without the quotes).
# The VALUE must not be enclosed in quotes as well!
`
var openOrCreate = os.OpenFile
// Parse should be called by the user instead of `flag.Parse()` after all flags
// have been added. It will read the following sources with the given priority:
// 1. flags given on the command line
// 2. values read from the config file
// 3. default values from the flags
// Then it will update the config file only using sources 2 and 3, so given flag
// values will not be persisted in the config file. The default value from the
// flag will only be used if the flag could not be found in the config file
// already. Values from the config file, which don't have a corresponding flag
// anymore will be appended in a special section at the end of the new version
// of the config file so their values won't get lost and a warning message will
// be printed to stderr.
//
// The location of the config file depends on the appName argument. An appName
// of `Ingo` would resolve to the config file path `$HOME/.ingorc`. This default
// location can also be overwritten temporarily by setting an environment
// variable like `INGORC` to point to the config file path.
func Parse(appName string) error {
if flag.Parsed() {
return fmt.Errorf("flags have been parsed already")
}
envname := strings.ToUpper(appName) + "RC"
cPath := os.Getenv(envname)
if cPath == "" {
usr, err := user.Current()
if err != nil {
return fmt.Errorf("%v\nYou can set the environment variable %s to point to your config file as a workaround", err, envname)
}
cPath = path.Join(usr.HomeDir, "."+strings.ToLower(appName)+"rc")
}
cf, err := openOrCreate(cPath, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
return fmt.Errorf("unable to open %s config file %v for reading and writing: %v", appName, cPath, err)
}
defer cf.Close()
// read config to buffer and parse
oldConf := new(bytes.Buffer)
obsoleteKeys := parseConfig(io.TeeReader(cf, oldConf))
if len(obsoleteKeys) > 0 {
fmt.Fprintf(os.Stderr, updateWarning, appName, cPath)
}
// write updated config to another buffer
newConf := new(bytes.Buffer)
fmt.Fprintf(newConf, configHeader, appName)
saveConfig(newConf, obsoleteKeys)
// only write the file if it changed
if !bytes.Equal(oldConf.Bytes(), newConf.Bytes()) {
if ofs, err := cf.Seek(0, 0); err != nil || ofs != 0 {
return fmt.Errorf("failed to seek to beginning of %s: %v", cPath, err)
} else if err = cf.Truncate(0); err != nil {
return fmt.Errorf("failed to truncate %s: %v", cPath, err)
} else if _, err = newConf.WriteTo(cf); err != nil {
return fmt.Errorf("failed to write %s: %v", cPath, err)
}
}
flag.Parse()
return nil
}
func parseConfig(r io.Reader) map[string]string {
obsKeys := make(map[string]string)
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "#") {
continue
}
// find first assignment symbol and parse key, val
i := strings.IndexAny(line, "=:")
if i == -1 {
continue
}
key, val := strings.TrimSpace(line[:i]), strings.TrimSpace(line[i+1:])
if err := flag.Set(key, val); err != nil {
obsKeys[key] = val
continue
}
}
return obsKeys
}
func saveConfig(w io.Writer, obsKeys map[string]string) {
// find flags pointing to the same variable. We will only write the longest
// named flag to the config file, the shorthand version is ignored.
deduped := make(map[flag.Value]flag.Flag)
flag.VisitAll(func(f *flag.Flag) {
if cur, ok := deduped[f.Value]; !ok || utf8.RuneCountInString(f.Name) > utf8.RuneCountInString(cur.Name) {
deduped[f.Value] = *f
}
})
flag.VisitAll(func(f *flag.Flag) {
if cur, ok := deduped[f.Value]; ok && cur.Name == f.Name {
_, usage := flag.UnquoteUsage(f)
usage = strings.Replace(usage, "\n \t", "\n# ", -1)
fmt.Fprintf(w, "\n# %s (default %v)\n", usage, f.DefValue)
fmt.Fprintf(w, "%s=%v\n", f.Name, f.Value.String())
}
})
// if we have obsolete keys left from the old config, preserve them in an
// additional section at the end of the file
if obsKeys != nil && len(obsKeys) > 0 {
fmt.Fprintln(w, "\n\n# The following options are probably deprecated and not used currently!")
for key, val := range obsKeys {
fmt.Fprintf(w, "%v=%v\n", key, val)
}
}
}
|