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

# 動機

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

  - [sedで改行を出力する](https://rcmdnk.com/blog/2014/09/21/computer-bash/#bsd-sed-1)
  - [sedコマンドで文字列を改行に置換する、しかもスマートに置換する。](https://qiita.com/richmikan@github/items/3c74212b0d8dec9bd00f)
  - [sed による置換で改行\nを出力する](https://qiita.com/kkdd/items/725e53572bc69e4b51b7)
   
ワンライナーで処理するやり方として、これらで挙げられている方法では、

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

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

# 例題

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

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

```tcsh:BSD&nbsp;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`)

```tcsh:GNU&nbsp;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`のオプション引数を改行して繋いでから実行しているようで、

```tcsh:BSD&nbsp;sed
% echo "kani:wani:uni:hebi:oni" | sed -e "s/wani:uni/wani\" -e"uni/g"
kani:wani
uni:hebi:oni
```
とすれば、ワンライナー化してうまく行く。一方で`GNU sed`は、`-e`のオプションをそれぞれを解釈して順次実行しているようで、

```tcsh:GNU&nbsp;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`コマンドを使ってみる。ホールドスペースが空なら、パターンスペースの末尾に改行を追加してくれる。そうすればパターンスペースの末尾に来た改行文字を正規表現でマッチさせて、置換文字列の所望の所で展開させればよい。

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

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

```tcsh:BSD&nbsp;sed&nbsp;残念な例
% 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
```
```tcsh:GNU&nbsp;sed&nbsp;残念な例
% 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`コマンドが実行されないようにアドレス指定する必要がある。

```tcsh:BSD&nbsp;sed&nbsp;期待通りに動作する例
% 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
```
```tcsh:GNU&nbsp;sed&nbsp;期待通りに動作する例
% 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 sed`で`GNU sed`でも共通に動作する改行文字に置換するワンライナー**が得られた。この方法ならば`Makefile`の中でも使えそうだ。

# 蛇足

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