Idiotproof

インクルードガード

Motivation

たとえシェルスクリプトであっても、コードが育ってくるうちに、 関数や定数を別ファイルにまとめたくなることがある。
その時C++の#pragma onceのように、複数のスクリプトが同一の外部スクリプトを参照しているとき、 複数回読み込まないようにしたい。

TL; DR: Use Python

冗談はともかく、Bashにこだわる意味はないと思う。

TOKEN=$(readlink -f "${BASH_SOURCE}" | md5sum)
if [[ -n "${INCLUDE_GUARD["TOKEN${TOKEN:0:32}"]}" ]]; then return; fi
INCLUDE_GUARD["TOKEN${TOKEN:0:32}"]="INCLUDE_GUARD"

このコードはStackOverflow の回答と違って、コピー&ペースト可能というメリットがある。 筆者はシェル芸人ではないので、もっといいやり方を知っているという方はご教授ください。

説明

まず、スクリプトを一意に特定するトークンを定義する。

TOKEN=$(readlink -f "${BASH_SOURCE}" | md5sum)

変数$BASH_SOURCEは、そのコードが書かれているスクリプトの名前が自動的に代入される。
$0を使うと、sourceされたファイルではなく、呼び出し側のスクリプトの名前が入ってしまう。
これだけでは名前が被るので、readlink -fで絶対パスに変換。
これは後で連想配列の添え字に使いたいのだが、パスは/.などを含むので、そのままでは使用できない。
sedで取り除いてもいいが、エスケープ漏れがあると面倒くさいのでmd5sumでハッシュを計算してしまう。

if [[ -n "${INCLUDE_GUARD["TOKEN${TOKEN:0:32}"]}" ]]; then return; fi
INCLUDE_GUARD["TOKEN${TOKEN:0:32}"]="INCLUDE_GUARD"

基本的な仕組みとしては、ある変数を定義し(二行目)、2回以上インクルードしないよう、 その変数が定義されているかどうかを調べる(1行目)という流れ。
連想配列INCLUDE_GUARDの添え字に、ファイルごとにユニークなトークンを指定することで、 そのファイルが読み込まれたというフラグを立てる。
変数名の前にTOKENという文字列を入れているのは、 そのままでは$TOKEN変数の中身であるハッシュを、数字として解釈しようとしてしまうため。
さらにmd5sumは標準入力をハッシュ化する場合でもハイフンとからのファイル名を出力しようとしてしまうため、 ${TOKEN:0:32}でハッシュ部分のみを取り出す。

[[ -n ... ]]で変数がセットされているかを調べ、 セットされていた場合はその時点でスクリプトを終了し、二重インクルードを防ぐ。

実行結果

test.sh

TOKEN=$(readlink -f "${BASH_SOURCE}" | md5sum)
if [[ -n "${INCLUDE_GUARD["TOKEN${TOKEN:0:32}"]}" ]]; then return; fi
INCLUDE_GUARD["TOKEN${TOKEN:0:32}"]="INCLUDE_GUARD"

python3 -c "print('Hello, Python!')"  # 良い子はライブラリーで副作用を起こすのをやめよう!

test2.sh

#!/bin/bash

if [[ -z "$SHELL_HOME" ]]; then SHELL_HOME="$(dirname "$0")"; fi

source "${SHELL_HOME}/test.sh"
source "${SHELL_HOME}/test.sh"
source "${SHELL_HOME}/test.sh"

echo "GoodBye, Bash..."

実行結果

$ ./test2.sh
Hello, Python!
GoodBye, Bash...
$

sourceを三回読んでいるにもかかわらず、メッセージは一度しか出力されない。

弱点

  • INCLUDE_GUARDを上書きされると死ぬ
  • こんな良くわからんことをしてまで、二重インクルードを防ぐ意味はあるのか
  • 実行コストは気にするな

参考