2025-06-28 17:14:54 -06:00
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 )
}
2025-06-29 01:31:27 -06:00
defer watcher . Close ( )
2025-06-28 17:14:54 -06:00
for key := range files {
watcher . Add ( key )
2025-06-29 01:31:27 -06:00
fmt . Println ( watcher . WatchList ( ) )
2025-06-28 17:14:54 -06:00
}
go func ( ) {
for {
2025-06-29 01:31:27 -06:00
var renameAdd [ ] string = [ ] string { }
// hasher := sha256.New()
2025-06-28 17:14:54 -06:00
trigger := false
select {
case event , ok := <- watcher . Events :
if ! ok {
return
}
2025-06-29 01:31:27 -06:00
// if _, err := io.Copy(hasher, event.); err != nil {
// }
// fmt.Printf("%x", event)
2025-06-28 17:14:54 -06:00
for key := range files {
2025-06-29 01:31:27 -06:00
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
2025-06-28 17:14:54 -06:00
}
}
2025-06-29 01:31:27 -06:00
} // end for
2025-06-28 17:14:54 -06:00
if condition == "all" {
trigger = true
for value := range maps . Values ( files ) {
if ! value {
trigger = false
break
}
}
2025-06-29 01:31:27 -06:00
} // end if condition
// end case event
2025-06-28 17:14:54 -06:00
case _ , ok := <- watcher . Errors :
if ! ok {
return
}
2025-06-29 01:31:27 -06:00
} // end switch
// end case errors
2025-06-28 17:14:54 -06:00
if trigger {
if len ( command ) >= 1 {
2025-06-29 01:31:27 -06:00
cmd := exec . Command ( command [ 0 ] , command [ 1 : ] ... )
cmd . Stdout = os . Stdout
cmd . Stderr = os . Stderr
cmd . Run ( )
2025-06-28 17:14:54 -06:00
} else {
quit <- true
}
2025-06-29 01:31:27 -06:00
} // end if trigger
for _ , value := range renameAdd {
err := watcher . Add ( value )
if err != nil {
fmt . Println ( err )
}
2025-06-28 17:14:54 -06:00
}
2025-06-29 01:31:27 -06:00
} // end for
} ( ) // end go func
2025-06-28 17:14:54 -06:00
} 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 )
}
}
}
}