改行コード CRLF と LF の違い:実際に壊れる四つの形
CRLF と LF の差はたった 1 バイトだ。LF は \n(0x0A)1 文字で、Linux と macOS の行末を表す。CRLF は \r\n(0x0D 0x0A)の 2 バイトで、Windows の行末を表す。
ここからが、多くの記事で取り違えられている部分だ。CRLF の改行コードを持つシェルスクリプトは、たいてい失敗しない。ちゃんと走り、期待どおりの出力を出し、終了コード 0 で終わる。壊れるのは変数に入った値のほうである。代入が末尾の \r をそのまま抱え込み、それについて誰も文句を言わないからだ。
実測した挙動は、気づきにくい順に四つのパターンに分かれる。
- 黙って成功する。出力は正しく見え、変数には見えない
\rが入り、終了コードは 0。 - 何も変えないエラー。空行で
: command not foundが出るが、スクリプトは最後まで走り、終了コードは 0。 - 構文エラー。
if、for、関数定義が壊れ、終了コードは 2。 bad interpreter。シバン行が\rを抱え込み、終了コードは 126。
何より先に file yourfile を実行してほしい。1 行の出力で、CRLF と LF のどちら側にいるのかが分かる。
以下はすべて macOS(Darwin arm64)上の bash 3.2.57(1)-release、zsh 5.9、dash、git 2.55.0、node v26.7.0、Python 3.14.6 で計測し、
docker run --rm bash:5(GNU bash 5.3.15(1)-release、aarch64-unknown-linux-musl)を通して Linux 側でも突き合わせた結果である。
1. CRLF と LF と CR:バイトの正体
CRLF は carriage return line feed の略で、文字どおり 2 つの文字を貼り合わせたものだ。
| 名前 | エスケープ | バイト |
|---|---|---|
| 復帰(carriage return) | \r | 0x0D |
| 改行(line feed) | \n | 0x0A |
| CRLF | \r\n | 0x0D 0x0A |
どのプラットフォームがどれを書くか。
| プラットフォーム | 改行コード |
|---|---|
| Windows | CRLF(\r\n) |
| Linux、現行の macOS | LF(\n) |
| Mac Classic、OS 9 以前 | 単独の CR(\r) |
名前の由来は機械そのものだ。テレタイプでは、復帰が印字ヘッドを左端まで戻し、改行が紙を 1 行分送り上げた。Windows はこの 2 つの動作を 2 バイトのまま残し、Unix は 1 つで十分だと決めた。単独の CR は今も古いエクスポートデータに顔を出す。それだけで埋まったファイルは、たいていの Unix ツールから見ると 1 本の巨大な行に見える。
コマンド 1 つでどちらか分かる
file はバイトを読んで終端文字の名前を返す。
$ file d1.txt
d1.txt: ASCII text, with CRLF line terminators
$ file d2.txt
d2.txt: ASCII text ← no suffix means pure LF
$ file d3.txt
d3.txt: ASCII text, with CRLF, LF line terminators ← mixed, file lists both
覚える価値があるのは 3 番目のケースだ。混在した改行コードを持つファイルは、設定の違う 2 つのツールが順番に書き込んだという意味である。エディタが LF で保存し、スクリプトが CRLF で追記し、マージが 2 つのバージョンを縫い合わせた。od -c を使えば、その分かれ目がバイト単位で見える。
$ od -c d3.txt
0000000 a \r \n b \n
0000005
改行コードはバイト層、つまり文字エンコーディングの隣に住んでいる。1 つ上の層で何が起きるかは UTF-8 と UTF-16 のエンコーディングガイド で扱っている。
2. 四つの失敗パターン:黙った成功から終了コード 126 まで
同じ \r\n のファイルでも、行の中身によって結果は 5 通りに分かれる。
| スクリプトの中身 | 実際に起きること | 終了コード |
|---|---|---|
echo hello などの単純なコマンド | 黙って成功し、出力も正しい | 0 |
X=abc の代入 | 黙って成功するが、値の末尾に \r が残る | 0 |
空行、末尾の \ による継続行 | : command not found が出るが、スクリプトは最後まで走り続ける | 0 |
if/fi、for/do/done、f() { | syntax error near unexpected token | 2 |
\r を含むシバン行 | bad interpreter: No such file or directory | 126 |
肝心なのは上の 2 行だ。「CRLF が原因で command not found になる」という説明はあちこちで繰り返されているが、実際に起きているのはそれではない。単純なコマンドは何ひとつ文句を言わない。
黙って成功するのがいちばん危ない
$ printf 'echo hello\r\necho world\r\n' > win.sh && bash win.sh
hello
world
[exit=0]
2 行入れて 2 行出て、終了コードは 0。デバッグするものも、ログから grep するものもない。同じスクリプトに比較を 1 つやらせると、様子が変わる。
$ printf 'V=1.2.3\r\ntest "$V" = "1.2.3" && echo MATCH || echo NO-MATCH\r\n' > v.sh
$ bash v.sh
NO-MATCH
[exit=0]
$V が保持しているのは 1.2.3 ではなく 1.2.3\r だ。macOS でも Linux でも結果は同じだった。バージョンの判定も、if [ "$ENV" = "prod" ] も、ファイルから読んだフィーチャーフラグも、終了コード 0 のまま間違ったほうの分岐へ入っていく。ビルドは緑でログもきれいなので、これが CI でいちばん見つけにくい改行コードのバグである。
何も止めないエラー
CRLF ファイルの中では、空行が \r だけの行として現れる。シェルはそれを実行すべきコマンドとして扱い、失敗してメッセージを出し、それでもスクリプトは次の行へ進んで終了コード 0 で終わる。正確な文言は使っているシェルによって変わる。それが次の節の話だ。
末尾のバックスラッシュによる継続行も同じ壊れ方をする。バックスラッシュと改行の間に \r が挟まるため、継続行が継続行でなくなり、次の行が単独で実行される。
構文エラーと、fi が fi でなくなる理由
c_if.sh: line 5: syntax error: unexpected end of file
[exit=2]
Linux の bash 5.3.15 は、同じファイルについてもう少し詳しく言う。
i.sh: line 5: syntax error: unexpected end of file from `if' command on line 2
ループと関数定義のほうは、5 行目ではなく 1 行目で失敗する。
c_for.sh: line 1: syntax error near unexpected token `do'
c_for.sh: line 1: `for i in 1 2; do'
c_func.sh: line 1: syntax error near unexpected token `{'
c_func.sh: line 1: `f() {'
この仕組みが分かれば、この種類のエラーはまとめて予測できる。bash は閉じる語を fi ではなく fi\r として読む。fi\r はキーワード fi ではないので if ブロックは永遠に閉じず、bash はファイルが尽きるまで読み続ける。エラーが壊れている行ではなく最終行を指すのは、そのためだ。
bad interpreter と終了コード 126
$ ./s.sh
bash: ./s.sh: /bin/bash^M: bad interpreter: No such file or directory
[exit=126]
macOS も Linux も同じ文言を出し、どちらも 126 で終了する。メッセージの中のパスを読んでほしい。/bin/bash^M である。カーネルは #! の後ろから改行までをそのままインタープリタのパスとして受け取るので、\r もその一部になる。そんなファイルは存在しないため、スクリプトの 1 行目が走る前に exec が失敗する。
終了コード 126 は「見つかったが実行できない」場合もカバーするので、起動しないスクリプトが必ず改行コードの問題だとは限らない。ファイルパーミッション も同じ系統の失敗を生む。両者を見分ける手がかりが、パスの中の ^M だ。
\r が目に見えない理由
出力を読んでも助けにはならない。このバイトには見るものも選択するものも何もないので、無理やり可視化するしかない。
$ bash d.sh # script: HOST=example.com / echo "connecting to $HOST:8080"
connecting to example.com:8080 ← what you get
$ bash d.sh | cat -v
connecting to example.com^M:8080^M ← what is actually there
$ bash d.sh | od -c
0000000 c o n n e c t i n g t o e x
0000020 a m p l e . c o m \r : 8 0 8 0 \r
0000040 \n
\r が 2 バイト。通常の出力には現れないまま、どちらもこれからホスト名として使われる文字列の中に紛れている。このバイトが引数へ紛れ込み、無関係なツールが濡れ衣を着せられることもある。
$ bash v.sh # the script pipes through: ... | head -2
head: illegal line count -- 2\r
head の動作は正しい。スクリプトが 2\r を渡したのだ。値の中に迷子の \r が見えるエラーメッセージは、このバグが他人の名前を借りて出てきたものだと思っていい。
3. 自分のエラーメッセージがネットの記事と似ても似つかない理由
1 つのファイル、printf 'echo a\r\n\r\necho b\r\n'、2 行目は \r だけの空行。これを 4 つのシェルで走らせた。
| シェル | バージョン | 正確なメッセージ |
|---|---|---|
| bash(macOS 同梱) | 3.2.57(1)-release | s_blank.sh: line 2: : command not found |
| bash(主要な Linux) | 5.3.15(1)-release | t.sh: line 2: $'\r': command not found |
| zsh | 5.9 | s_blank.sh:2: command not found: ^M |
| dash | — | s_blank.sh: 2: : not found |
検索結果のほぼすべてが引用しているのは 2 行目だ。bash 4 と 5 は印字できない文字を ANSI-C クオートで表示するので、復帰文字が $'\r' になる。macOS に同梱されている bash は 3.2 でそれをやらないため、コロンと空白のあいだに何も入らない。zsh は ^M と出す。dash に至っては command という語ごと落とす。
原因は 1 つで、メッセージだけが 4 通りある。だから自分のエラーをそのまま検索窓へ貼っても、役に立つ結果はまず返ってこない。素っ気ない : command not found は、あの有名なメッセージとまったく同じバグだ。
4. Git:core.autocrlf がリポジトリに実際に入れるもの
git autocrlf の説明は、たいてい定義で止まっている。知りたいのはその先だ。コミットには何が入り、チームメイトがチェックアウトすると何が出てくるのか。保存された blob は git cat-file -p HEAD:f.txt で、作業コピーは rm f.txt && git checkout -- f.txt で実測した。
core.autocrlf | ソースファイル | リポジトリ内の blob | チェックアウト後の作業コピー |
|---|---|---|---|
true | CRLF | LF | CRLF |
true | LF | LF | CRLF |
input | CRLF | LF | LF |
input | LF | LF | LF |
false | CRLF | CRLF | CRLF |
false | LF | LF | LF |
この表から結論が 3 つ出てくる。
trueもinputも、リポジトリに LF が入ることは保証する。違いはチェックアウトだけだ。trueは CRLF へ戻し、inputはファイルに触らない。- CRLF をコミットに入れるのは
falseだけである。 誰が復帰文字をコミットしたのかと聞かれたら、この行が答えだ。 - 意外なのは 2 行目。
trueの下では、ディスク上で LF だったファイルがチェックアウト後に CRLF になって戻ってくる。
Git は書き換えの前に、次の 2 つのどちらかの形で予告する。
warning: in the working copy of 'f.txt', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'f.txt', CRLF will be replaced by LF the next time Git touches it
「何も変えていないのに、git はファイル全体が変わったと言う」
また 2 行目の話だ。git config core.autocrlf true にしていると、チェックアウトが作業ディレクトリの LF ファイルを CRLF へ書き換える。すると全行が blob と 1 バイトずつ違うことになるので、git diff は全行を変更ありとして報告し、プルリクエストには誰も触っていないファイルが丸ごと書き直されたように表示される。やったのはチェックアウトのフィルタである。
逆向きでも同じノイズが出る。false のチームメイトが CRLF をコミットし、こちらは input にしていると、一度も開いたことのないファイルが自分の差分に現れる。
5. .gitattributes がチーム単位の答えになる
core.autocrlf は 1 台のマシンの設定であり、他の誰からも見えない。.gitattributes はリポジトリの中のファイルなので、クローンのたびに一緒についてくる。両者が食い違ったときは属性のほうが勝つ。4 つの形すべてを試した。
.gitattributes | core.autocrlf | ソース | blob | チェックアウト後 | 勝者 |
|---|---|---|---|---|---|
* text=auto | false | CRLF | LF | LF | 属性 |
* text eol=crlf | input | LF | LF | CRLF | 属性 |
* -text | true | CRLF | CRLF | CRLF | 属性 |
* text eol=lf | true | CRLF | LF | LF | 属性 |
3 行目は覚えておく価値がある。-text は変換を完全に無効にし、それでいて core.autocrlf=true に打ち勝つ。バイトが一切変わってはいけないファイルは、これで守る。
そのままコピーできる .gitattributes
* text=auto
*.sh text eol=lf
*.bash text eol=lf
Makefile text eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
*.png -text
*.jpg -text
*.pdf -text
*.zip -text
text=auto は、Git がテキストと判定したものをすべてリポジトリ内では LF へ正規化する。明示的な eol=lf の行は、誰が書いたかによらず LF でなければならないファイルを押さえるためのものだ。CRLF の .sh は、そのまま終了コード 126 へ直行する。Windows 用のスクリプトには逆の理由で eol=crlf を、バイナリのパターンには一切変換されないよう -text を与えている。
core.safecrlf と core.eol
core.autocrlf の隣に顔を出すが、やることが違う設定が 2 つある。
core.safecrlfは変換器ではなく防護柵だ。変換が往復しない場合、つまり正規化で情報が失われる混在ファイルのような場合に、trueは操作そのものを拒否し、warnは警告を出したうえで通す。どのバイトが保存されるかを変えることはない。損失のある変換を黙って実行するのを拒むだけである。core.eolは、core.autocrlfがfalseのときに、textと印の付いたファイルを Git が作業ディレクトリへどの改行コードで書き出すかを決める。値はlf、crlf、nativeの 3 つ。core.autocrlfのほうが優先されるので、autocrlf を有効にしたままのマシンでcore.eolを設定しても、何も起きていないように見える。
.gitattributes を変えても、すでにコミット済みのファイルは直らない
属性が効くのは Git がファイルを書くときと読むときなので、既存の内容は何かが書き直すまでそのままだ。その 1 回を自分で起こす。
$ git add --renormalize .
$ git commit -m "Normalize line endings"
巨大な差分が出るが、それが目的である。専用のブランチでやり、1 コミットにまとめてマージし、着地する前に全員へ知らせておくこと。
6. CRLF を LF へ、そして元へ戻す
4 通りの方法があり、いずれも Darwin 上で \r が取れることを確認した。
| コマンド | 結果 |
|---|---|
tr -d '\r' < f > f.out | 動く |
perl -pi -e 's/\r\n/\n/g' f | 動く |
sed -i '' -e 's/\r$//' f(BSD 形式) | 動く |
sed -i -e 's/\r$//' f(GNU 形式を macOS で実行) | 動くが、ゴミファイルが 1 つ残る |
macOS における GNU 版 sed -i の罠
BSD の sed は、-i の直後にバックアップ用の接尾辞を要求する。Linux 向けのチュートリアルをそのまま貼り付けると、BSD の sed は -e をその接尾辞として飲み込む。編集自体は行われるが、同時にこうなる。
a5.txt
a5.txt-e
a5.txt-e はバックアップコピーだ。sed が -e を「指定された接尾辞」として読んだために作られている。macOS での正しい形は、空文字列を明示的に渡す sed -i '' -e 's/\r$//' f である。-e の付いたファイルがコミットされているリポジトリは、誰かが Mac の上で GNU 用のワンライナーを走らせたリポジトリだ。
macOS に dos2unix は入っていない
素の macOS で command -v dos2unix を実行しても何も返ってこない。このバイナリは brew install dos2unix で入れるものだ。ネット上でいちばんコピーされている答えが、多くの開発者の手元では動かない。tr -d '\r' ならインストール不要で同じ仕事をする。
逆方向の unix2dos にも同じ入手性の問題があり、代わりに sed -e 's/$/\r/' が使える。
ファイルがきれいな LF へ戻れば、また行のリストとして安心して扱える。これは行を丸ごと 1 つの文字列として比較するあらゆる処理に効いてくる。テキスト行の並べ替え も 重複行の削除 も value\r と value を別々の行として読むので、迷い込んだ復帰文字 1 つで重複排除は静かに機能しなくなる。
エディタでの操作
VS Code は現在のファイルの改行コードを右下のステータスバーに CRLF または LF と表示し、そこをクリックすれば切り替えられる。新規ファイルの既定値は files.eol 設定で決まり、ワークスペース単位で設定しておけば混成のチームでも揃う。他のエディタにも、名前は違うが同じ 2 つの操作がある。見落とされがちなのは、ファイル単位の表示と既定の設定が別物だという点だ。片方を変えても、もう片方は変わらない。
7. コードの中で改行コードを扱う
line1\r\nline2\r\n という 1 つのファイルを、7 つの入り口から読んだ。
| 入り口 | 得られるもの | \r |
|---|---|---|
Node fs.readFileSync(f,"utf8") | "line1\r\nline2\r\n" | 残る |
Node、同じ値に .split("\n") | ["line1\r","line2\r",""] | 全行に残る |
Node readline に crlfDelay:Infinity | ["line1","line2"] | 取れる |
Python open(f)、既定モード | 'line1\nline2\n' | 変換される |
Python open(f).readlines() | ['line1\n','line2\n'] | 変換される |
Python open(f, newline="") | 'line1\r\nline2\r\n' | 残る |
Python open(f,"rb") | b'line1\r\nline2\r\n' | 残る |
この表は、よく見るバグ報告に決着をつける。Python では動くのに Node では壊れる、というやつだ。Python の既定のテキストモードはユニバーサル改行を適用し、こちらが目にする前に \r\n を \n へ翻訳している。Node はバイトをそのまま渡してくる。どちらも間違ってはいないが、同じファイルを読んだ瞬間から両者の言い分は食い違う。
rstrip("\n") は \r を残す
original: 'line1\r\n' | rstrip("\n"): 'line1\r' | strip(): 'line1'
rstrip("\n") は指定した文字だけをきっかり取り除くもので、\r はその指定に入っていなかった。結果として、一致するはずのあらゆるものと不一致になる。「取り除いたのに、まだ等しくならない」という現象の正体はこれである。strip() か、引数なしの rstrip() を使えば、復帰文字を含む末尾の空白がすべて落ちる。
両プラットフォームで安全に行分割する
JavaScript では、両方の改行コードを許容するパターンで分割する。text.split(/\r?\n/) だ。Python では、既定のテキストモードのままユニバーサル改行に任せるか、\r\n、\n、単独の \r のいずれも扱える splitlines() を呼ぶ。
もう半分は書き込み側の話だ。Node は渡されたバイトをそのまま書くので、文字列は \n で組み立てて、ディスクへ何が載るかは .gitattributes に決めさせればいい。Python の open(path, "w") は、newline="" を渡さないかぎり \n をプラットフォームの改行コードへ翻訳する。CSV ライターがまさにこの理由で要求してくるフラグである。
行末の復帰文字とファイル先頭のバイトオーダーマークは、両端から現れた同じ種類のバグだ。コピー&ペーストを生き延び、等値比較を壊す見えないバイトである。反対側の端は UTF-8 BOM のトラブルシューティングガイド で扱っている。
8. 改行コードが噛みついてくるその他の場所
CSV と Excel。 RFC 4180 はレコード区切りとして CRLF を規定しており、CSV は CRLF が欠陥ではなく正解である数少ない場所の 1 つになっている。LF 前提で書かれたパーサーは各行の最後のフィールドに \r を残すので、CSV から JSON への変換で得た値が正しく見えるのに比較だけ合わないときは、まずこのバイトを疑ってほしい。
Docker。 CRLF の .sh をイメージへコピーすれば、それは失敗パターンの 4 番目だ。COPY はバイトをそのまま保つのでシバンは \r を抱えたままになり、コンテナは 126 で終了する。.gitattributes に *.sh text eol=lf を 1 行入れておけば、この種類はまとめて防げる。
差分とプルリクエスト。 目に見える編集がないのに全体が変更扱いになるファイルは、4 節の仕組みがコードレビューへ姿を現したものだ。テキスト差分 で 2 つのバージョンを比べれば数秒で確かめられるし、結果の読み方は テキスト差分ガイド で解説している。
混在ファイル。 ASCII text, with CRLF, LF line terminators は、設定の違う書き手が 2 人いたという意味だ。たまたま編集した行だけでなくファイル全体を正規化しないと、次の差分も同じくらい騒がしくなる。
FAQ
CRLF と LF の違いは何か
CRLF は \r\n(0x0D 0x0A)の 2 バイト、LF は \n(0x0A)の 1 バイトである。Windows は CRLF を書き、Linux と macOS は LF を書く。どちらも行の終わりを示す。エディタで見るかぎりテキストはまったく同じに見え、違いはバイト、文字列比較、差分にしか現れない。
スクリプトが CRLF なのに、なぜエラーが出ないのか
単純なコマンドが余分な 1 バイトを平気で受け流すからだ。末尾に \r が付いた echo hello は普通に走り、終了コード 0 で終わる。エラーが出るのはパーサーが気にする場所だけである。空行、fi のようなキーワード、そしてシバン行だ。危ないのは代入で、成功したうえで \r を変数へ保存してしまう。
$'\r': command not found は何を意味し、なぜ自分の環境では出ないのか
復帰文字だけを含む行をシェルが実行しようとした、という意味だ。bash 4 と 5 はその文字を ANSI-C クオートで表示するので $'\r' になる。macOS 同梱の bash 3.2 はコロンとコロンのあいだに何も出さず、zsh は代わりに ^M と出す。原因はどれも同じ復帰文字 1 バイトで、文言だけがシェルごとに違う。
core.autocrlf は true、input、false のどれにすべきか
Linux と macOS では input、Windows では true、そして両方より .gitattributes を優先するのがよい。実測では true も input もリポジトリには LF を保存し、コミットへ CRLF を通すのは false だけだった。true はさらに、チェックアウト時に作業コピーの LF ファイルを CRLF へ書き換える。
.gitattributes と core.autocrlf が食い違ったらどちらが勝つか
.gitattributes が勝つ。試した 4 つの形(* text=auto、* text eol=crlf、* -text、* text eol=lf)はいずれもローカルの core.autocrlf の値を上書きした。属性を使う理由もそこにある。属性はコミットされて全員に適用されるが、core.autocrlf はマシンごとの設定で、他人からは見ることも強制することもできない。
ファイルが CRLF と LF のどちらなのかを確認するには
file yourfile を実行する。CRLF なら ASCII text, with CRLF line terminators、素の LF なら接尾辞のない ASCII text、混在ファイルなら with CRLF, LF line terminators と出る。バイト単位で確実に知りたければ od -c を実行し、各 \n の手前に \r が座っていないか見ればいい。
CRLF を実際に使うべきなのはどんなときか
フォーマットやプロトコルが要求している場合だ。RFC 4180 は CSV のレコード区切りとして CRLF を定義しているし、HTTP ヘッダーや SMTP も通信路の上では同じものを使う。Windows のバッチファイルや PowerShell のファイルも CRLF のほうが安全である。それ以外の場所、つまりソースコード、シェルスクリプト、設定ファイルはすべて LF でいい。