The recursive file watcher. That runs a command based on the command you give it. Checks if you're running inside of command or powershell on windows. Or defaults to
sh
.
I didn't have something easy to use on Windows 10. Everything wanted me to have extra steps. What better way to practice. Just building the tool myself.
The goal is for a windows folder watcher for my golang development.
After the program is compiled for your Operating System. You should be
able to use it like ./watcher -d /path/to/directory -c "clear; go run hello.go"
- Setup a project folder
watcher
cd watcher
go mod init github.com/YOURUSERNAME/directory_watcher
- This sets up the module folder to allow downloads into. Without it, we cannot rungo get github.com/fsnotify/fsnotify
go get github.com/fsnotify/fsnotify
vim watcher.go
- this is where we will put the program./watcher -d /path/to/directory -c "clear; go run hello.go"
- This is the use of the command.-d
: directory to watch-c
: commands to run
watcher.go
:
package main
import (
"flag"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"github.com/fsnotify/fsnotify"
"github.com/mitchellh/go-ps"
)
var debounceDuration = 5000 * time.Millisecond
func debounce(execution func(), duration time.Duration) func() {
var timer *time.Timer
var mu sync.Mutex
return func() {
mu.Lock()
defer mu.Unlock()
if timer != nil {
timer.Stop()
}
timer = time.AfterFunc(duration, execution)
}
}
func getParentShell() (string, error) {
proc, err := ps.FindProcess(os.Getppid())
if err != nil {
return "", err
}
return proc.Executable(), nil
}
func main() {
dirFlag := flag.String("d", ".", "Directory to watch for changes")
cmdFlag := flag.String("c", "", "Shell command to run on file changes")
flag.Parse()
dirToWatch := *dirFlag
commandToRun := *cmdFlag
// Check if the directory exists
if _, err := os.Stat(dirToWatch); os.IsNotExist(err) {
log.Fatalf("Directory %s does not exist\n", dirToWatch)
}
// Create a new watcher
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
// Add directories and subdirectories to the watcher
err = filepath.Walk(dirToWatch, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
err = watcher.Add(path)
if err != nil {
log.Fatal(err)
}
}
return nil
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Watching directory: %s\n", dirToWatch)
executeCommand := func() {
if commandToRun != "" {
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
shell, err := getParentShell()
if err != nil {
log.Fatalf("Failed to detect parent shell: %s\n", err)
}
if strings.Contains(strings.ToLower(shell), "powershell") ||
strings.Contains(strings.ToLower(shell), "pwsh") {
cmd = exec.Command("powershell.exe", "-Command", commandToRun)
} else {
cmd = exec.Command("cmd.exe", "/C", commandToRun)
}
} else {
cmd = exec.Command("sh", "-c", commandToRun)
}
cmd.Dir = dirToWatch
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
log.Printf("Command execution failed: %s\n", err)
}
}
}
debouncedExecuteCommand := debounce(executeCommand, debounceDuration)
// Process file system events
for {
select {
case event := <-watcher.Events:
if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create {
relativePath, _ := filepath.Rel(dirToWatch, event.Name)
log.Printf("File changed: %s\n", relativePath)
debouncedExecuteCommand()
}
case err := <-watcher.Errors:
log.Printf("Watcher error: %s\n", err)
}
}
}
This first section is just the import statement. What do we want to include?
package main
import (
"flag"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"sync"
"time"
"github.com/fsnotify/fsnotify"
"github.com/mitchellh/go-ps"
)
flag
: how we will digest-c
and-d
or any other flags we would like to havefmt
: Standard package that provides formatted I/O operations, similar to C'sprintf
andscanf
.log
: For logging messages. I could also usefmt
withprintln
but it doesn't have extra features like error handling. Log is more full featured on those fronts.os
: Provides a platform-independent interface to operating system functionalityos/exec
: Our interface to perform executed commands on the systempath/filename
: This is how we will manipulate the filesruntime
: This is how we can interact with the runtime system. Garbage collection, scheduling, and other low-level componentssync
: Provides synchronization primitive, such as Mutexes and WaitGroups, for coordinating the execution of multiple Goroutines.These primitives help you avoid race conditions and ensure that your concurrent code is safe and efficient. Mutexes are used to protect shared resources from concurrent accesstime
:github.com/fsnotify/fsnotify
: This is the package that will watch the directory and alert for changes.github.com/mitchellh/go-ps
: This checks the current process being used to we can tell if it's powershell or not
Now that we understand the imports at the top we can work on the
debounce
functionality.
var debounceDuration = 5000 * time.Millisecond
func debounce(execution func(), duration time.Duration) func() {
var timer *time.Timer
var mu sync.Mutex
return func() {
mu.Lock()
defer mu.Unlock()
if timer != nil {
timer.Stop()
}
timer = time.AfterFunc(duration, execution)
}
}
What is really going on in here? Let's break it down step by step.