この記事の狙い
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_NAMEはlogger連携やファイル名・一時ディレクトリ名に使える。必要なら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_XTRACEFDとPS4でトレース分離。retry_until timeout、with_lock(flock)などのヘルパーを同居。validate_regex VAR REGEX、validate_range INT MIN MAX等を足しても良い。
運用メモ
- ライブラリは
lib/common.shとして配置し、ローダ(require関数)で一度だけ読み込むと拡張しやすい。 - すべての関数はクォート徹底、stdout=データ原則を守る。
- ShellCheck を常時かけ、
SC2086/SC2046/SC2154あたりは潰す。
参考リンク
- GNU Bash Reference Manual(Functions / Redirections / Traps / Parameter Expansion)
https://www.gnu.org/software/bash/manual/bash.html - ShellCheck(典型ミスの検知)
https://www.shellcheck.net/
