シェルスクリプトでの改行文字の削除にハマる

僕はただ改行を削除したいだけなんだ。。。

POSIX準拠の範囲でなんとかしたい。

単純に全改行を削除

1文字の削除にはtrコマンドを使いましょ。
(infileは改行を削除したいファイル)

tr -d '\n' < infile

指定パターンにマッチした行のみ改行を削除

改行文字の削除の例でこんな感じのを見かける。 GNU系とBSD系で書き方が違う時点で目的とはかけ離れるんだけど、何かヒントになりそう。

sed ':loop; N; $!b loop; s/\n//g' < infile   # GNU版sed
sed -e ':loop' -e 'N; $!b loop' -e 's/\n//g' < infile   # BSD, Mac

sedは通常\nがマッチしないのだけど、上のやり方だとマッチするっぽい。

sedの挙動解説

通常はsedのパターン検索では\nにマッチしない。

それはなぜか。

sed内部では指定の操作を行う際に、以下のような動作をするらしい。

1. 次の入力行を、改行文字を削除して、パターンスペースというバッファに格納
2. そのパターンスペースに対してマッチングやら指定の操作(置換や削除など)をする
3. パターンスペースの内容の末尾に改行を追加して出力

つまり、従来はパターンスペースという検索対象バッファに\nがそもそも含まれていないため、'\n'を指定してもマッチしない。

それがNコマンドを使うと、検索対象バッファに改行文字+次の1行を追加する。
それでめでたく'\n'がマッチするということらしい。

詳細はこちらのサイトさんを参照されたし。

普段から\nくらいマッチしてほしいんだけど。
なんだろう、sedが開発された頃の環境の都合があったのかな。。。

コード解説

先程のコード再掲。

sed -e ':loop' -e 'N; $!b loop' -e 's/\n//g' < infile   # BSD, Mac

最初これ見てもサッパリだったのだけれども、複数行に分けてみる。

:loop       # ラベル定義
N           # パターンスペースに改行文字+次の1行を追加
$!b loop    # 最終行($)じゃなければ(!)、:loopへジャンプ(b loop)
s/\n//g     # `\n`を全て削除

Nよってパターンスペースに改行文字が入る。
その後、パターン検索すれば\nもめでたくマッチするので、削除や置換が可能というカラクリ。

でも、行頭という意味での^が使えない。
代わりに`[^\n]*'を指定しても挙動がおかしい。なんでじゃ。

妥協策(行末パターンを指定して削除)

上の削除例とこちらのサイトさんを参考に下のような妥協案。
引数で指定した行末パターン(/xxxx$/)にマッチする行だけ改行文字を削除する。

#!/bin/sh
PATTERN=$1
sed -e ':loop' \
    -e '/'"$PATTERN"'/N;' \
    -e 's/\([^\n]*\)\n\([^\n]*\)$/\1\2/' \
    -e '/'"$PATTERN"'$/b loop' \

うーん、あと一歩最後の行を工夫すればできそうな気がする。。。

あるパターンにはさまれた複数行を1行にまとめる

下みたいにユニークな文字(<start>,<end>)で挟まれている複数行を、1行にまとめるパターンならいける。
<start><end>は他に出てこない文字列ならなんでもいい。

> cat infile
へっだ
<start>
まとめたい
内容の
行たち
1
<end>
ごみ
<start>
まとめたい
内容の
行たち
2
<end>
ふった

Step 1. sedでまとめたい範囲だけを抜き出す。

> sed -n '/<start>/,/<end>/p' < infile
<start>
まとめたい
内容の
行たち
1
<end>
<start>
まとめたい
内容の
行たち
2
<end>

Step 2. grep -v<start>行を省いて、<end>を置換可能な一文字にする。

> sed -n '/<start>/,/<end>/p' < infile  | grep -v "<start>" | sed 's/<end>/>/g'
まとめたい
内容の
行たち
1
>
まとめたい
内容の
行たち
2
>

Step 3. 改行を一旦消して、<end>を置換した文字を改行に置換

> sed -n '/<start>/,/<end>/p' < infile | grep -v "<start>" | sed 's/<end>/>/g' | tr -d '\n' | tr '>' '\n'
まとめたい内容の行たち1
まとめたい内容の行たち2

さいごに

いやぁもっと簡単な方法あるでしょ。。。
みんなどうやってんだ。
最終兵器awkしかないのかしら。

その他参考