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) } for key := range files { watcher.Add(key) } go func() { for { trigger := false select { case event, ok := <-watcher.Events: if !ok { return } fmt.Println(event) for key := range files { if event.Has(fsnotify.Write) && event.Name == key { if condition == "any" { trigger = true break } files[key] = true } } if condition == "all" { trigger = true for value := range maps.Values(files) { if !value { trigger = false break } } } case _, ok := <-watcher.Errors: if !ok { return } } if trigger { if len(command) >= 1 { exec.Command(command[0], command[1:]...) } else { quit <- true } } } }() } 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) } } } }