-
Notifications
You must be signed in to change notification settings - Fork 9
/
in.go
149 lines (134 loc) · 4.83 KB
/
in.go
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)
}
}
}