rcmdnk's blog
Last update

sed & awk (Nutshell Handbooks)

sedを使って文字列を変換する際、 Mac等BSD系sedだと変換後に改行を出すのが一筋縄ではいかない、と言う話。

GNU sedでの改行変換

環境:

  • Linux
  • Bash 4.2.37
  • GNU sed version 4.2.1

GNU sedでは改行の出力は\nを指定してあげるだけで出来ます。

$ echo  "aaa\nbbb"|sed "s/\\\\n/\n/"
aaa
bbb

入力の方の\nを実際の改行文字に変えたいような場合、 出力側に\nを指定してあげれば良いだけです 1

BSD sedでの改行出力

環境:

  • Mac
  • Bash 4.3.24
  • BSD sed (May 10, 2005, バージョンの見方が分からない。。。)

同じコマンドを打ってみると

$ echo  "aaa\nbbb"|sed "s/\\\\n/\n/"
aaanbbb

な感じでダメです。\nnに解釈されてしまっています。

$ echo  "aaa\nbbb"|sed "s/\\\\n/\\n/"
aaanbbb
$ echo  "aaa\nbbb"|sed "s/\\\\n/\\\\n/"
aaa\nbbb

となり、まず最初に\nの方が解釈されてしまっている?感じでしょうか。

$ echo  "aaa\nbbb"|sed 's/\\n/\n/'
aaanbbb
$ echo  "aaa\nbbb"|sed 's/\\n/\\n/'
aaa\nbbb

シングルクォートでもこんな感じ。

探してみるとまさにこの話についてのものが。

ShellScript - sedコマンドで文字列を改行に置換する、しかもスマートに置換する。 - Qiita

コレ見て思い出しましたが、確かに昔作ったスクリプトでは 何故か\nで上手くいかない、と思った時に

echo  "aaa\nbbb"|sed "s/\\\\n/\\
/"

みたいに書いてたことがありました 2

最初の頃はGNUとBSDで違いがあることに気づかずに Macで作業してる時に何故か上手くいかないな、とか思って色々やっていた感じ。

上のページではさらにスマートに行うために

LF=$(printf '\\\012_')
LF=${LF%_}
echo  "aaa\nbbb"|sed "s/\\\\n/$LF/g"

と、一度改行文字を作ってからそれを使っています。 バックスラッシュでエスケープして実際に改行した改行コードは なんとなく嫌、というのは同意。

上の方法で_があるのは、 直接改行コードだけを代入してしまうと 代入時に改行コードが文字列の最後にあるとは トリミングされるらしく結局何も代入してないことになってしまうので、 一度改行コード+適当な文字を代入してから要らない部分を消しています。

ただ、これだと余計な作業が増えるので、なんとか一行に入れようとすると、 まず、

$ echo  "aaa\nbbb"|sed "s/\\\\n/$(printf '\\\012')/g"
sed: 1: "s/\\n/\/g": unterminated substitute in regular expression

直接改行文字のところだけ入れようとすると、改行文字のところは無視されて \だけに見えてしまってエラーになります。

これを

$ echo  "aaa\nbbb"|sed "s/\\\\n/$(printf '\\\012 ')/g"
aaa
 bbb

みたいな感じで空白でも何でも一文字後ろに入れてあげれば改行になります。

ので、じゃあこれを消してみれば、というと、

$ echo  "aaa\nbbb"|sed "s/\\\\n/$(printf '\\\012_'|sed 's/_//')/g"
sed: 1: "s/\\n/\/g": unterminated substitute in regular expression

な感じになって最終的に$()の中身で判断されるので途中でごちゃごちゃやっても 意味がないみたいです。

上みたいな代入しか無いなら改行を直接書くほうが良いかな とも思っていましたが、コメントの最後に在る

LF=$'\\\x0A'
echo "hogehoge\nfoo\nbar" | sed 's/\\n/'"$LF"'/g'

を参考にしてもっとスマートなものが出来ました。

BSD sedでのスマートな改行出力

$ echo  "aaa\nbbb"|sed s/\\\\n/\\$'\n'/
aaa
bbb

でBSDの場合でもスマートに改行を出力できます。 GNUの場合でもOKです。 なのでなるべくスクリプトでは単に\nと書く代わりにこちらを 使った方が良いです。

これはsedのコマンドで、クォートなしで$'\n'を使っているだけ。 $'\n'$'\x0A'(と$'\012')は同義です(上の例でLF=$'\\\nでも同じ)。

クォートなしの場合だとダブルクォートした時と同じようになるので $に対して\\$としているわけですが、 全体をダブルクォートしてしまうと、

$ echo  "aaa\nbbb"|sed "s/\\\\n/\\$'\n'/"
aaa$'n'bbb

な感じになってしまいます。 $'\n'の部分を$VALみたいに普通の変数にしてみると ダブルクォートで囲っても囲まなくてもきちんと変換されるので、 この場合には'\n'の部分の シングルクォートの部分がどうも上手く理解できなくなるようです。

また、ダブルクォート内にある場合には改行コードはコメントにあるように\ でエスケープしないといけません。(LF=$'\\\x0A'の最初の\\の部分。)

一方、クォートしない状態だとそのまま解釈してくれて上手く行きます。

ただ、変更の前後で空白とか使いたい時もあってクォートしないと上手くいかなくなるので、 その場合には

$ echo  "aa a\nbbb"|sed "s/a a\\\\n/"\\$'\n'"c c/"
a
c cbbb

こんな感じで、\\$'\n'の部分の前後をそれぞれでクォートしてあげて くっつけてあげればOK。 なので、基本的にこれで何でも出来ます。

シングルクォートの場合でも同じ様にその場だけ外してあげればOK。

$ echo  "aaa\nbbb"|sed s/'\\n'/\\$'\n'/
aaa
bbb

上のページのコメントの例も\

おまけ

GNUとBSDのにはsedだけではなくて他にも色々と同じ名前のコマンドで 微妙に動作が違うものがあります。

Mac(BSD)でcpをGNU的に使う + おまけ

ここでもおまけにしている所でsedの話をしてますが、 -iオプションで元ファイルを置き換える時のバックアップ指定が 微妙に異なってバグを作ることがあります。

思い切って違う名前のコマンドとか、とか少なくともオプションが違うとかならまだあれですが、 同じような用途の同じ名前のオプションでこれだけ些細な違いは 流石にやめて欲しい。。。

おまけ2

ちなみに改行コードを\nという表示に変換したいとき、 sedで直接やると難しいのでtrを使ったりすることもありますが、 sentakuの中では下のようなawkを使った方法に落ち着きました。

# Change line breaks to \n (to be shown), and remove the last line break
_s_show="$(echo "${_s_inputs[$n_input]}"|awk -F\n -v ORS="\\\\\\\\n" '{print}' |sed 's|\\\\n$||')"

追記: 2018/08/30

シェルの文字列変数置換操作でもっと簡単に出来ます。

_s_show=${_s_inputs[$n_input]//$'\n'/\\\\n}
_s_show=${_s_show//$'\n/\\\\n}

数多くループで回したりすると100倍近い違いが出ることもあるので、 なるべく外部コマンド使わずシェルの機能を使ったほうが良いです。

追記ここまで

Sponsored Links
  1. 入力側はダブルクォートで囲っている場合、Bashのエスケープで一度\\\に変換され、その後sedのエスケープでもう一回変換されて 最終的に\nにするために\が4つ必要です。

    この例だとそうですが、もし、他に変数等を使ってない場合には シングルクォートで囲って

    $ echo  "aaa\nbbb"|sed 's/\\n/\n/'
    aaa
    bbb
    

    とすれば、今度は\\の部分は直接sedに渡されるため、 sedで一度エスケープされるだけで\nになります。

  2. ここでは変換後の方も改行を入れるときにはBash用エスケープが必要。

    シングルクォートなら

    echo  "aaa\nbbb"|sed 's/\\n/\
    /'
    

    でOK。

Sponsored Links

« Coverallsを使ってみた: GitHubのレポジトリにバッジを貼りたかったから2 sentakuで飛び飛びの複数選択を出来るようにした »

}