OpenCode 实践课:4 分钟用 Python 写出一个命令行文件变更自动重启工具

项目介绍

开发 Web 服务时,每次改完代码都要手动 Ctrl+C 停掉再重新运行,非常打断思路。nodemon 很好用,但有时我们想要一个更轻量、不依赖 npm 的方案。

今天我们用 OpenCode 在 4 分钟内写一个纯 Python 实现的命令行文件监控自动重启工具 watchrun,功能包括:

  • 监控指定目录的文件变更(新增、修改、删除)
  • 自动终止旧进程并重启命令
  • 支持按文件后缀过滤
  • 防抖机制(短时间内多次变更只重启一次)
  • 彩色终端输出

最终代码约 80 行,纯标准库,无任何第三方依赖。

准备工作

实践过程

第一步:一句话描述需求

打开终端,进入项目目录,我直接对 OpenCode 描述整个需求:

> 我对 OpenCode 说:
>
> "帮我用 Python 写一个命令行文件监控工具 watchrun.py,纯标准库实现,功能如下:
> 1. 接收一个要被执行的命令作为参数
> 2. 用 -d 指定监控目录,默认当前目录
> 3. 用 -e 指定要监控的文件后缀,如 .py .js,不指定则监控所有文件
> 4. 轮询检查文件 mtime 变化,发现有变化后自动杀掉旧进程并重启命令
> 5. 增加防抖机制:文件停止变动 2 秒后才重启,避免频繁重启
> 6. 终端输出加颜色,绿色表示启动、黄色表示重启、红色表示退出
> 7. 支持 Ctrl+C 优雅退出"

OpenCode 理解需求后,直接生成了完整的 watchrun.py

#!/usr/bin/env python3
"""watchrun - 文件变更自动重启工具"""
import argparse
import os
import subprocess
import sys
import time
from pathlib import Path

GREEN = "\033[32m"
YELLOW = "\033[33m"
RED = "\033[31m"
RESET = "\033[0m"

def get_file_snapshot(root, extensions):
    snapshot = {}
    root = Path(root)
    for f in root.rglob("*"):
        if f.is_file():
            if not extensions or f.suffix in extensions:
                try:
                    snapshot[str(f)] = f.stat().st_mtime
                except OSError:
                    pass
    return snapshot

def run_command(cmd):
    return subprocess.Popen(cmd, shell=True)

def main():
    parser = argparse.ArgumentParser(description="文件变更自动重启工具")
    parser.add_argument("command", help="要执行的命令")
    parser.add_argument("-d", "--dir", default=".", help="监控目录 (默认: 当前)")
    parser.add_argument("-e", "--ext", nargs="+", help="只监控指定后缀 (例: .py .js)")
    parser.add_argument("-i", "--interval", type=float, default=1.0, help="轮询间隔秒 (默认: 1)")
    parser.add_argument("--debounce", type=float, default=2.0, help="防抖时间秒 (默认: 2)")
    args = parser.parse_args()

    watch_dir = args.dir
    extensions = set(args.ext) if args.ext else None

    print(f"{GREEN}watchrun 启动 — 监控: {watch_dir}{RESET}")
    print(f"{GREEN}命令: {args.command}{RESET}")

    process = run_command(args.command)
    last_snapshot = get_file_snapshot(watch_dir, extensions)
    last_change = 0.0

    try:
        while True:
            time.sleep(args.interval)
            current_snapshot = get_file_snapshot(watch_dir, extensions)

            changed = False
            added = set(current_snapshot.keys()) - set(last_snapshot.keys())
            removed = set(last_snapshot.keys()) - set(current_snapshot.keys())
            for f in set(current_snapshot.keys()) & set(last_snapshot.keys()):
                if current_snapshot[f] != last_snapshot[f]:
                    changed = True
                    break

            if added or removed or changed:
                last_change = time.time()

            if last_change and (time.time() - last_change) >= args.debounce:
                print(f"{YELLOW}[检测到文件变更,重启中...]{RESET}")
                process.terminate()
                try:
                    process.wait(timeout=5)
                except subprocess.TimeoutExpired:
                    process.kill()
                process = run_command(args.command)
                last_snapshot = get_file_snapshot(watch_dir, extensions)
                last_change = 0.0

    except KeyboardInterrupt:
        print(f"\n{RED}退出...{RESET}")
        process.terminate()
        try:
            process.wait(timeout=5)
        except subprocess.TimeoutExpired:
            process.kill()
        sys.exit(0)

if __name__ == "__main__":
    main()

几秒之内,一个完整的工具就出来了。 代码逻辑清晰:用 Path.rglob 扫描文件并记录 mtime 快照,轮询比对上一轮快照,发现变化后触发重启。防抖通过记录最后变更时间实现。

第二步:测试与修复

写好代码当然要跑一下。我创建一个简单的测试脚本:

# test_app.py - 一个假服务
import time
print("服务启动...")
while True:
    time.sleep(1)

然后运行:

python watchrun.py "python test_app.py" -e .py

效果不错,绿色提示出现,服务成功运行。但我注意到一个问题:当监控目录下文件很多时(比如有 node_modules),扫描速度变慢。

> 我对 OpenCode 说:
>
> "增加一个 --exclude 参数,支持用 glob 模式排除某些目录,如 node_modules__pycache__.git 等默认排除"

OpenCode 在 get_file_snapshot 中增加了排除逻辑,使用 rglob 时跳过匹配到的目录。

第三步:增加跨平台文件系统事件监听

轮询方式虽然简单,但在大目录下效率不够高。我问 OpenCode 能否使用操作系统级别的文件事件监听来提升效率:

> 我对 OpenCode 说:
>
> "能不能优先用 watchdog 库做事件监听,如果没装就 fallback 到轮询模式?"

OpenCode 的建议很务实:"watchdog 确实更高效,但会引入第三方依赖。当前项目定位是零依赖的轻量工具,轮询的 CPU 开销对于 1 秒间隔来说完全可忽略。建议保持纯标准库方案不变。"

我接受了这个建议——保持简单零依赖是正确的取舍。

第四步:展示使用效果

工具完成后,实际使用非常方便。比如开发 Flask 应用时:

python watchrun.py "python app.py" -e .py -d src

修改 src/ 目录下任意 .py 文件,保存后等待 2 秒,服务自动重启:

watchrun 启动 — 监控: src
命令: python app.py
服务启动...
[检测到文件变更,重启中...]
服务启动...

整个过程完全自动,无需手动操作。

完整代码

#!/usr/bin/env python3
"""watchrun - 文件变更自动重启工具"""
import argparse
import os
import subprocess
import sys
import time
from pathlib import Path

GREEN = "\033[32m"
YELLOW = "\033[33m"
RED = "\033[31m"
RESET = "\033[0m"


def get_file_snapshot(root, extensions, exclude_dirs=None):
    if exclude_dirs is None:
        exclude_dirs = {"node_modules", "__pycache__", ".git", ".venv", "venv", ".tox"}
    snapshot = {}
    root = Path(root)
    for f in root.rglob("*"):
        parts = set(f.parts)
        if parts & exclude_dirs:
            continue
        if f.is_file():
            if not extensions or f.suffix in extensions:
                try:
                    snapshot[str(f)] = f.stat().st_mtime
                except OSError:
                    pass
    return snapshot


def run_command(cmd):
    return subprocess.Popen(cmd, shell=True)


def main():
    parser = argparse.ArgumentParser(description="文件变更自动重启工具")
    parser.add_argument("command", help="要执行的命令")
    parser.add_argument("-d", "--dir", default=".", help="监控目录 (默认: 当前)")
    parser.add_argument("-e", "--ext", nargs="+", help="只监控指定后缀 (例: .py .js)")
    parser.add_argument("-i", "--interval", type=float, default=1.0,
                        help="轮询间隔秒 (默认: 1)")
    parser.add_argument("--debounce", type=float, default=2.0,
                        help="防抖时间秒 (默认: 2)")
    parser.add_argument("--exclude", nargs="+",
                        help="额外排除的目录名")
    args = parser.parse_args()

    watch_dir = args.dir
    extensions = set(args.ext) if args.ext else None
    exclude_dirs = {"node_modules", "__pycache__", ".git", ".venv", "venv", ".tox"}
    if args.exclude:
        exclude_dirs |= set(args.exclude)

    print(f"{GREEN}watchrun 启动 — 监控: {watch_dir}{RESET}")
    if extensions:
        print(f"{GREEN}文件后缀: {', '.join(extensions)}{RESET}")
    print(f"{GREEN}命令: {args.command}{RESET}")

    process = run_command(args.command)
    last_snapshot = get_file_snapshot(watch_dir, extensions, exclude_dirs)
    last_change = 0.0

    try:
        while True:
            time.sleep(args.interval)
            current_snapshot = get_file_snapshot(watch_dir, extensions, exclude_dirs)

            changed = False
            added = set(current_snapshot.keys()) - set(last_snapshot.keys())
            removed = set(last_snapshot.keys()) - set(current_snapshot.keys())
            for f in set(current_snapshot.keys()) & set(last_snapshot.keys()):
                if current_snapshot[f] != last_snapshot[f]:
                    changed = True
                    break

            if added or removed or changed:
                last_change = time.time()

            if last_change and (time.time() - last_change) >= args.debounce:
                print(f"{YELLOW}[检测到文件变更,重启中...]{RESET}")
                process.terminate()
                try:
                    process.wait(timeout=5)
                except subprocess.TimeoutExpired:
                    process.kill()
                process = run_command(args.command)
                last_snapshot = get_file_snapshot(watch_dir, extensions, exclude_dirs)
                last_change = 0.0

    except KeyboardInterrupt:
        print(f"\n{RED}退出...{RESET}")
        process.terminate()
        try:
            process.wait(timeout=5)
        except subprocess.TimeoutExpired:
            process.kill()
        sys.exit(0)


if __name__ == "__main__":
    main()

小结

整个工具从想法到可运行,耗时不到 4 分钟。OpenCode 省去的不只是打字的时间,更重要的是省去了"回忆标准库 API"的心智开销——pathlib.Path.rglob 怎么写、argparse 怎么配、subprocess.Popen 的参数是什么,这些细节通通不用查文档。

与传统开发方式相比,使用 OpenCode 的核心区别在于:

  • 从"写代码"变成"描述需求":你负责说清楚要什么,AI 负责实现
  • 迭代极快:改需求、加功能只需一句话,秒级出代码
  • 即时验证:代码生成后立刻能跑,反馈闭环极短

80 行 Python 代码,一个实用的开发辅助工具,这就是 OpenCode 带来的效率。