Read-then-diff before writing a file to ensure the write is skipped when the content already matches, preventing unnecessary modifications and downstream side e
复制下面这句话,粘贴给 Claude Code、Codex、Cursor 等 AI 编程工具,它会读取安装说明并在你确认后完成安装。
请阅读 https://ai.atlankj.com/install/asset/gh-idempotent-file-replace-5350849299f0 ,按照其中的说明把「idempotent-file-replace」安装到你(当前 AI 工具)中。执行前先告诉我将运行的命令和写入的位置,等我确认。
查看 AI 将读取的安装说明正在读取 GitHub 原文…
内容来自 GitHub 原始文件,由原作者维护。在 GitHub 查看
When tasked with completely replacing a file with specific content, always perform a read-and-compare step before writing. Only write when the file actually differs from the desired content.
| Risk of blind writes | Benefit of idempotent check |
|---|---|
| Triggers filesystem watchers / hot-reload loops | No spurious rebuild when content is unchanged |
| Wastes a tool call on a no-op | One extra read saves a write + all downstream costs |
| Pollutes VCS history with empty diffs | Clean commit history |
| Breaks CI caching layers | Stable mtimes keep caches valid |
Always read the file first, even when you are confident about the replacement.
read_file(path="<target_file>")
If the file does not yet exist, treat its current content as an empty string and proceed to Step 3.
Perform an exact string comparison (whitespace-sensitive).
current = <content returned by read_file>
desired = <full replacement content>
needs_write = (current.strip() != desired.strip())
# Use .strip() to tolerate a single trailing newline difference,
# or compare verbatim if byte-exact output is required.
Alternatively, a unified diff gives a human-readable explanation of what would change and is useful for logging:
import difflib, sys
diff = list(difflib.unified_diff(
current.splitlines(keepends=True),
desired.splitlines(keepends=True),
fromfile="current",
tofile="desired",
))
if not diff:
print("Files are identical — skipping write.")
needs_write = False
else:
print("Diff detected:\n" + "".join(diff))
needs_write = True
if needs_write:
write_file(path="<target_file>", content=desired)
print("File written.")
else:
# Report completion immediately — no write needed.
print("File already matches desired content. No action taken.")
Task: "Replace <file> with <content>"
│
▼
read_file(<file>)
│
▼
content == desired?
┌────┴────┐
YES NO
│ │
▼ ▼
SKIP write_file(<file>, desired)
write │
│ ▼
└──► Report COMPLETE
TARGET = "src/components/Panel.ts"
DESIRED_CONTENT = """// Auto-generated — do not edit
export const Panel = () => { ... };
"""
# Step 1: read
current = read_file(path=TARGET) # tool call
# Step 2: compare
if current.strip() == DESIRED_CONTENT.strip():
print(f"{TARGET} already matches. Skipping write.")
# → COMPLETE
else:
# Step 3: write
write_file(path=TARGET, content=DESIRED_CONTENT) # tool call
print(f"{TARGET} updated.")
# → COMPLETE
| Situation | Handling |
|---|---|
| File does not exist | read_file raises / returns empty → treat as "" → always write |
| Encoding differences | Normalize to UTF-8 before comparison |
| Line-ending differences (CRLF vs LF) | Normalize with .replace("\r\n", "\n") before comparing |
| Byte-exact requirement | Skip .strip() and compare verbatim |
| Large files | Compute SHA-256 hash of both strings for efficiency before full diff |
Never assume a file needs to be written. One cheap read prevents an expensive (and potentially harmful) write.
This pattern applies to any "replace entire file" task regardless of file type: source code, configuration, templates, lock files, generated assets, etc.