`export VAR=value`と`VAR=value`は何が違うのか?環境変数の伝達範囲を一度に理解する

ターミナルでNAME=valueと入力すると変数が作成されます。しかし、次に実行したプログラムではその値が見えない場合があります。一方、export NAME=valueで宣言すると、プログラムでもその値を読み取ることができます。この2つの違いは、値の形式ではなく、その値を子プロセスに渡すかどうかにあります。

まず最初に覚えておくべき文はこれです。

VAR=valueは現在のシェルのみが使用するシェル変数であり、export VAR=valueは以降に実行される子プロセスにも渡される環境変数である。

ここで、シェル(shell)とはBashやZshのようにコマンドを解釈するプログラムであり、プロセス(process)とは実行中のプログラム1つを指します。ターミナルのシェルでpython、node、dockerのようなコマンドを実行すると、そのプログラムはシェルの子プロセスとして起動します。

現在のシェル内にのみ残る変数と子プロセスに渡される環境変数

変数は同じだが、伝達の表示が異なる

Bashでシェル変数と環境変数が完全に別々のストレージに存在すると考えると、かえって混乱します。実務的には、シェル変数を作成し、その変数にexport属性を付与すると子プロセスに渡されると理解する方が正確です。

PROJECT_MODE=development

このコマンドは現在のシェルにPROJECT_MODEという変数を作成します。同じシェル内では値を読み取ることができます。

printf '%s
' "$PROJECT_MODE"
# development

しかし、新しいBashを子プロセスとして実行すると、値は渡されません。

bash -c 'printf "%s
" "${PROJECT_MODE-unset}"'
# unset

次に、既存の変数にexport属性を付与してみましょう。

export PROJECT_MODE

bash -c 'printf "%s
" "$PROJECT_MODE"'
# development

export PROJECT_MODEは、値を新しくコピーするコマンドというよりも、今後実行するコマンドの環境にこの変数を含めるという指示です。以下のように値の指定とexportを1行にまとめても結果は同じです。

export PROJECT_MODE=development

GNU Bashの公式ドキュメントも、実行されるコマンドが親シェルの環境を継承し、exportが変数を以降のコマンドの環境に含めるようにマークすると説明しています。POSIXシェル標準も、export属性が付与された変数が以降の実行コマンドの環境に含まれると定義しています。

親シェルと子プロセスを家族関係のように見ると理解しやすい

現在のターミナルのBashやZshを親だと考えてみましょう。このシェルからPythonを実行すると、Pythonは子です。

터미널의 셸
├── python app.py
├── node server.js
└── bash script.sh

通常のシェル変数は、親が自分のメモ帳にだけ書き留めた値です。exportされた変数は、子が生まれるときに渡される環境リストに含まれます。ここで、この比喩には重要な限界が1つあります。子が受け取った値を変更しても、親の値が逆方向に変わることはありません。

export COLOR=blue

bash -c 'COLOR=red; printf "child=%s
" "$COLOR"'
printf 'parent=%s
' "$COLOR"

# child=red
# parent=blue

プロセス環境は基本的に親から子へと伝達されます。子プロセスが親シェルの変数ストレージを直接修正する経路ではありません。

4つの宣言方法の比較

記述方法 現在のシェルで維持 子プロセスに伝達 主な用途
— — — —
VAR=value はい いいえ 現在のシェル内での計算・設定
export VAR=value はい はい 以降に実行する複数のプログラムの設定
VAR=value command いいえ 該当commandにのみはい 1つのコマンドにのみ一時設定を伝達
env VAR=value command いいえ 該当commandにのみはい 明示的な一時環境構成

3番目の方法は、デプロイとテストで特に有用です。

APP_ENV=test python app.py

python app.pyはAPPENV=testを受け取りますが、コマンド終了後、現在のシェルにAPPENVが新しく残ることはありません。

APP_ENV=test bash -c 'printf "inside=%s
" "$APP_ENV"'
printf 'after=%s
' "${APP_ENV-unset}"

# inside=test
# after=unset

GNU Bashマニュアルは、コマンドの前の変数代入がそのコマンドの実行中のみ環境に追加され、現在のシェル環境には影響を与えないと説明しています。シェル関数や特殊な組み込みコマンドでは詳細な規則が異なる場合があるため、外部プログラムを実行する一般的なケースから理解することをお勧めします。

sourceで実行する際、なぜexportなしでも見えるのか?

スクリプトの実行方法も変数の可視性に影響を与えます。

TOKEN=demo
./check.sh

./check.shは別の独立した子プロセスとして実行されます。TOKENをexportしていなければ、スクリプトは通常この値を受け取れません。

一方、次のコマンドは異なります。

TOKEN=demo
source ./check.sh

sourceまたはドットコマンドである. ./check.shは、新しいプロセスを開始せずに現在のシェル内でファイルのコマンドを実行します。したがって、現在のシェル変数も読み取ることができ、スクリプトが変更した変数も現在のシェルに残ります。

この違いのため、Python仮想環境をアクティブ化する際には、./venv/bin/activateではなく、次のようにsourceを使用します。

source .venv/bin/activate

これは、アクティベーションスクリプトが現在のシェルのPATHを変更する必要があるためです。

exportしても永続的に保存されるわけではない

exportを「永続的な環境変数登録」と誤解しがちですが、exportは現在のシェルプロセスとその子にのみ適用されます。ターミナルウィンドウを閉じてシェルが終了すると、その設定も消滅します。

新しいシェルを開くたびに設定するには、使用しているシェルの起動ファイルに宣言する必要があります。

# Bashの対話型シェルでよく使われるファイル
export APP_HOME="$HOME/apps/example"
  • Bashでは、状況に応じて~/.bashrc、~/.bash_profile、~/.profileなどが使用されます。
  • Zshの対話型設定には通常~/.zshrcを使用します。
  • ログインシェルか、対話型シェルか、スクリプト実行かによって読み込むファイルが異なります。

設定ファイルを修正した後、現在のシェルに即座に適用するには、そのファイルをsourceすることができます。

source ~/.zshrc

ただし、設定を複数のファイルに重複して宣言すると、どのファイルが最後に値を上書きしたのか追跡が難しくなります。目的に合ったファイル1箇所に置くのが良いでしょう。

.envファイルは自動的に環境変数にならない

次のような.envファイルを作成したと仮定してみましょう。

DATABASE_HOST=localhost
DATABASE_PORT=5432

ファイルが存在するという事実だけで、オペレーティングシステムやシェルが自動的に読み込むわけではありません。アプリケーションのdotenvライブラリ、Docker Compose、開発ツール、または別途のシェルコードがファイルを読み込む必要があります。

シェルで代入文が含まれるファイルをすべてexportしたい場合、Bashでは次のパターンが見られます。

set -a
source .env
set +a

set -aは、以降に作成または修正する変数に自動的にexport属性を付与します。ただし、.envが必ずしも安全なシェル構文であるという保証はありません。信頼できないファイルをsourceすると、その中のコマンドも現在のシェルで実行されるため危険です。単純なデータファイルは、使用するツールの専用.envパーサーを利用する方が安全です。

値を確認するコマンドも異なる

現在のシェル変数まで幅広く見たい場合は、Bashのsetを使用できます。出力が多く、関数まで含まれる可能性があるため、通常は名前を知ってから照会する方が良いでしょう。

printf '%s
' "$PROJECT_MODE"
declare -p PROJECT_MODE

exportされた変数リストは次のように確認します。

export -p

外部プログラムが受け取る環境はenvまたはprintenvで確認できます。

printenv PROJECT_MODE
env | grep '^PROJECT_MODE='

変数のexport属性のみを削除し、値は現在のシェルに残したい場合は、Bashでexport -nを使用できます。

export -n PROJECT_MODE

値自体を削除するにはunsetを使用します。

unset PROJECT_MODE

パスワードとAPIキーもexportすれば安全か?

環境変数は秘密のストレージではありません。ソースコードに直接キーを埋め込むよりも分離しやすいですが、同じ権限で実行されるプロセス、デバッグツール、エラー収集、プロセスダンプ、CIログ、または誤って記述された診断コードに露出する可能性があります。子プロセスはexportされた秘密を継承するため、必要以上に広く伝達しない方が良いでしょう。

1つのプログラムだけが値を必要とする場合、グローバルに長くexportするよりも範囲を狭めることができます。

API_TOKEN='temporary-value' ./deploy.sh

運用環境では、クラウドの秘密管理サービス、Vault、Kubernetes Secret、短寿命の資格情報といった専用の方式を検討する必要があります。コマンドラインに直接秘密を書き込む方式もシェル履歴などに残る可能性があるため、安全な注入経路を使用する必要があります。

また、sudoはセキュリティポリシーに従って環境変数を削除または制限する場合があります。親シェルでexportしたからといって、sudoで実行したコマンドがすべての値をそのまま受け取ると仮定すべきではありません。

よくある4つの誤解

1. 「大文字で書けば環境変数だ」

違います。大文字は慣例に過ぎません。MY_VALUE=testは大文字であっても、exportしなければ通常のシェル変数です。

2. 「exportは値をコピーするコマンドだ」

核心は変数にexport属性を付与することです。その後値を変更すると、新しい値が次の子プロセスの環境に伝達されます。

export LEVEL=one
LEVEL=two
bash -c 'printf "%s
" "$LEVEL"'
# two

3. 「子が変えれば親も変わる」

違います。継承は親から子への方向です。子の変更が親シェルに自動的に逆伝播することはありません。

4. 「exportすれば再起動後も残る」

違います。exportは現在のシェルのライフサイクル内で適用されます。永続的な設定は、シェル起動ファイル、サービスマネージャー、コンテナ設定、またはデプロイプラットフォームの環境設定に別途記録する必要があります。

実務ではこのように選択すればよい

  • シェルスクリプト内部の計算にのみ使用する場合: VAR=value
  • 以降に実行する複数のプログラムが読み取る必要がある場合: export VAR=value
  • 1つのコマンドに一時的に渡す場合: VAR=value command
  • 現在のシェル設定を変更するスクリプトの場合: source script.sh
  • 再接続後も必要な場合: 正しいシェル起動ファイルまたはサービス設定に記録
  • 秘密の値の場合: 伝達範囲を最小限にし、専用の秘密管理手段を検討

結局、exportの意味は「変数をより強く宣言する」ではありません。現在のシェルにのみ存在した名前と値を、以降に実行される子プロセスの環境にも載せて送るという意味です。この一文を覚えておけば、.env、Docker、Python、Node.js、CI/CDで環境変数が見えたり見えなかったりする理由をはるかに簡単に追跡できます。

参考資料


Comments

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です