共通関数ライブラリ|die / log / require / retry / validate

設計パターン&テンプレ

この記事の狙い

Bash スクリプトで毎回書いている定番関数を、再利用可能な共通ライブラリとしてまとめます。
最小機能は die(異常終了)、log(レベル付き stderr)、require(前提コマンド/ファイル検証)、retry(指数バックオフ)、validate(入力検証)。**安全設計(set -Eeuo pipefail / クォート徹底)**で提供します。

TL;DR(“コピペ可”最小ライブラリ)

lib/common.sh

# shellcheck shell=bash
[ "${__LOADED_common_sh-}" = 1 ] && return 0; __LOADED_common_sh=1
set -Eeuo pipefail

# ---- 定数(終了コード) ----
E_USAGE=2; E_IO=3; E_RUNTIME=4; E_DEP=5; E_INVALID=6

# ---- ログ(stderr) ----
: "${APP_NAME:=app}" : "${APP_LOG_LEVEL:=info}"  # info|warn|error
__log_ok(){ case "$APP_LOG_LEVEL:$1" in error:info|error:warn|warn:info) return 1;; esac; }
log(){ __log_ok "$1" || return 0
  printf '%s %-5s %s\n' "$(date +'%Y-%m-%dT%H:%M:%S%z')" "$1" "${*:2}" >&2; }
log_info(){ log info "$@"; }  log_warn(){ log warn "$@"; }  log_err(){ log error "$@"; }

# ---- 異常終了 ----
die(){ local code="${2:-$E_RUNTIME}"; log_err "$1"; exit "$code"; }

# ---- 前提満たすか?(コマンド/ファイル/書込先) ----
require_cmd(){ local c; for c in "$@"; do command -v "$c" >/dev/null 2>&1 || die "command not found: $c" $E_DEP; done; }
require_readable(){ local f; for f in "$@"; do [[ -r "$f" ]] || die "cannot read: $f" $E_IO; done; }
require_writable_path(){ local p="$1"; { : >"$p" ; } 2>/dev/null || die "cannot write: $p" $E_IO; : >"$p"; } # truncate OKなら上書き注意

# ---- 再試行(指数バックオフ+ジッタ) ----
retry(){ # usage: retry [max=5] [base=0.2] -- cmd args...
  local max="${1:-5}" base="${2:-0.2}"; shift 2 || true
  [[ "${1-}" == -- ]] || { die "retry usage: retry MAX BASE -- cmd..." $E_USAGE; }
  shift
  local i=1 sleep_s
  while :; do
    if "$@"; then return 0; fi
    (( i>=max )) && return 1
    sleep_s="$(awk -v i="$i" -v b="$base" 'BEGIN{s=b*(2^(i-1)); printf "%.3f", s + (s*rand()/2)}')"
    log_warn "retry $i/$max in ${sleep_s}s: $*"
    sleep "$sleep_s"; ((i++))
  done
}

# ---- 入力検証(validate_*) ----
validate_int(){ [[ "${1-}" =~ ^-?[0-9]+$ ]] || { log_err "not an integer: ${1-}"; return $E_INVALID; }; }
validate_in(){ local v="$1"; shift; local x; for x in "$@"; do [[ "$v" == "$x" ]] && return 0; done
  log_err "invalid value: ${v@Q} (allowed: ${*@Q})"; return $E_INVALID; }
validate_fileglob(){ shopt -s nullglob; local a=($1); shopt -u nullglob; (( ${#a[@]} )) || { log_err "no match: $1"; return $E_INVALID; }; }

# ---- 一時資源(trapで掃除) ----
_tmpdir=''
mktempdir(){ _tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/${APP_NAME}.XXXXXX")" || die "mktemp failed" $E_IO; }
cleanup(){ local st=$?; [[ -n "${_tmpdir-}" && -d "$_tmpdir" ]] && rm -rf -- "$_tmpdir"; (( st!=0 )) && log_err "exit $st"; exit "$st"; }

使い方(エントリ)

#!/usr/bin/env bash
set -Eeuo pipefail; set -o pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/common.sh
. "$SCRIPT_DIR/lib/common.sh"
trap cleanup EXIT INT TERM

main(){
  require_cmd awk curl
  require_readable "./input.txt"
  validate_int "${1-}" || return $E_USAGE
  validate_in "${2-}" low medium high || return $E_USAGE

  mktempdir
  log_info "tmpdir=${_tmpdir@Q}"

  retry 5 0.2 -- curl -fsSLo "$_tmpdir/out" "https://example.com/data" \
    || die "download failed" $E_IO

  awk '{s+=$1} END{print s}' <"./input.txt" >"./result.txt" \
    || die "calc failed" $E_RUNTIME

  log_info "done"
}
main "$@" || exit $?

設計の要点

  • stdout=結果、stderr=ログで分離。log_* を経由させ、フォーマット/レベルを一元管理。
  • 終了コードの分類E_*)を定数化して読みやすく。ライブラリは return、エントリだけ exit $?
  • require_*早期失敗を促進。I/O や依存が欠けている状態で先へ進まない。
  • retry指数+ジッタ(一斉再試行のスパイクを避ける)。-- で後続コマンドを確実に区切る。
  • validate_*メッセージ付き非0で返し、呼び出し側で || return / || die のどちらでも使える。

レベル設計と環境変数

  • APP_LOG_LEVEL=warn で情報系を抑制。CI や本番でノイズを減らす。
  • APP_NAMElogger 連携やファイル名・一時ディレクトリ名に使える。必要なら log() 内で logger 併用へ拡張。

代表ユースケース

1) 依存のチェックと安全な開始

require_cmd jq gzip
require_readable "$CONFIG"
require_writable_path "$OUT"   # 事前に書込可否を確かめておく

2) ネットワーク呼び出しの堅牢化

retry 6 0.3 -- curl -fsS --retry 0 "https://api/v1" >"$_tmpdir/resp.json" \
  || die "API unavailable" $E_IO

3) 入力検証で即時フィードバック

validate_in "$MODE" list create delete || die "unsupported MODE=$MODE" $E_USAGE
validate_fileglob "./data/*.csv"       || die "no CSVs" $E_INVALID

“コピペ可”テストブロック(最小)

#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$SCRIPT_DIR/lib/common.sh"

# 1) log レベル判定(info→出る)
APP_LOG_LEVEL=info
out="$({ log_info "hello"; } 2>&1 >/dev/null)"
grep -q 'INFO' <<<"$out" || { echo "log info NG"; exit 1; }

# 2) require_cmd 失敗で終了コード
set +e; (require_cmd _no_such_command); st=$?; set -e
[[ $st -ne 0 ]] || { echo "require_cmd NG"; exit 1; }

# 3) retry:2回失敗→3回目成功を確認
i=0
retry 3 0.01 -- bash -c '(( ++i < 3 )) && exit 1 || exit 0' || { echo "retry NG"; exit 1; }

# 4) validate_int
set +e; validate_int "12x"; st=$?; set -e
[[ $st -ne 0 ]] || { echo "validate_int NG"; exit 1; }

echo "PASS"

失敗しやすい点(アンチパターン)

  • ログを stdout に書く → 下流処理が壊れる。ログは stderr
  • die をライブラリの深部で乱発 → 再利用性が下がる。検証関数は非0 return、エントリで die に束ねる。
  • retry のコマンドに -- を入れない → オプション解釈の衝突。必ず --
  • require_writable_path で実体を破壊 → 追記/上書きポリシーを決める。必要なら別に require_writable_dir を用意し、tmp→mv の原子更新に。

拡張ポイント

  • log_sys() を追加し logger へも送る、BASH_XTRACEFDPS4 でトレース分離。
  • retry_until timeoutwith_lockflock)などのヘルパーを同居。
  • validate_regex VAR REGEXvalidate_range INT MIN MAX 等を足しても良い。

運用メモ

  • ライブラリは lib/common.sh として配置し、ローダrequire 関数)で一度だけ読み込むと拡張しやすい。
  • すべての関数はクォート徹底stdout=データ原則を守る。
  • ShellCheck を常時かけ、SC2086/SC2046/SC2154 あたりは潰す。

参考リンク

Bash玄

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

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

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

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

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

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

Bash玄をフォローする