同じテキストファイルなのに、Windowsで開くと1行に見えたり、Gitの差分が全行に出たり、シェルスクリプトが bad interpreter で動かなかったりする。原因はたいてい改行コードです。CRLF と LF の違いを押さえておくと、これらは同じ1つの問題として片付きます。
CR と LF は何を意味していたか
もともとは機械式のタイプライタやテレタイプの動作に由来します。
- CR(Carriage Return,
\r, 0x0D)— 印字位置を行の先頭に戻す。 - LF(Line Feed,
\n, 0x0A)— 用紙を1行送る。
紙を1行送って先頭に戻す、という2つの動作が必要だったので、両方を送っていました。それがそのまま CRLF として残ったのが Windows 系です。一方 Unix 系は「改行はLFだけでよい」と割り切りました。どちらが正しいということはなく、歴史的な選択の違いです。
| 表記 | バイト列 | 主な環境 |
|---|---|---|
| LF | 0A | Linux, macOS, 多くのプログラミング言語の標準 |
| CRLF | 0D 0A | Windows, HTTP ヘッダ, 多くのネットワークプロトコル |
| CR | 0D | Mac OS 9 以前(現在はほぼ見ない) |
実際に壊れる場面
シェルスクリプトが実行できない
最もよく遭遇する症状です。
$ ./deploy.sh
bash: ./deploy.sh: /bin/bash^M: bad interpreter: No such file or directory
1行目が #!/bin/bash ではなく #!/bin/bash\r になっているため、/bin/bash\r という実行ファイルを探しに行って失敗しています。^M が表示されているのが手がかりです。
Gitの差分が全行になる
1文字も内容を変えていないのに、差分が全行に出ることがあります。エディタが保存時に改行コードを一括変換したためです。差分は行単位で比較するので、全行の末尾が変われば全行が変更扱いになります。レビューが不可能になるので、コミット前に気づく必要があります。
設定ファイルの値に見えない文字が混ざる
.env のようなファイルを CRLF で保存すると、値の末尾に CR が付きます。PORT=8080\r は文字列として比較すると 8080 と一致しません。「設定は合っているのに動かない」という、原因の見えにくい不具合になります。
確認と変換の手順
いま何になっているかを見る
# file コマンドで判定する
$ file deploy.sh
deploy.sh: Bourne-Again shell script, ASCII text executable, with CRLF line terminators
# バイト列を直接見る
$ head -1 deploy.sh | od -c | head -2
0000000 # ! / b i n / b a s h \r \n
変換する
# CRLF を LF にする
$ sed -i 's/\r$//' deploy.sh
# dos2unix があればこちらでもよい
$ dos2unix deploy.sh
Git で再発を防ぐ
リポジトリに .gitattributes を置いて、コミットされる内容を統一します。
# .gitattributes
* text=auto eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.png binary
text=auto eol=lf は「テキストと判定したファイルはリポジトリ内で LF に揃える」という指定です。Windows 専用のバッチファイルは CRLF でなければ動かないので、明示的に例外にします。core.autocrlf は各利用者の設定で、リポジトリ側の .gitattributes のほうが優先されます。チームで揃えるなら .gitattributes を使ってください。
.gitattributes を後から入れると、一度は全ファイルが変更として出ます。改行コードの統一だけを行うコミットとして分けておくと、後から履歴を追いやすくなります。
よくある質問(FAQ)
CRLFとLFはどちらを使うべきですか?
リポジトリの中では LF に統一するのが一般的です。Windowsのエディタは CRLF で保存することが多いため、Gitの .gitattributes で text=auto や eol=lf を指定して、コミットされる内容を LF に揃える運用が広く使われています。ただし Windows 専用のバッチファイルなど、CRLF でなければ動かないファイルは例外として残します。
シェルスクリプトで bad interpreter と出るのはなぜですか?
ファイルが CRLF で保存されていると、1行目の #!/bin/bash の末尾に CR が付いた状態になり、インタプリタのパスが「/bin/bash\r」という存在しないパスとして扱われるためです。改行コードを LF に変換すれば解消します。
Gitで差分が全行になるのはなぜですか?
改行コードが一括で変換されたためです。差分は行単位で比較するので、全行の末尾が変わると全行が変更として表示されます。中身は変わっていないことが多いので、コミット前に改行コードの変換が起きていないかを確認してください。