Skip to content

Instantly share code, notes, and snippets.

@soypat
Last active January 27, 2022 11:59
Show Gist options
  • Save soypat/56e8db7a30baff25c06d01020c96b2b3 to your computer and use it in GitHub Desktop.
Save soypat/56e8db7a30baff25c06d01020c96b2b3 to your computer and use it in GitHub Desktop.
fsnotify tool
package main
import (
"context"
"fmt"
"io/fs"
stdlog "log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"time"
"github.com/fsnotify/fsnotify"
"github.com/spf13/pflag"
)
var (
dir = "."
recursive = false
ignoreExt = []string{}
cmdPeriod = time.Second
hidden = false
)
var log *stdlog.Logger
func main() {
pflag.Usage = func() {
fmt.Fprintf(os.Stderr, "fsnotify - run a command when file writes occur\nUsage of fsnotify:\n")
pflag.PrintDefaults()
}
// Disable log
fp, _ := os.Open(os.DevNull)
stdlog.SetOutput(fp)
log = stdlog.New(os.Stdout, "[fs]", stdlog.Ltime)
pflag.StringVarP(&dir, "dir", "d", dir, "Directory to observe")
pflag.StringArrayVarP(&ignoreExt, "ignore-ext", "i", ignoreExt, "array of ignored file extensions")
pflag.BoolVarP(&recursive, "recursive", "r", recursive, "Recursive directory observation.")
pflag.DurationVarP(&cmdPeriod, "period", "", cmdPeriod, "Minimum time between subsequent command runs")
pflag.BoolVarP(&hidden, "hidden", "", hidden, "Ignore hidden directories.")
pflag.Parse()
args := pflag.Args()
if len(ignoreExt) > 0 {
log.Println("ignoring extensions:", ignoreExt)
}
if len(args) < 1 {
log.Fatal("no arguments found!")
}
ctx, cancel := context.WithCancel(context.Background())
intChan := make(chan os.Signal)
signal.Notify(intChan, os.Interrupt)
go func() {
log.Println("listening for interrupts")
<-intChan
cancel()
log.Println("got interrupt, context cancelled")
}()
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
go func() {
lastCmd := time.Now()
for {
select {
case <-ctx.Done():
log.Println("context done. ending listening.")
return
case event, ok := <-watcher.Events:
if !ok {
return
}
ignore := false
event.Name = strings.TrimSpace(event.Name)
for i := range ignoreExt {
if strings.HasSuffix(event.Name, ignoreExt[i]) {
ignore = true
break
}
}
if ignore {
break // break switch
}
if event.Op&fsnotify.Write == fsnotify.Write && time.Since(lastCmd) > cmdPeriod {
lastCmd = time.Now()
log.Println("event:", event)
log.Println("The modified file:", event.Name)
err = ExecCMD(ctx)
if err != nil {
log.Printf("error running command: %v", err)
}
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Println("watcher error:", err)
}
}
}()
if !recursive {
err = watcher.Add(".")
} else {
err = filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return nil
}
if !info.IsDir() {
return nil
}
dirs := filepath.SplitList(path)
for i := 0; !hidden && i < len(dirs); i++ { // start at 1 to include base dir
if strings.HasPrefix(dirs[i], ".") && dirs[i] != "." {
return nil // hidden parent directory
}
}
totalPath := filepath.Join(dir, path)
log.Println("watch: adding " + totalPath)
return watcher.Add(totalPath)
})
}
if err != nil {
log.Fatal(err)
}
<-ctx.Done()
time.Sleep(time.Millisecond * 400)
}
func ExecCMD(ctx context.Context) error {
if ctx.Err() != nil {
panic("context done entering execCMD")
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
args := pflag.Args()
if len(args) < 1 {
panic("no arguments!")
}
var cmd *exec.Cmd
out := &strings.Builder{}
if len(args) > 1 {
cmd = exec.CommandContext(ctx, args[0], args[1:]...)
} else {
cmd = exec.CommandContext(ctx, args[0])
}
cmd.Stderr = out
cmd.Stdout = out
log.Println("running: ", cmd.String())
err := cmd.Run()
if err != nil {
return fmt.Errorf("%s: %s", err, out)
}
return nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment