T-CREATOR

Shell Script 設計 7 原則:可読性・再利用・堅牢性を高める実践ガイド

Shell Script 設計 7 原則:可読性・再利用・堅牢性を高める実践ガイド

Shell Script は、システム管理や自動化タスクにおいて欠かせないツールです。しかし、保守性の低いスクリプトは、後々の運用において大きな課題となってしまいます。

本記事では、実務で培われた 7 つの設計原則を通じて、可読性・再利用性・堅牢性の高い Shell Script を書くための実践的な手法をご紹介します。初心者の方でも理解しやすいよう、具体的なコード例と図解を交えて解説していきますね。

背景

Shell Script は UNIX/Linux システムにおいて、長年にわたり自動化や運用の中核を担ってきました。その手軽さゆえに、誰でも簡単に書き始められる一方で、設計思想を持たずに作成されたスクリプトは、後々の保守や拡張において大きな障害となります。

多くの組織では、「動けばいい」という発想で書かれたスクリプトが蓄積され、エラーハンドリングの欠如、可読性の低さ、再利用性のなさといった問題を抱えています。

以下の図は、設計原則を適用しない場合と適用した場合の影響範囲を示しています。

mermaidflowchart TB
  script["Shell Script"]

  subgraph bad["設計原則なし"]
    bad1["可読性低下"]
    bad2["バグ混入"]
    bad3["保守困難"]
    bad4["運用リスク増大"]
  end

  subgraph good["7原則適用"]
    good1["可読性向上"]
    good2["エラー制御"]
    good3["保守容易"]
    good4["安定運用"]
  end

  script -->|原則なし| bad1
  bad1 --> bad2
  bad2 --> bad3
  bad3 --> bad4

  script -->|原則適用| good1
  good1 --> good2
  good2 --> good3
  good3 --> good4

図で理解できる要点:

  • 設計原則の有無が、スクリプトのライフサイクル全体に影響する
  • 原則適用により、可読性から運用品質まで段階的に向上する
  • 逆に原則なしでは、負の連鎖が発生する

プロジェクトが成長するにつれて、スクリプトの品質が開発速度や運用安定性に直結します。だからこそ、初期段階から設計原則を意識することが重要なのです。

課題

Shell Script の開発現場では、以下のような課題が頻繁に発生しています。

1. エラーハンドリングの不足

スクリプトが途中でエラーになっても処理を続行してしまい、予期しない結果を生み出します。特に本番環境での実行時に、データ破損や不整合が発生するリスクがあります。

2. 可読性の低さ

変数名が不明確、コメントがない、処理の流れが追いにくいといった問題により、後から見た人(あるいは数ヶ月後の自分)がコードを理解できません。

3. ハードコーディング

設定値やパスがスクリプト内に直接書き込まれており、環境が変わるたびにコードを修正する必要があります。これは再利用性を大きく損なう要因です。

4. 依存関係の不明確さ

必要なコマンドやファイルが存在しない場合の確認がなく、実行時に突然エラーになります。

5. ログ出力の欠如

トラブル発生時に、何が起きたのか追跡できず、原因究明に時間がかかります。

これらの課題を解決するために、体系的な設計原則が必要となります。

以下の図は、よくある課題とその影響を示しています。

mermaidflowchart LR
  issue1["エラー<br/>ハンドリング不足"]
  issue2["可読性低下"]
  issue3["ハード<br/>コーディング"]
  issue4["依存関係<br/>不明確"]
  issue5["ログ不足"]

  impact1["予期しない<br/>動作"]
  impact2["保守困難"]
  impact3["環境依存"]
  impact4["実行時エラー"]
  impact5["調査遅延"]

  issue1 --> impact1
  issue2 --> impact2
  issue3 --> impact3
  issue4 --> impact4
  issue5 --> impact5

  impact1 --> result["運用品質低下"]
  impact2 --> result
  impact3 --> result
  impact4 --> result
  impact5 --> result

図で理解できる要点:

  • 各課題が独立した影響を持ちながら、最終的に運用品質を低下させる
  • 一つの課題だけでも改善すれば、全体的な品質向上につながる
  • すべての課題は設計原則によって解決可能

解決策

これらの課題を解決するため、以下の 7 つの設計原則を提案します。それぞれの原則は、可読性・再利用性・堅牢性のいずれか、または複数の観点から Shell Script の品質を向上させます。

原則 1: エラー時即座停止(set -e)

スクリプトの冒頭で set -e を設定することで、コマンドがエラーを返した時点で即座に処理を停止します。これにより、エラーの連鎖を防ぎ、予期しない動作を回避できます。

bash#!/bin/bash

# エラー時に即座停止
set -e

echo "処理を開始します"

set -e の効果:

bash# エラーが発生すると、この時点でスクリプトが停止する
cp /nonexistent/file /tmp/

# set -e がないと、エラー後もこの行が実行されてしまう
echo "処理が完了しました"

さらに厳密なエラーハンドリングを行いたい場合は、set -euo pipefail を使用します。

bash#!/bin/bash

# より厳格なエラーハンドリング
set -euo pipefail

# -e: コマンドがエラーを返したら停止
# -u: 未定義変数を使用したらエラー
# -o pipefail: パイプライン内でエラーが発生したら全体をエラーとする

原則 2: 変数の明確な定義と検証

変数は用途が明確にわかる名前を付け、必須変数は事前に検証します。これにより、スクリプトの意図が理解しやすくなり、実行時エラーを防げます。

良い変数命名の例:

bash#!/bin/bash
set -euo pipefail

# 明確な変数名を使用
BACKUP_SOURCE_DIR="/var/www/html"
BACKUP_DEST_DIR="/backup/daily"
BACKUP_DATE=$(date +%Y%m%d)
LOG_FILE="/var/log/backup_${BACKUP_DATE}.log"

必須変数の検証:

bash# 環境変数が設定されているか確認
if [[ -z "${DATABASE_URL:-}" ]]; then
  echo "Error: DATABASE_URL が設定されていません" >&2
  exit 1
fi

# ディレクトリの存在確認
if [[ ! -d "${BACKUP_SOURCE_DIR}" ]]; then
  echo "Error: バックアップ元ディレクトリが存在しません: ${BACKUP_SOURCE_DIR}" >&2
  exit 1
fi

変数のデフォルト値設定:

bash# デフォルト値を設定(環境変数が未設定の場合)
RETRY_COUNT="${RETRY_COUNT:-3}"
TIMEOUT_SECONDS="${TIMEOUT_SECONDS:-30}"
DEBUG_MODE="${DEBUG_MODE:-false}"

原則 3: 関数化による処理の分割

処理を関数に分割することで、コードの再利用性が高まり、可読性も向上します。各関数は単一の責任を持つように設計しましょう。

関数の基本構造:

bash#!/bin/bash
set -euo pipefail

# ログ出力関数
log_info() {
  local message="$1"
  echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') - ${message}"
}

log_error() {
  local message="$1"
  echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') - ${message}" >&2
}

バックアップ処理の関数化:

bash# バックアップディレクトリを作成する関数
create_backup_directory() {
  local backup_dir="$1"

  log_info "バックアップディレクトリを作成: ${backup_dir}"

  if ! mkdir -p "${backup_dir}"; then
    log_error "ディレクトリ作成に失敗: ${backup_dir}"
    return 1
  fi

  return 0
}

ファイル圧縮の関数化:

bash# ファイルを圧縮する関数
compress_files() {
  local source_dir="$1"
  local dest_file="$2"

  log_info "ファイルを圧縮: ${source_dir} -> ${dest_file}"

  if tar -czf "${dest_file}" -C "$(dirname "${source_dir}")" "$(basename "${source_dir}")"; then
    log_info "圧縮完了: ${dest_file}"
    return 0
  else
    log_error "圧縮に失敗: ${source_dir}"
    return 1
  fi
}

メイン処理:

bash# メイン処理
main() {
  local backup_dir="/backup/daily"
  local source_dir="/var/www/html"
  local backup_file="${backup_dir}/backup_$(date +%Y%m%d_%H%M%S).tar.gz"

  create_backup_directory "${backup_dir}"
  compress_files "${source_dir}" "${backup_file}"

  log_info "バックアップ処理が完了しました"
}

# スクリプト実行
main "$@"

原則 4: 設定の外部化

ハードコーディングを避け、設定を外部ファイルや環境変数から読み込むことで、環境ごとの変更が容易になります。

設定ファイルの作成:

bash# config.env
BACKUP_SOURCE_DIR="/var/www/html"
BACKUP_DEST_DIR="/backup/daily"
RETENTION_DAYS=7
NOTIFICATION_EMAIL="admin@example.com"
LOG_LEVEL="INFO"

設定ファイルの読み込み:

bash#!/bin/bash
set -euo pipefail

# 設定ファイルのパス
CONFIG_FILE="${CONFIG_FILE:-./config.env}"

# 設定ファイルの存在確認と読み込み
if [[ -f "${CONFIG_FILE}" ]]; then
  # shellcheck source=/dev/null
  source "${CONFIG_FILE}"
  echo "設定ファイルを読み込みました: ${CONFIG_FILE}"
else
  echo "Warning: 設定ファイルが見つかりません: ${CONFIG_FILE}" >&2
  echo "デフォルト値を使用します"
fi

デフォルト値との組み合わせ:

bash# 設定ファイルまたは環境変数から読み込み、なければデフォルト値を使用
BACKUP_SOURCE_DIR="${BACKUP_SOURCE_DIR:-/var/www/html}"
BACKUP_DEST_DIR="${BACKUP_DEST_DIR:-/backup/daily}"
RETENTION_DAYS="${RETENTION_DAYS:-7}"
LOG_LEVEL="${LOG_LEVEL:-INFO}"

原則 5: 依存関係の事前チェック

スクリプトが依存するコマンドやファイルの存在を事前に確認することで、実行時エラーを防ぎます。

必要なコマンドの確認:

bash#!/bin/bash
set -euo pipefail

# 必要なコマンドのリスト
REQUIRED_COMMANDS=(
  "tar"
  "gzip"
  "date"
  "mysql"
  "mysqldump"
)

コマンド存在チェック関数:

bash# コマンドの存在を確認する関数
check_required_commands() {
  local missing_commands=()

  for cmd in "${REQUIRED_COMMANDS[@]}"; do
    if ! command -v "${cmd}" &> /dev/null; then
      missing_commands+=("${cmd}")
    fi
  done

  if [[ ${#missing_commands[@]} -gt 0 ]]; then
    echo "Error: 以下のコマンドが見つかりません:" >&2
    printf '  - %s\n' "${missing_commands[@]}" >&2
    exit 1
  fi
}

ディレクトリとファイルの確認:

bash# 必要なディレクトリの確認
check_required_directories() {
  local dirs=(
    "${BACKUP_SOURCE_DIR}"
    "${BACKUP_DEST_DIR}"
  )

  for dir in "${dirs[@]}"; do
    if [[ ! -d "${dir}" ]]; then
      echo "Error: ディレクトリが存在しません: ${dir}" >&2
      exit 1
    fi
  done
}

ディスク容量の確認:

bash# ディスク容量の確認
check_disk_space() {
  local required_space_mb="$1"
  local target_dir="$2"

  # 利用可能な容量を取得(MB単位)
  local available_space_mb
  available_space_mb=$(df -m "${target_dir}" | awk 'NR==2 {print $4}')

  if [[ ${available_space_mb} -lt ${required_space_mb} ]]; then
    echo "Error: ディスク容量が不足しています" >&2
    echo "  必要: ${required_space_mb}MB, 利用可能: ${available_space_mb}MB" >&2
    exit 1
  fi
}

原則 6: ログ出力とトレース機能

適切なログ出力により、処理の進行状況を把握でき、トラブル発生時の原因究明が容易になります。

ログレベルの定義:

bash#!/bin/bash
set -euo pipefail

# ログレベルの定義
LOG_LEVEL_DEBUG=0
LOG_LEVEL_INFO=1
LOG_LEVEL_WARN=2
LOG_LEVEL_ERROR=3

# 現在のログレベル(環境変数から設定可能)
CURRENT_LOG_LEVEL="${LOG_LEVEL:-${LOG_LEVEL_INFO}}"

ログ出力関数の実装:

bash# ログ出力関数
log_message() {
  local level="$1"
  local level_num="$2"
  local message="$3"
  local timestamp
  timestamp=$(date '+%Y-%m-%d %H:%M:%S')

  if [[ ${level_num} -ge ${CURRENT_LOG_LEVEL} ]]; then
    echo "[${level}] ${timestamp} - ${message}"
  fi
}

各レベルのログ関数:

bashlog_debug() {
  log_message "DEBUG" "${LOG_LEVEL_DEBUG}" "$1"
}

log_info() {
  log_message "INFO" "${LOG_LEVEL_INFO}" "$1"
}

log_warn() {
  log_message "WARN" "${LOG_LEVEL_WARN}" "$1" >&2
}

log_error() {
  log_message "ERROR" "${LOG_LEVEL_ERROR}" "$1" >&2
}

ログファイルへの出力:

bash# ログファイルのパス
LOG_FILE="${LOG_FILE:-/var/log/script_$(date +%Y%m%d).log}"

# ログをファイルと標準出力の両方に出力
exec 1> >(tee -a "${LOG_FILE}")
exec 2> >(tee -a "${LOG_FILE}" >&2)

log_info "スクリプトを開始しました"
log_info "ログファイル: ${LOG_FILE}"

トレースモードの実装:

bash# デバッグモード(環境変数 DEBUG=true で有効化)
if [[ "${DEBUG:-false}" == "true" ]]; then
  set -x  # コマンドをトレース出力
  log_debug "デバッグモードが有効です"
fi

原則 7: クリーンアップとシグナルハンドリング

スクリプト終了時やエラー発生時に、一時ファイルの削除やリソースの解放を確実に行います。

一時ディレクトリの作成:

bash#!/bin/bash
set -euo pipefail

# 一時ディレクトリを作成
TEMP_DIR=$(mktemp -d)
log_info "一時ディレクトリを作成: ${TEMP_DIR}"

クリーンアップ関数の実装:

bash# クリーンアップ関数
cleanup() {
  local exit_code=$?

  log_info "クリーンアップ処理を開始します"

  # 一時ファイルの削除
  if [[ -n "${TEMP_DIR:-}" ]] && [[ -d "${TEMP_DIR}" ]]; then
    log_info "一時ディレクトリを削除: ${TEMP_DIR}"
    rm -rf "${TEMP_DIR}"
  fi

  # ロックファイルの削除
  if [[ -n "${LOCK_FILE:-}" ]] && [[ -f "${LOCK_FILE}" ]]; then
    log_info "ロックファイルを削除: ${LOCK_FILE}"
    rm -f "${LOCK_FILE}"
  fi

  if [[ ${exit_code} -eq 0 ]]; then
    log_info "スクリプトが正常に終了しました"
  else
    log_error "スクリプトがエラーで終了しました(終了コード: ${exit_code})"
  fi

  exit "${exit_code}"
}

シグナルハンドリングの設定:

bash# EXIT、INT、TERM シグナルでクリーンアップを実行
trap cleanup EXIT INT TERM

# ERR シグナルでエラーハンドリング
trap 'log_error "エラーが発生しました(行番号: ${LINENO}, コマンド: ${BASH_COMMAND})"' ERR

ロックファイルによる多重実行防止:

bash# ロックファイルのパス
LOCK_FILE="/var/run/backup_script.lock"

# 多重実行チェック
if [[ -f "${LOCK_FILE}" ]]; then
  log_error "スクリプトが既に実行中です(ロックファイル: ${LOCK_FILE})"
  exit 1
fi

# ロックファイルを作成
echo $$ > "${LOCK_FILE}"
log_info "ロックファイルを作成: ${LOCK_FILE}"

具体例

ここまでの 7 原則をすべて適用した、実用的なバックアップスクリプトの完全な例をご紹介します。このスクリプトは、ファイルのバックアップと古いバックアップの削除を行います。

以下の図は、スクリプト全体の処理フローを示しています。

mermaidflowchart TD
  start["スクリプト開始"]
  init["初期設定<br/>set -euo pipefail"]
  load_config["設定ファイル読み込み"]
  check_deps["依存関係チェック"]
  check_lock["ロックファイル確認"]

  setup_trap["シグナルハンドリング設定"]
  create_temp["一時ディレクトリ作成"]

  backup["バックアップ実行"]
  compress["ファイル圧縮"]
  cleanup_old["古いバックアップ削除"]

  notify["通知送信"]
  cleanup_func["クリーンアップ処理"]
  finish["終了"]

  error["エラー処理"]

  start --> init
  init --> load_config
  load_config --> check_deps
  check_deps --> check_lock
  check_lock -->|OK| setup_trap
  check_lock -->|NG| error

  setup_trap --> create_temp
  create_temp --> backup
  backup --> compress
  compress --> cleanup_old
  cleanup_old --> notify
  notify --> cleanup_func
  cleanup_func --> finish

  backup -.->|エラー| error
  compress -.->|エラー| error
  error --> cleanup_func

図で理解できる要点:

  • 7 原則が処理の各段階に組み込まれている
  • エラー発生時も必ずクリーンアップ処理が実行される
  • 依存関係チェックとロック確認により、実行前に問題を検出

完全なバックアップスクリプト例

スクリプトのヘッダーと初期設定:

bash#!/bin/bash
#
# ファイルバックアップスクリプト
# 説明: 指定されたディレクトリをバックアップし、古いバックアップを削除
# 使用方法: ./backup.sh [設定ファイルパス]
#

# 原則1: エラー時即座停止
set -euo pipefail

# スクリプトのディレクトリを取得
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

設定の読み込み(原則 4):

bash# 原則4: 設定の外部化
CONFIG_FILE="${1:-${SCRIPT_DIR}/config.env}"

if [[ -f "${CONFIG_FILE}" ]]; then
  # shellcheck source=/dev/null
  source "${CONFIG_FILE}"
else
  echo "Warning: 設定ファイルが見つかりません: ${CONFIG_FILE}" >&2
fi

# デフォルト値の設定
BACKUP_SOURCE_DIR="${BACKUP_SOURCE_DIR:-/var/www/html}"
BACKUP_DEST_DIR="${BACKUP_DEST_DIR:-/backup/daily}"
RETENTION_DAYS="${RETENTION_DAYS:-7}"
LOG_FILE="${LOG_FILE:-/var/log/backup_$(date +%Y%m%d).log}"
LOCK_FILE="${LOCK_FILE:-/var/run/backup.lock}"

ログ関数の定義(原則 6):

bash# 原則6: ログ出力とトレース機能
LOG_LEVEL_INFO=1
LOG_LEVEL_WARN=2
LOG_LEVEL_ERROR=3
CURRENT_LOG_LEVEL="${LOG_LEVEL:-${LOG_LEVEL_INFO}}"

log_message() {
  local level="$1"
  local level_num="$2"
  local message="$3"
  local timestamp
  timestamp=$(date '+%Y-%m-%d %H:%M:%S')

  if [[ ${level_num} -ge ${CURRENT_LOG_LEVEL} ]]; then
    echo "[${level}] ${timestamp} - ${message}"
  fi
}

log_info() {
  log_message "INFO" "${LOG_LEVEL_INFO}" "$1"
}

log_warn() {
  log_message "WARN" "${LOG_LEVEL_WARN}" "$1" >&2
}

log_error() {
  log_message "ERROR" "${LOG_LEVEL_ERROR}" "$1" >&2
}

依存関係チェック(原則 5):

bash# 原則5: 依存関係の事前チェック
check_dependencies() {
  local required_commands=("tar" "gzip" "date" "find" "df")
  local missing_commands=()

  for cmd in "${required_commands[@]}"; do
    if ! command -v "${cmd}" &> /dev/null; then
      missing_commands+=("${cmd}")
    fi
  done

  if [[ ${#missing_commands[@]} -gt 0 ]]; then
    log_error "以下のコマンドが見つかりません:"
    printf '  - %s\n' "${missing_commands[@]}" >&2
    return 1
  fi

  # ディレクトリの存在確認
  if [[ ! -d "${BACKUP_SOURCE_DIR}" ]]; then
    log_error "バックアップ元ディレクトリが存在しません: ${BACKUP_SOURCE_DIR}"
    return 1
  fi

  # ディスク容量の確認(100MB以上必要)
  local available_mb
  available_mb=$(df -m "${BACKUP_DEST_DIR%/*}" 2>/dev/null | awk 'NR==2 {print $4}')

  if [[ ${available_mb} -lt 100 ]]; then
    log_error "ディスク容量が不足しています: ${available_mb}MB"
    return 1
  fi

  log_info "依存関係チェック完了"
  return 0
}

クリーンアップとシグナルハンドリング(原則 7):

bash# 原則7: クリーンアップとシグナルハンドリング
cleanup() {
  local exit_code=$?

  log_info "クリーンアップ処理を開始します"

  # 一時ディレクトリの削除
  if [[ -n "${TEMP_DIR:-}" ]] && [[ -d "${TEMP_DIR}" ]]; then
    log_info "一時ディレクトリを削除: ${TEMP_DIR}"
    rm -rf "${TEMP_DIR}"
  fi

  # ロックファイルの削除
  if [[ -n "${LOCK_FILE:-}" ]] && [[ -f "${LOCK_FILE}" ]]; then
    log_info "ロックファイルを削除: ${LOCK_FILE}"
    rm -f "${LOCK_FILE}"
  fi

  if [[ ${exit_code} -eq 0 ]]; then
    log_info "スクリプトが正常に終了しました"
  else
    log_error "スクリプトがエラーで終了しました(終了コード: ${exit_code})"
  fi

  exit "${exit_code}"
}

# シグナルハンドリングの設定
trap cleanup EXIT INT TERM
trap 'log_error "エラーが発生しました(行番号: ${LINENO})"' ERR

バックアップ処理関数(原則 3):

bash# 原則3: 関数化による処理の分割
create_backup() {
  local source_dir="$1"
  local dest_dir="$2"
  local backup_name="backup_$(date +%Y%m%d_%H%M%S).tar.gz"
  local backup_path="${dest_dir}/${backup_name}"

  log_info "バックアップを作成: ${source_dir} -> ${backup_path}"

  # バックアップディレクトリの作成
  mkdir -p "${dest_dir}"

  # 圧縮してバックアップ
  if tar -czf "${backup_path}" -C "$(dirname "${source_dir}")" "$(basename "${source_dir}")"; then
    log_info "バックアップ作成完了: ${backup_path}"

    # バックアップファイルのサイズを表示
    local file_size
    file_size=$(du -h "${backup_path}" | cut -f1)
    log_info "バックアップサイズ: ${file_size}"

    return 0
  else
    log_error "バックアップ作成に失敗しました"
    return 1
  fi
}

古いバックアップの削除:

bash# 古いバックアップファイルを削除
cleanup_old_backups() {
  local backup_dir="$1"
  local retention_days="$2"

  log_info "古いバックアップを削除(保持期間: ${retention_days}日)"

  # retention_days より古いファイルを検索して削除
  local deleted_count=0
  while IFS= read -r -d '' file; do
    log_info "削除: ${file}"
    rm -f "${file}"
    ((deleted_count++))
  done < <(find "${backup_dir}" -name "backup_*.tar.gz" -type f -mtime +"${retention_days}" -print0)

  if [[ ${deleted_count} -gt 0 ]]; then
    log_info "削除したバックアップ数: ${deleted_count}"
  else
    log_info "削除対象のバックアップはありませんでした"
  fi

  return 0
}

メイン処理:

bash# メイン処理
main() {
  log_info "=== バックアップスクリプト開始 ==="
  log_info "設定ファイル: ${CONFIG_FILE}"

  # 多重実行チェック
  if [[ -f "${LOCK_FILE}" ]]; then
    log_error "スクリプトが既に実行中です: ${LOCK_FILE}"
    exit 1
  fi

  # ロックファイル作成
  echo $$ > "${LOCK_FILE}"

  # 依存関係チェック
  check_dependencies

  # 一時ディレクトリ作成
  TEMP_DIR=$(mktemp -d)
  log_info "一時ディレクトリ: ${TEMP_DIR}"

  # バックアップ実行
  create_backup "${BACKUP_SOURCE_DIR}" "${BACKUP_DEST_DIR}"

  # 古いバックアップの削除
  cleanup_old_backups "${BACKUP_DEST_DIR}" "${RETENTION_DAYS}"

  log_info "=== バックアップスクリプト完了 ==="
}

# スクリプト実行
main "$@"

実行例とログ出力

スクリプトを実行すると、以下のようなログが出力されます。

bash# スクリプト実行
$ ./backup.sh

# 出力例
[INFO] 2025-11-10 10:00:00 - === バックアップスクリプト開始 ===
[INFO] 2025-11-10 10:00:00 - 設定ファイル: ./config.env
[INFO] 2025-11-10 10:00:00 - 依存関係チェック完了
[INFO] 2025-11-10 10:00:01 - 一時ディレクトリ: /tmp/tmp.xYz123
[INFO] 2025-11-10 10:00:01 - バックアップを作成: /var/www/html -> /backup/daily/backup_20251110_100001.tar.gz
[INFO] 2025-11-10 10:00:15 - バックアップ作成完了: /backup/daily/backup_20251110_100001.tar.gz
[INFO] 2025-11-10 10:00:15 - バックアップサイズ: 256M
[INFO] 2025-11-10 10:00:15 - 古いバックアップを削除(保持期間: 7日)
[INFO] 2025-11-10 10:00:15 - 削除: /backup/daily/backup_20251103_100001.tar.gz
[INFO] 2025-11-10 10:00:15 - 削除したバックアップ数: 1
[INFO] 2025-11-10 10:00:15 - クリーンアップ処理を開始します
[INFO] 2025-11-10 10:00:15 - 一時ディレクトリを削除: /tmp/tmp.xYz123
[INFO] 2025-11-10 10:00:15 - ロックファイルを削除: /var/run/backup.lock
[INFO] 2025-11-10 10:00:15 - スクリプトが正常に終了しました
[INFO] 2025-11-10 10:00:15 - === バックアップスクリプト完了 ===

設定ファイルの例

bash# config.env - バックアップスクリプトの設定

# バックアップ元ディレクトリ
BACKUP_SOURCE_DIR="/var/www/html"

# バックアップ先ディレクトリ
BACKUP_DEST_DIR="/backup/daily"

# バックアップの保持期間(日数)
RETENTION_DAYS=7

# ログファイルのパス
LOG_FILE="/var/log/backup_$(date +%Y%m%d).log"

# ロックファイルのパス
LOCK_FILE="/var/run/backup.lock"

# ログレベル(1=INFO, 2=WARN, 3=ERROR)
LOG_LEVEL=1

エラーハンドリングの例

エラーが発生した場合の動作を確認してみましょう。

bash# ディレクトリが存在しない場合
$ BACKUP_SOURCE_DIR=/nonexistent ./backup.sh

# 出力例
[INFO] 2025-11-10 10:05:00 - === バックアップスクリプト開始 ===
[ERROR] 2025-11-10 10:05:00 - バックアップ元ディレクトリが存在しません: /nonexistent
[ERROR] 2025-11-10 10:05:00 - エラーが発生しました(行番号: 145)
[INFO] 2025-11-10 10:05:00 - クリーンアップ処理を開始します
[INFO] 2025-11-10 10:05:00 - ロックファイルを削除: /var/run/backup.lock
[ERROR] 2025-11-10 10:05:00 - スクリプトがエラーで終了しました(終了コード: 1)

このように、エラーが発生しても適切にハンドリングされ、クリーンアップ処理が確実に実行されます。

7 原則の適用箇所まとめ

以下の表は、完全なスクリプト例における各原則の適用箇所を示しています。

#原則適用箇所効果
1エラー時即座停止set -euo pipefailコマンドエラー時に即座停止
2変数の明確な定義と検証設定値の定義とデフォルト値設定未定義変数エラーの防止
3関数化による処理の分割create_backup(), cleanup_old_backups()コードの再利用性と可読性向上
4設定の外部化config.env の読み込み環境ごとの設定変更が容易
5依存関係の事前チェックcheck_dependencies()実行前の環境検証
6ログ出力とトレース機能log_info(), log_error()トラブルシューティングの効率化
7クリーンアップとシグナルハンドリングcleanup(), trapリソースの確実な解放

まとめ

本記事では、Shell Script の品質を向上させる 7 つの設計原則をご紹介しました。これらの原則を適用することで、可読性・再利用性・堅牢性の高いスクリプトを作成できます。

7 つの原則のおさらい:

  1. エラー時即座停止 - set -euo pipefail でエラーの連鎖を防ぐ
  2. 変数の明確な定義と検証 - 意図が伝わる命名と事前検証で予期しないエラーを防ぐ
  3. 関数化による処理の分割 - 単一責任の原則で再利用性と可読性を向上
  4. 設定の外部化 - ハードコーディングを避け、環境ごとの変更を容易に
  5. 依存関係の事前チェック - 実行前に環境を検証し、実行時エラーを防止
  6. ログ出力とトレース機能 - 適切なログでトラブルシューティングを効率化
  7. クリーンアップとシグナルハンドリング - リソースを確実に解放し、安定した運用を実現

これらの原則は、一度に全て適用する必要はありません。既存のスクリプトに段階的に導入していくことで、着実に品質を向上させることができます。

まずは「エラー時即座停止」と「ログ出力」から始めて、徐々に他の原則も取り入れていくことをお勧めします。

最初は少し手間に感じるかもしれませんが、保守性の高いスクリプトは長期的に大きな価値を生み出します。ぜひ、日々のスクリプト作成にこれらの原則を活用してみてください。

関連リンク