初めての変数

前回は、Makefileで変数を利用する方法を取り上げた。2つのターゲットにおいて同じ値を使う部分を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の変数は、現在主流のプログラミング言語で使われている変数とはかなり趣向が異なっている。前回取り上げた使い方は比較的どのプログラミング言語の経験であっても理解しやすいものだったのではないかと思うが、もっと突っ込んだ使い方をしていくと、Makefileの変数は独特であることがわかる。ターゲットやファイルが依存関係でツリー状に整理されてから処理されるという性質上、変数も同じような仕組みになっているのだ。

Makefileの変数には多くの機能があるため、一度にその全てを紹介することは難しい。しかし、ちょっと複雑なMakefileになると必ずそうした変数が使われているので、わかるようにしておくことに越したことはない。今回は、そうしたMakefileの変数の中でも、特に基本的な挙動について説明しようと思う。

「=」は実行時に再起的に変数を処理する

前回のサンプルでは「変数名=値」で変数を定義していた。次のMakefileを見てみよう。

val1 = 1つ目の値
val2 = $(val1)

test:
    echo val2: $(val2)

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

% make
echo val2: 1つ目の値
val2: 1つ目の値
% 

この動きは理解しやすいだろう。「$(val2)」で参照する対象は「$(val1)」だ。$(val1)は「1つ目の値」が設定されているので、「echo val2: $(val2)」の出力結果は「val2: 1つ目の値」となる。この動きはMakefileにおける「=」で定義された変数の挙動を知らなくても、何となく理解できるはずだ(実態は違うのだが……)。

今度は次のMakefileを見てみよう。

val1 = 1つ目の値
val2 = $(val1)

val1 = 2つ目の値

test:
    echo val2: $(val2)

変数定義が終わった後で「val1 = 2つ目の値」として、val1を再度定義しているように見える。このMakefileを実行すると次のようになる。

実行サンプル

% make 
echo val2: 2つ目の値
val2: 2つ目の値
% 
  • 実行サンプル

    実行サンプル

GNU makeの挙動としてはこの動きは当然なのが、現在主流のプログラミング言語を使っている方にとっては意味不明な挙動に映るのではないだろうか。

手続き型のプログラミング言語、ないしは、手続き型プログラミング言語の仕組みを取り入れたプログラミング言語(そして現主流のプログラミング言語の多くがそうしている)の動きを考えると、「val2 = $(val1)」という定義を行なった時点で変数「val2」の値は「1つ目の値」になる。その後、val1にどの値が設定されようが、val2の値には変化を与えない。このように動くプログラミング言語に慣れている方が多いと思う。

Makefileの場合、「=」で定義した変数はそうは動かない。Makefileでは「=」で定義した変数は実行される段階に最終的に処理される。そのため、「$(val2)」を参照した段階で「val2 = $(val1)」が評価され、さらに「val1 = 2つ目の値」が評価されるので、$(val2)の値が「2つ目の値」になる。

Makefileにおけるこの挙動はとても強力だ。この仕組みを使うことでMakefileはさまざまなことを実現するのだが、この仕組みがユーザーの頭を混乱のるつぼに陥れるのもまた間違いないところだ。慣れないうちはあまり複雑なことはしない方が良いと思うのだが、実際のMakefileではこの仕組みが結構使われているので、その動きは知っておく必要がある。

「:=」は定義時に再起的に変数を処理する

GNU makeのMakefileでは変数を定義する方法として「:=」についても知っておく必要がある。「=」と同じように変数を定義するものなのだが、評価のタイミングが違う。現在主流のプログラミング言語における「変数」に慣れている方は、どちらかと言うと「:=」の方が理解しやすいだろう。

次のMakefileを考えてみよう。

val1 = 1つ目の値
val2 := $(val1)

val1 = 2つ目の値

test:
    echo val2: $(val2)

このMakefileを実行するとどうなるか考えてみよう。先ほどの例であれば「2つ目の値」が表示されるはずだが、実行すると次のようになる。

% make
echo val2: 1つ目の値
val2: 1つ目の値
% 

今度は「1つ目の値」が表示された。これは「:=」の評価のタイミングが「=」とは異なるためだ。

「:=」は変数が定義された段階で評価を行う。先ほどのMakefileの例であれば、「val2 := $(val1)」と書いた段階で評価が行われ、そこから$(val1)が「val1 = 1つ目の値」と評価されるので、$(val2)の値が「1つ目の値」としてフィックスされる。さらに後からval2の定義を行わない限り、$(val2)の値は「1つ目の値」になるわけだ。

GNUのMakefileにおいて「=」と「:=」はもっとも基本的なものだが、Makefileで変数を理解する上でまず最初に理解しなければならないところでもある。とても重要だ。この違いを理解していないと、Makefileはかなり難解なものになる。「=」と「:=」の違いはこの段階で理解しておこう。

「$($($($($(val5)))))」みたいに変数はネストして参照することができる

Makefileを見るとき、ユーザーを混乱のるつぼに陥れる書き方として最初に理解しておきたいのが、変数を書くときに「入れ子」構造で書くことだ。入れ子、もしくは「ネスト」といった呼び方をされることもある。次のMakefileを見てみよう。

val1=   1つ目の値
val2=   val1

test:
    echo $($(val2))

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

% make
echo 1つ目の値
1つ目の値
% 

ここでは変数に書き方に注目だ。「$($(val2))」のように書いてある。すでに意味不明な書き方になりつつあるが、これはこれで何かと必要な書き方だったりする。この書き方がどのように評価されるのかを1つずつ見ていこう。

まず、次が最初の状態だ。

test:
    echo $($(val2))

最初に評価できるのは「$(val2)」の部分だ。val2は「val2= val1」と定義されているので、「$(val2)」は「val1」になる。つまり、次の段階では次のように置き換わることになる。

test:
    echo $(val1)

次の処理できるのは「$(val1)」だ。この値は「val1= 1つ目の値」と書いてあるので、「1つ目の値」になる。次のような感じだ。

test:
    echo 1つ目の値

このように変数を展開した結果が、次の変数になっていくという連鎖を起こすことができる。$()の中に$()が書いてあるので、入れ子やネストと呼ばれているわけだ。

この入れ子は、中にもっとたくさん入れることもできる。次のような感じだ。

val1=   1つ目の値
val2=   val1
val3=   val2
val4=   val3
val5=   val4

test:
    echo $($($($($(val5)))))

かなり見慣れない書き方になっているかもしれないが、これはこれでちゃんと機能する。実行すると次のようになる。

% make
echo 1つ目の値
1つ目の値
% 

この場合には次のように展開が行われていく。

元の記述

test:
    echo $($($($($(val5)))))

1つ目の展開が終わった後

test:
    echo $($($($(val4))))

2つ目の展開が終わった後

test:
    echo $($($(val3)))

3つ目の展開が終わった後

test:
    echo $($(val2))

4つ目の展開が終わった後

test:
    echo $(val1)

5つ目の展開が終わった後

test:
    echo 1つ目の値

こんな感じで変数を使うことができる。

そしてお気づきの通り、この書き方は人間にとってはかなり理解しにくい。処理を切り分けるときなどにはこの書き方ができるとかなりよいのだが、人間にとって読みやすい書き方とは言い難い。利用は最小限にしておく方が無難だ。ただし、この機能もMakefileではよく使われているので、こういった書き方ができるということと、どのように展開されるかは理解しておいた方が良い。

「?=」で未定義の変数だけ定義できる

変数の定義でこのタイミングで覚えておきたい書き方に「?=」がある。これは変数定義なのだが、既に変数が存在する場合には何もしない。変数が定義されていない時だけ、その変数を定義する。

このように変数定義に条件があることから、「条件分岐変数代入演算子(conditional variable assignment operator)」といった名前で呼ばれている。

とりあえず動作を見てみよう。まず次のMakefileだ。

val1=   1つ目の値

val1=   2つ目の値

test:
    echo $(val1)

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

% make 
echo 2つ目の値
2つ目の値
% 

この挙動はこれまでに説明してきた通りだ。次に、Makefileを次のように書き換える。

val1=   1つ目の値

val1?=  2つ目の値

test:
    echo $(val1)

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

% make
echo 1つ目の値
1つ目の値
% 

「val1?= 2つ目の値」の方の定義が機能していないというか、変数を設定してないことがわかる。これはval1がすでに「val1= 1つ目の値」として定義されているためだ。「?=」を使うと、このように既に定義されている変数には上書きできなくなる。

こういう機能を知ったときは、最初に際どい動作について調べておくのが得策だ。次のようにMakefileを書き換えてみる。

val1=   

val1?=  2つ目の値

test:
    echo $(val1)

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

% make
echo 

% 

人によっては意味不明な挙動に見えるのではないだろうか。これは「val1= 」が変数定義として機能していることを意味している。空の値を持った変数が定義されているわけだ。

では次のMakefileを見てみよう。

val1?=  2つ目の値

test:
    echo $(val1)

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

% make 
echo 2つ目の値
2つ目の値
% 

今度は「?=」で定義した変数が設定されていることがわかる。ここではval1が定義されていないので、「?=」で定義されたval1が有効になっているわけだ。

「?=」は値が定義されていない場合のデフォルト値を設定する方法として利用できるほか、処理を分岐させるといった方法でも利用することができる。この機能を使った分岐処理もしばしば行われるのだが、読みやすいかと言われるとちょっとそれはどうかという気もする。必要がなければ使わない方が良いだろう。ただし、こういった書き方が使われているケースがあるのは間違いなので、どのように動作するかは知っておこう。

変数の定義だけでもこの動きの違い

Makefileの変数は、ひと口に変数と言っても「=」と「:=」というように決定的に挙動の違うものがあり、「?=」のような書き方もある。そして「=」がそもそも実行時に再起的に評価されていくという特徴的な動作を行うものになっている。Makefileの変数は現在主流のプログラミング言語から見るとなかなかに独特なのだ。

自前でMakefileを使う、特に仕事の内容を整理しておくと言った目的であれば、変数の利用はシンプルなものに収まることが多い。このため、Makefileの変数について深く知っておく必要性は低い。しかし、Makefileを使うので一度はMakefileの変数についてよく知っておくことが望ましい。全て覚える必要はないが、必要なものは覚えると共に、それ以外のものは後からマニュアルを読み返した時「あぁ、そんな機能もあったな」くらいに思い出せる程度に嗜んでおけばよいと思う。

基本的にはシンプルなMakefileなのだが、ちょっとした機能の追加でなかなかに興味深い動きをするようになる。Makefileの機能を知った上で、シンプルなMakefileを書けるようになるのが理想的だ。

参考