改行コード(CRLF・LF)
改行は文字である
テキストファイルの「行の終わり」は、画面には何も表示されませんが、実体は制御文字です。しかもその文字が OS によって違います。
| 表記 | バイト | 由来 | 主な環境 |
|---|---|---|---|
LF(\n) | 0x0A | Line Feed。紙を1行送る | Linux・macOS・ほぼすべてのサーバー |
CRLF(\r\n) | 0x0D 0x0A | Carriage Return + Line Feed。印字ヘッドを左端に戻してから紙を送る | Windows |
CR(\r) | 0x0D | 古い Mac OS(9以前) | 現在はほぼ絶滅 |
タイプライターの動作がそのまま名前になっています。Windows は互換性のため今も2文字の CRLF を既定にしており、これが同じファイルなのにバイト列が違う状況を生みます。[[文字コードと文字化け]] が「文字の表し方の違い」なら、改行コードは「行の区切りの表し方の違い」です。
どこで混ざるか
一番多い経路は [[Git]] の自動変換です。Windows で core.autocrlf=true にすると、リポジトリ内(コミット)は LF、作業ツリーは CRLF にチェックアウト時に変換されます。macOS や Linux、そして CI の Linux ランナーは LF のままです。
つまり同じコミットでも、手元と CI ではファイルのバイト列が違うということです。この差は git の管理下では見えず、実際にファイルを読むコードだけが影響を受けます。
エディタの既定設定、コピー&ペースト、Windows で生成したファイルを Linux へ持ち込む、といった経路でも混ざります。1つのファイルの中で行ごとに CRLF と LF が混在することもあります。
静かに壊れるパターン
改行コードのずれは、エラーにならず結果が微妙に違う形で現れます。
- 行末アンカーが効かない — 正規表現の
$は\nの直前に一致しますが、\rの直前には一致しません。/--.*$/でコメントを除去するつもりが、CRLF のファイルでは\rが残るため1行も除去されない、といった形でWindows だけテストが落ちる。逆に CI(LF)では通るので、レビューでも気づけない - 文字数が1文字ずれる — 「区切り行の後ろの改行を1つ消費する」実装は、LF なら正しく動き、CRLF なら
\rが本文側に残る。ファイルの長さを比較する検証で「全件が同じ1文字差」になったら、ほぼこれが原因 - シェルスクリプトが動かない — 1行目の
#!/bin/bash\rはbash\rという存在しないコマンドを探す。bad interpreter: No such file or directoryの正体 - diff が全行差分になる — 中身は1行しか変えていないのに、改行コードが変わったせいで全行が差分として出る。レビューが実質不可能になる
- フォーマッタが警告を出し続ける —
endOfLineの既定が LF のツールに CRLF の作業ツリーを見せると、全ファイルが違反になる。CI は LF なので通る、という逆転が起きる
共通するのは「エラーで止まらない」ことです。落ちるならまだ良く、片方の環境では正しく動くため、環境要因のフレークと誤認されやすい点が厄介です([[フレーキーテスト]])。
対処の作法
入口で正規化するのが基本です。
const text = fs.readFileSync(path, 'utf8').replaceAll('\r\n', '\n')
ファイルを読み込んだ直後にこれを1行入れておけば、後段の正規表現も文字数計算も環境に依存しなくなります。行単位に処理するコードでは特に、読み込みと正規化をセットにします。
書き戻すときは逆で、元ファイルの改行を検出して保持します。読み込んで LF に揃え、そのまま LF で書き戻すと、Windows の作業ツリーでは全行が差分になります。
リポジトリ全体では .gitattributes で方針を固定します。
* text=auto eol=lf
*.bat text eol=crlf
これで OS の設定に関わらずチェックアウトが LF に揃い、「手元と CI で違う」状態自体をなくせます。Windows 固有のバッチファイルだけ例外にします。
比較や検証の指標を選ぶときは、改行差の影響を受けないものを使います。「更新待ちファイル数」のようにバイト一致で判定する指標は Windows では常に全件になるため、同期の判断には使えません。本文の長さや内容のハッシュを正規化してから比べます。
初学者向けポイント
- 「Windows でだけ落ちる」「CI でだけ通る」と聞いたら、文字コードの次に改行コードを疑いましょう。
file <ファイル>やcat -Aで^Mが見えれば CRLF です - 正規表現で行末を扱うコードには、テストに CRLF のケースを1つ足しておくと再発を防げます
- エディタのステータスバーに「CRLF / LF」の表示があります。保存前に一度見る習慣で、混在ファイルを作らずに済みます
関連技術とのつながり
- [[Git]] —
core.autocrlfと.gitattributesが改行を変換する場所 - [[文字コードと文字化け]] — 「同じ文字列でもバイト列が違う」問題の兄弟
- [[シェルとコマンドライン]] — シェバン行の
\rで動かなくなる典型例 - [[Linux]] — LF が既定の世界。サーバーと CI の前提
- [[静的解析とリンター]] — フォーマッタの
endOfLine設定と警告の読み方 - [[CI/CD]] — 手元(CRLF)と CI(LF)で結果が逆転する背景
- [[フレーキーテスト]] — 環境要因と誤認されやすい失敗の代表
Q: Windows の既定の改行コードはどれ?
- [ ] LF(0x0A)
- [x] CRLF(0x0D 0x0A)
- [ ] CR(0x0D)
解説: Windows は Carriage Return + Line Feed の2文字を使います。Linux・macOS・CI ランナーは LF です。
Q: 正規表現の行末アンカー $ が CRLF のファイルで期待どおり働かない理由はどれ?
- [x] $ は \n の直前に一致するが、\r の直前には一致しないため、行末に \r が残る
- [ ] CRLF のファイルは正規表現エンジンが読めないため
- [ ] $ は Windows では別の意味になるため
解説: \r が行の末尾に残り、`.*$` のような行末までの一致がずれます。読み込み直後に \r\n を \n へ正規化すれば環境に依存しません。
Q: リポジトリ全体でチェックアウト時の改行を LF に揃える設定はどれ?
- [ ] package.json の engines
- [x] .gitattributes の `* text=auto eol=lf`
- [ ] .prettierignore にファイルを追加する
解説: .gitattributes で方針を固定すると、各開発者の core.autocrlf 設定に依存せず「手元と CI で違う」状態をなくせます。