こんにちは、大久保です。

今回は、Linuxコマンドで実行に利用される環境変数PATHについて、どのように設定されるか改めて調べてみました。

環境変数PATHとは?


コマンドの実行ファイル(バイナリ)が配置されているディレクトリの情報が格納されている環境変数です。

root@ip-10-0-255-130:~ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin

本来、何か実行ファイルを実行する際は、絶対パスか相対パスで指定する必要があります。

しかし普段コマンドを実行する際は、上記のパスを指定せずコマンド名の入力だけで済んでいます。
これは、bashが環境変数PATHの情報を利用してコマンドの実行ファイルを検索してくれているためです。

なんともありがたい環境変数、それがPATHです。

環境変数 PATH 内のディレクトリは「:」で区切られており、bash は左側のディレクトリから順番にコマンドの実行ファイルを検索します。

基本的に、最初に見つかったディレクトリの実行ファイルが利用され、見つかった時点で検索は終了します。
※後続のディレクトリは検索されません。

そのため、ディレクトリ名の順番はとても重要です!

またbashにはキャッシュのような機能があり、1度コマンドを実行するとハッシュテーブルと呼ばれるところに実行ファイルのフルパスが記録され、以降のコマンド実行はハッシュテーブルの情報を使うようになります。

Bash uses a hash table to remember the full pathnames of executable files to avoid multiple PATH searches (see the description of hash in Bourne Shell Builtins). A full search of the directories in $PATH is performed only if the command is not found in the hash table.

3.7.2 Command Search and Execution

Bash は、複数回 PATH を検索しないようにするため、実行可能ファイルのフルパスを記憶するためにハッシュテーブルを使用します(Bourne Shell の組み込みコマンド「hash」の説明を参照)。コマンドがハッシュテーブルに見つからない場合に限り、$PATH に含まれるディレクトリ全体の検索が行われます。

※ 和訳
# コマンドを実行するとハッシュテーブルにフルパスが記録される
root@ip-10-0-255-130:~ hash
hash: hash table empty
root@ip-10-0-255-130:~ ls
snap
root@ip-10-0-255-130:~ hash
hits    command
   1    /usr/bin/ls

# 環境変数PATHを更新するとハッシュテーブルがリセットされる
root@ip-10-0-255-130:~ PATH="$PATH"
root@ip-10-0-255-130:~ hash
hash: hash table empty

※ 環境変数PATHの値を変えるような処理を行うと、ハッシュテーブルはリセットされていました。

環境変数PATHの情報はどうやって決まるか?


今回は以下の環境で、どのようにして環境変数PATHが設定されるか確認したいと思います。

項目
環境Amazon EC2
OSUbuntu 24.04.2 LTS
ログインシェルbash 5.2.21
Linuxユーザーroot

基本的にいくつかの設定ファイルを読み込んで、環境変数は設定されます。

読み込みの流れは、ログインシェル、非ログインシェル、対話型、非対話型などの組み合わせによって違いますが、今回は対話型ログインシェル(SSHやsu -コマンドでLinuxユーザーにログインした時)の流れについて調べていきます。

基本的な流れは以下になります。

  1. PAM(Pluggable Authentication Modules)により、/etc/environmentが読み込まれる
  2. bashにより、/etc/profileが読み込まれる
    • /etc/profile.d/配下の設定ファイルも読み込まれる
  3. bashにより、各ユーザーのホームディレクトリに存在するどれか1つの設定ファイルが読み込まれる
    • ~/.bash_profile
    • ~/.bash_login
    • ~/.profile

/etc/environment

/etc/environmentはOS側で用意してある設定ファイルで、ログイン時にPAMによって読み込まれます。
中身を見てみるとPATHの情報が記載されており、これがPAMの処理によって環境変数として設定されるようです。

root@ip-10-0-255-130:~ cat /etc/environment
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"

ちなみに、/etc/environmentの読み込みの有無は、/etc/pam.d/loginに設定されていました。

※ 抜粋
# This module parses environment configuration file(s)
# and also allows you to use an extended config
# file /etc/security/pam_env.conf.
#
# parsing /etc/environment needs "readenv=1"
session       required   pam_env.so readenv=1

試しに適当な変数を/etc/environmentに追記して確認したところ、ちゃんと環境変数として設定されました。

root@ip-10-0-255-130:~ sh -c 'echo "TEST_VAR=HelloWorld" >> /etc/environment'
root@ip-10-0-255-130:~ exit
logout
$ sudo su -
root@ip-10-0-255-130:~ printenv | grep TEST_VAR
TEST_VAR=HelloWorld
root@ip-10-0-255-130:~ echo $TEST_VAR
HelloWorld

/etc/profile

/etc/profileは、Linuxユーザー全体の設定に関する設定ファイルです。

なんでこのファイルが読み込まれるかというと、bashの仕組みで読み込むようになっているためです。
以下、bashのマニュアルの抜粋です。

When Bash is invoked as an interactive login shell, or as a non-interactive shell with the –login option, it first reads and executes commands from the file /etc/profile, if that file exists. After reading that file, it looks for ~/.bash_profile, ~/.bash_login, and ~/.profile, in that order, and reads and executes commands from the first one that exists and is readable. The –noprofile option may be used when the shell is started to inhibit this behavior.
When an interactive login shell exits, or a non-interactive login shell executes the exit builtin command, Bash reads and executes commands from the file ~/.bash_logout, if it exists.

6.2 Bash Startup Files Invoked as an interactive login shell, or with –login

Bash が対話型ログインシェルとして、または –login オプション付きの非対話型シェルとして呼び出された場合、まず /etc/profile ファイルが存在すれば、それを読み込んでコマンドを実行します。そのファイルを読み込んだ後、~/.bash_profile、~/.bash_login、~/.profile の順にファイルを探し、最初に存在していて読み取り可能なファイルからコマンドを読み込んで実行します。–noprofile オプションを使用すると、この動作を抑制することができます。
対話型ログインシェルが終了する場合、あるいは非対話型ログインシェルが exit 組み込みコマンドを実行する場合、Bash は ~/.bash_logout ファイルが存在すれば、それを読み込んでコマンドを実行します。

※ 和訳

/etc/profileには以下の処理が記載されており、今回の場合は対話型ログインシェルとして/bin/bashを起動するので、/etc/bash.bashrcが読み込まれてから/etc/profile.d配下の設定ファイルが読み込まれます。
※ここでは割愛しますが、/etc/bash.bashrcではプロンプトの設定やsudoコマンドの実行時の警告などの設定が行われます。

# /etc/profile: system-wide .profile file for the Bourne shell (sh(1))
# and Bourne compatible shells (bash(1), ksh(1), ash(1), ...).

if [ "${PS1-}" ]; then
  if [ "${BASH-}" ] && [ "$BASH" != "/bin/sh" ]; then
    # The file bash.bashrc already sets the default PS1.
    # PS1='\h:\w\$ '
    if [ -f /etc/bash.bashrc ]; then
      . /etc/bash.bashrc
    fi
  else
    if [ "$(id -u)" -eq 0 ]; then
      PS1='# '
    else
      PS1='$ '
    fi
  fi
fi

if [ -d /etc/profile.d ]; then
  for i in /etc/profile.d/*.sh; do
    if [ -r $i ]; then
      . $i
    fi
  done
  unset i
fi

/etc/profile.d配下には、デフォルトで以下のような設定ファイルがあり、機能ごとに設定ファイルが分けられていました。

root@ip-10-0-255-130:~ ll /etc/profile.d/
total 40
drwxr-xr-x   2 root root 4096 Mar  5 08:39 ./
drwxr-xr-x 108 root root 4096 Mar 31 01:05 ../
-rw-r--r--   1 root root   96 Apr 22  2024 01-locale-fix.sh
-rw-r--r--   1 root root 1557 Feb 10  2024 Z97-byobu.sh
-rwxr-xr-x   1 root root 3396 Feb  4 22:36 Z99-cloud-locale-test.sh*
-rwxr-xr-x   1 root root  841 Feb  4 22:36 Z99-cloudinit-warnings.sh*
-rw-r--r--   1 root root  835 Oct 11 08:05 apps-bin-path.sh
-rw-r--r--   1 root root  726 Sep 18  2023 bash_completion.sh
-rw-r--r--   1 root root 1107 Mar 31  2024 gawk.csh
-rw-r--r--   1 root root  757 Mar 31  2024 gawk.sh

試しにapps-bin-path.shの内容を確認したところ、パッケージ管理コマンドのSnapでインストールしたアプリケーションに必要なPATHの追加などの処理が記載されていました。

※ 抜粋 /snap/binが環境変数PATHの中になければ、追加するという処理
# Expand $PATH to include the directory where snappy applications go.
snap_bin_path="/snap/bin"
if [ -n "${PATH##*${snap_bin_path}}" ] && [ -n "${PATH##*${snap_bin_path}:*}" ]; then
    export PATH="$PATH:${snap_bin_path}"
fi

上記のように、システム全体で環境変数やコマンドエイリアスなどを追加したい場合は、/etc/profileに直接追記するのではなく、/etc/profile.d配下に設定ファイルを作成して読み込ませる方が、管理しやすく取り回しが良いと個人的には思っています。

またここでの設定はサーバー全体に影響があるため、変更する場合は慎重に行いましょう!
環境変数PATHの内容が書き換わることで、サーバー上で動いている他の処理が失敗し大事故につながる可能性があります。

各ユーザーのホームディレクトリにある設定ファイル

最後に、ログインしたユーザーのホームディレクトリにある設定ファイルです。

ユーザー個別に環境変数や、コマンドエイリアスなどを設定したい場合は以下のファイルに設定を追記します。

  1. ~/.bash_profile
  2. ~/.bash_login
  3. ~/.profile

/etc/profileのところで引用したマニュアルに書いてあるとおり、上記の順番でファイルがあるか確認し、最初に見つけたファイルのみ読み込みます。

また、これらの設定ファイルはすべてが揃っているとは限らず、ディストリビューションによって有無が異なります。
今回の環境のubuntu24.04 LTSでは、デフォルトの状態で.profileしかありませんでした。

root@ip-10-0-255-130:~ ll
total 32
drwx------  4 root root 4096 Mar 31 00:18 ./
drwxr-xr-x 22 root root 4096 Mar 31 00:11 ../
-rw-------  1 root root  988 Mar 31 03:38 .bash_history
-rw-r--r--  1 root root 3106 Apr 22  2024 .bashrc
-rw-------  1 root root   20 Mar 31 00:18 .lesshst
-rw-r--r--  1 root root  161 Apr 22  2024 .profile
drwx------  2 root root 4096 Mar 27 04:50 .ssh/
drwx------  3 root root 4096 Mar 27 04:51 snap/

~/.profileに以下の通りになっており、ログインシェルがbashであれば~/.bashrcを読み込む処理をしていました。

root@ip-10-0-255-130:~ cat .profile
# ~/.profile: executed by Bourne-compatible login shells.

if [ "$BASH" ]; then
  if [ -f ~/.bashrc ]; then
    . ~/.bashrc
  fi
fi

mesg n 2> /dev/null || true

~/.bashrcでは、コマンド履歴の保持設定やコマンドエイリアスの設定などの処理がされていました。
※普段、llとコマンドを打つことで、ls -lが実行されるのはここでコマンドエイリアスが設定されているためです。

※ 抜粋
alias ll='ls -alF'
alias la='ls -A'
alias l='ls -CF'

なお、ユーザー個別に環境変数PATHを設定したい場合は、~/.profileに記載した方が良さそうです。
理由は、~/.bashrcは仕組み上複数回呼ばれる可能性があるため、以下のような書き方を~/.bashrcにするとPATHが重複して保存され、予期せぬ問題が起こりそうです。
~/.profileは基本的にログイン時に1度しか呼び出されません。

export PATH="$PATH:<追加したいPATH>"

というわけで、これらの設定ファイルが読み込まれ、必要となるPATHが追加されていき、環境変数PATHの情報が設定されていました。
いくつか設定ファイルが出てきましたが、個人的に環境変数PATHは以下のパターンで設定するのが良いと思っています。

パターン設定ファイル
システム全体で環境変数PATHを設定したい場合/etc/profile.d/配下の設定ファイル
ユーザー個別で環境変数PATHを設定したい場合~/.bash_profile
~/.bash_login
~/.profile

環境変数PATHの設定で怖いところ


どのように環境変数PATHが設定されていくかわかったところで、私が思う環境変数PATHの設定不備で起こる怖いことについて話そうと思います。

既存のPATHの情報を上書きしてしまう

これはIT業界に入ったばかりの時に、プライベートでLinuxサーバー構築中にやってしまった内容です。

作成したシェルスクリプトが配置されているディレクトリを追加しようとして、以下のように設定をしてしまいました。

export PATH="<追加したいPATH>"

この設定により既存のPATHの情報がふっとび、内部コマンド以外使えなくなりました。
そのセッション内で一時的に変更しただけだったため、ログインし直すことで問題は解消しました。
今となっては早めに経験しておいて良かったと思います。

環境変数PATHにディレクトリを追加したい場合は、以下のようにして必ず既存のPATH情報を入れましょう!

export PATH="$PATH:<追加したいPATH>"

最初に見つかった実行ファイルが優先される

これもIT業界に入ったばかりの時に、プライベートで友人とLinuxサーバー構築して遊んでいたときの話です。

冒頭に書きましたが、bashは環境変数PATHに記載されているディレクトリを順番に左から確認し、最初に見つかった実行ファイルを実行します。
この仕組みを利用して、以下のようなイタズラを互いにしていました。

mkdir /private-cmd

cat << "EOF" > /private-cmd/ls
echo "lsは実行できません"
EOF

chmod a+x /private-cmd/ls
export PATH="/private-cmd:$PATH"
root@ip-10-0-255-130:~ ls
lsは実行できません
root@ip-10-0-255-130:~ which ls
/private-cmd/ls
root@ip-10-0-255-130:~ echo $PATH
/private-cmd:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin

本来lsコマンドは/usr/bin配下にありますが、独自にlsという実行ファイルを作成し、環境変数PATH内で/usr/binより左に作成した実行ファイルが配置されているディレクトリを追加することで、本来のlsコマンドでなく、独自に作成したlsコマンドを優先して実行させることが出来ます。

このように環境変数PATH内のディレクトリの順番が変わることで、本来実行したいコマンドではなく、まったく別の処理をする同名のコマンドが実行されてしまうことがあります。

上記のイタズラのようなことは、悪意のある第3者などに侵入されない限り実際に起こらないと思いますが、改めて環境変数PATH内のディレクトリの順番は大事ということを覚えておきましょう!

まとめ


今回は、改めて環境変数PATHの仕組みについて調べてみました。

当時私が勉強した時はPAMの処理まで追えてなかったため、 /etc/environmentの存在や処理の流れを知れて良かったです。
今回はサーバー環境での話でしたが、コンテナ環境になるとまた流れが違うと思うので時間が出来た時に調査したいと思います。

この情報が誰か助けになれば幸いです。

参考にしたもの


By okubo

主にAWS上でのインフラ構築を担当してます。 ・AWS Certified Solutions Architect - Professional ・AWS Certified DevOps Engineer - Professional ・AWS Certified Database - Specialty ・AWS Certified Security - Specialty