改行コード(CRLF・LF)

改行は文字である

テキストファイルの「行の終わり」は、画面には何も表示されませんが、実体は制御文字です。しかもその文字が OS によって違います。

表記バイト由来主な環境
LF(\n)0x0ALine Feed。紙を1行送るLinux・macOS・ほぼすべてのサーバー
CRLF(\r\n)0x0D 0x0ACarriage 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 で違う」状態をなくせます。