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

项目介绍

你是否遇到过这样的场景:改完代码要手动切到终端重新运行,改完 Markdown 要手动执行构建脚本?今天我们用 OpenCode 写一个 watchrun 工具——监控指定目录下的文件变化,一旦检测到改动就自动执行你指定的命令。

  • 技术栈:Python 3,纯标准库,零依赖
  • 功能:监控目录、按文件模式过滤、自动执行命令并输出结果
  • 代码量:约 90 行

准备工作

确保本地安装了 Python 3.7+ 和 OpenCode。安装 OpenCode 可参考官方文档

python --version
# Python 3.11.0

实践过程

第一步:让 OpenCode 生成项目骨架

我先向 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 编程助手带来的效率质变。