使用 fsnotify 实现 Golang 文件变更自动执行命令
在 Golang 中,可以使用 fsnotify
库来监控文件系统变更并触发相应操作。下面我将介绍如何使用 fsnotify
实现文件变更时自动执行命令的功能。
安装 fsnotify
首先安装 fsnotify
库:
go get github.com/fsnotify/fsnotify
基本使用示例
以下是一个简单的示例,当指定文件或目录发生变化时执行命令:
package main
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"github.com/fsnotify/fsnotify"
)
func main() {
// 创建新的 watcher
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
// 启动 goroutine 处理事件
done := make(chan bool)
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
// 只处理写入和创建事件
if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create {
log.Println("检测到变更:", event.Name)
// 执行命令
runCommand("echo", "文件已变更:", event.Name)
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Println("错误:", err)
}
}
}()
// 添加要监控的路径
path := "./" // 监控当前目录
err = watcher.Add(path)
if err != nil {
log.Fatal(err)
}
log.Printf("开始监控目录: %s\n", path)
<-done
}
// 执行命令的函数
func runCommand(name string, arg ...string) {
cmd := exec.Command(name, arg...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
log.Printf("命令执行失败: %v\n", err)
}
}
更完善的实现
下面是一个更完善的实现,包含递归监控子目录、过滤特定文件扩展名等功能:
package main
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/fsnotify/fsnotify"
)
type Config struct {
WatchPath string // 监控路径
Extensions []string // 监控的文件扩展名
Command string // 要执行的命令
CommandArgs []string // 命令参数
DebounceTime int // 防抖时间(毫秒)
}
func main() {
config := Config{
WatchPath: ".",
Extensions: []string{".go", ".txt"},
Command: "echo",
CommandArgs: []string{"文件变更:", "%file%"},
DebounceTime: 500,
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
// 递归添加监控目录
err = addWatchRecursive(watcher, config.WatchPath)
if err != nil {
log.Fatal(err)
}
// 防抖机制
var debounceTimer *time.Timer
lastEventTime := time.Now()
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
// 检查文件扩展名
if !hasValidExtension(event.Name, config.Extensions) {
continue
}
// 只处理写入、创建、重命名事件
if event.Op&fsnotify.Write == fsnotify.Write ||
event.Op&fsnotify.Create == fsnotify.Create ||
event.Op&fsnotify.Rename == fsnotify.Rename {
// 防抖处理
now := time.Now()
if debounceTimer != nil {
debounceTimer.Stop()
}
// 设置新的定时器
debounceTimer = time.AfterFunc(
time.Duration(config.DebounceTime)*time.Millisecond,
func() {
// 替换命令参数中的占位符
args := make([]string, len(config.CommandArgs))
for i, arg := range config.CommandArgs {
args[i] = strings.ReplaceAll(arg, "%file%", event.Name)
}
runCommand(config.Command, args...)
},
)
lastEventTime = now
}
// 如果是新创建的目录,添加到监控
if event.Op&fsnotify.Create == fsnotify.Create {
if stat, err := os.Stat(event.Name); err == nil && stat.IsDir() {
watcher.Add(event.Name)
}
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Println("监控错误:", err)
}
}
}()
log.Printf("开始监控目录: %s (扩展名: %v)\n", config.WatchPath, config.Extensions)
select {}
}
// 递归添加监控目录
func addWatchRecursive(watcher *fsnotify.Watcher, path string) error {
err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return watcher.Add(path)
}
return nil
})
return err
}
// 检查文件扩展名是否匹配
func hasValidExtension(filename string, extensions []string) bool {
if len(extensions) == 0 {
return true
}
ext := filepath.Ext(filename)
for _, e := range extensions {
if strings.EqualFold(ext, e) {
return true
}
}
return false
}
// 执行命令
func runCommand(name string, args ...string) {
cmd := exec.Command(name, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
log.Printf("执行命令: %s %v\n", name, args)
err := cmd.Run()
if err != nil {
log.Printf("命令执行失败: %v\n", err)
}
}
使用说明
-
基本功能:
- 监控指定目录及其子目录
- 当文件变更时执行指定命令
- 支持防抖机制,避免短时间内多次触发
-
配置选项:
WatchPath
: 要监控的路径
Extensions
: 只监控这些扩展名的文件(空数组表示监控所有文件)
Command
: 要执行的命令
CommandArgs
: 命令参数,可用 %file%
占位符表示变更的文件路径
DebounceTime
: 防抖时间(毫秒)
-
扩展功能:
- 自动监控新创建的目录
- 支持递归监控子目录
- 可以过滤特定扩展名的文件
实际应用示例
假设你想在 .go
文件变更时自动运行测试:
config := Config{
WatchPath: ".",
Extensions: []string{".go"},
Command: "go",
CommandArgs: []string{"test", "-v"},
DebounceTime: 1000,
}
或者想在 Markdown 文件变更时重新生成文档:
config := Config{
WatchPath: "./docs",
Extensions: []string{".md"},
Command: "make",
CommandArgs: []string{"docs"},
DebounceTime: 2000,
}
这个实现提供了灵活的文件监控功能,可以根据实际需求进行调整和扩展。