package main import ( "crypto/sha256" "fmt" "io" "io/fs" "log" "os" "path/filepath" "time" flags "github.com/jessevdk/go-flags" ) const VERSION string = "v0.2.0" type Options struct { Version bool `short:"v" long:"version" description:"Displays version info and exits"` Quiet bool `short:"q" long:"quiet" description:"Suppresses status messages (stderr lines beginning with '&')"` 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 writes/modification time"` Interval string `short:"i" long:"interval" description:"Use this time interval (ex. 5m30s, 1s) between filesystem checks instead of watching kernel events. If the interval is effectively 0 (the default), kernel events are used" default:"0s"` RunFirst bool `short:"r" long:"run" description:"Runs COMMAND (if specified) before starting listen"` } func validateArgs(opts Options, commandLen int) error { // file checks if len(opts.Files) <= 0 { fmt.Println("listen: at least one file (-f) is required") return flags.ErrCommandRequired } for _, file := range opts.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 } } // interval checks if _, err := time.ParseDuration(opts.Interval); err != nil { fmt.Printf("listen: %s\n", err) return err } // run first check if commandLen < 1 && opts.RunFirst { fmt.Println("listen: -r cannot be specified without a command") return flags.ErrInvalidChoice } return nil } func setup(opts Options, command []string) { var filesMap map[string]bool = make(map[string]bool) var cksumMap map[string][]byte = make(map[string][]byte) var intervalMap map[string]time.Time = make(map[string]time.Time) intervalDuration, _ := time.ParseDuration(opts.Interval) success := true for _, file := range opts.Files { hasher := sha256.New() f, err := os.Open(file) if err != nil { success = false break } defer f.Close() s, err := os.Stat(file) if err != nil { success = false break } if _, err := io.Copy(hasher, f); err != nil { log.Fatal(err) } abs, err := filepath.Abs(file) if err == nil { filesMap[abs] = false cksumMap[abs] = hasher.Sum(nil) intervalMap[abs] = s.ModTime() } else { success = false break } } if success { l := Listen{filesMap, cksumMap, intervalMap, opts.Condition, opts.Checksum, intervalDuration, command, opts.Quiet, opts.RunFirst} l.Run() } } func main() { opts := Options{} 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 := validateArgs(opts, len(remaining)); err == nil { setup(opts, remaining) } } }