混在する改行コードをBSD版sedで変換する

環境

CR+LF, CR, LF を全て LF に統一したい

改行コード CR+LF, CR, LF が混在した邪悪なファイル file.txt がある。

$ echo -n 'あいうえお\r\nかきくけこ\rさしすせそ\n' > file.txt
# file.txt
あいうえお<CR+LF>
かきくけこ<CR>
さしすせそ<LF>

これをmacOS標準のBSDsed コマンドで LF に統一したい。

こうする。

$ sed -e ':L' -e 'N' -e '$!bL' -e 's/\r\n/\n/g' -e 's/\r/\n/g' file.txt
あいうえお
かきくけこ
さしすせそ

file.txt 全体を読み込んでから CR+LFLF に置換し、最後に残った CRLF に置換する。ファイル全体を読み込むので、ファイルサイズが大きい場合は注意。

仕組み

処理の流れは下記のとおり。

  1. :L: ラベル L を定義する。
  2. N: ファイルから次の行を読み込み、パターンスペース(sedが読み込んだ行を保存する領域)に追加する。
  3. $!bL: ファイル最終行 $ でない ! 場合は分岐(ジャンプ) b する。分岐先はラベル L*1
  4. s/\r\n/\n/g: パターンスペース内で \r\n\n に置換する。
  5. s/\r/\n/g: パターンスペース内で \r\n に置換する。

1.〜3.のループにより、ファイル全体がパターンスペースに保存される。 4.〜5.では、パターンスペースに保存されたファイル全体の文字列に対して、それぞれ CR+LFLF, CRLF を行っている。

後述のとおり、通常sedは1行ずつの読み込み時に改行コード LF が含まれないため、CR+LFの行が /\r\n/ にはマッチしない。 しかし2.のようにNコマンドで読み込むことで、改行コード LF も含めてパターンスペースに保存されるため /\r\n/CR+LF の行にマッチするようになり、CR+LFLF の置換ができる。

※後から気づいたんだけどこっちのが楽

$ sed -e 's/\r$//g' -e 's/\r/\n/g' file.txt
あいうえお
かきくけこ
さしすせそ

CR+行末(つまり CR+LF での改行)の CR を削除してから、残りの CRLF に置換する。

後述のとおり、sedの出力時にはもともと行末についていた LF も出てくるので、行末の CR を削除してしまえば自動的に LF での改行として出力される。 あとは CR で改行されているものだけ LF に置換すればよい。

うまくいかない例

単純に CR を削除する

$ sed -e 's/\r//g' file.txt
あいうえお
かきくけこさしすせそ

よくあるやつ。CR+LFLF に変換するだけであればOKだが、CR のみの場合は改行コードが消えてしまうので今回の条件ではNG。

行ごとに CR+LF でマッチさせようとする

$ sed -e 's/\r\n/\n/g' -e 's/\r/\n/g' file.txt
あいうえお

かきくけこ
さしすせそ

通常 sed は1行ごとに文字列を処理していくが、そのときの処理対象の文字列には改行コード LF は含まれないため、's/\r\n/\n/g' では CR+LF にマッチしない。結局 's/\r/\n/g' しか処理されないので、CR+LFLF が二重になってしまう。

(例えば あいうえお\r\n という文字列は、sed コマンドで普通に読み込まれると あいうえお\r に見えており、\n が含まれない)

行ごとに CR+行末 でマッチさせようとする

$ sed -e 's/\r$/\n/g' -e 's/\r/\n/g' file.txt
あいうえお

かきくけこ
さしすせそ

\r$CR+LF の行にマッチするものの、出力時には置換後の \n に加えて、もともと行末についていた \n も出てくるため、改行コードが二重になってしまう。

*1:つまりファイル最終行に到達するまで、1.に戻って、2.で次の行を読み込んで、…を繰り返す