-
Notifications
You must be signed in to change notification settings - Fork 136
/
config.go
161 lines (145 loc) · 4.44 KB
/
config.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
150
151
152
153
154
155
156
157
158
159
160
161
package main
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"time"
"github.com/kballard/go-shellquote"
flag "github.com/ogier/pflag"
)
type Config struct {
command []string
source string
regexes []string
globs []string
inverseRegexes []string
inverseGlobs []string
subSymbol string
startService bool
shutdownTimeout time.Duration
onlyFiles bool
onlyDirs bool
allFiles bool
}
func (c *Config) registerFlags(f *flag.FlagSet) {
f.VarP(newMultiString(nil, &c.regexes), "regex", "r", `
A regular expression to match filenames. (May be repeated.)`)
f.VarP(newMultiString(nil, &c.inverseRegexes), "inverse-regex", "R", `
A regular expression to exclude matching filenames.
(May be repeated.)`)
f.VarP(newMultiString(nil, &c.globs), "glob", "g", `
A shell glob expression to match filenames. (May be repeated.)`)
f.VarP(newMultiString(nil, &c.inverseGlobs), "inverse-glob", "G", `
A shell glob expression to exclude matching filenames.
(May be repeated.)`)
f.StringVar(&c.subSymbol, "substitute", defaultSubSymbol, `
The substitution symbol that is replaced with the filename
in a command.`)
f.BoolVarP(&c.startService, "start-service", "s", false, `
Indicates that the command is a long-running process to be
restarted on matching changes.`)
f.DurationVarP(&c.shutdownTimeout, "shutdown-timeout", "t", 500*time.Millisecond, `
Allow services this long to shut down.`)
f.BoolVar(&c.onlyFiles, "only-files", false, `
Only match files (not directories).`)
f.BoolVar(&c.onlyDirs, "only-dirs", false, `
Only match directories (not files).`)
f.BoolVar(&c.allFiles, "all", false, `
Include normally ignored files (VCS and editor special files).`)
}
// ReadConfigs reads configurations from either a file or, as a special case,
// stdin if "-" is given for path.
func ReadConfigs(path string) ([]*Config, error) {
var r io.Reader
name := path
if path == "-" {
r = os.Stdin
name = "standard input"
} else {
f, err := os.Open(flagConf)
if err != nil {
return nil, err
}
defer f.Close()
r = f
}
return readConfigsFromReader(r, name)
}
func readConfigsFromReader(r io.Reader, name string) ([]*Config, error) {
scanner := bufio.NewScanner(r)
lineNo := 0
var configs []*Config
parseFile:
for scanner.Scan() {
lineNo++
// Skip empty lines and comments (lines starting with #).
trimmed := strings.TrimSpace(scanner.Text())
if len(trimmed) == 0 || strings.HasPrefix(trimmed, "#") {
continue
}
// Found a command line; begin parsing it
errorf := fmt.Sprintf("error on line %d of %s: %%s", lineNo, name)
c := &Config{}
c.source = fmt.Sprintf("%s, line %d", name, lineNo)
line := scanner.Text()
parts, err := shellquote.Split(line)
// Loop while the input line ends with \ or an unfinished quoted string
for err != nil {
if err == shellquote.UnterminatedEscapeError {
// Strip the trailing backslash
line = line[:len(line)-1]
}
if !scanner.Scan() {
if scanner.Err() != nil {
// Error reading the file, not EOF, so return that
break parseFile
}
// EOF, return the most recent error with the line where the command started
return nil, fmt.Errorf(errorf, err)
}
// append the next line and parse again
lineNo++
line += "\n" + scanner.Text()
parts, err = shellquote.Split(line)
}
flags := flag.NewFlagSet("", flag.ContinueOnError)
flags.SetOutput(ioutil.Discard)
c.registerFlags(flags)
if err := flags.Parse(parts); err != nil {
return nil, fmt.Errorf(errorf, err)
}
c.command = flags.Args()
configs = append(configs, c)
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading config from %s: %s", name, err)
}
return configs, nil
}
// A multiString is a flag.Getter which collects repeated string flags.
type multiString struct {
vals *[]string
set bool // If false, then vals contains the defaults.
}
func newMultiString(vals []string, p *[]string) *multiString {
*p = vals
return &multiString{vals: p}
}
func (s *multiString) Set(val string) error {
if s.set {
*s.vals = append(*s.vals, val)
} else {
*s.vals = []string{val}
s.set = true
}
return nil
}
func (s *multiString) Get() interface{} {
return s.vals
}
func (s *multiString) String() string {
return fmt.Sprintf("[%s]", strings.Join(*s.vals, " "))
}