File: config.go

package info (click to toggle)
reflex 0.3.1-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 164 kB
  • sloc: makefile: 4
file content (161 lines) | stat: -rw-r--r-- 4,548 bytes parent folder | download | duplicates (3)
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, " "))
}