ln -sf 的陷阱:為什麼你的 symlink 會跑進目標目錄裡

Posted on Mar 24, 2026

全文為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 -sfln -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。