シェルスクリプトをMakefileへ書き換える

前回、Makefileにコマンドを書いていく方法として「まず実装したい内容をシェルスクリプトとして作成し、これをMakefileへ書き換える」というやり方について説明した。その際作成した2つのシェルスクリプトはそれぞれ次の通りだ。

create2022dirs.sh

#!/bin/sh

sday="20220101"
fmt="%Y%m%d"

for i in $(seq 0 364)
do
    mkdir $(date -d "$sday +$i day" +"$fmt")
done

delete2022dirs.sh

#!/bin/sh

sday="20220101"
fmt="%Y%m%d"

for i in $(seq 0 364)
do
    rm -r $(date -d "$sday +$i day" +"$fmt")
done

「create_2022_dirs.sh」は2022年の日付、例えば「20220308」のような「YYYYmmdd」というフォーマットのディレクトリを作成するシェルスクリプトであり、もう一方の「delete_2022_dirs.sh」はcreate_2022_dirs.shで作成したディレクトリを削除するものだ。日付を名前にしたディレクトリを作成してデータファイルなどを配置しておくというのはよくある使い方なので、これは結構実用的なシェルスクリプトだと言えるだろう。

前回はこの2つのシェルスクリプトを統合して、1つのMakefileを作成する場合の例を紹介した。作成したMakefileは次の通りだ。

sday=20220101
fmt=%Y%m%d

create_2022_dirs:
    # 2022年の日付ディレクトリを作成
    for i in $$(seq 0 364);                 \
    do                          \
        mkdir $$(date -d "${sday} +$$i day" +${fmt});   \
    done

delete_2022_dirs:
    # 2022年の日付ディレクトリを消去
    for i in $$(seq 0 364);                 \
    do                          \
        rm -r $$(date -d "${sday} +$$i day" +${fmt});   \
    done

実行すると次のようになる。

  • 前回作成したMakefileの実行サンプル

    前回作成したMakefileの実行サンプル

このまま説明しようかと思ったのだが、上記MakefileだとワンライナーとMakefileの変数の両方について同時に説明をしなければならない。そこでまずは、前回のMakefileを書き換えた次のMakefileを使って説明しよう。

create_2022_dirs:
    # 2022年の日付ディレクトリを作成
    sday="20220101";                    \
    fmt="%Y%m%d";                       \
    for i in $$(seq 0 364);                 \
    do                          \
        mkdir $$(date -d "$$sday +$$i day" +$$fmt); \
    done

delete_2022_dirs:
    # 2022年の日付ディレクトリを消去
    sday="20220101";                    \
    fmt="%Y%m%d";                       \
    for i in $$(seq 0 364);                 \
    do                          \
        rm -r $$(date -d "$$sday +$$i day" +$$fmt); \
    done

書き方は変えたものの、実行される内容は同じだ。以下に、書き換えた方のMakefileの実行結果を示す。

  • 今回作成するMakefileの実行サンプル

    今回作成するMakefileの実行サンプル

では早速、今回のMakefileの作り方を説明していく。

シェルスクリプトをワンライナーへ書き換える方法

前回説明したように、Makefileのターゲット行以降に続く行は、先頭がタブで、それに1行で書くワンライナーが続く。ここにシェルスクリプトのような書き方をすることはできない。1つのシェルスクリプトは、1行にまとめる必要がある。

create_2022_dirs.shのうち、メインの処理となる次の部分を考えてみよう。

for i in $(seq 0 364)
do
    mkdir $(date -d "$sday +$i day" +"$fmt")
done

まずはこの部分を4行から1行へまとめていく。

ここでは、制御構文として「for-do-done」が使われている。「do」と「done」の間にコマンドを書き、それを「for」で指定されている分だけ繰り返すという処理だ。「for-do-done」の書き方なのだが、「do」の後は「空白」「タブ」「改行」「コマンド」のいずれかでなければならない。必ずしも改行する必要はなく、空白やタブ区切りで続けてコマンドを書き出してもよいことになっている。つまり、次のように書くことができる。

for i in $(seq 0 364)
do mkdir $(date -d "$sday +$i day" +"$fmt")
done

これで3行になる。

シェルでは、「;」を挟むことで「改行」と同じような意味を持たせることができる。つまり、先ほどの3行は「;」で連結することで次のように1行にまとめることができる。

for i in $(seq 0 364); do mkdir $(date -d "$sday +$i day" +"$fmt"); done

1行になった。これにシェルスクリプトcreate_2022_dirs.shの残りの部分をくっつけると、次のようになる。

sday="20220101"
fmt="%Y%m%d"
for i in $(seq 0 364); do mkdir $(date -d "$sday +$i day" +"$fmt"); done

追加した2行は変数を設定する部分だ。この部分も「;」を使って連結することができるので、最終的に次のようなワンライナーを得ることができる。

sday="20220101"; fmt="%Y%m%d"; for i in $(seq 0 364); do mkdir $(date -d "$sday +$i day" +"$fmt"); done

この状態ですでにMakefileに書くことができるわけだが、このままでは人間が見たときに理解しにくい。そこで、ここから「ワンライナーでありながら複数行に展開する」という逆の作業を加えていく。

Makefileでは行末に「\」を書くことで、1つの行を2つの行へ分けることができる。先ほど作成したワンライナーは基本的には改行部分を「;」に置き換えることで1行にしていったわけだから、今度は基本的には「;」の後に「\」加えてから改行すれば、複数行に渡って書くことができるようになる。次のようなイメージだ。

sday="20220101";\
fmt="%Y%m%d";\
for i in $(seq 0 364);\
do mkdir $(date -d "$sday +$i day" +"$fmt");\
done

もしくは、最初のシェルスクリプトでは「for-do-done」の「do」の後は改行してあったので、次のように展開してもよい。

sday="20220101";\
fmt="%Y%m%d";\
for i in $(seq 0 364);\
do\
    mkdir $(date -d "$sday +$i day" +"$fmt");\
done

最後に行末の「\」をタブや空白を使って揃えると次のようになる。

sday="20220101";                    \
fmt="%Y%m%d";                       \
for i in $(seq 0 364);                  \
do                          \
    mkdir $(date -d "$sday +$i day" +"$fmt");   \
done

「\」ではなく「;」で揃えてもよい。

sday="20220101"                     ;\
fmt="%Y%m%d"                        ;\
for i in $(seq 0 364)                   ;\
do                           \
    mkdir $(date -d "$sday +$i day" +"$fmt")    ;\
done

行末が「\」しかない行と「;\」になる行の2つが出てきてしまうので、基本的には「\」を行末に揃えたほうが見栄えはよくなるのではないかと思うが、その辺りは好みで整えてもらえばよい。

※ 「for-do-done」のワンライナーには一点注意が必要だ。「do」はその後に直接「;」を書くことはできない。必ず「do コマンド;」のようにコマンドが挟まっている必要がある。つまり、ワンライナーとして「do; コマンド;」といった書き方はできず、「do コマンド;」でなければならない。

ワンライナーをMakefileへはめ込む

先ほど整理したものをMakefileへはめ込むと次のようになる。

create_2022_dirs:
    sday="20220101";                    \
    fmt="%Y%m%d";                       \
    for i in $(seq 0 364);                  \
    do                          \
        mkdir $(date -d "$sday +$i day" +"$fmt");   \
    done

この状態で「make create_2022_dirs」を実行すると次のようになる。

% make create_2022_dirs
sday="20220101";                                        \
fmt="%Y%m%d";                                           \
for i in ;                                      \
do                                                      \
        mkdir ; \
done
% 

ご覧の通り、この状態だと思ったようには実行されない。「$」から始まる文字列がMakefileではMakefileの変数として解釈されるためだ。したがって、今度はワンライナー化したものをMakefile向けにエスケープ処理しなければならない。

Makefileと衝突する表記を書き換える

書き換えは単純だ。「$」はMakefileの変数として解釈されるので、この部分を「$$」に書き換える。書き換えると次のようになる。

create_2022_dirs:
    sday="20220101";                    \
    fmt="%Y%m%d";                       \
    for i in $$(seq 0 364);                 \
    do                          \
        mkdir $$(date -d "$$sday +$$i day" +"$$fmt");   \
    done

この状態で実行すると次のようになる。

% make create_2022_dirs
sday="20220101";                                        \
fmt="%Y%m%d";                                           \
for i in $(seq 0 364);                                  \
do                                                      \
    mkdir $(date -d "$sday +$i day" +"$fmt");       \
done
% 

意図した通りに動作していることがわかる。ここまでがシェルスクリプトからMakefileへの書き換えの一連の流れということになる。

makeがコマンドを実行するまでの流れを把握する

今度は逆に、Makefileに書いてあるコマンドがどのように評価されるのかを追ってみよう。一旦Makefileに起こした後は、処理を変更する場合はMakefileを直接書き換えることになるので、どのように処理されるのかを理解していないと困ったことになるからだ。

まず、次の処理を考える。

create_2022_dirs:
    sday="20220101";                    \
    fmt="%Y%m%d";                       \
    for i in $$(seq 0 364);                 \
    do                          \
        mkdir $$(date -d "$$sday +$$i day" +"$$fmt");   \
    done

makeは、次の記述を1行であると解釈する。

    sday="20220101";                    \
    fmt="%Y%m%d";                       \
    for i in $$(seq 0 364);                 \
    do                          \
        mkdir $$(date -d "$$sday +$$i day" +"$$fmt");   \
    done

実際にどう処理されるかは実装系に依存するのだが、大体の流れは同じだ。まず、行頭のタブは削除される。

sday="20220101";                    \
    fmt="%Y%m%d";                       \
    for i in $$(seq 0 364);                 \
    do                          \
        mkdir $$(date -d "$$sday +$$i day" +"$$fmt");   \
    done

次に「\」で分割していた部分が連結されて1行になる。

sday="20220101";                        fmt="%Y%m%d";                           for i in $$(seq 0 364);                     do                                  mkdir $$(date -d "$$sday +$$i day" +"$$fmt");       done

「$$」が「$」に変換される。

sday="20220101";                        fmt="%Y%m%d";                           for i in $(seq 0 364);                      do                                  mkdir $(date -d "$sday +$i day" +"$fmt");       done

この状態で、文字列がシェルへ渡り実行される、という流れだ。

シェルスクリプトを書くのと比べると、Makefileにワンライナーを書く作業はやや面倒だと思うかも知れない。だが、今回説明したように、基本的な書き換え方はそれほど難しいものではない。ある程度慣れてしまえば、むしろ簡単なはずだ。

何度か書き換えていると、長いシェルスクリプトも書き換えることができるようになる。長いコマンドをMakefileに書き込むことがありそうなら、練習しておくとよいのではないだろうか。

参考