🧨

ちょっとMakefileくん! .envに書いた環境変数読んで!!!!!なんで読んでくれないの!?

5 min read 8

あらすじ
「メ、Makefileちゃん!変数引き継いでよ!」
「うるさいですね……」make,make

って感じで、Makefileと同じディレクトリ上にある.envファイルから、Makefileに環境変数を引き継ぐのが全然できなくて無限に時間を使ってしまった!!悔しいからメモにしちゃう。も〜最悪!

2021/10/22追記
記事中のやり方はすべて「シェル変数」として読み込まれるやり方です!!!!!!
コメントにてtel.aokiさんより、「環境変数」として読み込む方法をご紹介いただきました!!
こちらぜひ読んでください!!

2021/10/21追記
arai-taさん,tel.aokiさんよりincludeを使う方法も紹介いただきました!!
こちらも最後にあるのでぜひ読んでください!!

2021/10/20追記
magicantさんのコメントによって謎が解明されました!!!
最後の追記まで読んでいただけると助かります!!

詳しい方いたら、最後の謎について教えて欲しいです!

解決しないといけなかったこと

こんな.envファイルがあるとするじゃろ

.env
ENV=XX

makeで、例えばこれをkeyとしてcurlなんかを打ちたいとしよう

Makefile
example:
	sh .env
	curl http://XXXX -X POST -H "Key: ${ENV}"

なぜかこれが通らなかった!

tl;dr

例えば.envが"ENV=XX"って内容のとき、その変数を引き継ぎたいmakeコマンドに下記の呪文を忍ばせることダナ。これで成功を祈るといい!

2021/10/20時点で禁止された魔法の呪文
$(eval ENV=$(shell . ./.env && echo $$ENV))

最初のENVはmake変数、最後のENVはシェル変数になっていることに注意!
(2021/10/20,21追記)
下記のいずれかの方法を採りましょう。これらがおそらく正攻法です。
上のようわからん黒魔術は避けましょう!!

curlにシェル変数ENVを渡すZE☆
example:
    . ./.env && curl -H "Key: $${ENV}"
includeでmake変数として引き継ぐZE☆
include .env
example:
    curl http://XXXX -X POST -H "Key: $(ENV)"

上記のいずれも環境変数として変数を定義しているわけではありません。
1つ目の例ではシェル変数、2つ目の例ではmake変数になっています!

環境変数として定義する方法はコメント欄にて最後のtel.aokiさんとの会話を追ってください!

いくつかの驚きの事実

環境変数とシェル変数、make変数の違い(20201/10/22追記)

  • 環境変数: シェルで定義される。定義を実施したプロセスだけでなく、そのプロセスを親とする子プロセスまで受け継がれる

シェルで"export XX=YY"と打ってみよう。これで環境変数XXが定義された。"echo XX"とすれば、YYと出力されるはず。
そしてそれはshやzshで立ち上げた子プロセスでも、"echo XX"とすれば、YYと出力される。有効になってる。
ただ、一度exitしてしまえばもう無効になる。我々がよくやる「.profileにexportを書き込んで環境変数を定義する」という行為は、.profileが毎回シェルを立ち上げる時に読み込まれる(=sourceコマンドで、一字一句手打ちで実行したのと同じ効果を生じさせている)のを利用しているわけ。
立ち上がり時に毎回.profileを読むのをシェルがやめたら人類は滅ぶ。

  • シェル変数: シェルで定義される。定義を実施したプロセスだけでしか有効でない

シェルで"XX=YY"とだけ打ってみよう。これでシェル変数XXが定義される。"echo XX"とすれば、YYと出力されるはず。
だけどそれはshやzshで立ち上げた子プロセスで"echo XX"としても何も出てこない。無効になってる。

  • make変数: Makefileの中で定義される変数。そのMakefile中で有効

Makefile中で"${}"という表記をすることで参照される

Makefileで改行したら違うプロセスになりますよ

Makefileは各行を個別のシェルプロセスで動作させます。
shellを-cつけて各行を実行してるのと同じなんだってさ。
1プロセスでやりたいなら、&&とかで繋ぐことだね!

https://stackoverflow.com/questions/54481072/source-env-variables-files-inside-makefile

shは新プロセスを立ち上げちゃう。sourceと.(dot)は現プロセスでファイルの中身を実行する

shは新たなプロセスを立ち上げちゃうってことが分かった。

https://www.softel.co.jp/blogs/tech/archives/5971
shで環境変数やシェル変数を読み込んでも、うまく引き継げないってわけ。
これに対して、sourceや.(dot)は、現在のプロセスに環境変数をロードしてくれる。頼もしい最高の仲間。

余談だけど、例えば下記の記事の挙動も、上記のことを踏まえれば説明がつくと思われる。

https://qiita.com/YumaInaura/items/00437e6ab14d96adb71f

sourceコマンドはMakefile中で動作しないことが多い。dotを使わんかい!

sourceコマンドは実行ファイルじゃないんだって……!!
まあとにかく、Makefile中では.(dot)で代用するのが無難らしい

https://stackoverflow.com/questions/44052093/makefile-with-source-get-error-no-such-file-or-directory

解明されなかった謎(2021/10/20に解明された謎)

ここまでの話から、要は.(dot)で読み込んでワンラインで処理すればいいんでしょと思ったあなた。私もそう思っていましたが、下記ではなんか上手く動作しませんでした。謎です。汝の環境でもしも動作したのならばそれで良かろう。

やっぱりうまくいかなかったコード(Makefile)
example:
	. ./.env && curl http://XXXX -X POST -H "Key: ${ENV}"

結局どうしたのか(2021/10/20段階でもうやっちゃいけないことになったやつ)

上のように、.(dot)で読み込み、そのままワンラインでcurl実行という作戦がうまくいかないので、無理矢理evalを使って、シェル変数をmake変数にぶち込んでしのいだんだってさ、めでたしめでたし(うーん????)

いちおう動いたコード(Makefile)
example:
	$(eval ENV=$(shell . ./.env && echo $$ENV))
	curl http://XXXX -X POST -H "Key: ${ENV}"

こんな記事ですが、詳しい方いたら教えて頂ければ幸いです😭

あー そーゆーことね 完全に理解した (2021/10/20追記)

magicantさんからのコメントで、解明されなかった謎は消え、完全に解決しました!!!
本当にありがとうございます😭

これならmakeさんもシェル変数ENVを受け取ってくれる
. ./.env && curl -H "Key: $${ENV}"

$を2つ重ねるとシェルで定義されている変数(シェル変数もしくは環境変数)、1つだとmake変数になるようです!
ちなみにダブルクォーテーションじゃないとダメです
記述されたコマンド(この例ではcurl)を実行するのはシェルであり、シェルっちはシェル変数でも環境変数でも、シングルで囲まれると変数だって認識できなくなってしまう(=文字列だと思っちゃう)からです。

みんなもちゃんとドルマーク2つ重ねて、ダブルで囲って、楽しいmake生活を送れよな!

↓公式ドキュメントを参照するまで眠れない人は下のリンクから見てみるといいぜ〜〜?
「$を2つ重ねるとシェルで定義されている変数(シェル変数もしくは環境変数)、1つだとmake変数」

For shells like the default shell, that use dollar signs to introduce variables, it’s important to keep clear in your mind whether the variable you want to reference is a make variable (use a single dollar sign) or a shell variable (use two dollar signs).
https://www.gnu.org/software/make/manual/html_node/Variables-in-Recipes.html

↓こっちは公式じゃないけど、公式ドキュメントあたる気力はなかったぜ〜?
シェルにおけるシングルクォーテーションとダブルクォーテーションの違い

Double Quotes
*Use when you want to enclose variables or use shell expansion inside a string.
*All characters within are interpreted as regular characters except for $ or ` which will be expanded on the shell.
Single Quotes
*All characters within single quotes are interpreted as a string character.
https://www.howtogeek.com/howto/29980/whats-the-difference-between-single-and-double-quotes-in-the-bash-shell/

こーいうのもあるってわけね!? (2021/10/21追記)

includeってワザもありみたいです!
こちらはarai-taさん、tel.aokiさんのコメントで紹介いただきました!ありがとうございます泣

奥義include
include .env
example:
    curl http://XXXX -X POST -H "Key: $(ENV)"

includeはもともと他のMakefileを読み込んでくれるってGNUは言ってるけど、上記みたいな使い方もできるんだねえ

The include directive tells make to suspend reading the current makefile and read one or more other makefiles before continuing.
https://www.gnu.org/software/make/manual/html_node/Include.html

なんかMakefileとかbashのあたりって知らないといけないことがたくさんあるけど、がんばってやっていこうぜ🤗

Discussion

. ./.env && curl http://XXXX -X POST -H "Key: ${ENV}"

. ./.env && curl http://XXXX -X POST -H "Key: $${ENV}"

にしたらたぶん動くと思います

magicantさん、ありがとうございます!
ご教示頂いた方法で通りました!!!!
ドルマーク重ねて環境変数として出力すればよかったんですね!!!
本当にありがとうございます。記事にも追記させていただきます!😊

makeには「include」で他のファイルを読み込む機能があり、
かつ.envは文法がMakefile互換です。

なのでこれで動くかと思いました。試してみてください。

include .env

example:
    curl http://XXXX -X POST -H "Key: $(ENV)"

include ディレクティブを使う解決方法は .env 内の変数を make の変数としても利用できるので良いですね。

厳密に Makefile 互換といえるかは難しいところがあると思います。.env の仕様を調査したのですが統一されたものはないようですので、できるかぎり互換になるように工夫する必要があります。.source を利用する方法では = まわりにスペースを置かないように、include ディレクティブでは $(...) や 変数中に # を書かないようにしなければなりません―― make の関数、コメントとして処理されてしまうからです。

arai-taさん、tel.aokiさん
includeを使う方法もあるのですね!!!書式の注意喚起まで、色々とご紹介くださりありがとうございます😭
こんなに色々な方からたくさん教えて頂けると思っていなかったので大変嬉しいです!
こちらも本文に追記させていただきます🤗

こちらこそ .env ファイルというものを知らなかったので勉強になりました。これは便利そうですね。

たびたびすいません... 訂正があります。
.env ファイルに列挙された定義はすべて「環境変数」だということを忘れていました。シェルスクリプトとして普通に . で読み込むと「シェル変数」として読まれてしまいます。include ディレクティブでも「make 変数」として定義したことになってしまいます。「シェル変数」はコマンド実行時に、「make 変数」はシェルに渡される以前のレシピ実行時に展開されてコマンドラインを形成する文字列になります。どちらでも curl のコマンドにオプションとして渡しているので今回の例では意図したとおりに動くのですが、別のスクリプトを呼び出してその内部で使ったりするときなどは「環境変数」になっている必要があります。記事の目的も環境変数として読ませること、ですよね? なので正確にはこうするのがよいです。

set -a && . ./.env && set +a && curl http://XXXX -X POST -H "Key: $${ENV}"
include .env
.EXPORT_ALL_VARIABLES:
example:
    curl http://XXXX -X POST -H "Key: $(ENV)"

どちらも“定義した変数を環境変数としても利用できるようにエクスポートする”機能を使っています。 Makefile の例では$(ENV)として「make変数」としても従来どおりにも使えています。ただ、シェルの例ではset +aで全エキスポートをやめているのに対し、Makefile では.EXPORT_ALL_VARIABLES特殊ターゲットの効果を戻す機能はありません。ファイル中で定義した全変数が、実行されるコマンドから環境変数として見えるようになってしまいます。

個人的には、運用上問題が生じなければ… まあいいのではないかと思います。
全面的に厳密な解決は可能ではあります。調べたところ、 shdotenv というプロジェクトが立ち上がっていて、これが .env ファイルを正しく読んで環境変数を作ってくれるので、その力を利用して .env を正当なメイクファイルに変換して include します。

大変丁寧にわかりやすくご説明くださりありがとうございます!!!
めちゃくちゃ勉強になりました。set -aをやったあと、set +aで戻せるんですね……!
.EXPORT_ALL_VARIABLESもありがとうございます。全然知らなかったです👶

私も少し考えていたのですが、場合によっては.envファイルの書き方を下記のようにしてsourceしちゃうのもアリだと気づきました。.(dot)がファイルの文字を手打ちして実行したのと変わらない結果をもたらすなら、これでも大丈夫だろうと考えました(いちおう動きはしました)。

exportを直接書きこんでおく
export ENV=XX

ところで、1つ下のコメントのENVには特殊な役割があるというのはけっこう注意ですね!?
例に使う文字列には気をつけないといけませんね笑 こちらもありがとうございました!

かなり詳細に書いて下さっているので、コメント参照する旨を記事中に追記させていただきます、よろしくお願いします😊

ところで調査中に偶然知ったのですが、例としてたまたま使われているENV環境変数には、特殊な役割があるらしいです。
sh や ksh では ENV に指定されたファイルが profile スクリプトの直後に読み込まれるようです。

$ echo echo nice > nice
$ export ENV=nice
$ sh -i
nice
sh-4.4$ 

詳しい仕様はわかりません。ファイルがないとどうやらエラーにもならないようなので、実際問題になることはないでしょう。Makefile のデフォルトシェルは sh だし、万が一ハマったら結構時間取られそうな罠なので、お伝えします。

ログインするとコメントできます