Linux: Bashスクリプト応用 — 関数 / 配列 / エラーハンドリング

基礎的なスクリプトは簡単なタスクを処理できますが、本番レベルのスクリプトにはより堅牢なコードが必要です — ロジックを分割する関数、「サイレントフェイル」を防ぐエラーハンドリング、スクリプトを使いやすくするパラメータ解析。

📋 前提条件:まず以下を完了していること

1. このレッスンで学ぶこと


2. 小明のスクリプトがサーバーで暴走したストーリー

(1) 悩み:エラー後もスクリプトが実行を継続

小明はデプロイスクリプトを書きました:

BASH
#!/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を追加すれば、エラーで停止するよ。」

TEXT
#!/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) 関数

TEXT
# 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) 配列

TEXT
# 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) エラーハンドリング

TEXT
# 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 — パラメータ解析

TEXT
# 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) デバッグ手法

TEXT
# 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) ▶ サンプル:関数のカプセル化

TEXT
#!/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) ▶ サンプル:配列操作

TEXT
#!/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エラーハンドリング

BASH
#!/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

出力:

TEXT
Starting execution...
Cleaning temporary files...
Done

(9) ▶ サンプル:getoptsパラメータ解析

BASH
#!/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

出力:

TEXT
Usage: ./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) ▶ サンプル:デバッグ手法

TEXT
#!/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) ▶ 総合例:本番レベルのデプロイスクリプト

BASH
#!/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シグナル)。他にHUPQUITUSR1USR2など。

Q: getoptsとgetoptの違いは? A: getoptsはbash組み込みでシンプル、長いオプション(--helpなど)はサポートしません。getoptは外部コマンド(util-linuxパッケージ)で長短オプションの混在に対応。互換性を優先するならgetoptsを推奨。

Q: スクリプト内で一時ファイルを作成するには? A: mktempを使用:tempfile=$(mktemp)で一時ファイル、tempdir=$(mktemp -d)で一時ディレクトリを作成。一時ファイルは通常/tmpに作成され、使い終わったら削除を忘れずに。


📖 まとめ


📝 練習問題

  1. 基本(難易度 ⭐):レッスン20のバックアップスクリプトを関数(log/die/check_dependencyなど)を使ってリファクタリングし、set -euo pipefailtrapエラーハンドリングを追加する
  2. 中級(難易度 ⭐⭐)getopts-s(ソースディレクトリ)と-d(宛先ディレクトリ)パラメータを追加し、連想配列で設定を格納し、反復して全設定項目を出力する
  3. 上級(難易度 ⭐⭐⭐)mktempで完了時に自動クリーンアップされる一時ディレクトリを作成し、パラメータ解析、カラーログ、エラーハンドリング、ヘルスチェック付きの本番レベルのデプロイスクリプトを書く
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%