日常开发中经常需要对比两个文件的变化——代码审查、配置变更、日志对比都少不了 diff。虽然 Git 自带了 git diff,但对比非仓库文件或需要导出 HTML 报告时就不太方便了。
今天用 OpenCode 从零写一个命令行文本差异对比工具,技术栈为 Python + 标准库(零外部依赖),最终约 85 行代码。功能包括:
npm install -g @opencode-ai/cli)在终端中进入项目目录,输入 opencode 启动交互会话。
我需要一个命令行工具,接收两个文件路径,读取内容后用 difflib 对比。
> 我对 OpenCode 说:
>
> "帮我写一个 Python 命令行工具,接受两个文件路径作为参数,读取文件内容,用 Python 的 difflib 生成 unified diff 并打印到终端。用 argparse 处理参数,支持 --context 指定上下文行数。如果文件不存在要报错。"
> OpenCode 响应:
>
> OpenCode 先检查了 Python 版本,然后一次性生成了完整的代码骨架,包括 argparse 参数解析、文件读取函数、difflib.unified_diff 调用。代码约 30 行,核心逻辑清晰。
关键要点:
argparse + difflib + pathlib,无需额外安装。裸文本 diff 可读性差,我需要给不同的行加颜色——新增行绿色,删除行红色,位置标记青色。
> 我对 OpenCode 说:
>
> "给 diff 输出加 ANSI 颜色。--- / +++ 开头行用粗体白色,@@ 行用青色,+ 开头行用绿色,- 开头行用红色。用 \033 转义序列,Windows 也支持。"
> OpenCode 响应:
>
> OpenCode 定义了 ANSI 颜色常量,新增了 color_line() 函数,根据行首字符返回对应颜色的字符串。然后在打印循环中调用该函数,替换原有的 print(line)。
关键要点:
color_line() 函数。color_line,风格统一。有时候需要把 diff 分享给非技术人员看,HTML 报告比截图更好。
> 我对 OpenCode 说:
>
> "增加 --html OUTPUT 参数,把 diff 结果导出为 HTML 文件。用暗色主题(背景 #1e1e1e),新增行用绿色背景,删除行用红色背景,位置行用蓝色。使用内联 CSS。"
> OpenCode 响应:
>
> OpenCode 新增了 generate_html() 函数,用类名区分增/删/位置行,并在 CSS 中定义对应的暗色主题样式。HTML 使用 <pre> 标签保留格式,用 <span> 包裹着色行。最后在 main() 中追加了写入文件的逻辑。
关键要点:
最后加一个统计功能,显示总共新增/删除了多少行。
> 我对 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 编程助手的实际价值。