pax_global_header00006660000000000000000000000064141122525520014511gustar00rootroot0000000000000052 comment=13e5691dcde5f7c29c144d1cb8c34f453d78505d reflex-0.3.1/000077500000000000000000000000001411225255200127775ustar00rootroot00000000000000reflex-0.3.1/.gitignore000066400000000000000000000000241411225255200147630ustar00rootroot00000000000000/reflex /Reflexfile reflex-0.3.1/CONTRIBUTING.md000066400000000000000000000017671411225255200152430ustar00rootroot00000000000000## Contributing to Reflex There are just a few simple guidelines for reporting issues or submitting patches. ### Issues * For bugs: - Make sure you're using the latest version of Reflex - Describe the expected and actual behavior - Include your operating system with the bug report * Feature requests are unlikely to be implemented unless you're willing to contribute the code. ### Pull Requests * For nontrivial new features, open an issue to describe the change first so we can all be on the same page about whether it's a good fit for Reflex. Tiny improvements and bug fixes don't need this. * Make a branch just for the bugfix/feature. Split it up into reasonably-sized commits. Make one new branch/pull request for each logically distinct bugfix/feature. * If you're not already on the list, add your name to the authors list in README.md (in a commit by itself with your pull request). ### Code style * Run `gofmt` on any code before committing. * Stick to the code style around you. reflex-0.3.1/LICENSE000066400000000000000000000020441411225255200140040ustar00rootroot00000000000000Copyright (c) 2013-2014 Caleb Spare Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. reflex-0.3.1/README.md000066400000000000000000000307711411225255200142660ustar00rootroot00000000000000# Reflex Reflex is a small tool to watch a directory and rerun a command when certain files change. It's great for automatically running compile/lint/test tasks and for reloading your application when the code changes. ## A simple example # Rerun make whenever a .c file changes reflex -r '\.c$' make ## Installation You'll need Go 1.11+ installed: $ go get github.com/cespare/reflex Reflex probably only works on Linux and Mac OS. TODO: provide compiled downloads for linux/darwin amd64. ## Usage The following is given by running `reflex -h`: ``` Usage: reflex [OPTIONS] [COMMAND] COMMAND is any command you'd like to run. Any instance of {} will be replaced with the filename of the changed file. (The symbol may be changed with the --substitute flag.) OPTIONS are given below: --all=false: Include normally ignored files (VCS and editor special files). -c, --config="": A configuration file that describes how to run reflex (or '-' to read the configuration from stdin). -d, --decoration="plain": How to decorate command output. Choices: none, plain, fancy. -g, --glob=[]: A shell glob expression to match filenames. (May be repeated.) -G, --inverse-glob=[]: A shell glob expression to exclude matching filenames. (May be repeated.) -R, --inverse-regex=[]: A regular expression to exclude matching filenames. (May be repeated.) --only-dirs=false: Only match directories (not files). --only-files=false: Only match files (not directories). -r, --regex=[]: A regular expression to match filenames. (May be repeated.) -e, --sequential=false: Don't run multiple commands at the same time. -t, --shutdown-timeout=500ms: Allow services this long to shut down. -s, --start-service=false: Indicates that the command is a long-running process to be restarted on matching changes. --substitute="{}": The substitution symbol that is replaced with the filename in a command. -v, --verbose=false: Verbose mode: print out more information about what reflex is doing. Examples: # Print each .txt file if it changes $ reflex -r '\.txt$' echo {} # Run 'make' if any of the .c files in this directory change: $ reflex -g '*.c' make # Build and run a server; rebuild and restart when .java files change: $ reflex -r '\.java$' -s -- sh -c 'make && java bin/Server' ``` ### Overview Reflex watches file changes in the current working directory and re-runs the command that you specify. The flags change what changes cause the command to be rerun and other behavior. ### Patterns You can specify files to match using either shell glob patterns (`-g`) or regular expressions (`-r`). If you don't specify either, reflex will run your command after any file changes. (Reflex ignores some common editor and version control files; see Ignored files, below.) You can specify inverse matches by using the `--inverse-glob` (`-G`) and `--inverse-regex` (`-R`) flags. If you specify multiple globs/regexes (e.g. `-r foo -r bar -R baz -G x/*/y`), only files that match all patterns and none of the inverse patterns are selected. The shell glob syntax is described [here](http://golang.org/pkg/path/filepath/#Match), while the regular expression syntax is described [here](https://code.google.com/p/re2/wiki/Syntax). The path that is matched against the glob or regular expression does not have a leading `./`. For example, if there is a file `./foobar.txt` that changes, then it will be matched by the regular expression `^foobar`. If the path is a directory, it has a trailing `/`. ### --start-service The `--start-service` flag (short version: `-s`) inverts the behavior of command running: it runs the command when reflex starts and kills/restarts it each time files change. This is expected to be used with an indefinitely-running command, such as a server. You can use this flag to relaunch the server when the code is changed. ### Substitution Reflex provides a way for you to determine, inside your command, what file changed. This is via a substitution symbol. The default is `{}`. Every instance of the substitution symbol inside your command is replaced by the filename. As a simple example, suppose you're writing Coffeescript and you wish to compile the CS files to Javascript when they change. You can do this with: $ reflex -r '\.coffee$' -- coffee -c {} In case you need to use `{}` for something else in your command, you can change the substitution symbol with the `--substitute` flag. ### Configuration file What if you want to run many watches at once? For example, when writing web applications I often want to rebuild/rerun the server when my code changes, but also build SCSS and Coffeescript when those change as well. Instead of running multiple reflex instances, which is cumbersome (and inefficient), you can give reflex a configuration file. The configuration file syntax is simple: each line is a command, and each command is composed of flags and arguments -- just like calling reflex but without the initial `reflex`. Lines that start with `#` are ignored. Commands can span multiple lines if they're \\-continued, or include multi-line strings. Here's an example: # Rebuild SCSS when it changes -r '\.scss$' -- \ sh -c 'sass {} `basename {} .scss`.css' # Restart server when ruby code changes -sr '\.rb$' -- \ ./bin/run_server.sh If you want to change the configuration file and have reflex reload it on the fly, you can run reflex inside reflex: reflex -s -g reflex.conf -- reflex -c reflex.conf This tells reflex to run another reflex process as a service that's restarted whenever `reflex.conf` changes. ### --sequential When using a config file to run multiple simultaneous commands, reflex will run them at the same time (if appropriate). That is, a particular command can only be run once a previous run of that command finishes, but two different commands may run at the same time. This is usually what you want (for speed). As a concrete example, consider this config file: -- sh -c 'for i in `seq 1 5`; do sleep 0.1; echo first; done' -- sh -c 'for i in `seq 1 5`; do sleep 0.1; echo second; done' When this runs, you'll see something like this: [01] second [00] first [01] second [00] first [00] first [01] second [01] second [00] first [01] second [00] first Note that the output is interleaved. (Reflex does ensure that each line of output is not interleaved with a different line.) If, for some reason, you need to ensure that your commands don't run at the same time, you can do this with the `--sequential` (`-e`) flag. Then the output would look like (for example): [01] second [01] second [01] second [01] second [01] second [00] first [00] first [00] first [00] first [00] first ### Decoration By default, each line of output from your command is prefixed with something like `[00]`, which is simply an id that reflex assigns to each command. You can use `--decoration` (`-d`) to change this output: `--decoration=none` will print the output as is; `--decoration=fancy` will color each line differently depending on which command it is, making it easier to distinguish the output. ### Ignored files Reflex ignores a variety of version control and editor metadata files by default. If you wish for these to be included, you can provide reflex with the `--all` flag. You can see a list of regular expressions that match the files that reflex ignores by default [here](https://github.com/cespare/reflex/blob/master/defaultexclude.go#L5). ## Notes and Tips If you don't use `-r` or `-g`, reflex will match every file. Reflex only considers file creation and modification changes. It does not report attribute changes nor deletions. For ignoring directories, it's easiest to use a regular expression: `-R '^dir/'`. Many regex and glob characters are interpreted specially by various shells. You'll generally want to minimize this effect by putting the regex and glob patterns in single quotes. If your command has options, you'll probably need to use `--` to separate the reflex flags from your command flags. For example: `reflex -r '.*\.txt' -- ls -l`. If you're going to use shell things, you need to invoke a shell as a parent process: reflex -- sh -c 'sleep 1 && echo {}' If your command is running with sudo, you'll need a passwordless sudo, because you cannot enter your password in through reflex. It's not difficult to accidentally make an infinite loop with certain commands. For example, consider this command: `reflex -r '\.txt' cp {} {}.bak`. If `foo.txt` changes, then this will create `foo.txt.bak`, `foo.txt.bak.bak`, and so forth, because the regex `\.txt` matches each file. Reflex doesn't have any kind of infinite loop detection, so be careful with commands like `cp`. The restart behavior works as follows: if your program is still running, reflex sends it SIGINT; after 1 second if it's still alive, it gets SIGKILL. The new process won't be started up until the old process is dead. ### Batching Part of what reflex does is apply some heuristics to batch together file changes. There are many reasons that files change on disk, and these changes frequently come in large bursts. For instance, when you save a file in your editor, it probably makes a tempfile and then copies it over the target, leading to several different changes. Reflex hides this from you by batching some changes together. One thing to note, though, is that the the batching is a little different depending on whether or not you have a substitution symbol in your command. If you do not, then updates for different files that all match your pattern can be batched together in a single update that only causes your command to be run once. If you are using a substitution symbol, however, each unique matching file will be batched separately. ### Argument list splitting When you give reflex a command from the commandline (i.e., not in a config file), that command is split into pieces by whatever shell you happen to be using. When reflex parses the config file, however, it must do that splitting itself. For this purpose, it uses [this library](https://github.com/kballard/go-shellquote) which attempts to match `sh`'s argument splitting rules. This difference can lead to slightly different behavior when running commands from a config file. If you're confused, it can help to use `--verbose` (`-v`) which will print out each command as interpreted by reflex. ### Open file limits Reflex currently must hold an open file descriptor for every directory it's watching, recursively. If you run reflex at the top of a big directory tree, you can easily run into file descriptor limits. You might see an error like this: open some/path: too many open files There are several things you can do to get around this problem. 1. Run reflex in the most specific directory possible. Don't run `reflex -g path/to/project/*.c ...` from `$HOME`; instead run reflex in `path/to/project`. 2. Ignore large subdirectories. Reflex already ignores, for instance, `.git/`. If you have other large subdirectories, you can ignore those yourself: `reflex -R '^third_party/' ...` ignores everything under `third_party/` in your project directory. 3. Raise the fd limit using `ulimit` or some other tool. On some systems, this might default to a restrictively small value like 256. See [issue #6](https://github.com/cespare/reflex/issues/6) for some more background on this issue. ## The competition * https://github.com/guard/guard * https://github.com/alexch/rerun * https://github.com/mynyml/watchr * https://github.com/eaburns/Watch * https://github.com/alloy/kicker * https://github.com/clibs/entr ### Why you should use reflex instead * Reflex has no dependencies. No need to install Ruby or anything like that. * Reflex uses an appropriate file watching mechanism to watch for changes efficiently on your platform. * Reflex gives your command the name of the file that changed. * No DSL to learn -- just give it a shell command. * No plugins. * Not tied to any language, framework, workflow, or editor. ## Authors * Benedikt Böhm ([hollow](https://github.com/hollow)) * Caleb Spare ([cespare](https://github.com/cespare)) * PJ Eby ([pjeby](https://github.com/pjeby)) * Rich Liebling ([rliebling](https://github.com/rliebling)) * Seth W. Klein ([sethwklein](https://github.com/sethwklein)) * Vincent Vanackere ([vanackere](https://github.com/vanackere)) reflex-0.3.1/backlog.go000066400000000000000000000046121411225255200147330ustar00rootroot00000000000000package main // A Backlog represents a queue of file paths that may be received while we're // still running a command. There are a couple of different policies for how to // handle this. If there are no {} (substitution sequences) in the command, then // we only need to preserve one of the paths. If there is a {}, then we need to // preserve each unique path in the backlog. type Backlog interface { // Add a path to the backlog. Add(path string) // Show what path should be processed next. Next() string // Remove the next path from the backlog and return whether // the backlog is now empty. RemoveOne() (empty bool) } // A UnifiedBacklog only remembers one backlog item at a time. type UnifiedBacklog struct { s string empty bool } func NewUnifiedBacklog() *UnifiedBacklog { return &UnifiedBacklog{empty: true} } // Add adds path to b if there is not a path there currently. // Otherwise it discards it. func (b *UnifiedBacklog) Add(path string) { if b.empty { b.s = path b.empty = false } } // Next returns the path in b. func (b *UnifiedBacklog) Next() string { if b.empty { panic("Next() called on empty backlog") } return b.s } // RemoveOne removes the path in b. func (b *UnifiedBacklog) RemoveOne() bool { if b.empty { panic("RemoveOne() called on empty backlog") } b.empty = true b.s = "" return true } // A UniqueFilesBacklog keeps a set of the paths it has received. type UniqueFilesBacklog struct { empty bool next string rest map[string]struct{} } func NewUniqueFilesBacklog() *UniqueFilesBacklog { return &UniqueFilesBacklog{ empty: true, next: "", rest: make(map[string]struct{}), } } // Add adds path to the set of files in b. func (b *UniqueFilesBacklog) Add(path string) { defer func() { b.empty = false }() if b.empty { b.next = path return } if path == b.next { return } b.rest[path] = struct{}{} } // Next returns one of the paths in b. func (b *UniqueFilesBacklog) Next() string { if b.empty { panic("Next() called on empty backlog") } return b.next } // RemoveOne removes one of the paths from b (the same path that was returned by // a preceding call to Next). func (b *UniqueFilesBacklog) RemoveOne() bool { if b.empty { panic("RemoveOne() called on empty backlog") } if len(b.rest) == 0 { b.next = "" b.empty = true return true } for next := range b.rest { b.next = next break } delete(b.rest, b.next) return false } reflex-0.3.1/backlog_test.go000066400000000000000000000014001411225255200157620ustar00rootroot00000000000000package main import ( "reflect" "sort" "testing" ) func TestUnifiedBacklog(t *testing.T) { b := NewUnifiedBacklog() b.Add("foo") b.Add("bar") if got, want := b.Next(), "foo"; got != want { t.Errorf("Next(): got %q; want %q", got, want) } if got := b.RemoveOne(); !got { t.Error("RemoveOne(): got !empty") } } func TestUniqueFilesBacklog(t *testing.T) { b := NewUniqueFilesBacklog() b.Add("foo") b.Add("bar") s := []string{b.Next()} if got := b.RemoveOne(); got { t.Error("RemoveOne(): got empty") } s = append(s, b.Next()) if got := b.RemoveOne(); !got { t.Error("RemoveOne(): got !empty") } sort.Strings(s) if want := []string{"bar", "foo"}; !reflect.DeepEqual(s, want) { t.Errorf("Next() result set: got %v; want %v", s, want) } } reflex-0.3.1/config.go000066400000000000000000000107041411225255200145750ustar00rootroot00000000000000package 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, " ")) } reflex-0.3.1/config_test.go000066400000000000000000000040671411225255200156410ustar00rootroot00000000000000package main import ( "reflect" "strings" "testing" "time" "github.com/kr/pretty" ) func TestReadConfigs(t *testing.T) { const in = `-g '*.go' echo {} # Some comment here -r '^a[0-9]+\.txt$' --only-dirs --substitute='[]' echo [] -g '*.go' -s --only-files echo hi -r foo -r bar -R baz -g a \ -G b -G c echo "hello world" ` got, err := readConfigsFromReader(strings.NewReader(in), "test input") if err != nil { t.Fatal(err) } want := []*Config{ { command: []string{"echo", "{}"}, source: "test input, line 1", globs: []string{"*.go"}, subSymbol: "{}", shutdownTimeout: 500 * time.Millisecond, }, { command: []string{"echo", "[]"}, source: "test input, line 4", regexes: []string{`^a[0-9]+\.txt$`}, subSymbol: "[]", shutdownTimeout: 500 * time.Millisecond, onlyDirs: true, }, { command: []string{"echo", "hi"}, source: "test input, line 6", globs: []string{"*.go"}, subSymbol: "{}", startService: true, shutdownTimeout: 500 * time.Millisecond, onlyFiles: true, }, { command: []string{"echo", "hello\nworld"}, source: "test input, line 8", regexes: []string{"foo", "bar"}, globs: []string{"a"}, inverseRegexes: []string{"baz"}, inverseGlobs: []string{"b", "c"}, subSymbol: "{}", shutdownTimeout: 500 * time.Millisecond, }, } if !reflect.DeepEqual(got, want) { t.Errorf("readConfigsFromReader: got diffs:\n%s", strings.Join(pretty.Diff(got, want), "\n")) } } func TestReadConfigsBad(t *testing.T) { for _, in := range []string{ "", "--abc echo hi", "-g '*.go'", "--substitute='' echo hi", "-s echo {}", "--only-files --only-dirs echo hi", } { r := strings.NewReader(in) if configs, err := readConfigsFromReader(r, "test input"); err == nil { for _, config := range configs { if _, err := NewReflex(config); err == nil { t.Errorf("readConfigsFromReader(%q): got nil error", in) } } } } } reflex-0.3.1/defaultexclude.go000066400000000000000000000006421411225255200163260ustar00rootroot00000000000000package main import "regexp" var defaultExcludes = []string{ // VCS dirs `(^|/)\.git/`, `(^|/)\.hg/`, // Vim `~$`, `\.swp$`, // Emacs `\.#`, `(^|/)#.*#$`, // OS X `(^|/)\.DS_Store$`, } var defaultExcludeMatcher multiMatcher func init() { for _, pattern := range defaultExcludes { m := newRegexMatcher(regexp.MustCompile(pattern), true) defaultExcludeMatcher = append(defaultExcludeMatcher, m) } } reflex-0.3.1/go.mod000066400000000000000000000004761411225255200141140ustar00rootroot00000000000000module github.com/cespare/reflex go 1.15 require ( github.com/creack/pty v1.1.11 github.com/fsnotify/fsnotify v1.4.7 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kr/pretty v0.1.0 github.com/ogier/pflag v0.0.1 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e // indirect ) reflex-0.3.1/go.sum000066400000000000000000000024771411225255200141440ustar00rootroot00000000000000github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/ogier/pflag v0.0.1 h1:RW6JSWSu/RkSatfcLtogGfFgpim5p7ARQ10ECk5O750= github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= reflex-0.3.1/main.go000066400000000000000000000115541411225255200142600ustar00rootroot00000000000000package main import ( "fmt" "log" "os" "os/signal" "strings" "sync" "syscall" "time" "github.com/fsnotify/fsnotify" flag "github.com/ogier/pflag" ) const defaultSubSymbol = "{}" var ( reflexes []*Reflex flagConf string flagSequential bool flagDecoration string decoration Decoration verbose bool globalFlags = flag.NewFlagSet("", flag.ContinueOnError) globalConfig = &Config{} reflexID = 0 stdout = make(chan OutMsg, 1) cleanupMu = &sync.Mutex{} ) func usage() { fmt.Fprintf(os.Stderr, `Usage: %s [OPTIONS] [COMMAND] COMMAND is any command you'd like to run. Any instance of {} will be replaced with the filename of the changed file. (The symbol may be changed with the --substitute flag.) OPTIONS are given below: `, os.Args[0]) globalFlags.PrintDefaults() fmt.Fprint(os.Stderr, ` Examples: # Print each .txt file if it changes $ reflex -r '\.txt$' echo {} # Run 'make' if any of the .c files in this directory change: $ reflex -g '*.c' make # Build and run a server; rebuild and restart when .java files change: $ reflex -r '\.java$' -s -- sh -c 'make && java bin/Server' `) } func init() { globalFlags.Usage = usage globalFlags.StringVarP(&flagConf, "config", "c", "", ` A configuration file that describes how to run reflex (or '-' to read the configuration from stdin).`) globalFlags.BoolVarP(&verbose, "verbose", "v", false, ` Verbose mode: print out more information about what reflex is doing.`) globalFlags.BoolVarP(&flagSequential, "sequential", "e", false, ` Don't run multiple commands at the same time.`) globalFlags.StringVarP(&flagDecoration, "decoration", "d", "plain", ` How to decorate command output. Choices: none, plain, fancy.`) globalConfig.registerFlags(globalFlags) } func anyNonGlobalsRegistered() bool { any := false walkFn := func(f *flag.Flag) { switch f.Name { case "config", "verbose", "sequential", "decoration": default: any = true } } globalFlags.Visit(walkFn) return any } func printGlobals() { fmt.Println("Globals set at commandline") walkFn := func(f *flag.Flag) { fmt.Printf("| --%s (-%s) '%s' (default: '%s')\n", f.Name, f.Shorthand, f.Value, f.DefValue) } globalFlags.Visit(walkFn) fmt.Println("+---------") } func cleanup(reason string) { cleanupMu.Lock() fmt.Println(reason) wg := &sync.WaitGroup{} for _, reflex := range reflexes { if reflex.Running() { wg.Add(1) go func(reflex *Reflex) { reflex.terminate() wg.Done() }(reflex) } } wg.Wait() // Give just a little time to finish printing output. time.Sleep(10 * time.Millisecond) os.Exit(0) } func main() { log.SetFlags(0) if err := globalFlags.Parse(os.Args[1:]); err != nil { log.Fatal(err) } globalConfig.command = globalFlags.Args() globalConfig.source = "[commandline]" if verbose { printGlobals() } switch strings.ToLower(flagDecoration) { case "none": decoration = DecorationNone case "plain": decoration = DecorationPlain case "fancy": decoration = DecorationFancy default: log.Fatalf("Invalid decoration %s. Choices: none, plain, fancy.", flagDecoration) } var configs []*Config if flagConf == "" { if flagSequential { log.Fatal("Cannot set --sequential without --config (because you cannot specify multiple commands).") } configs = []*Config{globalConfig} } else { if anyNonGlobalsRegistered() { log.Fatal("Cannot set other flags along with --config other than --sequential, --verbose, and --decoration.") } var err error configs, err = ReadConfigs(flagConf) if err != nil { log.Fatalln("Could not parse configs:", err) } if len(configs) == 0 { log.Fatal("No configurations found") } } for _, config := range configs { reflex, err := NewReflex(config) if err != nil { log.Fatalln("Could not make reflex for config:", err) } if verbose { fmt.Println(reflex) } reflexes = append(reflexes, reflex) } // Catch ctrl-c and make sure to kill off children. signals := make(chan os.Signal, 1) signal.Notify(signals, os.Interrupt) signal.Notify(signals, os.Signal(syscall.SIGTERM)) go func() { s := <-signals reason := fmt.Sprintf("Interrupted (%s). Cleaning up children...", s) cleanup(reason) }() defer cleanup("Cleaning up.") watcher, err := fsnotify.NewWatcher() if err != nil { log.Fatal(err) } defer watcher.Close() changes := make(chan string) broadcastChanges := make([]chan string, len(reflexes)) done := make(chan error) for i := range reflexes { broadcastChanges[i] = make(chan string) } go watch(".", watcher, changes, done, reflexes) go broadcast(broadcastChanges, changes) go printOutput(stdout, os.Stdout) for i, reflex := range reflexes { reflex.Start(broadcastChanges[i]) } log.Fatal(<-done) } func broadcast(outs []chan string, in <-chan string) { for e := range in { for _, out := range outs { out <- e } } } reflex-0.3.1/match.go000066400000000000000000000121611411225255200144230ustar00rootroot00000000000000package main import ( "fmt" "path/filepath" "regexp" "regexp/syntax" "strings" "sync" ) // A Matcher decides whether some filename matches its set of patterns. type Matcher interface { // Match returns whether a filename matches. Match(name string) bool // ExcludePrefix returns whether all paths with this prefix cannot match. // It is allowed to return false negatives but not false positives. // This is used as an optimization for skipping directory watches with // inverted matches. ExcludePrefix(prefix string) bool String() string } // ParseMatchers combines multiple (possibly inverse) regex and glob patterns // into a single Matcher. func ParseMatchers(regexes, inverseRegexes, globs, inverseGlobs []string) (m Matcher, err error) { var matchers multiMatcher if len(regexes) == 0 && len(globs) == 0 { matchers = multiMatcher{matchAll{}} } for _, r := range regexes { regex, err := regexp.Compile(r) if err != nil { return nil, err } matchers = append(matchers, newRegexMatcher(regex, false)) } for _, r := range inverseRegexes { regex, err := regexp.Compile(r) if err != nil { return nil, err } matchers = append(matchers, newRegexMatcher(regex, true)) } for _, g := range globs { matchers = append(matchers, &globMatcher{glob: g}) } for _, g := range inverseGlobs { matchers = append(matchers, &globMatcher{ glob: g, inverse: true, }) } return matchers, nil } // matchAll is an all-accepting Matcher. type matchAll struct{} func (matchAll) Match(name string) bool { return true } func (matchAll) ExcludePrefix(prefix string) bool { return false } func (matchAll) String() string { return "(Implicitly matching all non-excluded files)" } type globMatcher struct { glob string inverse bool } func (m *globMatcher) Match(name string) bool { matches, err := filepath.Match(m.glob, name) if err != nil { return false } return matches != m.inverse } func (m *globMatcher) ExcludePrefix(prefix string) bool { return false } func (m *globMatcher) String() string { s := "Glob" if m.inverse { s = "Inverted glob" } return fmt.Sprintf("%s match: %q", s, m.glob) } type regexMatcher struct { regex *regexp.Regexp inverse bool mu *sync.Mutex // protects following canExcludePrefix bool // This regex has no $, \z, or \b -- see ExcludePrefix excludeChecked bool } func (m *regexMatcher) Match(name string) bool { return m.regex.MatchString(name) != m.inverse } func newRegexMatcher(regex *regexp.Regexp, inverse bool) *regexMatcher { return ®exMatcher{ regex: regex, inverse: inverse, mu: new(sync.Mutex), } } // ExcludePrefix returns whether this matcher cannot possibly match any path // with a particular prefix. The question is: given a regex r and some prefix p // which r accepts, is there any string s that has p as a prefix that r does not // accept? // // With a classic regular expression from CS, this can only be the case if r // ends with $, the end-of-input token (because once the NFA is in an accepting // state, adding more input will not change that). In Go's regular expressions, // I think the only way to construct a regex that would not meet this criteria // is by using zero-width lookahead. There is no arbitrary lookahead in Go, so // the only zero-width lookahead is provided by $, \z, and \b. For instance, the // following regular expressions match the "foo", but not "foobar": // // foo$ // foo\b // (foo$)|(baz$) // // Thus, to choose whether we can exclude this prefix, m must be an inverse // matcher that does not contain the zero-width ops $, \z, and \b. func (m *regexMatcher) ExcludePrefix(prefix string) bool { if !m.inverse { return false } if !m.regex.MatchString(prefix) || m.regex.String() == "" { return false } m.mu.Lock() defer m.mu.Unlock() if !m.excludeChecked { r, err := syntax.Parse(m.regex.String(), syntax.Perl) if err != nil { panic("Cannot compile regex, but it was previously compiled!?!") } r = r.Simplify() stack := []*syntax.Regexp{r} for len(stack) > 0 { cur := stack[len(stack)-1] stack = stack[:len(stack)-1] switch cur.Op { case syntax.OpEndLine, syntax.OpEndText, syntax.OpWordBoundary: m.canExcludePrefix = false goto after } if cur.Sub0[0] != nil { stack = append(stack, cur.Sub0[0]) } stack = append(stack, cur.Sub...) } m.canExcludePrefix = true after: m.excludeChecked = true } return m.canExcludePrefix } func (m *regexMatcher) String() string { s := "Regex" if m.inverse { s = "Inverted regex" } return fmt.Sprintf("%s match: %q", s, m.regex.String()) } // A multiMatcher returns the logical AND of its sub-matchers. type multiMatcher []Matcher func (m multiMatcher) Match(name string) bool { for _, matcher := range m { if !matcher.Match(name) { return false } } return true } func (m multiMatcher) ExcludePrefix(prefix string) bool { for _, matcher := range m { if matcher.ExcludePrefix(prefix) { return true } } return false } func (m multiMatcher) String() string { var s []string for _, matcher := range m { s = append(s, matcher.String()) } return strings.Join(s, "\n") } reflex-0.3.1/match_test.go000066400000000000000000000050111411225255200154560ustar00rootroot00000000000000package main import ( "regexp" "testing" ) func TestMatchers(t *testing.T) { var ( glob = &globMatcher{glob: "foo*"} globInv = &globMatcher{glob: "foo*", inverse: true} regex = newRegexMatcher(regexp.MustCompile("foo.*"), false) regexInv = newRegexMatcher(regexp.MustCompile("foo.*"), true) multi = multiMatcher{ newRegexMatcher(regexp.MustCompile("foo"), false), newRegexMatcher(regexp.MustCompile(`\.go$`), false), newRegexMatcher(regexp.MustCompile("foobar"), true), } ) for _, tt := range []struct { m Matcher s string want bool }{ {glob, "foo", true}, {glob, "foobar", true}, {glob, "bar", false}, {globInv, "foo", false}, {globInv, "foobar", false}, {globInv, "bar", true}, {regex, "foo", true}, {regex, "foobar", true}, {regex, "bar", false}, {regexInv, "foo", false}, {regexInv, "foobar", false}, {regexInv, "bar", true}, {multi, "foo.go", true}, {multi, "foo/bar.go", true}, {multi, "foobar/blah.go", false}, } { if got := tt.m.Match(tt.s); got != tt.want { t.Errorf("(%v).Match(%q): got %t; want %t", tt.m, tt.s, got, tt.want) } } } func TestExcludePrefix(t *testing.T) { m := newRegexMatcher(regexp.MustCompile("foo"), false) if m.ExcludePrefix("bar") { t.Error("m.ExcludePrefix gave true for a non-inverted matcher") } for _, tt := range []struct { re string prefix string want bool }{ {"foo", "foo", true}, {"((foo{3,4})|abc*)+|foo", "foo", true}, {"foo$", "foo", false}, {`foo\b`, "foo", false}, {`(foo\b)|(baz$)`, "foo", false}, } { m := newRegexMatcher(regexp.MustCompile(tt.re), true) if got := m.ExcludePrefix(tt.prefix); got != tt.want { t.Errorf("(%v).ExcludePrefix(%q): got %t; want %t", m, tt.prefix, got, tt.want) } } } func TestDefaultExcludes(t *testing.T) { for _, tt := range []struct { name string want bool }{ {".git/HEAD", false}, {"foo.git", true}, {"foo/bar.git", true}, {"foo/bar/.git/HEAD", false}, {"foo~", false}, {"foo/bar~", false}, {"~foo", true}, {"foo~bar", true}, {"foo.swp", false}, {"foo.swp.bar", true}, {"foo/bar.swp", false}, {"foo.#123", false}, {"foo#123", true}, {"foo/bar.#123", false}, {"#foo#", false}, {"foo/#bar#", false}, {".DS_Store", false}, {"foo/.DS_Store", false}, } { if got := defaultExcludeMatcher.Match(tt.name); got != tt.want { if got { t.Errorf("%q was excluded by the default excludes matcher", tt.name) } else { t.Errorf("%q was not excluded by the default excludes matcher", tt.name) } } } } reflex-0.3.1/print.go000066400000000000000000000024151411225255200144640ustar00rootroot00000000000000package main import ( "fmt" "io" "strings" ) type Decoration int const ( DecorationNone = iota DecorationPlain DecorationFancy ) const ( colorRed = 31 // ANSI colors -- using 32 - 36 colorStart = 32 numColors = 5 ) type OutMsg struct { reflexID int msg string } func infoPrintln(id int, args ...interface{}) { stdout <- OutMsg{id, strings.TrimSpace(fmt.Sprintln(args...))} } func infoPrintf(id int, format string, args ...interface{}) { stdout <- OutMsg{id, fmt.Sprintf(format, args...)} } func printMsg(msg OutMsg, writer io.Writer) { tag := "" if decoration == DecorationFancy || decoration == DecorationPlain { if msg.reflexID < 0 { tag = "[info]" } else { tag = fmt.Sprintf("[%02d]", msg.reflexID) } } if decoration == DecorationFancy { color := (msg.reflexID % numColors) + colorStart if reflexID < 0 { color = colorRed } fmt.Fprintf(writer, "\x1b[01;%dm%s ", color, tag) } else if decoration == DecorationPlain { fmt.Fprintf(writer, tag+" ") } fmt.Fprint(writer, msg.msg) if decoration == DecorationFancy { fmt.Fprintf(writer, "\x1b[m") } if !strings.HasSuffix(msg.msg, "\n") { fmt.Fprintln(writer) } } func printOutput(out <-chan OutMsg, outWriter io.Writer) { for msg := range out { printMsg(msg, outWriter) } } reflex-0.3.1/reflex.go000066400000000000000000000203021411225255200146100ustar00rootroot00000000000000package main import ( "bufio" "bytes" "errors" "fmt" "os" "os/exec" "os/signal" "strings" "sync" "syscall" "time" "github.com/creack/pty" ) // A Reflex is a single watch + command to execute. type Reflex struct { id int source string // Describes what config/line defines this Reflex startService bool backlog Backlog matcher Matcher onlyFiles bool onlyDirs bool command []string subSymbol string done chan struct{} mu *sync.Mutex // protects killed and running killed bool running bool timeout time.Duration // Used for services (startService = true) cmd *exec.Cmd tty *os.File } // NewReflex prepares a Reflex from a Config, with sanity checking. func NewReflex(c *Config) (*Reflex, error) { matcher, err := ParseMatchers(c.regexes, c.inverseRegexes, c.globs, c.inverseGlobs) if err != nil { return nil, fmt.Errorf("error parsing glob/regex: %s", err) } if !c.allFiles { matcher = multiMatcher{defaultExcludeMatcher, matcher} } if len(c.command) == 0 { return nil, errors.New("must give command to execute") } if c.subSymbol == "" { return nil, errors.New("substitution symbol must be non-empty") } substitution := false for _, part := range c.command { if strings.Contains(part, c.subSymbol) { substitution = true break } } var backlog Backlog if substitution { if c.startService { return nil, errors.New("using --start-service does not work with a command that has a substitution symbol") } backlog = NewUniqueFilesBacklog() } else { backlog = NewUnifiedBacklog() } if c.onlyFiles && c.onlyDirs { return nil, errors.New("cannot specify both --only-files and --only-dirs") } if c.shutdownTimeout <= 0 { return nil, errors.New("shutdown timeout cannot be <= 0") } reflex := &Reflex{ id: reflexID, source: c.source, startService: c.startService, backlog: backlog, matcher: matcher, onlyFiles: c.onlyFiles, onlyDirs: c.onlyDirs, command: c.command, subSymbol: c.subSymbol, done: make(chan struct{}), timeout: c.shutdownTimeout, mu: &sync.Mutex{}, } reflexID++ return reflex, nil } func (r *Reflex) String() string { var buf bytes.Buffer fmt.Fprintln(&buf, "Reflex from", r.source) fmt.Fprintln(&buf, "| ID:", r.id) for _, matcherInfo := range strings.Split(r.matcher.String(), "\n") { fmt.Fprintln(&buf, "|", matcherInfo) } if r.onlyFiles { fmt.Fprintln(&buf, "| Only matching files.") } else if r.onlyDirs { fmt.Fprintln(&buf, "| Only matching directories.") } if !r.startService { fmt.Fprintln(&buf, "| Substitution symbol", r.subSymbol) } replacer := strings.NewReplacer(r.subSymbol, "") command := make([]string, len(r.command)) for i, part := range r.command { command[i] = replacer.Replace(part) } fmt.Fprintln(&buf, "| Command:", command) fmt.Fprintln(&buf, "+---------") return buf.String() } // filterMatching passes on messages matching the regex/glob. func (r *Reflex) filterMatching(out chan<- string, in <-chan string) { for name := range in { if !r.matcher.Match(name) { continue } if r.onlyFiles || r.onlyDirs { stat, err := os.Stat(name) if err != nil { continue } if (r.onlyFiles && stat.IsDir()) || (r.onlyDirs && !stat.IsDir()) { continue } } out <- name } } // batch receives file notification events and batches them up. It's a bit // tricky, but here's what it accomplishes: // * When we initially get a message, wait a bit and batch messages before // trying to send anything. This is because the file events come in bursts. // * Once it's time to send, don't do it until the out channel is unblocked. // In the meantime, keep batching. When we've sent off all the batched // messages, go back to the beginning. func (r *Reflex) batch(out chan<- string, in <-chan string) { const silenceInterval = 300 * time.Millisecond for name := range in { r.backlog.Add(name) timer := time.NewTimer(silenceInterval) outer: for { select { case name := <-in: r.backlog.Add(name) if !timer.Stop() { <-timer.C } timer.Reset(silenceInterval) case <-timer.C: for { select { case name := <-in: r.backlog.Add(name) case out <- r.backlog.Next(): if r.backlog.RemoveOne() { break outer } } } } } } } // runEach runs the command on each name that comes through the names channel. // Each {} is replaced by the name of the file. The output of the command is // passed line-by-line to the stdout chan. func (r *Reflex) runEach(names <-chan string) { for name := range names { if r.startService { if r.Running() { infoPrintln(r.id, "Killing service") r.terminate() } infoPrintln(r.id, "Starting service") r.runCommand(name, stdout) } else { r.runCommand(name, stdout) <-r.done r.mu.Lock() r.running = false r.mu.Unlock() } } } func (r *Reflex) terminate() { r.mu.Lock() r.killed = true r.mu.Unlock() // Write ascii 3 (what you get from ^C) to the controlling pty. // (This won't do anything if the process already died as the write will // simply fail.) r.tty.Write([]byte{3}) timer := time.NewTimer(r.timeout) sig := syscall.SIGINT for { select { case <-r.done: return case <-timer.C: if sig == syscall.SIGINT { infoPrintln(r.id, "Sending SIGINT signal...") } else { infoPrintln(r.id, "Sending SIGKILL signal...") } // Instead of killing the process, we want to kill its // whole pgroup in order to clean up any children the // process may have created. if err := syscall.Kill(-1*r.cmd.Process.Pid, sig); err != nil { infoPrintln(r.id, "Error killing:", err) if err.(syscall.Errno) == syscall.ESRCH { // no such process return } } // After SIGINT doesn't do anything, try SIGKILL next. timer.Reset(r.timeout) sig = syscall.SIGKILL } } } func replaceSubSymbol(command []string, subSymbol string, name string) []string { replacer := strings.NewReplacer(subSymbol, name) newCommand := make([]string, len(command)) for i, c := range command { newCommand[i] = replacer.Replace(c) } return newCommand } var seqCommands = &sync.Mutex{} // runCommand runs the given Command. All output is passed line-by-line to the // stdout channel. func (r *Reflex) runCommand(name string, stdout chan<- OutMsg) { command := replaceSubSymbol(r.command, r.subSymbol, name) cmd := exec.Command(command[0], command[1:]...) r.cmd = cmd if flagSequential { seqCommands.Lock() } tty, err := pty.Start(cmd) if err != nil { infoPrintln(r.id, err) return } r.tty = tty // Handle pty size. chResize := make(chan os.Signal, 1) signal.Notify(chResize, syscall.SIGWINCH) go func() { for range chResize { // Intentionally ignore errors in case stdout is not a tty pty.InheritSize(os.Stdout, tty) } }() chResize <- syscall.SIGWINCH // Initial resize. go func() { scanner := bufio.NewScanner(tty) // Allow for lines up to 100 MB. scanner.Buffer(nil, 100e6) for scanner.Scan() { stdout <- OutMsg{r.id, scanner.Text()} } if err := scanner.Err(); errors.Is(err, bufio.ErrTooLong) { infoPrintln(r.id, "Error: subprocess emitted a line longer than 100 MB") } // Intentionally ignore other scanner errors. Unfortunately, // the pty returns a read error when the child dies naturally, // so I'm just going to ignore errors here unless I can find a // better way to handle it. }() r.mu.Lock() r.running = true r.mu.Unlock() go func() { err := cmd.Wait() if !r.Killed() && err != nil { stdout <- OutMsg{r.id, fmt.Sprintf("(error exit: %s)", err)} } r.done <- struct{}{} signal.Stop(chResize) close(chResize) if flagSequential { seqCommands.Unlock() } }() } func (r *Reflex) Start(changes <-chan string) { filtered := make(chan string) batched := make(chan string) go r.filterMatching(filtered, changes) go r.batch(batched, filtered) go r.runEach(batched) if r.startService { // Easy hack to kick off the initial start. infoPrintln(r.id, "Starting service") r.runCommand("", stdout) } } func (r *Reflex) Killed() bool { r.mu.Lock() defer r.mu.Unlock() return r.killed } func (r *Reflex) Running() bool { r.mu.Lock() defer r.mu.Unlock() return r.running } reflex-0.3.1/watch.go000066400000000000000000000041571411225255200144430ustar00rootroot00000000000000package main import ( "os" "path/filepath" "strings" "github.com/fsnotify/fsnotify" ) const chmodMask fsnotify.Op = ^fsnotify.Op(0) ^ fsnotify.Chmod // watch recursively watches changes in root and reports the filenames to names. // It sends an error on the done chan. // As an optimization, any dirs we encounter that meet the ExcludePrefix // criteria of all reflexes can be ignored. func watch(root string, watcher *fsnotify.Watcher, names chan<- string, done chan<- error, reflexes []*Reflex) { if err := filepath.Walk(root, walker(watcher, reflexes)); err != nil { infoPrintf(-1, "Error while walking path %s: %s", root, err) } for { select { case e := <-watcher.Events: if verbose { infoPrintln(-1, "fsnotify event:", e) } stat, err := os.Stat(e.Name) if err != nil { continue } path := normalize(e.Name, stat.IsDir()) if e.Op&chmodMask == 0 { continue } names <- path if e.Op&fsnotify.Create > 0 && stat.IsDir() { if err := filepath.Walk(path, walker(watcher, reflexes)); err != nil { infoPrintf(-1, "Error while walking path %s: %s", path, err) } } // TODO: Cannot currently remove fsnotify watches // recursively, or for deleted files. See: // https://github.com/cespare/reflex/issues/13 // https://github.com/go-fsnotify/fsnotify/issues/40 // https://github.com/go-fsnotify/fsnotify/issues/41 case err := <-watcher.Errors: done <- err return } } } func walker(watcher *fsnotify.Watcher, reflexes []*Reflex) filepath.WalkFunc { return func(path string, f os.FileInfo, err error) error { if err != nil || !f.IsDir() { return nil } path = normalize(path, f.IsDir()) ignore := true for _, r := range reflexes { if !r.matcher.ExcludePrefix(path) { ignore = false break } } if ignore { return filepath.SkipDir } if err := watcher.Add(path); err != nil { infoPrintf(-1, "Error while watching new path %s: %s", path, err) } return nil } } func normalize(path string, dir bool) string { path = strings.TrimPrefix(path, "./") if dir && !strings.HasSuffix(path, "/") { path = path + "/" } return path }