Last active
May 14, 2017 20:11
-
-
Save sczizzo/8362cd34823726ed0b043cd98fe8f467 to your computer and use it in GitHub Desktop.
rerun
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import "os" | |
func envBool(key string, defaultValue bool) bool { | |
value, ok := os.LookupEnv(key) | |
if !ok { | |
return defaultValue | |
} | |
if value == "true" { | |
return true | |
} | |
return false | |
} | |
func envString(key, defaultValue string) string { | |
value, ok := os.LookupEnv(key) | |
if !ok { | |
return defaultValue | |
} | |
return value | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"fmt" | |
"os" | |
) | |
func main() { | |
opts := ParseOptions() | |
err := NewWatch(opts).Loop() | |
if err != nil { | |
fmt.Fprintf(os.Stderr, "FATAL: %s\n", err) | |
os.Exit(1) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bufio" | |
"flag" | |
"os" | |
) | |
type Options struct { | |
ShowAll bool | |
NoClear bool | |
Replacement string | |
Globs []string | |
Command []string | |
} | |
func ParseOptions() Options { | |
opts := Options{} | |
// Parse regular flags | |
flag.BoolVar(&opts.ShowAll, "showAll", envBool("SHOW_ALL", false), "Ignore .gitignore, .dockerignore, and dotfiles") | |
flag.BoolVar(&opts.NoClear, "noClear", envBool("NO_CLEAR", false), "Don't clear the screen on reloads") | |
flag.StringVar(&opts.Replacement, "replacement", envString("REPLACEMENT", "{}"), "Replace matching opts when a file changes") | |
flag.Parse() | |
// We'll split args at `--` | |
args := flag.Args() | |
idx := 0 | |
for i, arg := range args { | |
if arg == "--" { | |
idx = i | |
break | |
} | |
} | |
// Before `--` are the Globs | |
opts.Globs = []string{} | |
for i := 0; i < idx; i++ { | |
opt := args[0] | |
args = args[1:] | |
opts.Globs = append(opts.Globs, opt) | |
} | |
// Remove the `--` | |
if idx > 0 { | |
_, args = args[0], args[1:] | |
} | |
// What's left is the Command | |
opts.Command = args | |
// Read additional Globs from STDIN | |
stat, _ := os.Stdin.Stat() | |
if (stat.Mode() & os.ModeCharDevice) == 0 { | |
scanner := bufio.NewScanner(os.Stdin) | |
for scanner.Scan() { | |
opts.Globs = append(opts.Globs, scanner.Text()) | |
} | |
} | |
// By default, watch everything | |
if len(opts.Globs) == 0 { | |
opts.Globs = []string{"*", "**/*"} | |
} | |
// By default, echo changed files | |
if len(opts.Command) == 0 { | |
opts.Command = []string{"echo", opts.Replacement} | |
} | |
return opts | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"os" | |
"syscall" | |
) | |
type Ino struct { | |
maj uint64 | |
dev uint64 | |
rdev uint64 | |
} | |
type Stat struct { | |
mtime int64 | |
size int64 | |
ino Ino | |
} | |
func NewStat(fpath string) *Stat { | |
fi, err := os.Stat(fpath) | |
if err != nil { | |
return nil | |
} | |
if fi.IsDir() { | |
return nil | |
} | |
stat, ok := fi.Sys().(*syscall.Stat_t) | |
if !ok { | |
return nil | |
} | |
return &Stat{ | |
mtime: syscall.TimespecToNsec(stat.Mtim), | |
size: stat.Size, | |
ino: Ino{ | |
maj: stat.Ino, | |
dev: stat.Dev, | |
rdev: stat.Rdev, | |
}, | |
} | |
} | |
func WasChanged(old *Stat, new *Stat) bool { | |
if old == nil && new != nil { | |
return true | |
} | |
if old != nil && new == nil { | |
return true | |
} | |
if old == nil && new == nil { | |
return false | |
} | |
if new.mtime != old.mtime { | |
return true | |
} | |
if new.size != old.size { | |
return true | |
} | |
if new.ino != old.ino { | |
return true | |
} | |
return false | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"fmt" | |
"io/ioutil" | |
"os" | |
"os/exec" | |
"path/filepath" | |
"strings" | |
"time" | |
) | |
const ( | |
updateStatsInterval = 1 * time.Second | |
) | |
type watch struct { | |
Options | |
stats map[string]*Stat | |
ch chan []string | |
} | |
func NewWatch(opts Options) watch { | |
return watch{ | |
opts, | |
make(map[string]*Stat), | |
make(chan []string), | |
} | |
} | |
func (w watch) expandGlobs() { | |
for _, glob := range w.Globs { | |
fpaths, err := filepath.Glob(glob) | |
if err != nil { | |
continue | |
} | |
for _, fpath := range fpaths { | |
if _, ok := w.stats[fpath]; !ok { | |
w.stats[fpath] = nil | |
} | |
} | |
} | |
} | |
func (w watch) ignored(fpath string) bool { | |
if w.ShowAll { | |
return false | |
} | |
for _, glob := range []string{".*", "**/.*"} { | |
if dotPaths, err := filepath.Glob(glob); err == nil { | |
for _, dotPath := range dotPaths { | |
if strings.Contains(fpath, dotPath) { | |
return true | |
} | |
} | |
} | |
} | |
ignoreFiles := []string{} | |
for _, glob := range []string{".*ignore", "**/.*ignore"} { | |
if fpaths, err := filepath.Glob(glob); err == nil { | |
ignoreFiles = append(ignoreFiles, fpaths...) | |
} | |
} | |
for _, ignoreFile := range ignoreFiles { | |
if contents, err := ioutil.ReadFile(ignoreFile); err == nil { | |
for _, ignore := range strings.Split(string(contents), "\n") { | |
if ignore == "" { | |
continue | |
} | |
if strings.Contains(fpath, ignore) { | |
return true | |
} | |
} | |
} | |
} | |
return false | |
} | |
func (w watch) updateStats() { | |
w.expandGlobs() | |
fpaths := []string{} | |
for fpath := range w.stats { | |
if ignore := w.ignored(fpath); ignore { | |
continue | |
} | |
old := w.stats[fpath] | |
new := NewStat(fpath) | |
if WasChanged(old, new) { | |
fpaths = append(fpaths, fpath) | |
} | |
if new == nil { | |
delete(w.stats, fpath) | |
} else { | |
w.stats[fpath] = new | |
} | |
} | |
if len(fpaths) > 0 { | |
w.ch <- fpaths | |
} | |
} | |
func (w watch) goUpdateStats() { | |
for { | |
w.updateStats() | |
time.Sleep(updateStatsInterval) | |
} | |
} | |
func (w watch) clear() { | |
if w.NoClear { | |
return | |
} | |
cmd := exec.Command("/usr/bin/env", "clear") | |
cmd.Stdout = os.Stdout | |
cmd.Stderr = os.Stderr | |
if err := cmd.Run(); err != nil { | |
fmt.Fprintf(os.Stderr, "ERROR: clear %s failed: %v\n", w.Command, err) | |
} | |
} | |
func (w watch) constructCommand(replacement string) []string { | |
cmd := make([]string, len(w.Command)) | |
copy(cmd, w.Command) | |
for i, part := range w.Command { | |
if part == w.Replacement { | |
cmd[i] = replacement | |
} else { | |
cmd[i] = part | |
} | |
} | |
return cmd | |
} | |
func (w watch) runCommand(command []string) { | |
cmd := exec.Command(command[0], command[1:]...) | |
cmd.Stdout = os.Stdout | |
cmd.Stderr = os.Stderr | |
if err := cmd.Run(); err != nil { | |
fmt.Fprintf(os.Stderr, "ERROR: command output %s failed: %v\n", w.Command, err) | |
} | |
} | |
func (w watch) Loop() error { | |
go w.goUpdateStats() | |
for { | |
fpaths := <-w.ch | |
w.clear() | |
fmt.Printf("-- %#q %s ---\n\n", w.Command, time.Now().Format(time.UnixDate)) | |
replacement := strings.Join(fpaths, " ") | |
command := w.constructCommand(replacement) | |
w.runCommand(command) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment