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, " "))
}
|