OpenCode 实践课:3 分钟用 Python 写出一个命令行文本差异对比工具

项目介绍

日常开发中经常需要对比两个文件的变化——代码审查、配置变更、日志对比都少不了 diff。虽然 Git 自带了 git diff,但对比非仓库文件或需要导出 HTML 报告时就不太方便了。

今天用 OpenCode 从零写一个命令行文本差异对比工具,技术栈为 Python + 标准库(零外部依赖),最终约 85 行代码。功能包括:

  • 对比两个文件并以彩色格式显示差异(增/删/上下文行用不同颜色)
  • 支持自定义上下文行数
  • 支持导出为 HTML 文件(暗色主题)
  • 显示增删行统计

准备工作

  • Python 3.7+
  • OpenCode 已安装(npm install -g @opencode-ai/cli

在终端中进入项目目录,输入 opencode 启动交互会话。

实践过程

第一步:搭建基础框架

我需要一个命令行工具,接收两个文件路径,读取内容后用 difflib 对比。

> 我对 OpenCode 说:
>
> "帮我写一个 Python 命令行工具,接受两个文件路径作为参数,读取文件内容,用 Python 的 difflib 生成 unified diff 并打印到终端。用 argparse 处理参数,支持 --context 指定上下文行数。如果文件不存在要报错。"

> OpenCode 响应:
>
> OpenCode 先检查了 Python 版本,然后一次性生成了完整的代码骨架,包括 argparse 参数解析、文件读取函数、difflib.unified_diff 调用。代码约 30 行,核心逻辑清晰。

关键要点:

  • OpenCode 自动识别并使用了标准库 argparse + difflib + pathlib,无需额外安装。
  • 用自然语言描述需求即可,不用纠结 API 细节——这正是 OpenCode 的价值。

第二步:添加颜色输出

裸文本 diff 可读性差,我需要给不同的行加颜色——新增行绿色,删除行红色,位置标记青色。

> 我对 OpenCode 说:
>
> "给 diff 输出加 ANSI 颜色。--- / +++ 开头行用粗体白色,@@ 行用青色,+ 开头行用绿色,- 开头行用红色。用 \033 转义序列,Windows 也支持。"

> OpenCode 响应:
>
> OpenCode 定义了 ANSI 颜色常量,新增了 color_line() 函数,根据行首字符返回对应颜色的字符串。然后在打印循环中调用该函数,替换原有的 print(line)

关键要点:

  • 我把 ANSI 转义序列的规则描述清楚,OpenCode 就直接生成了正确的 color_line() 函数。
  • 我没有指定函数名,但 OpenCode 的命名很自然地选择了 color_line,风格统一。

第三步:增加 HTML 导出功能

有时候需要把 diff 分享给非技术人员看,HTML 报告比截图更好。

> 我对 OpenCode 说:
>
> "增加 --html OUTPUT 参数,把 diff 结果导出为 HTML 文件。用暗色主题(背景 #1e1e1e),新增行用绿色背景,删除行用红色背景,位置行用蓝色。使用内联 CSS。"

> OpenCode 响应:
>
> OpenCode 新增了 generate_html() 函数,用类名区分增/删/位置行,并在 CSS 中定义对应的暗色主题样式。HTML 使用 <pre> 标签保留格式,用 <span> 包裹着色行。最后在 main() 中追加了写入文件的逻辑。

关键要点:

  • 只描述了视觉效果,OpenCode 自动选择了合适的内联 CSS 方案。
  • 新增代码完全没有破坏原有功能,接口设计良好。

第四步:添加统计信息和收尾

最后加一个统计功能,显示总共新增/删除了多少行。

> 我对 OpenCode 说:
>
> "加一个 --stats 参数,在 diff 输出后显示总计新增行数和删除行数。另外如果两个文件完全相同,输出绿色 ✓ 提示。"

> OpenCode 响应:
>
> OpenCode 在 main() 开头增加了空 diff 的检查,输出绿色提示并提前 return。然后新增 show_stats() 函数,遍历 diff 行统计 +- 的数量,用黄色输出统计结果。

到这里,一个完整的命令行 diff 工具就完成了。整个过程 4 轮对话,每轮 1-2 句话的自然语言描述,零手写代码。

完整代码

#!/usr/bin/env python3
"""命令行文本差异对比工具"""

import sys
import difflib
import argparse
from pathlib import Path

RED = '\033[91m'
GREEN = '\033[92m'
CYAN = '\033[96m'
YELLOW = '\033[93m'
RESET = '\033[0m'
BOLD = '\033[1m'


def read_file(filepath):
    path = Path(filepath)
    if not path.exists():
        print(f"错误: 文件不存在 - {filepath}")
        sys.exit(1)
    return path.read_text(encoding='utf-8').splitlines()


def color_line(line):
    if line.startswith('---') or line.startswith('+++'):
        return f"{BOLD}{line}{RESET}"
    elif line.startswith('@@'):
        return f"{CYAN}{line}{RESET}"
    elif line.startswith('+'):
        return f"{GREEN}{line}{RESET}"
    elif line.startswith('-'):
        return f"{RED}{line}{RESET}"
    return line


def generate_html(diff_lines):
    css = (
        '<style>body{font-family:monospace;background:#1e1e1e;color:#d4d4d4;'
        'padding:20px}.add{background:#1b3a1b;color:#4ec9b0}'
        '.del{background:#3a1b1b;color:#f44747}.info{color:#569cd6}'
        '.header{color:#fff;font-weight:bold}</style>'
    )
    html = ['<!DOCTYPE html><html><head><meta charset="utf-8">', css,
            '</head><body><pre>']

    for line in diff_lines:
        if line.startswith('---') or line.startswith('+++'):
            html.append(f'<span class="header">{line}</span>')
        elif line.startswith('@@'):
            html.append(f'<span class="info">{line}</span>')
        elif line.startswith('+'):
            html.append(f'<span class="add">{line}</span>')
        elif line.startswith('-'):
            html.append(f'<span class="del">{line}</span>')
        else:
            html.append(line)

    html.append('</pre></body></html>')
    return '\n'.join(html)


def show_stats(text1, text2):
    diff = list(difflib.unified_diff(text1, text2))
    added = sum(1 for l in diff if l.startswith('+') and not l.startswith('+++'))
    removed = sum(1 for l in diff if l.startswith('-') and not l.startswith('---'))
    print(f"\n{YELLOW}统计:{RESET} +{added} 行添加, -{removed} 行删除")


def main():
    parser = argparse.ArgumentParser(description='命令行文本差异对比工具')
    parser.add_argument('file1', help='第一个文件路径')
    parser.add_argument('file2', help='第二个文件路径')
    parser.add_argument('-c', '--context', type=int, default=3, help='上下文行数')
    parser.add_argument('--html', metavar='OUTPUT', help='导出为 HTML 文件')
    parser.add_argument('-s', '--stats', action='store_true', help='显示统计信息')

    args = parser.parse_args()

    text1 = read_file(args.file1)
    text2 = read_file(args.file2)

    diff = list(difflib.unified_diff(
        text1, text2,
        fromfile=args.file1, tofile=args.file2,
        n=args.context
    ))

    if not diff:
        print(f"{GREEN}✓ 文件内容相同{RESET}")
        return

    for line in diff:
        print(color_line(line))

    if args.stats:
        show_stats(text1, text2)

    if args.html:
        html_content = generate_html(diff)
        Path(args.html).write_text(html_content, encoding='utf-8')
        print(f"\n{GREEN}✓ HTML 已导出到: {args.html}{RESET}")


if __name__ == '__main__':
    main()

使用方法:

# 基本对比
python diff.py old.txt new.txt

# 指定上下文行数 + 显示统计
python diff.py config.json config.backup.json -c 5 -s

# 导出 HTML 报告
python diff.py v1.py v2.py --html diff.html

小结

用 OpenCode 开发这个工具的感受可以概括为三层:

效率层面——4 轮自然语言对话,约 3 分钟完成一个 85 行的实用工具。如果手写,查 argparse 文档、调 difflib 参数、写 CSS 内联样式,至少需要 20 分钟。

认知层面——不需要记忆 API 细节,只需描述"我想要什么效果"。OpenCode 负责翻译成代码,我的精力集中在产品逻辑上。这改变了编程的心智模型:从"我要怎么写"变成"我要做什么"。

实用层面——这个 diff 工具虽然小,但确实解决了一个真实需求:非 Git 场景下的文件对比 + HTML 导出。85 行、零依赖、即拿即用,正是小而精工具的典型代表。

如果你还没试过 OpenCode,这个 3 分钟的小项目是个不错的起点——用最小的成本体验 AI 编程助手的实际价值。