package main import ( "fmt" "io/fs" "log" "maps" "os" "os/exec" "os/signal" "path/filepath" "time" "github.com/fsnotify/fsnotify" flags "github.com/jessevdk/go-flags" ) const VERSION string = "v0.1.0" var opts struct { Version bool `short:"v" long:"version" description:"Displays version info and exits"` Files []string `short:"f" long:"file" description:"File(s) to listen to (watch)" value-name:"FILE"` Condition string `short:"w" long:"when" description:"If multiple files are specified, choose if any file or all files specified are needed to trigger COMMAND" default:"any" choice:"any" choice:"all"` Checksum bool `short:"c" long:"checksum" description:"Use checksum to determine when file(s) are changed instead of modification time (only used when -i is specified)"` Interval string `short:"i" long:"interval" description:"Use this time interval (ex. 5m30s, 1s) between filesystem checks instead of using kernel events"` } func validateFiles(files []string) error { if len(files) <= 0 { fmt.Println("listen: at least one file (-f) is required") return flags.ErrCommandRequired } for _, file := range files { info, err := os.Stat(file) if err != nil { fmt.Printf("listen: %s\n", err) return err } if info.IsDir() { fmt.Printf("listen: %s: not a file\n", file) return fs.ErrInvalid } _, err = os.Open(file) if err != nil { fmt.Printf("listen: %s\n", err) return err } } return nil } func listen(files map[string]bool, condition string, cksum bool, interval string, command []string) { // catch ^C quit := make(chan bool) c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) // main loop if interval == "" { watcher, err := fsnotify.NewWatcher() if err != nil { log.Fatal(err) } defer watcher.Close() for key := range files { watcher.Add(key) fmt.Println(watcher.WatchList()) } go func() { for { var renameAdd []string = []string{} // hasher := sha256.New() trigger := false select { case event, ok := <-watcher.Events: if !ok { return } // if _, err := io.Copy(hasher, event.); err != nil { // } // fmt.Printf("%x", event) for key := range files { if event.Name == key { if event.Has(fsnotify.Write) { if condition == "any" { trigger = true break } files[key] = true } if event.Has(fsnotify.Rename) { // we need to rewatch file // sleeping small amount to allow CREATE event to propogate time.Sleep(10 * time.Millisecond) // ... then adding to a list to allow additional time renameAdd = append(renameAdd, key) if condition == "any" { trigger = true break } files[key] = true } } } // end for if condition == "all" { trigger = true for value := range maps.Values(files) { if !value { trigger = false break } } } // end if condition // end case event case _, ok := <-watcher.Errors: if !ok { return } } // end switch // end case errors if trigger { if len(command) >= 1 { cmd := exec.Command(command[0], command[1:]...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Run() } else { quit <- true } } // end if trigger for _, value := range renameAdd { err := watcher.Add(value) if err != nil { fmt.Println(err) } } } // end for }() // end go func } else { go func() { for { fmt.Printf("running") time.Sleep(1 * time.Second) } }() } select { case <-c: case <-quit: } } func main() { parser := flags.NewParser(&opts, flags.Default) parser.Usage = "[OPTIONS] -- [COMMAND]" remaining, err := parser.Parse() if err == nil { if opts.Version { fmt.Printf("listen %s\n", VERSION) } else if err := validateFiles(opts.Files); err == nil { var filesMap map[string]bool = make(map[string]bool) success := true for _, file := range opts.Files { abs, err := filepath.Abs(file) if err == nil { filesMap[abs] = false } else { success = false } } if success { listen(filesMap, opts.Condition, opts.Checksum, opts.Interval, remaining) } } } }