シェルスクリプトのPATH設計|cron・systemd・SSH実行で環境差異を吸収する方法

運営:リナックスマスター.JP(代表 宮崎智広)
HOME > Linux技術 リナックスマスター.JP(Linuxマスター.JP) > シェルスクリプト > シェルスクリプトのPATH設計|cron・systemd・SSH実行で環境差異を吸収する方法
「手元では動くのにcronに登録したら『command not found』が返ってきた」「systemdのサービスに組み込んだら突然動かなくなった」。こういった経験は、Linuxサーバーを運用しているエンジニアなら一度は通る道だ。
大抵の場合、原因はスクリプトの論理的な誤りではない。実行環境によって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でスクリプト自身の配置場所を確実に取得できる


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

なぜ実行環境によって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 も見つかる

一般的なLinuxサーバーでは上記で十分だ。ただし/opt/以下に独自ツールがある場合は適宜追記する。cronで動かすスクリプトはほぼ全てこのパターンを採用するべきだ。

2. 既存のPATHに追記する方法

特定のディレクトリを追加するだけでよい場合は、既存のPATHを壊さず追加するほうが安全だ。

#!/bin/bash # 既存のPATHを保持しつつ独自ディレクトリを追加 export PATH="$PATH:/opt/myapp/bin:/usr/local/bin" # 注意: cronではPATHが既に狭いため、$PATHに依存した追記は効果が薄い場合がある # → 完全指定か、追記するディレクトリを明示するのが確実

この書き方は既存のPATHを壊すリスクがないが、cronのように元々PATHが狭い環境では/usr/bin:/bin:/opt/myapp/bin程度にしかならない。システムツールが見つからない問題は解決しない可能性があるため、cronで使うスクリプトには完全指定を推奨する。

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

シンプルなスクリプトには有効だが、コマンドの数が多くなると可読性が落ちる。またRHEL系(/usr/bin)とDebian系(/bin)でパスが違う場合があり、移植性が下がる。標準コマンドはPATH設定に頼り、/usr/local/bin以下の独自ツールのみ絶対パスを使うのが実用的なバランスだ。

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

command -vはbashシェルビルトインのため、外部コマンドへの依存がない。シェルスクリプトの移植性を考えるなら、which/typeより確実だ。

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 "全依存コマンドの確認が取れました"

このパターンを使えば、スクリプトを実行する環境にツールが揃っているかを実行開始時に判定でき、途中でエラーになって中途半端な状態で終わるリスクを減らせる。cronやsystemdで自動実行する前に、まずこのチェックを手動実行して確認する習慣をつけると、本番での障害を防ぎやすい。

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 となる

$0でも同様のことはできるが、スクリプトがsourceで読み込まれたときは$0が呼び出し側のスクリプト名になってしまう。BASH_SOURCE[0]は常に自ファイルを指し続けるため信頼性が高い。

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]}")"

realpathはRHEL 9/Rocky Linux 9/Ubuntu 24.04 LTSでは標準でインストールされている(coreutilsパッケージ)。macOSのデフォルトbashなど一部環境では使えないため、移植性が必要な場合はreadlink -fで代用する。

シェルスクリプトを社内ツールとして配布・配置する際の設計を体系的に学びたい場合は、シェルスクリプト実践講座でも詳しく解説している。

実行環境差異のトラブルシュート手順

スクリプトが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: /

PATHが/usr/bin:/binだけであれば、スクリプト先頭へのexport PATH追加が解決策になる。確認が済んだらこのデバッグコードは削除する。

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

手元で必ずenv -iテストを通してからcronに登録する習慣をつけると、「登録したら動かない」というケースを大幅に減らせる。

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

「No such file or directory」が出ていたらコマンドのパスを明示するかexport PATHを見直す。「Permission denied」だったらsystemdユニットのUser=設定やchmodの問題を疑う。systemdの場合はunit fileのEnvironment=でPATHを設定する方法もあるが、スクリプト自身がPATHを管理するほうがsystemd以外の実行環境でも同じコードが使えて保守しやすい。

本記事のまとめ

問題・状況 対処方法
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サーバー構築の「型」を体系的に身につけたい方へ、

シェルスクリプトの設計パターンから自動化・デプロイまで、実務で通用するスキルを体系的に習得できます。
まずは>>シェルスクリプト実践講座の詳細を見る

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録

運営

リナックスマスター.JP(代表 宮崎智広)

株式会社イーネットマーキュリーが運営するLinux教育サイトです。代表の宮崎智広は現役のLinuxサーバー管理者として20年以上の実務経験を持ち、累計3,100名以上のエンジニアを指導してきました。実践型のLinuxセミナーや無料マニュアルの配布を通じてLinux人材の育成に取り組んでいます。