Skip to content

Instantly share code, notes, and snippets.

@sczizzo
Last active May 14, 2017 20:11
Show Gist options
  • Save sczizzo/8362cd34823726ed0b043cd98fe8f467 to your computer and use it in GitHub Desktop.
Save sczizzo/8362cd34823726ed0b043cd98fe8f467 to your computer and use it in GitHub Desktop.
rerun
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
}
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)
}
}
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
}
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
}
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