Embedding sed script into Makefile

何らかの目的があって複数のシェルスクリプトを書き殴る必要があるとき、Makefileシェルスクリプトをガシガシ埋め込んでいくことが多いです。偽のターゲット(Phony Target)でmakeをラウンチャのように使えますし、ファイルの依存関係も見てくれます。並列化の恩恵を受けることも。何より一つのファイルになっているので見通しがよいように感じます。

シェルスクリプトMakefileに埋め込む際に注意することはさほど多くはありません。

  • 各行頭はタブで始める。タブは削除される
  • 1行シェルを複数行で書くときは行末に\(バックスラッシュ)を置く。\と改行文字は空白文字へと変換される
  • $は$ $として記載する。$ $は$へと変換される(Markdown記法とMathjaxが誤変換してしまうため$の間に空白を入れていますが、本来はありません)

具体的には以下のような内容になるでしょう。ここでは1〜10までの数値\(i\)とその二乗の数\(s = i^2\)を表示します。

doit: Makefile
  seq 1 10 | while read i; do \
    s=`echo $$i 2 ^ p | dc`; \
    echo "$$i $$s"; \
  done

これは以下のような1行シェルに変換され、サブシェルで処理されます。

seq 1 10 | while read i; do   s=`echo $$i 2 ^ p | dc`;   echo "$$i $$s"; done

さて、これをsedスクリプトでもやりたいと常々思っていました。GNU sedであれば1行でループを含んだスクリプトが書けるので(書けそうなので)問題なさそうなのですが、macOSに付属しているBSD sedでは明示的に改行を含んだスクリプトを記述しなければならないらしく、とても厄介です。今回用意したsedスクリプトは行末が,(コンマ)で終わっている行を次の行とつなげる簡単なものです。

:loop
/,$/ {
  N
  s/\n/ /
  b loop
}
p

ここは好みなのですが、自分はsedスクリプトを含んだシェルスクリプトを用意した方が他のシェルコマンドを応用できるため自由度が高く、また保守性も高いように感じています。

#!/bin/sh

sed -ne '
:loop
/,$/ {
  N
  s/\n/ /
  b loop
}
p
'

以下のようなファイルに対して適用すると、

1,
2,
3
4,
5

以下のような出力を得ます。

> ./sample.sh < sample.txt
1, 2, 3
4, 5

さて、ある程度大きいsedスクリプトにならないとき、これをMakefileに埋め込んめてしまえればなあと思うことが少なくありませんでした。重い腰を上げて色々と試してみたのですが、決定打のようなものは打ち出せませんでした。およそ綺麗とは言い難い解決法ですが、備忘録として記載しておきます。

  • (おそらくGNU make拡張の)define/endefを使ってヒアドキュメント的に記述する
  • echoコマンドを併用してsedファイルとして書き出し、sedコマンドから呼び出す

ヒアドキュメントを記述する際は、

  • $は$ $に置き換える
  • makeが展開する際に1行として認識するように、行末に\を付ける
  • echoが展開する際に改行がなされるように行末に\nを付ける
  • \nなどは\\\\n等に置き換える
define SED_SCRIPT
:loop\n\
/,$$/ {\n\
  N\n\
  s/\\\\n/ /\n\
  b loop\n\
}\n\
p
endef

doit: a.sed sample.txt
  cat sample.txt | \
    sed -n -f a.sed

a.sed: Makefile
  echo "$(SED_SCRIPT)" > $@

echo文で記載されている"(ダブルクォート)はとても大事です。これがないと\nは思ったように展開されません。つまるところ、makeはsedスクリプトを書き出す際に以下のことをしています。分かれば仕組みは簡単ですね。

echo ":loop\n /,$/ {\n N\n s/\\\\n/ /\n b loop\n }\n p" | sed -e '2,$s/^ //' > a.sed

ちなみに書き出されたsedスクリプトはmakeによる行末の\と改行文字を空白に直すこと、またechoコマンドの副作用のせいで空白の意味合いが変わってきてしまっています。あまりよくないですね。

:loop
 /,$/ {
 N
 s/\n/ /
 b loop
 }
 p