改行コードで壊れるものを一通り潰す — CRLF/LFとGitの.gitattributes
bad interpreter でスクリプトが動かない、1文字も直していないのにGitの差分が全ファイル全行になる、設定は合っているのにアプリが値を受け取れない。これらは別々の不具合に見えて、原因が同じことがある。改行コードだ。CRLFとLFの違いと、実際に壊れる場面、確認・変換・再発防止までを一通りまとめる。
CRとLFはそれぞれ別の動作を指していた
改行を表すバイトは2種類ある。
- CR(Carriage Return,
\r, 0x0D)— 印字位置を行頭に戻す - LF(Line Feed,
\n, 0x0A)— 用紙を1行送る
機械式の端末では紙送りと行頭復帰の両方が必要だったので、2バイトを続けて送っていた。それが CRLF としてそのまま残ったのがWindows系で、Unix系は LF だけで足りるとした。どちらが正しいという話ではなく、歴史的な分岐である。
| 表記 | バイト列 | 主な環境 |
|---|---|---|
| LF | 0A |
Linux, macOS, 多くの言語処理系 |
| CRLF | 0D 0A |
Windows, HTTPヘッダ, 多くのネットワークプロトコル |
| CR | 0D |
Mac OS 9 以前(現在はほぼ見ない) |
見た目はどれも「改行」なので、エディタで開いても違いは分からない。バイト列が違うことが問題になるのは、機械がその行を解釈するときだけだ。
実際に壊れる3つの場面
シェルスクリプトが実行できない
もっとも遭遇しやすい。
1 | |
1行目のシバンが #!/bin/bash ではなく #!/bin/bash\r になっているため、/bin/bash\r という名前の実行ファイルを探しに行って失敗している。エラーメッセージに出ている ^M が CR の表示で、これが手がかりになる。スクリプトの中身に問題は無い。
Gitの差分が全行になる
内容を1文字も変えていないのに、git diff が全行を変更として出すことがある。エディタが保存時に改行コードを一括変換したためだ。差分は行単位で比較するので、全行の末尾が変われば全行が変更扱いになる。レビューが成立しなくなるので、コミット前に気づく必要がある。
設定値に見えない文字が混ざる
.env のようなファイルをCRLFで保存すると、各行の値の末尾にCRが付く。
1 | |
これを文字列として比較すると 8080 と一致しない。数値としてパースすれば通ることもあるので、「ある環境変数だけ効かない」という形で出てくる。エディタで見ても何も見えないぶん、原因にたどり着くまでが長い。
いま何になっているかを確認する
まず file で判定できる。
1 | |
with CRLF line terminators と出れば CRLF。何も出なければ LF である。バイト列を直接見るなら od を使う。
1 | |
\r \n が並んでいるのが確認できる。特定のファイルではなくリポジトリ全体を調べたいなら、Gitに聞くのが速い。
1 | |
i/ がリポジトリ内(index)の改行コード、w/ が作業ツリーの改行コードを表す。i/crlf のものがコミット済みの内容としてCRLFになっているファイルだ。
変換する
1ファイルなら sed で足りる。
1 | |
macOS の BSD sed は -i に引数が要るので sed -i '' 's/\r$//' deploy.sh になる。dos2unix が入っていればそちらのほうが確実だ。
1 | |
まとめて変換するときは、対象を絞ってから実行する。バイナリファイルに sed をかけると壊れる。
1 | |
.gitattributes で再発を防ぐ
一度直しても、CRLFで保存するエディタを使っている人が触れば戻る。リポジトリ側で正規化を宣言しておくのが確実だ。
1 | |
text=auto はGitがテキストと判定したファイルを対象にする指定、eol=lf はコミットされる内容をLFに正規化する指定である。Windows専用のバッチファイルはCRLFでないと動かないので、明示的に例外にしておく。バイナリは binary を付けて正規化の対象から外す。
core.autocrlf という設定もあるが、これは各利用者のローカル設定なので、チームで揃えたいなら .gitattributes を使う。両方ある場合は .gitattributes が優先される。
既存のリポジトリに後から入れると、一度は全ファイルが変更として出る。正規化だけを行うコミットとして分けておくと、後から履歴を追うときに「この巨大な差分は改行の正規化だ」とすぐ分かる。
1 | |
--renormalize は、.gitattributes の指定に従ってインデックスを作り直す。ファイルを開いて保存し直す必要はない。
エディタ側も揃えておく
.editorconfig を置いておくと、対応しているエディタが保存時にLFで書いてくれる。Gitの正規化は「コミットされる内容」を揃えるものなので、作業ツリー側はこちらで揃えるほうが差分が出にくい。
1 | |
.gitattributes と .editorconfig は役割が違う。前者はGitに対する指示、後者はエディタに対する指示だ。両方置いて、同じ方針を書いておくのが分かりやすい。
まとめ
改行コードの問題は、症状がばらばらに見えるわりに原因と対処が1つにまとまっている。file か git ls-files --eol で現状を見て、dos2unix で直し、.gitattributes で再発を止める。この3手でだいたい片が付く。厄介なのは見えないことだけで、見えるようにしてしまえば難しい話ではない。