sedの置換で、BSD sedかGNU sedかを気にせずにワンライナーで改行(\n)文字を出力する方法。


動機

seds(置換)コマンドでの置換(後)の文字列内で改行を出力する方法については、色々な解説記事がある。

ワンライナーで処理するやり方として、これらで挙げられている方法では、

  • BSD sed (macOS標準)と、GNU sedでやり方が異なっていると、環境によって実装を使い分けないといけない。
  • シェル変数に改行コードを含める方法は、csh/tcshだとちょっと難しい。
  • シェル変数に改行コードを含める方法は、Makefileの中などの改行コードが別途展開されてしまうような場合には、そのままではうまくいかない。(makeとの相性)

そのため、より汎用的な方法を考えることにした。

例題

単にtrでも実現できる例ではつまらないので、ここでは、kani:wani:uni:hebi:oniという文字列の、waniuniの間の:だけを改行に置換する方法を考える。

BSD sedでは、sコマンドの第2引数に\nと書いても改行コードとは解釈されない。マニュアルによると'\'に続けて改行を入れればよい、と書いてある。

BSD sed
% echo "kani:wani:uni:hebi:oni" | sed -e "s/wani:uni/wani\\
> uni/g"
kani:wani
uni:hebi:oni

(>はシェルの継続行のプロンプト。) この方法は、GNU sedでもうまくいく。(以下、gsedはmacportsでインストールされたGNU sed)

GNU sed
% gsed --version
gsed (GNU sed) 4.8
% echo "kani:wani:uni:hebi:oni" | gsed -e "s/wani:uni/wani\\
> uni/g"
kani:wani
uni:hebi:oni

これをワンライナーで収めようと思うと問題に突き当たる。sedに、複数行にわたるsedコマンドを渡す際に、BSD sed と GNU sed で挙動が異なる。BSD sedでは、-eのオプション引数を改行して繋いでから実行しているようで、

BSD sed
% echo "kani:wani:uni:hebi:oni" | sed -e "s/wani:uni/wani\" -e"uni/g"
kani:wani
uni:hebi:oni

とすれば、ワンライナー化してうまく行く。一方でGNU sedは、-eのオプションをそれぞれを解釈して順次実行しているようで、

GNU sed
% echo "kani:wani:uni:hebi:oni" | gsed -e "s/wani:uni/wani\" -e"uni/g"
gsed: -e expression #1, char 16: `s' コマンドが終了していません

とエラーになる。さて、どうしたものか。

発想の転換

sedに改行を渡すのが難しいので、なんとかしてsedの中で改行を埋め込む方針で考えてみる。そこでGコマンドを使ってみる。ホールドスペースが空なら、パターンスペースの末尾に改行を追加してくれる。そうすればパターンスペースの末尾に来た改行文字を正規表現でマッチさせて、置換文字列の所望の所で展開させればよい。

BSD sed 単純な例
% echo "kani:wani:uni:hebi:oni" | sed -e "G;s/\(.*wani\):\(uni.*\)\(\n\)/\1\3\2/g"
kani:wani
uni:hebi:oni
GNU sed 単純な例
% echo "kani:wani:uni:hebi:oni" | gsed -e "G;s/\(.*wani\):\(uni.*\)\(\n\)/\1\3\2/g"
kani:wani
uni:hebi:oni

この方法ならばBSD sedGNU sedでも共通に動作するワンライナーになった。しかしこの置換では、sedsコマンドにgオプションが付いているが、一行に複数箇所該当した場合には期待する動作にならないので、もう一工夫必要である

BSD sed 残念な例
% echo "kani:wani:uni:hebi:wani:uni:oni" | sed -e "G;s/\(.*wani\):\(uni.*\)\(\n\)/\1\3\2/g"
kani:wani:uni:hebi:wani
uni:oni
GNU sed 残念な例
% echo "kani:wani:uni:hebi:wani:uni:oni" | gsed -e "G;s/\(.*wani\):\(uni.*\)\(\n\)/\1\3\2/g"
kani:wani:uni:hebi:wani
uni:oni

解決策。

sedの処理としては複雑になってしまうが、ループ処理することにする。BSD sedでは、ラベルの定義や、b,tコマンドの引数にラベルを与える場合にはそのあとに改行を入れることが必須なので、ラベル定義部は独立に-eオプションの引数とする必要がある。
** 2020/5/2 追記 **
入力行が複数行で、該当しない行ではGコマンドが実行されないようにアドレス指定する必要がある。

BSD sed 期待通りに動作する例
% echo "kani:wani:uni:hebi:wani:uni:oni" \
  | sed -e ":t" -e "/wani:uni/{G; s/\(.*wani\):\(uni.*\)\(\n\)/\1\3\2/;};/wani:uni/bt"
kani:wani
uni:hebi:wani
uni:oni
GNU sed 期待通りに動作する例
% echo "kani:wani:uni:hebi:wani:uni:oni" \
  | gsed -e ":t" -e "/wani:uni/{G;s/\(.*wani\):\(uni.*\)\(\n\)/\1\3\2/;};/wani:uni/bt"
kani:wani
uni:hebi:wani
uni:oni

同じようなアドレス指定を2回書いていたりして、ややスマートさに欠けるような気がするものの、BSD sedGNU sedでも共通に動作する改行文字に置換するワンライナーが得られた。この方法ならばMakefileの中でも使えそうだ。

蛇足

他のsedコマンドと組み合わせて使用する場合には、ホールドスペースが空でない場合が考えられる。その場合には、s/\(.*wani\):\(uni.*)\(\n\)/\1\3\2/;のところのマッチさせる正規表現を変更する必要がある。とくにホールドスペースが複数行の文字列(改行を含む)の場合には注意が必要と思われる。