3行まとめ
- CRLF・LF・CR を相互変換するブラウザ完結ツールを作った。肝は「一度LFに正規化してから目的の改行コードへ変換する」2段構え
- 正規化の置換は
\r\n→\nを先、\r→\nを後にやる。順序を逆にすると\r\nが\n\nになって空行が倍増する - 混在テキストのカウントは、先にCRLFを取り除いてから単独のLF・CRを数える。そうしないとCRLFの中の
\n\rを二重計上する
WindowsのExcelでLF改行のCSVを開いたら全部1行になった、Linuxにアップしたシェルスクリプトが /bin/bash^M: bad interpreter で動かない、Gitで改行コードの差分が大量に出る——改行コードの違いは、OSをまたぐと地味に牙をむく。
ぱんだツールズの改行コード変換は、テキストの改行を CRLF(Windows)・LF(Linux/macOS)・CR(旧Mac)に相互変換するツール。入力に含まれる各改行コードの数もリアルタイムで表示する。処理はブラウザ内で完結し、テキストはサーバーに送信されない。
実装は正規表現の置換だけだが、「置換の順序」と「混在のカウント」に、素朴にやると間違える落とし穴がある。実装ベースで解説する。
3つの改行コード
まず前提。改行コードは歴史的に3種類ある。
| 表記 | バイト | 主なOS |
|---|---|---|
| CRLF |
\r\n(0x0D 0x0A・2バイト) |
Windows |
| LF |
\n(0x0A・1バイト) |
Linux / macOS(OS X以降) |
| CR |
\r(0x0D・1バイト) |
旧 Mac OS(9以前) |
CR(キャリッジリターン)は「行頭に戻る」、LF(ラインフィード)は「1行送る」の意味で、タイプライター由来。Windowsはこの2つを繋げた CRLF、Unix系は LF だけ、という分岐が今も残っている。
変換は「LFに正規化してから」の2段構え
CRLF ⇄ LF ⇄ CR を全組み合わせで個別に書くと分岐が増える。代わりに、まず何であれ一度LFに正規化し、そこから目的の改行コードへ変換する2段構えにすると、変換関数が1本で済む。
function convertNewlines(text: string, to: NewlineType): string {
// ① まずすべての改行コードを LF に正規化
const normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
// ② 目的の改行コードへ
switch (to) {
case 'CRLF': return normalized.replace(/\n/g, '\r\n')
case 'LF': return normalized
case 'CR': return normalized.replace(/\n/g, '\r')
}
}
入力がCRLF混在でもCR単独でも、①でいったんLFに揃うので、②は「LFを目的コードに置換」の1手で済む。中間表現(LF)を1つ決めて、入力の多様さをそこへ潰す——という発想で、これは文字コードや日付の変換でもよく使う設計。
置換の順序が結果を変える
①の正規化に、順序の罠がある。\r\n → \n を先に、\r → \n を後にやらないといけない。
text.replace(/\r\n/g, '\n').replace(/\r/g, '\n') // ✅ 正しい
もし順序を逆にして \r → \n を先にやると、\r\n(CRLF)の中の \r が先に \n へ変わって \n\n になり、続く \r\n の置換は対象を失う。結果、CRLF1個が空行1つ分(\n\n)に化けて、行数が倍増する。
// ❌ 逆順だと CRLF が壊れる
'a\r\nb'.replace(/\r/g, '\n').replace(/\r\n/g, '\n')
// → 'a\n\nb'(空行が1つ増える)
「CRLFはCRとLFの2文字」という事実がここで効いてくる。長いパターン(\r\n)を短いパターン(\r)より先に処理するのが、この手の正規化の鉄則。逆に言えば、CRLFを1つの単位として先に消化してしまえば、残った \r は本物の単独CRだけになる。
混在のカウントも「CRLFを先に除く」
このツールは入力に含まれる CRLF・LF・CR の個数を表示して、改行コードが混在しているかを見えるようにしている。ここでも同じ「CRLFを先に処理」の考え方が要る。
function countNewlines(text: string) {
const crlf = (text.match(/\r\n/g) ?? []).length
const lf = (text.replace(/\r\n/g, '').match(/\n/g) ?? []).length // CRLFを除いてからLFを数える
const cr = (text.replace(/\r\n/g, '').match(/\r/g) ?? []).length // CRLFを除いてからCRを数える
return { crlf, lf, cr }
}
素朴に text.match(/\n/g) でLFを数えると、CRLFの中に含まれる \n まで数えてしまい、「LFの数」がCRLFの分だけ水増しされる。CR も同様。だから 先に \r\n を取り除いてから、残った単独のLF・CRを数える。こうすると crlf / lf / cr の3つが「単独で出現した回数」として正しく分離され、たとえば「CRLFが10・LFが3」と出れば混在していると一目で分かる。
match は該当なしのとき null を返すので、?? [] で空配列に受けてから .length を取っているのも地味だが必要な処理(null.length は例外になる)。
なぜ改行コードの変換が要るのか
最後に実用面。改行コードの不一致は、こういう場面で問題になる。
- ExcelでCSVが1行になる:LF改行のCSVをWindowsのExcelが改行と認識できず、全行が1行に潰れる。LF→CRLFで解決する(あるいはBOM付きUTF-8にする)
-
シェルスクリプトが動かない:Windowsで作ったCRLFのスクリプトをLinuxに置くと、行末の
\rが余分な文字として解釈され、/bin/bash^M: bad interpreterなどのエラーになる。CRLF→LFで直る -
Gitの差分が大量に出る:Windows勢とMac/Linux勢が混在すると改行コードがOSごとにコミットされ差分が膨れる。
.gitattributesやcore.autocrlfで統一するのが本筋だが、単発ならLFに変換してからコミットするだけでも収まる
現代のソースコードは LF に統一するのが一般的(GitHub/GitLab も LF が標準)。CSVをExcelに渡すような場面だけCRLFが要る、という住み分けを押さえておくと、どちらに変換すべきか迷わない。
まとめ
- 改行コード変換は「一度LFに正規化 → 目的コードへ」の2段構えにすると変換関数が1本で済む。中間表現を1つ決めるのが定石
- 正規化の置換は
\r\n→\nを先、\r→\nを後に。逆順だとCRLFが\n\nに化けて行数が倍増する。長いパターンを先に処理する - 混在カウントは先にCRLFを除いてから単独LF・CRを数える。素朴に数えるとCRLF内の
\n\rを二重計上する。matchのnullは?? []で受ける - 用途はExcelのCSV(CRLF)・シェルスクリプトやGit(LF)。ソースは基本LF、Excel渡しだけCRLF
OS間でテキストをやり取りして改行が崩れたときにどうぞ。テキストはブラウザの外に出ない。
ぱんだツールズ では他にも 文字コード変換・CSV変換・全角半角変換・テキスト整形など、テキストの実務処理に効くブラウザ完結ツールを多数公開中。全部無料・登録不要・ファイルはサーバーに送られない。
https://sakutto-panda.com
この記事は Zenn にも同じ内容を投稿しています。