大抵の場合、原因はスクリプトの論理的な誤りではない。実行環境によってPATHが異なることを見落としているだけだ。
この記事では、RHEL 9系~Rocky Linux 9・Ubuntu 24.04 LTSを想定環境として、対話シェル・cron・systemd・SSH・sudoの各実行環境でPATHがどう変わるかを比較し、どこから実行しても確実に動くシェルスクリプトの設計パターンを解説する。
この記事のポイント
・cronやsystemdではPATHが対話シェルより大幅に短縮されている
・スクリプト冒頭でexport PATHを明示設定するのが最も確実な対策
・command -vでコマンドの存在確認をすれば依存エラーを早期検知できる
・BASH_SOURCE[0]とrealpathでスクリプト自身の配置場所を確実に取得できる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜ実行環境によってPATHが変わるのか
Linuxシェルが起動するとき、どの設定ファイルを読み込むかは起動方式によって変わる。この仕組みを理解していないと、同じスクリプトが環境によって動いたり動かなかったりする原因がつかめない。ログインシェル(SSHでサーバーに接続した直後など)は、/etc/profile → ~/.bash_profile → ~/.bashrcの順に設定ファイルを読み込む。ここでPATHに/usr/local/bin:/home/user/.local/binといったディレクトリが追加される。pipやnpmでインストールしたツールが動くのは、これらの設定ファイルがPATHを拡張しているからだ。
一方、cronデーモンが起動するシェルはログインシェルではなく、非対話型シェルでもある。設定ファイルをほとんど読み込まないため、PATHはデフォルト値(/usr/bin:/binなど)のままだ。対話シェルで動かしたときは/usr/local/binにあるコマンドが見つかっていたのに、cronでは見つからない。これが典型的な失敗パターンだ。
bashシェルの起動方式と設定ファイルの対応
・ログインシェル:/etc/profile → ~/.bash_profile(またはprofile/bashrc)を読む。PATHが最も広い
・対話型非ログインシェル(tmux・screen内など):~/.bashrcのみ読む
・非対話型シェル(cron・systemd等):設定ファイルをほとんど読まない。PATHが最も狭い
・sudo実行:/etc/sudoersのsecure_pathが適用され、ユーザーのPATHはリセットされる場合がある
実行環境別のデフォルトPATH
実際にどの環境でPATHがどうなっているかをまとめた。これを把握することが設計の出発点だ。| 実行環境 | デフォルトPATHの特徴 | 代表的なデフォルト値 |
|---|---|---|
| 対話型ログインシェル(SSH接続直後) | 最も広い。~/.bash_profileで追加される | /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin |
| cron | 最も狭い。/etc/crontabのPATH行でのみ変更可能 | /usr/bin:/bin |
| systemdサービス | /etc/environmentを読む。Environment=指令でも設定可 | /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin |
| SSH非ログイン(コマンド直接実行) | sshd_configのPermitUserEnvironmentやAcceptEnvに依存 | 環境による(ログインシェルより狭い場合が多い) |
| sudo -u USER script.sh | /etc/sudoersのsecure_pathが適用される | /sbin:/bin:/usr/sbin:/usr/bin |
cronのデフォルトPATHに/usr/local/binが含まれないのは有名な落とし穴だ。pip/npmなどでインストールしたツールが/usr/local/binに入っている場合、cronから呼び出すと即座に「command not found」になる。
sudoの場合はさらに注意が必要だ。/etc/sudoersにsecure_pathが設定されていると、ユーザーのPATHは完全に無視される。sudo -Eを使ってもsecure_pathが優先されるため、スクリプト内でPATHを明示設定するのが安全だ。
スクリプト先頭に明示的なPATHを設定する方法
最も確実な対策は、スクリプトの先頭でPATHを明示的に設定することだ。どの実行環境から呼ばれても同じPATHが保証される。1. 完全なPATHを直接指定する方法
スクリプト冒頭に以下を入れる。これで実行環境に関係なく、同じPATHが使われる。#!/bin/bash # すべての実行環境で共通のPATHを保証する export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin # 以降のコマンドはこのPATHで解決される rsync -az /data/ /backup/ # /usr/local/bin/rsync も /usr/bin/rsync も見つかる
2. 既存のPATHに追記する方法
特定のディレクトリを追加するだけでよい場合は、既存のPATHを壊さず追加するほうが安全だ。#!/bin/bash # 既存のPATHを保持しつつ独自ディレクトリを追加 export PATH="$PATH:/opt/myapp/bin:/usr/local/bin" # 注意: cronではPATHが既に狭いため、$PATHに依存した追記は効果が薄い場合がある # → 完全指定か、追記するディレクトリを明示するのが確実
3. コマンドを絶対パスで記述する方法
スクリプトが特定の少数コマンドしか使わない場合、そのコマンドを絶対パスで書く方法も確実だ。#!/bin/bash # 絶対パスでコマンドを指定する例 /usr/bin/rsync -az /data/ /backup/ /usr/bin/find /tmp -mtime +7 -delete /usr/sbin/useradd -m newuser # コマンドの絶対パスを調べるには: command -v rsync # 出力例(環境により異なります) # /usr/bin/rsync
command -vでコマンドの存在を事前確認する方法
PATHを設定しても、必要なコマンドがインストールされていない環境に持ち込んだ場合は別の問題が起きる。コマンドの存在確認を起動時に行っておくと、途中でクラッシュして中途半端な状態で終わるリスクを防げる。1. command -vの基本的な使い方
コマンドの存在確認には、POSIXで定義されたcommand -vを使うのが移植性の高い書き方だ。whichは外部コマンドであり、インストールされていない環境や、ディストリビューションによって挙動が異なる場合がある。# コマンドが存在するか確認する(標準出力を捨て終了コードのみ使う) if command -v rsync > /dev/null 2>&1; then echo "rsync: 利用可" else echo "rsync: 見つかりません" >&2 fi # コマンドのフルパスを取得したい場合 RSYNC_PATH="$(command -v rsync)" echo "$RSYNC_PATH" # 出力例(環境により異なります) # /usr/bin/rsync
2. 起動前チェック関数の実装パターン
複数のコマンドが必要なスクリプトでは、起動前にまとめてチェックする関数を定義しておくと保守しやすい。要件を変えるときは配列を更新するだけでよい。#!/bin/bash export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin # 必要コマンドの確認関数 check_requirements() { local missing=0 local required_cmds=("rsync" "find" "tar" "gzip") for cmd in "${required_cmds[@]}"; do if ! command -v "$cmd" > /dev/null 2>&1; then echo "エラー: コマンドが見つかりません: $cmd" >&2 missing=1 fi done if [ "$missing" -ne 0 ]; then echo "必要なコマンドをインストールしてから再実行してください" >&2 exit 1 fi } check_requirements # 以降、必要コマンドが揃っていることが保証される echo "全依存コマンドの確認が取れました"
BASH_SOURCEとrealpathでスクリプト自身のパスを解決する方法
スクリプトが自分の隣にある設定ファイルや共有ライブラリをsourceで読み込む場合、スクリプト自身のパスを取得する必要がある。cdでどのディレクトリから実行されても正しく動くよう設計するポイントを押さえておこう。1. BASH_SOURCE[0]でスクリプトのディレクトリを取得する
bashスクリプトでは、BASH_SOURCE[0]が現在のスクリプトのパスを保持している。dirname と組み合わせることで、スクリプトが置かれているディレクトリを取得できる。#!/bin/bash export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin # スクリプト自身のディレクトリを取得(どこから実行しても正しく動く) SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")" # 同ディレクトリにある設定ファイルを読み込む source "${SCRIPT_DIR}/config.sh" # 同ディレクトリのlibディレクトリを参照 source "${SCRIPT_DIR}/lib/functions.sh" # 出力例(環境により異なります) # /home/admin/scripts に置いたスクリプトを /tmp から実行しても # SCRIPT_DIR=/home/admin/scripts となる
2. realpathでシンボリックリンクを解決する方法
スクリプトが/usr/local/binにシンボリックリンクとして配置されている場合、BASH_SOURCE[0]はシンボリックリンクのパスを返す。実体のディレクトリを取得するにはrealpathを使う。#!/bin/bash # シンボリックリンクを解決して実体のディレクトリを取得 REAL_SCRIPT="$(realpath "${BASH_SOURCE[0]}")" SCRIPT_DIR="$(dirname "$REAL_SCRIPT")" # 以降、$SCRIPT_DIR は実体スクリプトがあるディレクトリを指す source "${SCRIPT_DIR}/config.sh" # realpathが使えない環境向けの代替(readlink -f はGNUコアユーティリティ) # REAL_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
シェルスクリプトを社内ツールとして配布・配置する際の設計を体系的に学びたい場合は、シェルスクリプト実践講座でも詳しく解説している。
実行環境差異のトラブルシュート手順
スクリプトがcronやsystemdで失敗する場合の調査手順を示す。いきなりソースコードを読み返す前に、まず実行環境のPATHを確認するのが近道だ。1. デバッグ用PATH記録をスクリプトに追加する
まず、スクリプト内に以下を一時的に追記して実行環境のPATHを記録させる。cronであれば次回の定期実行時にログが書き出される。#!/bin/bash # デバッグ用: 実行環境の情報を記録する(確認後は削除する) { echo "=== $(date) ===" echo "PATH: $PATH" echo "USER: $(id)" echo "PWD: $(pwd)" } >> /tmp/debug_env.log # 出力例(環境により異なります) # === Tue Nov 04 10:00:01 JST 2026 === # PATH: /usr/bin:/bin # USER: uid=0(root) gid=0(root) groups=0(root) # PWD: /
2. env -iで対話シェル外の環境を再現してテストする
対話シェルからcronに近い環境を手動で再現するには、env -iを使う。これで設定ファイルが読み込まれない状態でのスクリプト実行を試せるため、cronに登録する前に問題を発見できる。# cronに近い最小環境でスクリプトを実行してテストする env -i PATH=/usr/bin:/bin HOME=/root bash /path/to/script.sh # コマンドが見つからない場合はスクリプト側のPATH設定が不足している # → スクリプト先頭にexport PATHを追加してから再テスト env -i PATH=/usr/bin:/bin HOME=/root bash /path/to/script.sh
3. journalctlでsystemdサービスのPATHエラーを確認する
systemdサービスでスクリプトが失敗する場合は、journalctlでログを確認する。# サービス名がmybackup.serviceの場合 journalctl -u mybackup.service --no-pager -n 50 # 出力例(環境により異なります) # Nov 04 10:00:01 hostname mybackup.sh[1234]: /usr/local/bin/restic: No such file or directory # → スクリプト内でexport PATHを追加するか、/usr/local/bin/resticを絶対パスで書く # systemdユニットのEnvironment=でPATHを設定する方法もある # [Service] # Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" # ExecStart=/usr/local/bin/myapp/backup.sh
本記事のまとめ
| 問題・状況 | 対処方法 |
|---|---|
| cronでcommand not foundが出る | スクリプト先頭にexport PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binを追加する |
| sudoで実行するとコマンドが見つからない | /etc/sudoersのsecure_pathの影響。スクリプト自身でexport PATHを設定する |
| コマンドがどこにあるか確認したい | command -v コマンド名で確認する。/usr/bin:/bin以外にあればスクリプトのPATHに追加 |
| 依存コマンドの有無を起動時に確認したい | 起動前チェック関数(check_requirements)を実装してexit 1で早期終了する |
| 設定ファイルが見つからないと言われる | BASH_SOURCE[0]とdirnameでスクリプト自身のディレクトリを取得し絶対パスで参照する |
| シンボリックリンク経由で配置したスクリプトがパスを誤認する | realpath "${BASH_SOURCE[0]}"でリンク先の実体パスを取得する |
| 実際のcron環境を対話シェルから再現してテストしたい | env -i PATH=/usr/bin:/bin HOME=/root bash script.shでテストする |
| systemdサービスのPATHエラーを調査したい | journalctl -u サービス名.serviceでエラーログを確認する |
シェルスクリプトはどこから呼ばれるかを意識して設計するかどうかで、安定性が大きく変わる。cronやsystemdに組み込んでから問題を発見するのではなく、「非対話実行では設定ファイルを読まない」という前提を初めから組み込んでおくのが実務でのコツだ。
3つの設計原則を最後にまとめておく。
・スクリプト先頭に
export PATHを明示する(全環境共通の基本)・
command -vで依存コマンドの存在を起動前に確認する(早期エラー検知)・
BASH_SOURCE[0]でスクリプト自身の場所を取得する(配置に依存しない設計)この3つを習慣化すれば、「手元では動くのに本番で動かない」という問題の大半を防げる。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、
シェルスクリプトの設計パターンから自動化・デプロイまで、実務で通用するスキルを体系的に習得できます。
まずは>>シェルスクリプト実践講座の詳細を見る
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:シェルスクリプトでOS・ディストリビューションを判定する方法|/etc/os-releaseとID_LIKEでRHEL系とDebian系に1本で対応する設計
- この記事の属するカテゴリ:シェルスクリプトへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます