この記事では、RHEL 9系/Ubuntu 24.04 LTSを想定環境として、BASH_SOURCE変数を使ってライブラリファイルの配置ディレクトリを正確に解決する方法を解説します。あわせてインクルードガードで多重読み込みを防ぐ実装パターンを実践例で紹介します。
この記事のポイント
・ライブラリのsourceパスは $0 ではなく BASH_SOURCE[0] を使う
・$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) でライブラリの場所を解決する
・インクルードガード変数でライブラリの多重読み込みを防止できる
・BASH_SOURCE[0] == $0 の判定でスタンドアロンテストを実現できる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
sourceで読み込んだライブラリが見つからない問題
source(または .)コマンドでライブラリを読み込む際に、相対パスを使うとスクリプトを別のディレクトリから呼んだときにパスが壊れます。次のようなディレクトリ構造があるとします。
myproject/ ├── bin/ │ └── main.sh └── lib/ └── utils.sh
#!/bin/bash # main.sh(問題のある書き方) source ./lib/utils.sh # 作業ディレクトリ依存 log_info "処理を開始します"
# bin/ の中にいると失敗する cd /home/user/myproject/bin bash main.sh
main.sh: line 3: ./lib/utils.sh: No such file or directory
BASH_SOURCE変数の仕組みと$0との違い
1. BASH_SOURCEとは何か
BASH_SOURCE は bash が持つ特殊配列で、現在実行中のスクリプトファイルのパスをインデックス順に積み上げたコールスタックです。BASH_SOURCE[0] が「現在処理中のファイルのパス」を保持します。ライブラリファイル(source で読み込まれるファイル)の中では、BASH_SOURCE[0] はそのライブラリファイル自身のパスを指します。これが $0 との決定的な違いです。
2. $0とBASH_SOURCE[0]の違いを実例で確認する
# lib/utils.sh echo "utils.sh 内での値:" echo " \$0 = $0" echo " BASH_SOURCE[0] = ${BASH_SOURCE[0]}"
utils.sh 内での値: $0 = bin/main.sh # メインスクリプトのパス BASH_SOURCE[0] = lib/utils.sh # ライブラリ自身のパス
ライブラリファイルの配置ディレクトリをBASH_SOURCEで解決する
1. BASH_SOURCE[0]からディレクトリパスを取り出す
dirname "${BASH_SOURCE[0]}" でライブラリファイルが置かれているディレクトリを取得できます。さらに cd && pwd で絶対パスに変換することで、シンボリックリンクや ../ を含む相対パスも確実に解決できます。# lib/utils.sh の先頭に書く LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
2. realpathが使える環境ではさらに簡潔に書ける
RHEL 9系・Ubuntu 24.04 LTS など最近の環境では realpath コマンドが使えます。シンボリックリンクを完全に解決したい場合に便利です。# realpath が使える環境(RHEL 9 / Ubuntu 24.04 以降) LIB_DIR="$(realpath "$(dirname "${BASH_SOURCE[0]}")")" # 移植性を重視する場合は cd && pwd パターン(bash 3.x でも動く) LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
多重読み込みを防ぐインクルードガードパターン
1. なぜ多重読み込みが問題になるのか
複数のスクリプトが同じライブラリを source する構成では、同一プロセス内で複数回 source が実行されます。関数定義の上書きは通常無害ですが、次のような問題を引き起こすことがあります。・readonly変数の再定義でエラーが出る
・グローバル変数の初期化が繰り返されて値がリセットされる
・source のたびに初期化処理(ファイル作成・ログ書き込みなど)が走る
2. 変数ベースのインクルードガードを実装する
C言語の #include ガードと同じ考え方で、「すでに読み込まれていればすぐに return する」ガード変数を先頭に置きます。# lib/logging.sh # インクルードガード: すでに読み込み済みなら即時リターン [[ -n "${_LOADED_LOGGING_SH:-}" ]] && return 0 readonly _LOADED_LOGGING_SH=1 # パス解決(ライブラリ自身のディレクトリを基点にする) _LOGGING_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # ライブラリ本体 log_info() { echo "[$(date '+%H:%M:%S')] INFO: $*"; } log_warn() { echo "[$(date '+%H:%M:%S')] WARN: $*" >&2; } log_error() { echo "[$(date '+%H:%M:%S')] ERROR: $*" >&2; }
インクルードガードがある状態で2回目の source を呼んでも安全です。
source "${LIB_DIR}/logging.sh" # 1回目: ロードされる source "${LIB_DIR}/logging.sh" # 2回目: ガードで即リターン(何も起きない)
実務での複数ライブラリ管理パターン
複数のライブラリファイルに共通処理を分割し、メインスクリプトからまとめてロードする設計が実務では一般的です。バックアップスクリプトや定期メンテナンススクリプトなど複数本のスクリプトが共通処理を使う場面に特に有効です。シェルスクリプトの設計をより体系的に学びたい方はシェルスクリプト実践講座も参考にしてください。1. ライブラリのディレクトリ構造
myproject/ ├── bin/ │ ├── backup.sh # メインスクリプト │ └── deploy.sh # メインスクリプト └── lib/ ├── logging.sh # ログ関数(log_info / log_error) ├── validate.sh # バリデーション関数 └── common.sh # 共通ユーティリティ
2. メインスクリプトからのロード実装
#!/bin/bash # bin/backup.sh set -euo pipefail # スクリプト自身のディレクトリを基点にライブラリをロード SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LIB_DIR="${SCRIPT_DIR}/../lib" source "${LIB_DIR}/logging.sh" source "${LIB_DIR}/validate.sh" source "${LIB_DIR}/common.sh" # ここからメイン処理 log_info "バックアップを開始します" validate_dir "${BACKUP_DST:-/var/backup}"
ライブラリをスタンドアロンで実行できる設計にする
ライブラリファイルを直接実行したときだけセルフテストを走らせるパターンです。BASH_SOURCE[0] と $0 が一致する(直接実行されている)かどうかで判定します。# lib/logging.sh の末尾に追加 # 直接実行された場合にのみセルフテストを実行する if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then echo "=== logging.sh のセルフテスト ===" # インクルードガードをリセット(テスト用) unset _LOADED_LOGGING_SH source "${BASH_SOURCE[0]}" # 再ロード log_info "INFOレベルのテスト" log_warn "WARNレベルのテスト" log_error "ERRORレベルのテスト(標準エラー出力)" echo "セルフテスト完了" fi
トラブルシュート:よくある問題と対処法
【注意】bash 以外のシェルでは BASH_SOURCE が使えない
BASH_SOURCE は bash 固有の変数です。シェバングに #!/bin/sh を使っていたり、dash や zsh で実行した場合は BASH_SOURCE が未定義になります。# NG: #!/bin/sh や dash では BASH_SOURCE が使えない #!/bin/sh LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # エラーになる # OK: シェバングを bash に変更する #!/bin/bash LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
【注意】bash -c "..." で実行すると BASH_SOURCE[0] が空になる
bash -c "source /path/to/lib.sh" のように文字列渡しで実行した場合、BASH_SOURCE[0] が空文字になります。この場合はファイルパスを明示的に引数で渡す設計に切り替えてください。シンボリックリンク経由で実行したときのパス解決
/usr/local/bin/backup → /opt/scripts/bin/backup.sh のようにシンボリックリンクが挟まる場合、dirname "${BASH_SOURCE[0]}" がリンク元のパス(/usr/local/bin)を返します。ライブラリが実体のディレクトリ(/opt/scripts/lib)にある場合はリンクが解決されず失敗します。この場合は realpath(または readlink -f)で実体パスを解決します。
# シンボリックリンクを解決して実体ディレクトリを取得する REAL_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" LIB_DIR="${REAL_DIR}/../lib"
本記事のまとめ
ライブラリのパス解決に相対パスや $0 を使うと、呼び出し元の作業ディレクトリに動作が左右されます。BASH_SOURCE[0] を使うことで、スクリプトの配置場所に依存した確実なパス解決ができます。| パターン | 用途 | 実装 |
|---|---|---|
| ライブラリのパス解決 | 作業ディレクトリ依存を排除する | $(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) |
| インクルードガード | 多重読み込みを防止する | [[ -n "${_LOADED_X:-}" ]] && return 0 |
| 直接実行判定 | ライブラリのセルフテストを実現する | [[ "${BASH_SOURCE[0]}" == "${0}" ]] |
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、ライブラリ分割設計・エラー処理・バックアップ自動化まで体系的に習得できるシェルスクリプト実践講座を開講しています。シェルスクリプト実践講座の詳細はこちら>>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

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