status messages, run first flag
This commit is contained in:
parent
1d3f8e48ae
commit
a4cdfc257d
4 changed files with 200 additions and 65 deletions
11
go.mod
11
go.mod
|
@ -4,6 +4,13 @@ go 1.24
|
|||
|
||||
require github.com/jessevdk/go-flags v1.6.1
|
||||
|
||||
require github.com/fsnotify/fsnotify v1.9.0
|
||||
require (
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.21.0 // indirect
|
||||
require (
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
)
|
||||
|
|
13
go.sum
13
go.sum
|
@ -1,6 +1,15 @@
|
|||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
|
||||
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
|
|
135
listen.go
135
listen.go
|
@ -12,6 +12,7 @@ import (
|
|||
"slices"
|
||||
"time"
|
||||
|
||||
color "github.com/fatih/color"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
|
@ -24,6 +25,7 @@ type Listen struct {
|
|||
Interval time.Duration
|
||||
Command []string
|
||||
Quiet bool
|
||||
RunFirst bool
|
||||
}
|
||||
|
||||
func cksumCheck(l Listen, k string, t *bool) bool {
|
||||
|
@ -61,6 +63,50 @@ func allCheck(l Listen) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func runTrigger(l Listen, f string, r bool, quit chan bool) bool {
|
||||
if !l.Quiet && !r {
|
||||
if l.Condition == "any" {
|
||||
color.New(color.FgWhite).Fprintf(os.Stderr, "& File %s modified. ", f)
|
||||
} else {
|
||||
color.New(color.FgWhite).Fprint(os.Stderr, "& All files have been modified. ")
|
||||
}
|
||||
}
|
||||
|
||||
if len(l.Command) >= 1 {
|
||||
if !l.Quiet {
|
||||
color.New(color.FgWhite).Fprintln(os.Stderr, "Running command...")
|
||||
}
|
||||
|
||||
cmd := exec.Command(l.Command[0], l.Command[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
if rc, ok := err.(*exec.ExitError); ok {
|
||||
if !l.Quiet {
|
||||
color.New(color.FgYellow).Fprintf(os.Stderr, "& WARNING: Command exited with code %d.\n", rc.ExitCode())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !l.Quiet {
|
||||
color.New(color.FgWhite).Fprintln(os.Stderr, "& Returned to listening...")
|
||||
}
|
||||
} else {
|
||||
if !l.Quiet {
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
quit <- true
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func loopFsnotify(l Listen, quit chan bool) {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
|
@ -74,6 +120,7 @@ func loopFsnotify(l Listen, quit chan bool) {
|
|||
|
||||
for {
|
||||
var renameAdd []string = []string{}
|
||||
fileMod := ""
|
||||
trigger := false
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
|
@ -86,11 +133,13 @@ func loopFsnotify(l Listen, quit chan bool) {
|
|||
if l.Cksum {
|
||||
// return value indicates a break is needed
|
||||
if cksumCheck(l, key, &trigger) {
|
||||
fileMod = key
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if event.Has(fsnotify.Write) {
|
||||
if l.Condition == "any" {
|
||||
fileMod = key
|
||||
trigger = true
|
||||
break
|
||||
}
|
||||
|
@ -103,6 +152,7 @@ func loopFsnotify(l Listen, quit chan bool) {
|
|||
// ... then adding to a list to allow additional time
|
||||
renameAdd = append(renameAdd, key)
|
||||
if l.Condition == "any" {
|
||||
fileMod = key
|
||||
trigger = true
|
||||
break
|
||||
}
|
||||
|
@ -125,13 +175,8 @@ func loopFsnotify(l Listen, quit chan bool) {
|
|||
// end case errors
|
||||
|
||||
if trigger {
|
||||
if len(l.Command) >= 1 {
|
||||
cmd := exec.Command(l.Command[0], l.Command[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Run()
|
||||
} else {
|
||||
quit <- true
|
||||
if runTrigger(l, fileMod, false, quit) {
|
||||
return
|
||||
}
|
||||
} // end if trigger
|
||||
|
||||
|
@ -147,12 +192,14 @@ func loopFsnotify(l Listen, quit chan bool) {
|
|||
func loopInterval(l Listen, quit chan bool) {
|
||||
for {
|
||||
time.Sleep(l.Interval)
|
||||
fileMod := ""
|
||||
trigger := false
|
||||
|
||||
for key := range l.FileMap {
|
||||
if l.Cksum {
|
||||
// return value indicates a break is needed
|
||||
if cksumCheck(l, key, &trigger) {
|
||||
fileMod = key
|
||||
break
|
||||
}
|
||||
} else {
|
||||
|
@ -164,6 +211,7 @@ func loopInterval(l Listen, quit chan bool) {
|
|||
if l.IntervalMap[key] != s.ModTime() {
|
||||
l.IntervalMap[key] = s.ModTime()
|
||||
if l.Condition == "any" {
|
||||
fileMod = key
|
||||
trigger = true
|
||||
break
|
||||
}
|
||||
|
@ -179,25 +227,79 @@ func loopInterval(l Listen, quit chan bool) {
|
|||
}
|
||||
|
||||
if trigger {
|
||||
if len(l.Command) >= 1 {
|
||||
cmd := exec.Command(l.Command[0], l.Command[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Run()
|
||||
} else {
|
||||
quit <- true
|
||||
if runTrigger(l, fileMod, false, quit) {
|
||||
return
|
||||
}
|
||||
} // end if trigger
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func startMessage(l Listen) {
|
||||
c := color.New(color.FgWhite)
|
||||
c.Fprintf(os.Stderr, "& listen %s\n", VERSION)
|
||||
c.Fprintln(os.Stderr, "& This program is free software, and comes with ABSOLUTELY NO WARRANTY.")
|
||||
c.Fprintln(os.Stderr, "& Run 'listen --license' for details.")
|
||||
c.Fprintln(os.Stderr, "&")
|
||||
if len(l.Command) >= 1 {
|
||||
c.Fprintln(os.Stderr, "& This command will run:")
|
||||
c.Fprint(os.Stderr, "& ")
|
||||
for _, value := range l.Command {
|
||||
c.Fprint(os.Stderr, value, " ")
|
||||
}
|
||||
|
||||
c.Fprintln(os.Stderr)
|
||||
c.Fprint(os.Stderr, "& When ")
|
||||
} else {
|
||||
c.Fprint(os.Stderr, "& listen will exit when ")
|
||||
}
|
||||
|
||||
if l.Cksum {
|
||||
c.Fprint(os.Stderr, "the checksum of ")
|
||||
}
|
||||
|
||||
if len(l.FileMap) >= 2 {
|
||||
if l.Condition == "any" {
|
||||
c.Fprint(os.Stderr, "any of ")
|
||||
} else {
|
||||
c.Fprint(os.Stderr, "all ")
|
||||
}
|
||||
|
||||
c.Fprint(os.Stderr, "these files have ")
|
||||
} else {
|
||||
c.Fprint(os.Stderr, "this file has ")
|
||||
}
|
||||
|
||||
if l.Cksum {
|
||||
c.Fprintln(os.Stderr, "changed:")
|
||||
} else {
|
||||
c.Fprintln(os.Stderr, "been modified:")
|
||||
}
|
||||
|
||||
for key := range l.FileMap {
|
||||
c.Fprintf(os.Stderr, "& %s\n", key)
|
||||
}
|
||||
}
|
||||
|
||||
func (l Listen) Run() {
|
||||
// catch ^C
|
||||
quit := make(chan bool)
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
|
||||
if !l.Quiet {
|
||||
startMessage(l)
|
||||
}
|
||||
|
||||
if l.RunFirst {
|
||||
if !l.Quiet {
|
||||
color.New(color.FgWhite).Fprintf(os.Stderr, "& -r option specified. ")
|
||||
}
|
||||
runTrigger(l, "", true, quit)
|
||||
} else {
|
||||
color.New(color.FgWhite).Fprintln(os.Stderr, "& Starting now...")
|
||||
}
|
||||
|
||||
// start main loop
|
||||
if l.Interval == time.Duration(0*time.Second) {
|
||||
go loopFsnotify(l, quit)
|
||||
|
@ -207,7 +309,12 @@ func (l Listen) Run() {
|
|||
|
||||
select {
|
||||
case <-c:
|
||||
fmt.Println()
|
||||
case <-quit:
|
||||
}
|
||||
|
||||
if !l.Quiet {
|
||||
color.New(color.FgWhite).Fprintln(os.Stderr, "& Exiting...")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
106
main.go
106
main.go
|
@ -13,25 +13,26 @@ import (
|
|||
flags "github.com/jessevdk/go-flags"
|
||||
)
|
||||
|
||||
const VERSION string = "v0.1.0"
|
||||
const VERSION string = "v0.2.0"
|
||||
|
||||
var opts struct {
|
||||
type Options struct {
|
||||
Version bool `short:"v" long:"version" description:"Displays version info and exits"`
|
||||
Quiet bool `short:"q" long:"quiet" description:"Suppresses all non-error output"`
|
||||
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(files []string, interval string) error {
|
||||
func validateArgs(opts Options, commandLen int) error {
|
||||
// file checks
|
||||
if len(files) <= 0 {
|
||||
if len(opts.Files) <= 0 {
|
||||
fmt.Println("listen: at least one file (-f) is required")
|
||||
return flags.ErrCommandRequired
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
for _, file := range opts.Files {
|
||||
info, err := os.Stat(file)
|
||||
if err != nil {
|
||||
fmt.Printf("listen: %s\n", err)
|
||||
|
@ -51,15 +52,64 @@ func validateArgs(files []string, interval string) error {
|
|||
}
|
||||
|
||||
// interval checks
|
||||
if _, err := time.ParseDuration(interval); err != nil {
|
||||
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()
|
||||
|
@ -67,46 +117,8 @@ func main() {
|
|||
if err == nil {
|
||||
if opts.Version {
|
||||
fmt.Printf("listen %s\n", VERSION)
|
||||
} else if err := validateArgs(opts.Files, opts.Interval); err == nil {
|
||||
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, remaining, opts.Quiet}
|
||||
l.Run()
|
||||
}
|
||||
} else if err := validateArgs(opts, len(remaining)); err == nil {
|
||||
setup(opts, remaining)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue