Linux: Bashスクリプト応用 — 関数 / 配列 / エラーハンドリング
基礎的なスクリプトは簡単なタスクを処理できますが、本番レベルのスクリプトにはより堅牢なコードが必要です — ロジックを分割する関数、「サイレントフェイル」を防ぐエラーハンドリング、スクリプトを使いやすくするパラメータ解析。
📋 前提条件:まず以下を完了していること
- レッスン20:Bashスクリプト基礎
1. このレッスンで学ぶこと
- 関数定義と戻り値
- インデックス配列と連想配列
- set -eとtrapエラーハンドリング
- getoptsコマンドラインパラメータ
- set -xデバッグ手法
2. 小明のスクリプトがサーバーで暴走したストーリー
(1) 悩み:エラー後もスクリプトが実行を継続
小明はデプロイスクリプトを書きました:
#!/bin/bash
cd /var/www/myapp
git pull origin main
npm install # ❌ This failed
npm run build # But the script kept going
pm2 restart app # Restarted with old code and broken build
(2) 1行追加するだけで安全に
ボブが言いました:「先頭にset -eを追加すれば、エラーで停止するよ。」
#!/bin/bash
set -e # Stop on error
cd /var/www/myapp
git pull origin main
npm install
npm run build
pm2 restart app
(3) 得られるもの:安全第一
set -eにより、npm installが失敗するとスクリプトは即座に終了し、壊れたコードでサービスを再起動することはなくなります。
3. 知識ポイント
(1) 関数
# Define a function
greet() {
echo "Hello, $1!"
echo "Today is $(date)"
}
# Alternative syntax
function greet {
echo "Hello, $1!"
}
# Call it
greet "Alice"
# Local variables
myfunc() {
local x=10 # Local variable, doesn't affect outer scope
echo "Inside: $x"
}
x=5
myfunc # Output: Inside: 10
echo "Outside: $x" # Output: Outside: 5
(2) 配列
# Indexed array
fruits=("apple" "banana" "orange")
fruits[3]="grape" # Add element
echo ${fruits[0]} # apple
echo ${fruits[@]} # All elements
echo ${#fruits[@]} # Array length
# Iterate over array
for fruit in "${fruits[@]}"; do
echo "$fruit"
done
# Associative array (like a dictionary)
declare -A config
config[host]="localhost"
config[port]="8080"
config[user]="admin"
echo ${config[host]} # localhost
(3) エラーハンドリング
# set -e: Exit on error
set -e
# set -u: Error on undefined variable
set -u
# set -o pipefail: Pipeline fails if any command fails
set -o pipefail
# Common combination
set -euo pipefail
> 💡 ヒント:`set -euo pipefail`は本番スクリプトの「安全の三種の神器」 — `-e`はエラーで停止、`-u`は未定義変数を検出、`-o pipefail`はパイプラインの失敗を伝播させます。重要なスクリプトにはすべてこの1行を含めるべきです。
# trap: Execute cleanup on signal or exit
trap 'echo "Script interrupted"; exit 1' INT TERM
trap 'rm -f /tmp/tempfile; echo "Cleanup done"' EXIT
> 💡 ヒント:`trap`はスクリプト終了時に自動的にクリーンアップを実行できます — 正常終了でもCtrl+Cで中断されても、`trap EXIT`がトリガーされます。一般的な用途:一時ファイルの削除、ファイルロックの解放、設定の復元。先頭に`trap 'rm -f $TMPFILE' EXIT`と書けば、各終了ポイントでの手動クリーンアップは不要です。
# Custom error handler
error_handler() {
echo "❌ Error on line $1"
echo "Command: $2"
exit 1
}
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
(4) getopts — パラメータ解析
# getopts syntax
while getopts "f:o:hv" opt; do
case $opt in
f) INPUT_FILE="$OPTARG" ;; # -f filename
o) OUTPUT_DIR="$OPTARG" ;; # -o output
h) show_help; exit 0 ;; # -h help
v) VERBOSE=true ;; # -v verbose
*) echo "Unknown option"; exit 1 ;;
esac
done
> ℹ️ 注意:`getopts`はbash組み込みのパラメータパーサー — オプション文字列`"f:o:hv"`で、コロン`:`はそのオプションが引数を必要とすることを示し(例:`-f filename`)、コロンなしはフラグオプション(例:`-v`)。長いオプション(`--help`など)はサポートしません。長いオプションが必要な場合は外部コマンド`getopt`または手動解析を使用してください。
(5) デバッグ手法
# Debug mode: show each command as it executes
set -x
# Turn off debugging
set +x
# Enable debugging for a specific section
echo "Normal execution"
set -x
echo "This section is traced"
for i in 1 2 3; do echo "$i"; done
set +x
echo "Back to normal"
# Or debug at script startup
# bash -x script.sh
(6) ▶ サンプル:関数のカプセル化
#!/bin/bash
# functions-demo.sh - Function examples
log() {
local level="${1:-INFO}"
local message="$2"
echo "[$(date '+%H:%M:%S')] [$level] $message"
}
die() {
log "ERROR" "$1"
exit 1
}
check_dependency() {
if ! command -v "$1" &> /dev/null; then
die "Required command '$1' not found"
fi
}
# Using the functions
log "INFO" "Starting dependency check..."
check_dependency "git"
check_dependency "node"
log "INFO" "Dependency check passed"
(7) ▶ サンプル:配列操作
#!/bin/bash
# array-demo.sh - Array examples
# Server list
servers=("web01" "web02" "db01" "cache01")
echo "Server count: ${#servers[@]}"
echo "--- Iterating servers ---"
for server in "${servers[@]}"; do
echo "Checking: $server"
done
echo "--- Associative array example ---"
declare -A disk_usage
disk_usage[/]="45%"
disk_usage[/home]="62%"
disk_usage[/var]="78%"
for mount in "${!disk_usage[@]}"; do
echo "$mount: ${disk_usage[$mount]}"
done
出力:
TEXT[10:23:01] [INFO] Starting dependency check... [10:23:01] [INFO] Dependency check passed Server count: 4 --- Iterating servers --- Checking: web01 Checking: web02 Checking: db01 Checking: cache01 --- Associative array example --- /: 45% /home: 62% /var: 78%
(8) ▶ サンプル:set -e / trapエラーハンドリング
#!/bin/bash
# safe-script.sh - Robust script
set -euo pipefail
cleanup() {
echo "Cleaning temporary files..."
rm -f /tmp/temp_*.txt
echo "Done"
}
error_handler() {
echo "❌ Error occurred on line $1"
cleanup
exit 1
}
trap error_handler ERR
trap cleanup EXIT
echo "Starting execution..."
mkdir -p /tmp/test
touch /tmp/test/file.txt
cat /tmp/test/file.txt
# If cat fails on a non-existent file, the script stops and triggers ERR trap
出力:
TEXTStarting execution... Cleaning temporary files... Done
(9) ▶ サンプル:getoptsパラメータ解析
#!/bin/bash
# backup-tool.sh - Backup tool with parameter parsing
usage() {
echo "Usage: $0 -s <source_dir> -d <dest_dir> [-c] [-v]"
echo " -s Source directory to back up"
echo " -d Backup destination directory"
echo " -c Compress backup"
echo " -v Verbose output"
echo " -h Show help"
exit 0
}
# Default values
COMPRESS=false
VERBOSE=false
while getopts "s:d:cvh" opt; do
case $opt in
s) SOURCE="$OPTARG" ;;
d) DEST="$OPTARG" ;;
c) COMPRESS=true ;;
v) VERBOSE=true ;;
h) usage ;;
*) usage ;;
esac
done
# Validate required parameters
if [ -z "$SOURCE" ] || [ -z "$DEST" ]; then
echo "Error: Must specify -s and -d"
usage
fi
if [ ! -d "$SOURCE" ]; then
echo "Error: Source directory '$SOURCE' does not exist"
exit 1
fi
# Execute backup
mkdir -p "$DEST"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="backup_${TIMESTAMP}"
if [ "$COMPRESS" = true ]; then
tar -czf "${DEST}/${BACKUP_NAME}.tar.gz" "$SOURCE"
echo "✅ Compressed backup complete: ${DEST}/${BACKUP_NAME}.tar.gz"
else
cp -r "$SOURCE" "${DEST}/${BACKUP_NAME}"
echo "✅ Backup complete: ${DEST}/${BACKUP_NAME}"
fi
if [ "$VERBOSE" = true ]; then
echo "Source: $SOURCE"
echo "Destination: $DEST"
echo "Size: $(du -sh "$SOURCE" | cut -f1)"
fi
出力:
TEXTUsage: ./backup-tool.sh -s <source_dir> -d <dest_dir> [-c] [-v] -s Source directory to back up -d Backup destination directory -c Compress backup -v Verbose output -h Show help ✅ Compressed backup complete: /backup/backup_20260707_102345.tar.gz Source: /var/www/myapp Destination: /backup Size: 12M
(10) ▶ サンプル:デバッグ手法
#!/bin/bash
# debug-demo.sh - Debugging example
# Only enable tracing where debugging is needed
echo "Script started PID: $$"
for i in 1 2 3; do
set -x # Start tracing
echo "Loop iteration $i"
result=$((i * 10))
echo "Result: $result"
set +x # Stop tracing
done
echo "Script execution complete"
(11) ▶ 総合例:本番レベルのデプロイスクリプト
#!/bin/bash
# deploy.sh - Production-grade deployment script
set -euo pipefail
APP_NAME="webapp"
APP_DIR="/var/www/$APP_NAME"
BACKUP_DIR="/backup/$APP_NAME"
LOG_FILE="/var/log/deploy.log"
# Color output
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
log() {
local level="$1"
local msg="$2"
echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] [${level}] ${msg}" | tee -a "$LOG_FILE"
}
info() { log "INFO" "$1"; }
success() { log "INFO" "${GREEN}$1${NC}"; }
error() { log "ERROR" "${RED}$1${NC}"; }
cleanup() {
info "Cleaning deployment temporary files..."
}
error_handler() {
error "Deployment failed at line $1"
error "Command: $BASH_COMMAND"
cleanup
exit 1
}
usage() {
echo "Usage: $0 [options]"
echo " -b Specify Git branch (default: main)"
echo " -e Specify environment (dev/staging/prod)"
echo " -n Skip backup"
echo " -h Show help"
exit 0
}
trap error_handler ERR
trap cleanup EXIT
# --- Parameter parsing ---
BRANCH="main"
ENV="prod"
SKIP_BACKUP=false
while getopts "b:e:nh" opt; do
case $opt in
b) BRANCH="$OPTARG" ;;
e) ENV="$OPTARG" ;;
n) SKIP_BACKUP=true ;;
h) usage ;;
*) usage ;;
esac
done
# --- Environment check ---
info "Starting deployment of $APP_NAME (branch: $BRANCH, environment: $ENV)"
if [ ! -d "$APP_DIR" ]; then
error "Application directory $APP_DIR does not exist"
exit 1
fi
# --- Backup ---
if [ "$SKIP_BACKUP" = false ]; then
info "Backing up current version..."
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"
tar -czf "${BACKUP_DIR}/${TIMESTAMP}.tar.gz" \
--exclude=node_modules \
--exclude=.git \
"$APP_DIR"
success "Backup complete: ${BACKUP_DIR}/${TIMESTAMP}.tar.gz"
fi
# --- Deploy ---
cd "$APP_DIR"
info "Pulling latest code..."
git fetch origin
git checkout "$BRANCH"
git pull origin "$BRANCH"
info "Installing dependencies..."
npm ci --production
info "Building application..."
if [ "$ENV" = "production" ]; then
npm run build:prod
else
npm run build
fi
info "Restarting service..."
pm2 restart "$APP_NAME" || pm2 start npm --name "$APP_NAME" -- start
# --- Health check ---
info "Running health check..."
sleep 3
if curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/health | grep -q "200"; then
success "✅ Deployment successful! Health check passed"
else
error "❌ Health check failed, please investigate manually"
exit 1
fi
❓ よくある質問
Q: 関数とエイリアスの違いは? A: 関数はエイリアスより強力です:パラメータ、ローカル変数、戻り値、再利用をサポート。エイリアスは単純なコマンド置換に過ぎません。複雑なロジックには関数を、単純なショートカットにはエイリアスを使用してください。
Q:
set -eが効かないケースは? A:set -eは次の場合にトリガーされません:1) if/while/untilの条件式内。2)&&や||の後。3) パイプライン内(set -o pipefailを追加)。4) コマンドが!で否定されている場合。
Q: trapで捕捉できるシグナルは? A: よく使われるもの:
EXIT(スクリプト終了)、ERR(コマンド失敗)、INT(Ctrl+C)、TERM(デフォルトのkillシグナル)。他にHUP、QUIT、USR1、USR2など。
Q: getoptsとgetoptの違いは? A:
getoptsはbash組み込みでシンプル、長いオプション(--helpなど)はサポートしません。getoptは外部コマンド(util-linuxパッケージ)で長短オプションの混在に対応。互換性を優先するならgetoptsを推奨。
Q: スクリプト内で一時ファイルを作成するには? A:
mktempを使用:tempfile=$(mktemp)で一時ファイル、tempdir=$(mktemp -d)で一時ディレクトリを作成。一時ファイルは通常/tmpに作成され、使い終わったら削除を忘れずに。
📖 まとめ
- 関数
func() { local vars; return N; }でロジックをカプセル化 - 配列:
${arr[@]}で反復、${#arr[@]}で長さ、declare -Aで連想配列 set -euo pipefailでスクリプトを堅牢に、trap ERR EXITでエラー捕捉とクリーンアップgetoptsでコマンドラインパラメータを解析set -x/+xでデバッグ、bash -x script.shでデバッグモード実行
📝 練習問題
- 基本(難易度 ⭐):レッスン20のバックアップスクリプトを関数(log/die/check_dependencyなど)を使ってリファクタリングし、
set -euo pipefailとtrapエラーハンドリングを追加する - 中級(難易度 ⭐⭐):
getoptsで-s(ソースディレクトリ)と-d(宛先ディレクトリ)パラメータを追加し、連想配列で設定を格納し、反復して全設定項目を出力する - 上級(難易度 ⭐⭐⭐):
mktempで完了時に自動クリーンアップされる一時ディレクトリを作成し、パラメータ解析、カラーログ、エラーハンドリング、ヘルスチェック付きの本番レベルのデプロイスクリプトを書く