シェルスクリプトのBASH_SOURCE変数でライブラリパスを解決する設計|インクルードガードと多重読み込み防止の実装パターン

運営:リナックスマスター.JP(代表 宮崎智広)
HOME > Linux技術 リナックスマスター.JP(Linuxマスター.JP) > シェルスクリプト > シェルスクリプトのBASH_SOURCE変数でライブラリパスを解決する設計|インクルードガードと多重読み込み防止の実装パターン
「source ./lib/utils.sh でライブラリを読み込んでいるのに、別のディレクトリからスクリプトを呼ぶと "No such file or directory" になる」は、作業ディレクトリ依存で起きる典型的な落とし穴です。

この記事では、RHEL 9系/Ubuntu 24.04 LTSを想定環境として、BASH_SOURCE変数を使ってライブラリファイルの配置ディレクトリを正確に解決する方法を解説します。あわせてインクルードガードで多重読み込みを防ぐ実装パターンを実践例で紹介します。

この記事のポイント

・ライブラリのsourceパスは $0 ではなく BASH_SOURCE[0] を使う
・$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) でライブラリの場所を解決する
・インクルードガード変数でライブラリの多重読み込みを防止できる
・BASH_SOURCE[0] == $0 の判定でスタンドアロンテストを実現できる


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

sourceで読み込んだライブラリが見つからない問題

source(または .)コマンドでライブラリを読み込む際に、相対パスを使うとスクリプトを別のディレクトリから呼んだときにパスが壊れます。

次のようなディレクトリ構造があるとします。

myproject/ ├── bin/ │ └── main.sh └── lib/ └── utils.sh

main.sh の中で次のように書いたとします。

#!/bin/bash # main.sh(問題のある書き方) source ./lib/utils.sh # 作業ディレクトリ依存 log_info "処理を開始します"

bin/ ディレクトリの中から実行すると失敗します。

# bin/ の中にいると失敗する cd /home/user/myproject/bin bash main.sh

出力例(環境により異なります):

main.sh: line 3: ./lib/utils.sh: No such file or directory

./lib/utils.sh は「現在の作業ディレクトリからの相対パス」であり、スクリプトファイルの場所ではありません。cron や systemd から起動する場合も、作業ディレクトリは予測できないため同様の問題が起きます。

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

main.sh から source lib/utils.sh を呼んだときの出力例(環境により異なります):

utils.sh 内での値: $0 = bin/main.sh # メインスクリプトのパス BASH_SOURCE[0] = lib/utils.sh # ライブラリ自身のパス

$0 は「ユーザーが実行したスクリプト(main.sh)のパス」を指したままです。ライブラリのパス解決に $0 を使うと、どこから呼ばれても main.sh のディレクトリが起点になってしまい、ライブラリが別の場所にあるケースで壊れます。

ライブラリファイルの配置ディレクトリをBASH_SOURCEで解決する

1. BASH_SOURCE[0]からディレクトリパスを取り出す

dirname "${BASH_SOURCE[0]}" でライブラリファイルが置かれているディレクトリを取得できます。さらに cd && pwd で絶対パスに変換することで、シンボリックリンクや ../ を含む相対パスも確実に解決できます。

# lib/utils.sh の先頭に書く LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

LIB_DIR には utils.sh が置かれているディレクトリの絶対パスが入ります。ここを起点にほかのライブラリを読み込めます。

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

シンボリックリンク先の実体ディレクトリが重要でない限り、移植性の高い cd && 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; }

_LOADED_LOGGING_SH という変数名は「アンダースコアで始まる」「ファイル名をそのまま使う」「大文字」というルールで衝突を避けます。

インクルードガードがある状態で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}"

SCRIPT_DIR はスクリプト自身のディレクトリの絶対パスなので、どこから backup.sh を実行しても LIB_DIR が正しく解決されます。

ライブラリをスタンドアロンで実行できる設計にする

ライブラリファイルを直接実行したときだけセルフテストを走らせるパターンです。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

source から呼んだときは if ブロックがスキップされるため、テストコードがメインスクリプトの実行に影響しません。

トラブルシュート:よくある問題と対処法

【注意】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)"

既存スクリプトが #!/bin/sh の場合、$0 を代わりに使えますが、source 経由での呼び出しには対応できません。共通ライブラリ設計には bash を明示するのが安全です。

【注意】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サーバー構築の「型」を体系的に身につけたい方へ、ライブラリ分割設計・エラー処理・バックアップ自動化まで体系的に習得できるシェルスクリプト実践講座を開講しています。シェルスクリプト実践講座の詳細はこちら>>

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

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

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

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

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

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

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

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

運営

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

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