CRLF 與 LF:換行字元差異與 Git 設定指南
明明只改了一行程式碼,送出 PR 卻看到整份檔案變成紅綠一片;或是同事傳來的 .sh 腳本在自己電腦上跑不起來,錯誤訊息還帶著一個看不懂的 ^M。這些狀況的源頭幾乎都是同一件事:換行字元。這篇文章會說明 CRLF 與 LF 的差異、Git 的 core.autocrlf 與 .gitattributes 該怎麼設,以及換行已經亂掉時要怎麼一次修好。
- LF(
\n)是 Linux、macOS 與 Git 內部儲存的標準;CRLF(\r\n)是 Windows 沿用自 DOS 的格式。 - 問題不在編輯器怎麼顯示,而在提交進倉庫的內容到底是哪一種。
- 正解是在 repo 根目錄放一份
.gitattributes,讓所有成員與 CI 都吃同一份規則。
還不熟悉開發環境?
可以先看 前端開發工具總覽 與 VS Code 入門教學,再回來調整換行設定。
換行字元是什麼
換行在檔案裡並不是「空白」,而是實實在在的控制字元。這兩個字元的名字來自打字機:CR(Carriage Return,回車) 是把列印頭推回該行最左邊,LF(Line Feed,換行) 是把紙往上捲一行。要換到下一行的開頭,兩個動作都得做,所以早期的電傳打字機需要 CR 加 LF 兩個字元。
後來 CP/M(1980 年代初微電腦的主流作業系統,MS-DOS 的前身)沿用了這個組合,DOS 又沿用 CP/M,Windows 再沿用 DOS,於是 CRLF 一路留到今天。Unix 則在設計之初就認為一個字元夠用,只保留 LF,Linux 與 macOS 都是這一脈。(早期的 classic Mac OS 曾經只用 CR,Mac OS X 之後已改為 LF,現在只會在很舊的檔案裡遇到。)
| 名稱 | 跳脫序列 | 位元組 | 主要平台 |
|---|---|---|---|
| LF | \n | 0A | Linux、macOS、Git 內部儲存 |
| CRLF | \r\n | 0D 0A | Windows、DOS,以及 HTTP、SMTP 等網路協定 |
| CR | \r | 0D | classic Mac OS,已淘汰 |
值得一提的是,網路協定這一側反而是 CRLF 的天下:HTTP 的標頭、SMTP 的信件格式都規定以 CRLF 斷行。只是這層由函式庫處理掉了,寫應用程式時通常感覺不到。
麻煩的地方在於,編輯器一律把這三種都畫成換行,肉眼完全看不出差別。兩個檔案實際上差了幾千個位元組,畫面上卻一模一樣。
怎麼看出檔案現在是哪一種
有三種常用查法:
VS Code 右下角狀態列會直接顯示
LF或CRLF,點一下就能切換目前這個檔案的換行字元(詳見 VS Code 入門教學)。file指令(macOS、Linux、Git Bash):bashfile src/main.js # src/main.js: JavaScript source, ASCII text, with CRLF line terminators輸出裡有
with CRLF line terminators就是 CRLF,沒有這段就是 LF。git ls-files --eol最精準,因為它同時告訴您倉庫裡與工作目錄裡分別是什麼:bashgit ls-files --eol src/ # i/lf w/crlf attr/text=auto src/main.js # i/crlf w/crlf attr/ src/legacy.js
三個欄位的意思是:
i/(index):倉庫裡存的是什麼。理想上全部都該是lf。w/(working tree):您硬碟上的檔案是什麼。這一欄是crlf沒有關係,只要i/是lf即可。attr/:這個檔案套用到的.gitattributes規則,空白代表沒有任何規則管到它。
上面第二行的 i/crlf 就是典型的問題檔案:CRLF 已經被提交進倉庫了。
為什麼會出問題
整份檔案的 diff 變成全紅
這是最常見的災情。當一位 Windows 成員把 LF 檔案存成 CRLF(或反過來),Git 會認為每一行都被改過,因為每行結尾的位元組都不同。結果是:
- PR 顯示「修改 1,200 行」,但真正的變更只有兩行,reviewer 根本找不到重點。
- 只要兩個人同時碰同一個檔案,就會變成整檔衝突,自動合併完全派不上用場。
git blame會把每一行都歸給那次換行變動的 commit,原本的作者與日期全部被蓋掉。
Shell 腳本與工具鏈壞掉
CRLF 對 JavaScript、CSS 這類檔案通常無害,但對要被直譯器執行的檔案是致命的。當 .sh 檔第一行的 shebang 結尾多了一個 \r:
./deploy.sh
# bash: ./deploy.sh: /bin/bash^M: bad interpreter: No such file or directory系統把直譯器路徑讀成了 /bin/bash 加上一個控制字元,自然找不到。node_modules/.bin/ 底下的執行檔、CI 腳本、Docker 的 entrypoint 都會踩到同一顆雷。若您是用 NVM 管理 Node.js 版本,shell 初始化腳本一旦被存成 CRLF,連 nvm 指令本身都會叫不出來。
ESLint 與 Prettier 互相打架
Prettier 的 endOfLine 預設值是 lf,ESLint 也有 linebreak-style 規則。如果編輯器持續產出 CRLF,這些工具會在每一行都報一個錯,錯誤列表瞬間上千條,真正的問題全被淹沒。
Git 的 core.autocrlf
Git 提供 core.autocrlf 做自動轉換。理解它只要抓住兩個方向:
- checkout:倉庫到您的工作目錄(
git clone、git switch時)。 - commit:您的工作目錄到倉庫(
git add寫進索引時)。
| 值 | checkout 時 | commit 時 | 適用 |
|---|---|---|---|
true | LF 轉成 CRLF | CRLF 轉成 LF | Windows 的傳統建議 |
input | 不轉換 | CRLF 轉成 LF | macOS 與 Linux 的建議 |
false | 不轉換 | 不轉換 | 交給 .gitattributes 決定(本文推薦) |
三個值的共同點是:只要不是 false,提交進倉庫的文字檔就會被正規化成 LF,差別只在硬碟上的檔案長什麼樣。這裡的「文字檔」由 Git 依內容自行判斷,被判定成二進位的檔案一律不轉換。
git config --global core.autocrlf true # Windows
git config --global core.autocrlf input # macOS / Linux
git config --global core.autocrlf false # 交給 .gitattributes
git config core.autocrlf # 查目前生效的值
git config --show-origin core.autocrlf # 順便看這個值是哪個設定檔給的core.safecrlf
core.safecrlf 是一道保險,用來偵測「轉換後再轉回來會與原檔不一致」的情況,例如檔案本身就混用了兩種換行:
git config --global core.safecrlf warn # 只警告,仍然提交
git config --global core.safecrlf true # 直接拒絕提交要注意它只負責攔截與提醒,不會幫您修好檔案,真正的修復還是得靠後面的重新正規化。
core.autocrlf 的三個侷限
- 它是每台機器各自的設定,沒有寫進 repo。只要團隊裡有一個人忘了設,問題就會再回來。
- 它靠猜判斷二進位檔。Git 以內容啟發式判斷哪些是文字檔,萬一猜錯,圖片或字型被插入
\r就直接壞掉。 - 它無法逐副檔名指定。
.sh必須是 LF、Windows 的.bat最好是 CRLF,這種需求它給不出來。
.gitattributes:更好的做法
.gitattributes 是放在 repo 根目錄、會一起提交進版控的設定檔。它解掉了上面三個侷限:
- 設定檔跟著專案走,新成員 clone 下來就自動生效,CI 與 GitHub 網頁編輯器也吃同一份。
- 可以逐副檔名指定不同規則。
- 可以明確標記哪些是二進位檔,不靠 Git 自己猜。
- 它的優先權高於
core.autocrlf,兩者同時存在時以它為準。
最小可用的版本只要一行:
* text=auto這行的意思是「所有 Git 判斷為文字的檔案,提交時一律正規化成 LF」。
要注意它只管定了提交的方向。checkout 到工作目錄時要用哪一種,在沒有寫 eol= 的情況下仍由 core.autocrlf 與 core.eol 決定(core.eol 預設為 native,在 Windows 上就是 CRLF)。想讓每台機器的工作目錄也完全一致,就得像下面這樣把 eol= 明確寫出來:
# 預設:所有文字檔在倉庫中一律存成 LF
* text=auto
# 必須是 LF,否則直譯器會讀到 ^M
*.sh text eol=lf
# 必須是 CRLF,Windows 的批次檔對換行敏感
*.bat text eol=crlf
*.cmd text eol=crlf
# 明確標記為二進位,不做任何轉換與 diff
*.png binary
*.woff2 binary各個屬性的意思:
| 屬性 | 意思 |
|---|---|
text | 強制視為文字檔,提交時正規化成 LF |
text=auto | 由 Git 判斷是不是文字檔,是的話才正規化 |
eol=lf | checkout 到工作目錄時也用 LF,不隨作業系統改變 |
eol=crlf | checkout 到工作目錄時用 CRLF |
-text | 明確關閉文字處理,不做任何換行轉換 |
binary | -text -diff 的縮寫,同時關閉轉換與 diff |
只對「之後」生效
.gitattributes 要放在 repo 根目錄並提交進版控才會生效,而且它只影響之後寫入索引的內容。既有檔案不會因為加了這個檔案就自動變乾淨,需要做一次重新正規化,見後面的說明。
前端專案的建議設定
可以直接貼進專案根目錄的完整版本:
* text=auto
# 原始碼與設定檔
*.js text eol=lf
*.ts text eol=lf
*.vue text eol=lf
*.css text eol=lf
*.scss text eol=lf
*.html text eol=lf
*.json text eol=lf
*.md text eol=lf
*.yml text eol=lf
# lock 檔跨平台一致,避免無意義的整檔變動
package-lock.json text eol=lf
# 腳本
*.sh text eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
# 二進位資源
*.png binary
*.jpg binary
*.gif binary
*.ico binary
*.woff binary
*.woff2 binary
*.pdf binaryVS Code 與 EditorConfig 設定
.gitattributes 管的是「進版控的內容」,但新建立的檔案一開始是什麼換行,仍由編輯器決定。兩邊都設好才算完整。
VS Code
在使用者或工作區的 settings.json 加上:
{
"files.eol": "\n",
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true
}files.eol 設為 \n 表示新檔一律用 LF(設 auto 則跟隨作業系統)。已經存在的舊檔不會被改,要改請點右下角狀態列的 CRLF 切換。更多設定可參考 VS Code 入門教學與快捷鍵。
EditorConfig
如果團隊用的編輯器不只一種,.editorconfig 比 VS Code 設定更通用,同樣放在專案根目錄並提交進版控:
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
[*.{bat,cmd}]
end_of_line = crlfVS Code 需要安裝 EditorConfig for VS Code 擴充套件才會讀取這個檔案,可參考 VS Code 必裝擴充套件;WebStorm 與多數 JetBrains IDE 則是內建支援。
與 Prettier、ESLint 對齊
三者要講同一種話,否則會互相覆蓋。Prettier 的設定:
{
"endOfLine": "lf"
}ESLint 若有啟用 linebreak-style,也要一併設成 unix。不過這類排版規則自 ESLint v8.53 起已標記為棄用、v9 也不再內建,新專案交給 Prettier 或 @stylistic 處理即可,不必特地加回來。
兩層各司其職
編輯器與 EditorConfig 管的是新寫出來的檔案,.gitattributes 管的是進版控的檔案。只設前者,別人的機器仍可能提交 CRLF;只設後者,本機新檔的換行仍會跟著作業系統跑。兩層都要有。
已經亂掉了怎麼修:重新正規化
如果倉庫裡已經有 CRLF(git ls-files --eol 看到 i/crlf),加上 .gitattributes 並不會自動修好,要主動做一次重新正規化。
動手前先確認兩件事
這個操作幾乎會碰到專案裡所有檔案,請先把進行中的 PR 都合併掉,否則每個人都會遇到大規模衝突。另外,工作目錄必須是乾淨的,未提交的改動請先 commit 或 stash。
先開一個獨立分支,並把 .gitattributes 單獨提交成一個 commit:
git status # 必須是乾淨的
git switch -c chore/normalize-line-endings
git add .gitattributes
git commit -m "chore: add .gitattributes"接著執行重新正規化。git add --renormalize 會依新規則把所有檔案重新寫進索引,它只動索引、不動您的工作目錄:
git add --renormalize .
git status # 列出換行被改寫的檔案
git diff --cached --stat # 確認只有換行變動,沒有誤含二進位檔
git commit -m "chore: normalize line endings"合併之後,其他成員只要 git pull 就會拿到正規化後的內容,不需要額外操作。
只想處理單一檔案時,把路徑帶上即可:
git add --renormalize path/to/file.js讓 git blame 跳過這次 commit
這個 commit 會蓋掉大量的 blame 資訊。在 repo 根目錄建立 .git-blame-ignore-revs,把正規化那次的 commit hash 寫進去:
git log -1 --format=%H >> .git-blame-ignore-revs
git config blame.ignoreRevsFile .git-blame-ignore-revsGitHub 也會自動讀取這個檔案。
舊版 Git 的做法
--renormalize 需要 Git 2.16 以上。更舊的版本只能用清空索引再還原的老方法:
git rm --cached -r .
git reset --hardgit reset --hard 會丟棄所有未提交的改動,執行前務必確認 git status 是乾淨的。能升級 Git 就不要用這一套。
常見問題
CRLF 和 LF 有什麼差別?
兩者都是檔案裡表示換行的控制字元。LF 是單一位元組 0A,用於 Linux、macOS 以及 Git 內部儲存;CRLF 是 0D 0A 兩個位元組,是 Windows 沿用自 DOS 的格式。差別只在位元組,畫面上看起來完全一樣。
core.autocrlf 應該設成什麼?
若專案已經有 .gitattributes,建議設為 false,把判斷交給版控裡的設定檔。若沒有,Windows 設 true、macOS 與 Linux 設 input,至少能避免把 CRLF 提交進倉庫。
有了 .gitattributes 還需要設 core.autocrlf 嗎?
大致上不需要。.gitattributes 的優先權高於 core.autocrlf,兩者同時存在時以前者為準;設定檔會跟著 repo 進版控,所有成員與 CI 都吃同一份,比要求每個人各自設定可靠得多。唯一的例外是只寫了 text=auto、沒寫 eol= 的檔案,它們 checkout 時仍會看 core.autocrlf 與 core.eol,把 eol= 補明確就不受影響。
為什麼改了設定,舊檔案的換行還是沒變?
換行設定只對之後寫入索引的內容生效,不會回頭改寫已經提交的檔案。要讓既有檔案套用新規則,必須執行 git add --renormalize . 後再提交一次。
執行 .sh 出現 bad interpreter 的 ^M 怎麼辦?
腳本第一行的 shebang 結尾多了 \r,系統把直譯器路徑讀成帶控制字元的字串因而找不到。最快的做法是用編輯器右下角切成 LF 再存檔,或用 dos2unix:
dos2unix deploy.sh沒有 dos2unix 時可用 tr 刪掉 CR,它在 macOS 與 Linux 的行為一致:
tr -d '\r' < deploy.sh > deploy.tmp && mv deploy.tmp deploy.sh(常見的 sed -i 's/\r$//' 只適用 GNU sed,macOS 內建的 BSD sed 參數與跳脫寫法都不同。)
修好單一檔案之後,記得在 .gitattributes 加上 *.sh text eol=lf,才不會下次 clone 又變回來。