你是否遇到过这样的场景:改完代码要手动切到终端重新运行,改完 Markdown 要手动执行构建脚本?今天我们用 OpenCode 写一个 watchrun 工具——监控指定目录下的文件变化,一旦检测到改动就自动执行你指定的命令。
确保本地安装了 Python 3.7+ 和 OpenCode。安装 OpenCode 可参考官方文档。
python --version # Python 3.11.0
我先向 OpenCode 描述需求,让它生成代码框架。
> 我对 OpenCode 说:
> "写一个 Python 命令行工具 watchrun,接收一个要执行的命令作为参数,支持 -d 指定监控目录、-p 指定文件匹配模式、-i 指定检测间隔。用 argparse 解析参数。"
OpenCode 分析需求后,生成了如下参数解析骨架:
import argparse
import sys
def main():
parser = argparse.ArgumentParser(description="监控文件变化并自动执行命令")
parser.add_argument("command", nargs=argparse.REMAINDER, help="要执行的命令")
parser.add_argument("-d", "--dir", default=".", help="监控目录")
parser.add_argument("-p", "--pattern", default="*", help="文件模式")
parser.add_argument("-i", "--interval", type=float, default=1.0, help="检测间隔秒")
args = parser.parse_args()
if not args.command:
print("错误:请提供要执行的命令", file=sys.stderr)
sys.exit(1)
要点:nargs=argparse.REMAINDER 很关键,它会把 -- 之后的所有内容作为命令参数完整保留,比如 watchrun -- pytest -v 中的 pytest -v 不会和 watchrun 自身参数混淆。
接着告诉 OpenCode 用轮询方式扫描文件修改时间。
> 我对 OpenCode 说:
> "定义一个 get_file_snapshots 函数,遍历指定目录,用 Path.rglob 收集所有文件的路径和修改时间,返回字典。然后在主循环里每隔 interval 秒比较两次快照,找出新增、修改、删除的文件。"
OpenCode 生成了核心检测逻辑:
from pathlib import Path
def get_file_snapshots(directory, pattern):
snapshots = {}
for filepath in Path(directory).rglob(pattern or "*"):
if filepath.is_file():
snapshots[str(filepath)] = filepath.stat().st_mtime
return snapshots
prev_snapshots = get_file_snapshots(args.dir, args.pattern)
while True:
time.sleep(args.interval)
current_snapshots = get_file_snapshots(args.dir, args.pattern)
changed = []
for path, mtime in current_snapshots.items():
if path not in prev_snapshots or abs(prev_snapshots[path] - mtime) > 0.001:
changed.append(path)
for path in prev_snapshots:
if path not in current_snapshots:
changed.append(path)
prev_snapshots = current_snapshots
要点:rglob 递归匹配所有子目录,st_mtime 浮点数比较时加了 0.001 的容差避免浮点精度问题。OpenCode 自动处理了文件删除的检测——遍历旧快照中不存在于新快照的路径。
> 我对 OpenCode 说:
> "当检测到文件变化时,打印变化文件列表和时间戳,然后调用 subprocess.run 执行命令。执行成功打印绿色 ✓,失败打印红色错误码。最后加上 Ctrl+C 优雅退出。"
OpenCode 补充了执行和输出逻辑:
import subprocess
from datetime import datetime
if changed:
ts = datetime.now().strftime("%H:%M:%S")
print(f"[{ts}] 检测到 {len(changed)} 个文件变化:")
for f in changed[:5]:
print(f" → {f}")
result = subprocess.run(args.command, shell=False)
if result.returncode == 0:
print(f"[{ts}] 执行成功 ✓\n")
else:
print(f"[{ts}] 执行失败 (退出码 {result.returncode})\n")
要点:限制打印最多 5 个变化文件,避免大批量变动时刷屏。shell=False 是安全实践——直接传列表而非拼接 shell 字符串。
最后让 OpenCode 添加彩色输出和启动信息。
> 我对 OpenCode 说:
> "加上 ANSI 颜色输出,启动时显示监控目录、文件模式、执行命令的摘要信息。加一个 --run-once 选项,启动时立即执行一次命令。"
OpenCode 在 main 函数开头添加了色彩常量和摘要打印,代码整体风格一致,一次通过。
#!/usr/bin/env python3
import os
import sys
import time
import argparse
import subprocess
from pathlib import Path
from datetime import datetime
CYAN = "\033[36m"
GREEN = "\033[32m"
RED = "\033[31m"
YELLOW = "\033[33m"
RESET = "\033[0m"
def get_file_snapshots(directory, pattern):
snapshots = {}
for filepath in Path(directory).rglob(pattern or "*"):
if filepath.is_file():
snapshots[str(filepath)] = filepath.stat().st_mtime
return snapshots
def main():
parser = argparse.ArgumentParser(description="监控文件变化并自动执行命令")
parser.add_argument("command", nargs=argparse.REMAINDER, help="要执行的命令")
parser.add_argument("-d", "--dir", default=".", help="监控目录 (默认: .)")
parser.add_argument("-p", "--pattern", default="*", help="文件匹配模式 (默认: *)")
parser.add_argument("-i", "--interval", type=float, default=1.0,
help="检测间隔秒数 (默认: 1)")
parser.add_argument("--run-once", action="store_true",
help="启动时立即执行一次命令")
args = parser.parse_args()
if not args.command:
print(f"{RED}错误:请提供要执行的命令{RESET}", file=sys.stderr)
sys.exit(1)
abs_dir = os.path.abspath(args.dir)
cmd_str = " ".join(args.command)
print(f"{CYAN}监控目录 : {abs_dir}{RESET}")
print(f"{CYAN}文件模式 : {args.pattern}{RESET}")
print(f"{CYAN}执行命令 : {cmd_str}{RESET}")
print(f"{CYAN}检测间隔 : {args.interval}s{RESET}")
print(f"{YELLOW}按 Ctrl+C 停止{RESET}\n")
if args.run_once:
ts = datetime.now().strftime("%H:%M:%S")
print(f"{YELLOW}[{ts}] 首次执行...{RESET}")
subprocess.run(args.command, shell=False)
prev_snapshots = get_file_snapshots(args.dir, args.pattern)
try:
while True:
time.sleep(args.interval)
current = get_file_snapshots(args.dir, args.pattern)
changed = []
for path, mtime in current.items():
if path not in prev_snapshots or abs(prev_snapshots[path] - mtime) > 0.001:
changed.append(path)
for path in prev_snapshots:
if path not in current:
changed.append(path)
if changed:
ts = datetime.now().strftime("%H:%M:%S")
print(f"{GREEN}[{ts}]{RESET} {len(changed)} 个文件变化:")
for f in changed[:5]:
print(f" → {f}")
result = subprocess.run(args.command, shell=False)
if result.returncode == 0:
print(f"{GREEN}[{ts}] 执行成功 ✓{RESET}\n")
else:
print(f"{RED}[{ts}] 执行失败 (退出码 {result.returncode}){RESET}\n")
prev_snapshots = current
except KeyboardInterrupt:
print(f"\n{YELLOW}监控已停止{RESET}")
if __name__ == "__main__":
main()
保存为 watchrun,加可执行权限即可使用:
# 监控当前目录,每次 .py 文件变化就运行测试 python watchrun -- pytest -v -p "*.py" # 监控 src 目录,变化时自动构建 python watchrun -d src -- npm run build # 启动时立即执行一次 python watchrun --run-once -- python main.py
整个开发过程我只用自然语言描述了 4 步需求,OpenCode 完成了从参数解析、文件扫描、进程调用到颜色输出的全部代码。对比传统开发方式,省去了查 subprocess API 文档、手写轮询循环、调试浮点比较等琐碎环节。真正有价值的不是我写出了什么,而是我只需专注于"想要什么"而不是"怎么实现"——这正是 AI 编程助手带来的效率质变。