ln -sf 的陷阱:為什麼你的 symlink 會跑進目標目錄裡
全文為AI參考實際開發狀況進行記錄並撰寫。
場景
你有一個安裝腳本,負責把 plugin 目錄 symlink 到統一的位置:
for skill_dir in plugins/*/skills/*/; do
ln -sf "$skill_dir" ~/.config/myapp/skills/"$(basename "$skill_dir")"
done
第一次執行 — 一切正常:
~/.config/myapp/skills/deep-research → /repo/plugins/deep-research/skills/deep-research/
問題出在第二次
再跑一次腳本。此時 ~/.config/myapp/skills/deep-research 已經存在,是一個指向目錄的 symlink。問題就出在 ln -sf。
-f(force)flag 只會刪除一般檔案和指向檔案的 symlink。當目標解析為目錄時 — 即使是透過 symlink — ln 不會取代它,而是走進去,在裡面建立 symlink:
# 你以為會發生的事:
ln -sf /repo/.../deep-research/ ~/.config/myapp/skills/deep-research
# ↑ 取代這個 symlink
# 實際發生的事:
ln -sf /repo/.../deep-research/ ~/.config/myapp/skills/deep-research/deep-research
# ↑ 進入目錄,在裡面建立 symlink
因為 ~/.config/myapp/skills/deep-research 是指回 repo 的 symlink,新的 symlink 實際落在:
/repo/plugins/deep-research/skills/deep-research/deep-research
→ /repo/plugins/deep-research/skills/deep-research/ (自己指向自己)
你的 repo 就這樣被一個自指的 symlink 污染了,還會出現在 git status 裡。第三次執行?多一層 deep-research/deep-research/deep-research。無限套娃。
修正方式:一個字元
- ln -sf "$skill_dir" ~/.config/myapp/skills/"$(basename "$skill_dir")"
+ ln -sfn "$skill_dir" ~/.config/myapp/skills/"$(basename "$skill_dir")"
-n(--no-dereference)告訴 ln:如果目標是 symlink,不管它指向什麼,都不要跟進去。搭配 -f 就會先刪掉舊 symlink 再建新的,腳本跑幾次結果都一樣。
行為對照表
| 目標狀態 | ln -sf | ln -sfn |
|---|---|---|
| 不存在 | 建立 symlink | 建立 symlink |
| 既有檔案 / 檔案 symlink | 取代 | 取代 |
| 既有的目錄 symlink | 在裡面建立 | 取代 |
| 既有的真實目錄 | 在裡面建立 | 在裡面建立 |
只要是 symlink 目錄,永遠用 ln -sfn。
快速診斷
如果懷疑已經踩到這個坑:
# 找出自指的 symlink
find . -type l -exec sh -c '
target=$(readlink "$1")
case "$target" in
*/$(basename "$1")/*|*/$(basename "$1")) echo "$1 → $target" ;;
esac
' _ {} \;
記住一件事就好:要 symlink 目錄,永遠用 ln -sfn。一個 n 省你幾小時的 debug。