Bashにおけるコールバック設計パターンと安全な実装テンプレート

設計パターン&テンプレ

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 スクリプト」を書けるようになります。

Bash玄

はじめまして!Bash玄です。

エンジニアとしてシステム運用に携わる中で、手作業の多さに限界を感じ、Bashスクリプトを活用して業務を効率化したのがきっかけで、この道に入りました。「手作業は負け」「スクリプトはシンプルに」をモットーに、誰でも実践できるBashスクリプトの書き方を発信しています。

このサイトでは、Bashの基礎から実践的なスクリプト作成まで、初心者でもわかりやすく解説しています。少しでも「Bashって便利だな」と思ってもらえたら嬉しいです!

# 好きなこと
- シンプルなコードを書くこと
- コマンドラインを快適にカスタマイズすること
- 自動化で時間を生み出すこと

# このサイトを読んでほしい人
- Bashに興味があるけど、何から始めればいいかわからない人
- 定型業務を自動化したい人
- 効率よくターミナルを使いこなしたい人

Bashの世界に一歩踏み出して、一緒に「Bash道」を極めていきましょう!

Bash玄をフォローする