Bash には、他のプログラミング言語のような「関数ポインタ」や「クロージャ」は存在しません。
しかし実務では、処理の流れは共通化し、特定の処理だけを差し替えたい場面が頻繁にあります。
この記事では、Bash で安全に実装できる コールバック(フック)設計パターンを、
実務で使えるテンプレートとして解説します。
コールバック設計を使う利点
Bash スクリプト、ないしはプログラミング全般が肥大化する原因の多くは、次のようなケースです。
- 成功時・失敗時の処理がスクリプト中に散らばる
- ログ出力や通知処理が毎回コピペされる
- 処理内容を少し変えたいだけなのに、全体を複製してしまう
これらは 「処理の枠」と「中身」が分離されていないことが原因です。
そこで有効なのが、
関数名を引数として受け取り、任意のタイミングで呼び出す設計です。
Bash におけるコールバックの基本的な考え方
Bash では、次のように 関数名を文字列として扱い、そのまま呼び出すことができます。
my_callback() {
echo "callback called"
}
callback_name="my_callback"
"$callback_name"
この仕組みを使うことで、
- 実行する処理を後から差し替える
- 成功時・失敗時の処理を外から注入する
といった「コールバック的な設計」が可能になります。
設計パターン①:リトライ処理+成功/失敗コールバック
最も実用的な例が リトライ処理の共通化です。
設計の役割分担
retry
処理の流れを管理する「枠」- 実処理関数
実際に行いたい処理 - コールバック関数
成功時・失敗時の後処理
実装テンプレート
#!/usr/bin/env bash
set -Eeuo pipefail
log() {
printf '[%(%F %T)T] %s\n' -1 "$*" >&2
}
# retry <max> <sleep> <command_func> [on_success] [on_failure]
retry() {
local max="$1"
local sleep_sec="$2"
local cmd_func="$3"
local on_success="${4:-}"
local on_failure="${5:-}"
if ! declare -F "$cmd_func" >/dev/null; then
log "ERROR: function '$cmd_func' not found"
return 2
fi
local attempt=1 rc=0
while (( attempt <= max )); do
log "Try ${attempt}/${max}"
if "$cmd_func"; then
[[ -n "$on_success" ]] && declare -F "$on_success" >/dev/null && "$on_success" "$attempt"
return 0
fi
rc=$?
sleep "$sleep_sec"
((attempt++))
done
[[ -n "$on_failure" ]] && declare -F "$on_failure" >/dev/null && "$on_failure" "$rc" "$max"
return "$rc"
}
実処理とコールバックの例
do_task() {
(( RANDOM % 2 == 0 ))
}
on_success() {
log "SUCCESS after $1 attempt(s)"
}
on_failure() {
log "FAILED (rc=$1) after $2 attempt(s)"
}
retry 5 1 do_task on_success on_failure
この設計のメリット
- リトライロジックは 完全に共通化
- 通知・ログ・後処理は 用途ごとに差し替え可能
- 本体コードを触らず振る舞いを変えられる
設計パターン②:before / after フック型テンプレート
もう一つの定番が、処理の前後にフックを差し込む設計です。
実装テンプレート
run_step() {
local name="$1"
local action="$2"
declare -F "before_${name}" >/dev/null && "before_${name}"
"$action"
local rc=$?
declare -F "after_${name}" >/dev/null && "after_${name}" "$rc"
return "$rc"
}
利用例
step_download() {
echo "download..."
}
before_download() {
echo "prepare download"
}
after_download() {
echo "download finished (rc=$1)"
}
run_step download step_download
この設計のメリット
- 特定の処理だけに前後処理を追加できる
- フックが不要なら定義しなければよい
- 大規模スクリプトでも拡張しやすい
安全設計としての注意点(アンチパターン)
eval を使ったコールバックは避ける
# 危険な例
eval "$callback"
- 外部入力が混ざるとコマンド注入の危険がある
- デバッグが困難になる
関数名を直接呼ぶ方式を基本にしましょう。
set -e 環境での戻り値管理
- コールバック内で失敗してもよいのか
- メイン処理を止めるべきか
を 設計として明示してください。
戻り値を返す/返さないを決めておくと事故が減ります。
まとめ:Bashでも「設計」はできる
Bash は小さなスクリプトから始まりがちですが、
運用が続くほど 構造化・再利用・安全設計が重要になります。
- 処理の流れは関数で固定する
- 振る舞いはコールバックで差し替える
- eval を使わず、関数名で安全に実装する
このパターンを押さえておくと、
「壊れにくく、育てられる Bash スクリプト」を書けるようになります。
