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 つの原則のおさらい:
- エラー時即座停止 -
set -euo pipefailでエラーの連鎖を防ぐ - 変数の明確な定義と検証 - 意図が伝わる命名と事前検証で予期しないエラーを防ぐ
- 関数化による処理の分割 - 単一責任の原則で再利用性と可読性を向上
- 設定の外部化 - ハードコーディングを避け、環境ごとの変更を容易に
- 依存関係の事前チェック - 実行前に環境を検証し、実行時エラーを防止
- ログ出力とトレース機能 - 適切なログでトラブルシューティングを効率化
- クリーンアップとシグナルハンドリング - リソースを確実に解放し、安定した運用を実現
これらの原則は、一度に全て適用する必要はありません。既存のスクリプトに段階的に導入していくことで、着実に品質を向上させることができます。
まずは「エラー時即座停止」と「ログ出力」から始めて、徐々に他の原則も取り入れていくことをお勧めします。
最初は少し手間に感じるかもしれませんが、保守性の高いスクリプトは長期的に大きな価値を生み出します。ぜひ、日々のスクリプト作成にこれらの原則を活用してみてください。
関連リンク
- Bash Reference Manual - Bash の公式リファレンスマニュアル
- ShellCheck - Shell Script の静的解析ツール
- Google Shell Style Guide - Google の Shell Script スタイルガイド
- Advanced Bash-Scripting Guide - Bash スクリプトの詳細ガイド
- Bash Pitfalls - よくある Bash の落とし穴と対策
articleShell Script 設計 7 原則:可読性・再利用・堅牢性を高める実践ガイド
articleShell Script チートシート:変数・配列・連想配列・算術展開を一枚で把握
articleShell Script 開発環境の作り方:Homebrew・エディタ・シェル選定・PATH 設計
articleShell Script とは?初心者が最短で理解する基本構文・実行モデル・活用領域
article【まとめ】よく使用するものだけを抜粋したUnixコマンドとVimコマンドの一覧まとめ
articleStorybook 代替ツール比較:Ladle/Histoire/Pattern Lab と何が違う?
articleAnsible Inventory 初期構築:静的/動的の基本とベストプラクティス
articleSolidJS で無限ループが止まらない!createEffect/onCleanup の正しい書き方
article伝搬方式を比較:Zustand の selector/derived-middleware/外部 reselect の使い分け
articleShell Script 設計 7 原則:可読性・再利用・堅牢性を高める実践ガイド
articleRuby 基本文法から学ぶ 90 分速習:変数・制御構文・ブロックを一気に理解
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来