前回作ったMakefileをもうちょっと加工する

前回、create_2022_dirs.shスクリプトの中身をワンライナー化してMakefileで利用する方法を説明した。最終的に出来上がったMakefileは次の通りだ。一見、シェルスクリプトが書いてあるように見えるが、実際には1行のワンライナーコマンドをそれっぽく書いてあるだけである。

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

シェルスクリプトとしてはcreate_2022_dirs.shだけではなく、create_2022_dirs.shで作成したディレクトリを削除するためのdelete_2022_dirs.shも作成した。

delete_2022_dirs.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

delete_2022_dirs.shも同じ要領でワンライナー化してMakefileへ書き換えると、次のようになる。

delete_2022_dirs:
    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とdelete_2022_dirs.shを1つの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

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

今回はこのMakefileをさらに整理するところから始めよう。

ターゲットに共通している部分をまとめる

create_2022_dirs.shとdelete_2022_dirs.shを1つのMakefileへ書き換えたものを見ていくと、create_2022_dirsターゲットにもdelete_2022_dirsターゲットにも、全く同じ次の2行が含まれていることを確認できる。

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

この2行は開始日と作成するディレクトリ名のフォーマットを指定するものだ。create_2022_dirsターゲットにもdelete_2022_dirsターゲットにも必要であり、かつ、共通になっている必要がある。

こういった値はワンライナーに含めておくのではなく、ターゲットよりも外側に共通データとして保持しておく方がよい。ここでは「Makefileの変数」としてこのデータを保持することでこれを実現する。

Makefileの変数

Makefileの変数と、ワンライナーで使われるシェルの変数は全く別のものだ。ここは混乱しやすいところなので、簡単なサンプルから動作の違いを追うことで中身を把握してみよう。

まず、次のようなMakefileを用意する。

test:
    sday="20220101"
    echo "1: $$sday"; sday="20220102"
    echo "2: $$sday"; sday="20220103"
    echo "3: $$sday"; sday="20220104"
    echo "4: $$sday"; sday="20220105"

ターゲットtestを実行すると次のようになる。

% make test
sday="20220101"
echo "1: $sday"; sday="20220102"
1: 
echo "2: $sday"; sday="20220103"
2: 
echo "3: $sday"; sday="20220104"
3: 
echo "4: $sday"; sday="20220105"
4: 
% 

「sday」はシェル変数だ。そして、設定したシェル変数が行を超えていないことがわかる。Makefileでは1行ごと1行ごとにコマンドの実行は独立している。要するに、1行1行が個別のシェルスクリプトのようなもので、その行でシェル変数を設定してもほかの行にはまるで影響を与えない。

create_2022_dirsターゲットや、delete_2022_dirsターゲットには、複数行にまたがるコマンドが書いてあるが、行末に「\」を指定して複数行に表記しているだけである。つまり、あれは「\」をまたいだ全体で1行なのだ。その行の中ではシェル変数は効果を持つが、次の行に移れば設定したシェル変数は全く意味を成さない。

試しに、シェル変数sdayを、ターゲットの前に移動させてみよう。

sday="20220101"

test:
    echo "1: $$sday"; sday="20220102"
    echo "2: $$sday"; sday="20220103"
    echo "3: $$sday"; sday="20220104"
    echo "4: $$sday"; sday="20220105"

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

% make test
echo "1: $sday"; sday="20220102"
1: 
echo "2: $sday"; sday="20220103"
2: 
echo "3: $sday"; sday="20220104"
3: 
echo "4: $sday"; sday="20220105"
4: 
% 

思ったようには機能していない。Makefileの変数の書き方はよいのだが、コマンド行に書いてあるのが「$$sday」なのでMakefileの変数としては参照してくれないためだ。

では、コマンド行の「$$sday」を「$sday」へ書き換えてみる。

sday="20220101"

test:
    echo "1: $sday"; sday="20220102"
    echo "2: $sday"; sday="20220103"
    echo "3: $sday"; sday="20220104"
    echo "4: $sday"; sday="20220105"

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

% make test
echo "1: day"; sday="20220102"
1: day
echo "2: day"; sday="20220103"
2: day
echo "3: day"; sday="20220104"
3: day
echo "4: day"; sday="20220105"
4: day
% 

ちょっとわかりにくいが、「$sday」という書き方で「$s」というMakefileの変数参照と、「day」という文字列という指定になってしまっていることがわかる。「$sday」では最初の1文字だけが変数名として解釈されるわけだ。

sday全体を変数名として認識させるには、「${sday}」のように、名前全体を「{}」で囲む必要がある。また、Makefile変数とシェル変数が別の動きをしていることを示したいので、シェル変数の設定とechoの書き順を逆にしておく。書き換えたMakefileは以下のようになる。

sday="20220101"

test:
    sday="20220102"; echo "1: ${sday}" 
    sday="20220103"; echo "2: ${sday}" 
    sday="20220104"; echo "3: ${sday}" 
    sday="20220105"; echo "4: ${sday}" 

実行すると次のようになる。「${sday}」という表記がMakefile変数の参照になっていること、ターゲットの外側に書いた変数設定が効果を持っていること、Makfile変数とシェル変数が異なるものであることがおわかりいただけるのではないだろうか。

% make test
sday="20220102"; echo "1: "20220101""
1: 20220101
sday="20220103"; echo "2: "20220101""
2: 20220101
sday="20220104"; echo "3: "20220101""
3: 20220101
sday="20220105"; echo "4: "20220101""
4: 20220101
% 

ただし、「${sday}」の展開された文字列が「"20220101"」になっている。シェルであれば「sday="20220101"」という記述で「20220101」が変数の値として保持されるのだが、Makefileでは書いた通りに「"20220101"」が変数の中身になっているのだ。

そこで、Makefileの変数設定でダブルクォーテーションを使わないように書き換えてみよう。

sday=20220101

test:
    sday="20220102"; echo "1: ${sday}"
    sday="20220103"; echo "2: ${sday}"
    sday="20220104"; echo "3: ${sday}"
    sday="20220105"; echo "4: ${sday}"

実行すると次のようになる。かなり想定に近い動きになった。

% make test
sday="20220102"; echo "1: 20220101"
1: 20220101
sday="20220103"; echo "2: 20220101"
2: 20220101
sday="20220104"; echo "3: 20220101"
3: 20220101
sday="20220105"; echo "4: 20220101"
4: 20220101
% 

次に、Makefileにおける変数定義がどの順序で評価されるのかを次のMakefileで調べてみる。

sday=20220101

test1:
    echo "1: ${sday}"

sday=20220102

test2:
    echo "2: ${sday}"

sday=20220103

test3:
    echo "3: ${sday}"

sday=20220104

test4:
    echo "4: ${sday}"

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

% make test1 test2 test3 test4
echo "1: 20220104"
1: 20220104
echo "2: 20220104"
2: 20220104
echo "3: 20220104"
3: 20220104
echo "4: 20220104"
4: 20220104
% 

Makefileで最後に評価された変数定義が有効になっていることがわかる。いったんMakefileの内容を全て評価してからターゲットが処理されていることになる。

つまり、今回やりたいことを実現するには、次のように変数を使えばよい。

sday=20220101

test1:
    echo "1: ${sday}"

test2:
    echo "2: ${sday}"

実行すると、以下のようになる。

% make test1 test2
echo "1: 20220101"
1: 20220101
echo "2: 20220101"
2: 20220101
% 

単純なように見えるが、「変数名=値」という変数定義の方法と「${変数名}」という表記はシェルでも使える方法なので、Makefile変数とシェルコマンドが入り交じるMakefileでは混同しがちになるのだ。この辺りの動作は、基本として最初に押さえておきたいところだ。

Makefileの変数を使うように書き換える

create_2022_dirsターゲットとdelete_2022_dirsターゲットの共通部分を、先ほどの説明に合わせて変数にまとめると次のようになる。

sday=20220101
fmt=%Y%m%d

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

delete_2022_dirs:
    for i in $$(seq 0 364);                 \
    do                          \
        rm -r $$(date -d "${sday} +$$i day" +${fmt});   \
    done

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

make create_2022_dirsの実行サンプル

% make create_2022_dirs
for i in $(seq 0 364);                                  \
do                                                      \
        mkdir $(date -d "20220101 +$i day" +%Y%m%d);    \
done
% 
  • make create_2022_dirsの実行サンプル

    make create_2022_dirsの実行サンプル

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

% make delete_2022_dirs
for i in $(seq 0 364);                                  \
do                                                      \
        rm -r $(date -d "20220101 +$i day" +%Y%m%d);    \
done
%
  • make delete_2022_dirsの実行サンプル

    make delete_2022_dirsの実行サンプル

これでcreate_2022_dirsターゲットとdelete_2022_dirsターゲットの共通部分を抜き出し、1カ所にまとめておくことができた。そして「$」がMakefile変数への参照で、「$$」がシェル変数における参照へのエスケープ表現であることも示された。

Makefile変数とシェル変数をもうちょっと区別した書き方

もう一度今回作成したMakefileを見てみよう。

sday=20220101
fmt=%Y%m%d

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

delete_2022_dirs:
    for i in $$(seq 0 364);                 \
    do                          \
        rm -r $$(date -d "${sday} +$$i day" +${fmt});   \
    done

ワンライナーの中で「$$」と「$」の表記が同居していてわかりにくいが、「$」がMakefileの変数で、「$$」がシェル変数へのエスケープ表記だ。Makefileを読み書きする上で、この2つの区別はとても重要になってくる。考えなくても区別できるくらいになると、Makefileを書くのも読むのもグッと簡単になるはずだ。

表記自体をもうちょっとわかりやすいように変えるというのも手だ。やり方はいろいろあるが、例えば次のような切り分けをするという方法もある。

  • Makefile変数には大文字を使う
  • Makefile変数の参照は「$(変数名)」の表記を使う
  • シェル変数には小文字を使う
  • シェル変数の参照は「$${変数名}」の表記を使う
  • シェルのコマンド置換には「``」を使う

このルールに従って先ほどのMakefileを書き換えると、次のようになる。

SDAY=20220101
FMT=%Y%m%d

create_2022_dirs:
    for i in `seq 0 364`;                   \
    do                          \
        mkdir `date -d "$(SDAY) +$${i} day" +$(FMT)`;   \
    done

delete_2022_dirs:
    for i in `seq 0 364`;                   \
    do                          \
        rm -r `date -d "$(SDAY) +$${i} day" +$(FMT)`;   \
    done

Makefile変数は「${変数名}」ではなく「$(変数名)」という書き方もできるので、明示的にこちらに書き換えるのだ。こうしておくとMakefile変数であることが明確になり、誤った解釈や書き方が減ることにもなる。Makefile変数とシェル変数、シェルのコマンド置換などを組み合わせたMakefileを何度か書いてみよう。慣れてくると、これだけでかなり多くのことができることに気づくはずだ。

参考