ログには『No such file or directory』と出るのに、手動で実行すると問題なく動く・・・」
そのエラーの正体は「実行場所の違い」です。
スクリプトの中で相対パスを使っていると、カレントディレクトリが変わった瞬間に動作が変わります。
cronやsystemdから呼び出されると、カレントディレクトリは
/ やユーザーのホームディレクトリになるため、相対パスで指定した設定ファイルやバックアップ先が「見えなくなる」のはそのためです。この記事では、
dirname・pushd・popd を使って、実行場所に依存しないシェルスクリプトの設計方法を解説します。cronやsystemd、SSH経由で呼び出されてもファイルパスが正しく解決されるスクリプトの書き方を、具体的なコードで説明します。
この記事では、RHEL 9系(bash 5.1)を想定環境として説明します。
この記事のポイント
・dirname $0 でスクリプト自身のディレクトリを絶対パスで取得できる
・pushd/popd で cd 前のディレクトリを保存・復元できる
・サブシェル ( ) でディレクトリ変更を外に影響させない設計がある
・trap EXIT と組み合わせて失敗時のクリーンアップを確実にできる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
シェルスクリプトの「実行場所」問題とは
シェルスクリプトの実行時の基準点は「カレントディレクトリ(現在の作業ディレクトリ)」です。スクリプト内のパス指定が相対パス(
./config.sh、../logs/ など)の場合、カレントディレクトリを起点に解決されます。次のようなスクリプトを考えてください。
#!/bin/bash # 設定ファイルを相対パスで読み込む(危険な例) source ./config.sh # ログも相対パスで指定(危険な例) LOG=./logs/backup.log echo "バックアップ開始" >> "$LOG"
/home/user/scripts/ から手動で実行すると正常に動きます。しかし cron から呼び出すと、カレントディレクトリは
/ やユーザーのホームディレクトリになります。./config.sh は /config.sh や /root/config.sh を指すことになり、当然見つかりません。この問題を根本から解消するには、スクリプトの中でパスを「スクリプトファイルの場所」を起点に組み立てる必要があります。
dirname でスクリプト自身の場所を特定する
1. SCRIPT_DIR を定義してスクリプト起点のパスを作る
最もシンプルかつ確実な方法は、スクリプトの冒頭でSCRIPT_DIR 変数を定義することです。#!/bin/bash set -euo pipefail # スクリプト自身のディレクトリを絶対パスで取得する SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) # 以降は SCRIPT_DIR を起点に絶対パスを組み立てる source "${SCRIPT_DIR}/config.sh" LOG="${SCRIPT_DIR}/logs/backup.log"
dirname "$0" がスクリプトへのパスからディレクトリ部分を取り出します(例:/home/user/scripts)。cd と pwd を組み合わせることで、相対パスや .. が含まれていても必ず絶対パスに変換されます。この1行をスクリプトの先頭に置くだけで、cronから実行しても、SSH越しに呼び出しても、どこから実行しても
SCRIPT_DIR は常に同じ値になります。出力例(環境により異なります):
# SCRIPT_DIR の中身を確認する例 $ echo "$SCRIPT_DIR" /home/user/scripts
2. BASH_SOURCE[0] との使い分け
スクリプトを別のスクリプトからsource(ドット読み込み)する場合、$0 は呼び出し側スクリプトのパスを指します。読み込まれる側のスクリプトのディレクトリが必要なときは、
BASH_SOURCE[0] を使います。# source で読み込まれるライブラリスクリプト(lib/utils.sh)の場合 LIB_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) CONFIG="${LIB_DIR}/../config/settings.conf"
$0、source で読み込まれるライブラリには BASH_SOURCE[0] と使い分けるのが基本です。3. シンボリックリンクがある場合の対処
/usr/local/bin にシンボリックリンクを置いて呼び出せるようにした場合、dirname $0 はリンク先ではなくリンク元(/usr/local/bin)を返します。スクリプト本体のディレクトリが必要なときは
readlink -f でシンボリックリンクを解決します。#!/bin/bash # シンボリックリンク越しに呼ばれても本体のディレクトリを取得する REAL_PATH=$(readlink -f "$0") SCRIPT_DIR=$(dirname "$REAL_PATH")
readlink -f はシンボリックリンクを再帰的に解決して実体のパスを返します。macOSでは
greadlink(GNU coreutils)が必要ですが、Linuxサーバー環境では標準で使えます。pushd と popd で安全にディレクトリを移動する
1. cd の罠:set -e 環境でのエラー停止
set -e(または set -euo pipefail)を設定したスクリプトでは、cd が失敗するとスクリプト全体がそこで停止します。さらに
cd は移動先のディレクトリを変更するため、移動後の処理で失敗した場合に元のディレクトリに戻ってくれません。次のコードはこの問題を抱えています。
# 危険な例:cd に失敗すると cd - に到達しない cd /data/backups tar czf archive.tar.gz ./target/ cd -
2. pushd と popd で移動前の場所を保存する
pushd はカレントディレクトリをスタックに積んでから移動します。popd でスタックから取り出すと、以前のディレクトリに戻ります。# pushd/popd を使ったパターン pushd /data/backups > /dev/null tar czf archive.tar.gz ./target/ popd > /dev/null # ここでは元のディレクトリに戻っている
> /dev/null でスタックの一覧表示(標準出力)を抑制しています。pushd が失敗するとスクリプトが停止するため、無効なパスへの移動も安全に捕捉できます。3. サブシェル ( ) でディレクトリ変更を隔離する
cd の変更を現在のスクリプトに影響させたくない場合は、サブシェルの中で処理するのが確実な設計です。# サブシェルで作業ディレクトリを隔離するパターン ( cd /data/backups || exit 1 tar czf archive.tar.gz ./target/ ) # サブシェルの外では元のディレクトリのまま
cd は外部に影響しません。exit 1 はサブシェルだけを終了させ、$? でサブシェルの終了コードを確認できます。ただし、サブシェル内で変更した変数は外部に持ち出せないため、戻り値が必要な場合はファイルや標準出力を介す必要があります。
シェルスクリプトの設計パターンを体系的に学びたい方は、シェルスクリプト実践講座の内容も参考にしてください。
実践パターン:バックアップスクリプトへの適用
1. 実行場所に依存しないバックアップスクリプト
ここまでの設計パターンを組み合わせた実装例です。#!/bin/bash set -euo pipefail # スクリプトのディレクトリを絶対パスで取得 SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) # 設定をスクリプト起点の絶対パスで指定 CONFIG="${SCRIPT_DIR}/config/backup.conf" BACKUP_DIR="${SCRIPT_DIR}/backups" LOG_DIR="${SCRIPT_DIR}/logs" LOG_FILE="${LOG_DIR}/backup_$(date +%Y%m%d).log" # 設定ファイルを読み込む # shellcheck source=/dev/null source "$CONFIG" # ログディレクトリが無ければ作成する mkdir -p "$LOG_DIR" "$BACKUP_DIR" echo "[$(date +%F\ %T)] バックアップ開始" >> "$LOG_FILE" # バックアップ対象ディレクトリに移動して tar を実行 ( cd "${SOURCE_DIR}" || exit 1 tar czf "${BACKUP_DIR}/backup_$(date +%Y%m%d_%H%M%S).tar.gz" . \ >> "$LOG_FILE" 2>&1 ) echo "[$(date +%F\ %T)] バックアップ完了" >> "$LOG_FILE"
SOURCE_DIR は config/backup.conf から読み込んだ変数です。tar コマンドの書式と圧縮形式の詳細については tar コマンドの実用例 も参考にしてください。2. trap EXIT と組み合わせたクリーンアップ設計
pushd を使う場合、スクリプトが予期せず終了してもスタックが乱れないよう、trap EXIT と組み合わせると安全です。#!/bin/bash set -euo pipefail SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) _PUSHED=0 # pushd 済みかどうかのフラグ # スクリプト終了時に必ず元のディレクトリに戻る cleanup() { if [[ "$_PUSHED" -eq 1 ]]; then popd > /dev/null 2>&1 || true fi } trap cleanup EXIT # 作業ディレクトリに移動 WORK_DIR="${SCRIPT_DIR}/work" mkdir -p "$WORK_DIR" pushd "$WORK_DIR" > /dev/null _PUSHED=1 # 作業処理 cp "${SCRIPT_DIR}/template.conf" ./current.conf popd > /dev/null _PUSHED=0
trap cleanup EXIT は正常終了・エラー終了・シグナル受信のどの場合でもクリーンアップ関数を呼び出します。popd || true とすることで、popd 自体の失敗でスクリプトが止まるのを防いでいます。よくあるエラーと対処法
dirname が「.」を返すケース$0 がファイル名だけ(パスなし)の場合、dirname は . を返します。$(cd "$(dirname "$0")" && pwd) は $(cd "." && pwd) となり、実行時のカレントディレクトリを返します。bash script.sh のようにカレントディレクトリにあるスクリプトを実行している場合は意図した動作になりますが、フルパス・相対パスどちらでも正しく動くよう書くのが基本です。cd が失敗して処理が止まる
set -e 環境では cd /path/that/does/not/exist で即座に停止します。「ディレクトリが無ければ作成してから移動する」設計にする場合は次のように書きます。
# ディレクトリが無ければ作成してから移動する mkdir -p "${TARGET_DIR}" pushd "${TARGET_DIR}" > /dev/null
( ... ) で囲んだサブシェル内の変数変更は外に持ち出せません。サブシェルの中で設定した変数を外で使いたい場合は、一時ファイルや標準出力を使って値を渡します。
# サブシェルの結果を外部に渡す例(標準出力を利用) RESULT=$( cd /data || exit 1 wc -l < target.txt ) echo "行数: ${RESULT}"
本記事のまとめ
実行場所に依存しないシェルスクリプト設計の主なパターンをまとめます。| 課題 | 解決パターン |
|---|---|
| cron・systemdで相対パスが解決されない | SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) でスクリプト起点の絶対パスを取得する |
| source で読み込まれるライブラリのパス解決 | BASH_SOURCE[0] を使って自身のディレクトリを取得する |
| シンボリックリンク越しに呼ばれた場合 | readlink -f "$0" で実体のパスを取得してから dirname する |
| 移動後の処理失敗時に元の場所に戻りたい | pushd/popd でディレクトリスタックを管理する |
| ディレクトリ変更を外に影響させたくない | サブシェル ( ... ) で処理を隔離する |
| 異常終了時もディレクトリを確実に復元したい | trap cleanup EXIT で終了ハンドラを登録する |
詳細・日程はこちら >> Linux Master Pro Seminar
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:シェルスクリプトのset -euo pipefailとtrap ERRによるエラーハンドリング設計|どこで失敗したかを確実に捕捉する実装パターン
- この記事の属するカテゴリ:シェルスクリプトへ戻る

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