「BACKUP_DIRという変数をうっかりセットし忘れたら、スクリプトが意図しない場所にファイルを書き込もうとして失敗した」
こういった「走り始めてから失敗する」問題は、スクリプトの冒頭に起動前チェック(プリフライトチェック)を組み込むだけでほとんど防げます。
この記事では、依存コマンドの存在確認・必須環境変数の検証・ディレクトリの確認を関数化し、メイン処理の前に一括実行する設計パターンを解説します。バックアップスクリプト・デプロイスクリプトへの適用例も含め、RHEL 9.4 / Ubuntu 24.04 LTSで動作確認済みです。
この記事のポイント
・command -v でコマンドの存在を検証し、未インストールを即座に検知する
・必須環境変数は require_env 関数で存在チェックして早期終了する
・require_cmd / require_env / require_dir を preflight_check() にまとめる
・チェックが1つでも失敗したらメイン処理は実行しない設計が安全
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜスクリプトは「走り始めてから」失敗するのか
多くのシェルスクリプトは $1 $2 のような位置引数チェックすら省略しており、「コマンドが存在しない」「保存先ディレクトリがない」「環境変数が未設定」という問題を、実際に処理が走ってから検知します。cronで夜間に動くバックアップスクリプトや、手順書に沿って実行するデプロイスクリプトで起きると、朝になって気づいた時にはデータが欠損していたり、本番環境の変更が中途半端な状態になっていたりします。
よくある失敗パターンは次の3つです。
・依存コマンドが存在しない: cronの実行環境のPATHは
/usr/bin:/bin 程度しかなく、/usr/local/bin/aws や追加インストールした rsync が見つからない・必須変数が未設定:
BACKUP_DIR="" または未定義の状態でスクリプトが進み、意図しないパスに書き込む・保存先ディレクトリが存在しない:
mkdir -p の呼び出しを忘れており、cp や rsync がファイル書き込み時にエラーになるいずれも「実行前に確認できていれば防げた」問題です。この確認をまとめて行う仕組みが起動前チェックです。
基本の起動前チェックパターン
1. 依存コマンドの存在確認(command -v)
コマンドの存在確認にはcommand -v を使います。which はシェル組み込みとして存在しない環境もあり、エイリアスに反応することもあるため、移植性と正確性を兼ね備えた command -v が実務では定番です。#!/bin/bash set -euo pipefail # rsyncが存在しなければ即エラー終了 if ! command -v rsync > /dev/null 2>&1; then echo "[ERROR] rsync がインストールされていません。yum install rsync で導入してください。" >&2 exit 1 fi echo "[INFO] rsync: $(command -v rsync)" # 出力例: [INFO] rsync: /usr/bin/rsync
command -v コマンド名 の戻り値が 0 であればコマンドが存在します。> /dev/null 2>&1 で余計な出力を捨て、if 文の判定だけを使います。コマンドのフルパスも取得できるため、
echo "[INFO] rsync: $(command -v rsync)" のようにログに記録しておくと、どのバージョンが使われているかをあとから確認できます。2. 必須環境変数の検証
変数が未設定または空文字の場合を検知するには、${VAR:?メッセージ} 構文か専用関数を使います。#!/bin/bash set -euo pipefail # 未設定・空文字の場合はエラーメッセージを出して終了 : "${BACKUP_DIR:?BACKUP_DIR が未設定です。export BACKUP_DIR=/mnt/backup のように設定してください。}" : "${REMOTE_USER:?REMOTE_USER が未設定です。}" : "${TARGET_HOST:?TARGET_HOST が未設定です。}"
:(コロン)は「何もしない」コマンドです。${VAR:?メッセージ} は VAR が未設定または空文字のときに、メッセージを標準エラーに出力してスクリプトを終了させます(終了コード1)。シンプルですが、エラーメッセージが画一的になる点が難点です。複数の変数に対して統一されたフォーマットでチェックしたい場合は、後述する
require_env 関数の設計が適しています。3. ディレクトリとファイルの確認
#!/bin/bash BACKUP_DIR="${BACKUP_DIR:-/mnt/backup}" LOG_DIR="/var/log/myapp" # ディレクトリの存在確認 if [ ! -d "${LOG_DIR}" ]; then echo "[ERROR] ログディレクトリ ${LOG_DIR} が存在しません。" >&2 exit 1 fi # 書き込み権限の確認 if [ ! -w "${BACKUP_DIR}" ]; then echo "[ERROR] バックアップ先 ${BACKUP_DIR} に書き込み権限がありません。" >&2 exit 1 fi
-d でディレクトリの存在、-w で書き込み権限を確認します。権限不足の場合は mkdir -p でも解決しないため、「ディレクトリがない」と「権限がない」を分けて確認し、エラーメッセージも使い分けることが重要です。プリフライト関数にまとめる設計
個別チェックをスクリプト本体にバラバラに書くと、チェック項目が増えるにつれて管理が難しくなります。チェック処理を専用関数(require_cmd・require_env・require_dir)にまとめ、preflight_check() から一括呼び出しするパターンが現場で定番です。4. require_cmd 関数の実装
# コマンドの存在チェック関数 require_cmd() { local cmd="${1}" if ! command -v "${cmd}" > /dev/null 2>&1; then echo "[FATAL] コマンド '${cmd}' が見つかりません。インストールを確認してください。" >&2 exit 1 fi echo "[INFO] ${cmd}: $(command -v "${cmd}")" }
require_cmd rsync、require_cmd aws のように1行ずつ書くだけで依存コマンドを確認できます。[INFO] 行でコマンドのパスをログに記録するため、「どのバイナリが使われているか」の追跡にも使えます。5. require_env と require_dir の実装
# 環境変数の存在チェック関数 require_env() { local var_name="${1}" local val="${!var_name}" # bash の間接参照で変数値を取得 if [ -z "${val}" ]; then echo "[FATAL] 環境変数 '${var_name}' が未設定または空です。" >&2 exit 1 fi } # ディレクトリの存在・権限チェック関数 # 第2引数: "r"=読み込み確認(省略可)、"w"=書き込み確認 require_dir() { local dir="${1}" local mode="${2:-r}" if [ ! -d "${dir}" ]; then echo "[FATAL] ディレクトリ '${dir}' が存在しません。" >&2 exit 1 fi if [ "${mode}" = "w" ] && [ ! -w "${dir}" ]; then echo "[FATAL] ディレクトリ '${dir}' に書き込み権限がありません。" >&2 exit 1 fi }
${!var_name} は bash の間接参照です。変数名の文字列(例: "BACKUP_DIR")を受け取り、その変数の実際の値を取り出します。require_env BACKUP_DIR と呼ぶと BACKUP_DIR の値が空かどうかを検証できます。6. preflight_check() にまとめてmain()の直前に呼ぶ
#!/bin/bash set -euo pipefail # ---- 設定値 ---- BACKUP_DIR="${BACKUP_DIR:-}" REMOTE_USER="${REMOTE_USER:-}" TARGET_HOST="${TARGET_HOST:-}" LOG_DIR="/var/log/myapp" # ---- 関数定義(require_cmd / require_env / require_dir は前節参照)---- preflight_check() { echo "[INFO] 起動前チェックを開始します..." require_cmd rsync require_cmd ssh require_env BACKUP_DIR require_env REMOTE_USER require_env TARGET_HOST require_dir "${LOG_DIR}" w require_dir "${BACKUP_DIR}" w echo "[INFO] 起動前チェック完了。メイン処理を開始します。" } main() { echo "[INFO] バックアップを開始します..." # ここに本体処理を書く } # ---- エントリーポイント ---- preflight_check main
preflight_check 内の require_* 関数がどれか1つでも失敗すれば exit 1 でスクリプト全体が終了し、main() は実行されません。チェック項目の追加・削除も preflight_check() 内を1行変えるだけで済みます。実務での活用例(バックアップスクリプトの実行ログ)
RHEL 9.4の実サーバーで上記パターンを組み込んだバックアップスクリプトを動かした実行結果です。# ケース1: BACKUP_DIRを設定せずに実行した場合 $ ./backup.sh [INFO] 起動前チェックを開始します... [INFO] rsync: /usr/bin/rsync [INFO] ssh: /usr/bin/ssh [FATAL] 環境変数 'BACKUP_DIR' が未設定または空です。 # ケース2: バックアップ先ディレクトリへの権限がない場合 $ BACKUP_DIR=/mnt/backup REMOTE_USER=srvadmin TARGET_HOST=192.168.1.50 ./backup.sh [INFO] 起動前チェックを開始します... [INFO] rsync: /usr/bin/rsync [INFO] ssh: /usr/bin/ssh [FATAL] ディレクトリ '/mnt/backup' に書き込み権限がありません。 # ケース3: 全チェック通過・正常実行 $ export BACKUP_DIR=/mnt/backup REMOTE_USER=srvadmin TARGET_HOST=192.168.1.50 $ ./backup.sh [INFO] 起動前チェックを開始します... [INFO] rsync: /usr/bin/rsync [INFO] ssh: /usr/bin/ssh [INFO] 起動前チェック完了。メイン処理を開始します。 [INFO] バックアップを開始します... [INFO] 転送完了: /mnt/backup/20260801-213045/
起動前チェックを含むシェルスクリプトの設計パターンを体系的に学びたい方は、シェルスクリプト実践講座(Linux Master Pro)もご覧ください。現場で即使える設計の「型」を実機ハンズオンで身につけられます。
トラブルシュート・よくある落とし穴
command -v が成功するのにcronからは「コマンドが見つからない」になる
原因はcronの実行環境のPATHが対話シェルと異なることです。対話ログインでは/usr/local/bin もPATHに入っていますが、cronの環境では /usr/bin:/bin 程度しかありません。対処法はスクリプト先頭でPATHを明示することです。
#!/bin/bash # cronの環境でも確実にコマンドを探せるようにPATHを明示 PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin export PATH
#!/bin/bash)の直後にこの2行を書き、preflight_check の前に置くことでcronでも対話シェルと同じコマンドが参照されます。${!var_name} の間接参照がPOSIXシェルで動かない
${!var_name} はbash固有の構文です。#!/bin/sh のスクリプトや、dashが /bin/sh に紐付いているUbuntu環境では使えません。対処法は2つあります。
・bash に統一する(推奨): スクリプトのシェバンを
#!/bin/bash と明示し、間接参照をそのまま使う・${VAR:?} 構文で代替する: 変数名を直接書ける状況なら
: "${BACKUP_DIR:?未設定}" のほうがPOSIX互換で安全bashで動かせる環境では
#!/bin/bash を明示して間接参照を使う方がコードの意図が読みやすくなります。require_dir でディレクトリを作成しないのはなぜか
「存在しなければmkdir -p で作ればよい」と思いがちですが、起動前チェックの責務は「必要な前提条件が揃っているかを確認してエラーを早期に返すこと」です。ディレクトリを自動生成してしまうと、パスの設定ミスに気づかずに処理が続行されるリスクがあります。mkdir -p が必要な場合は main() の冒頭で明示的に実行するか、require_dir_or_create のような別関数を用意し、意図を明確に分けることをすすめます。本記事のまとめ
| やりたいこと | 実装パターン |
|---|---|
| 依存コマンドの存在を確認する | require_cmd コマンド名(command -v ベース) |
| 必須環境変数が設定済みか確認する | require_env 変数名(${!var_name} で間接参照) |
| ディレクトリの存在・権限を確認する | require_dir パス w(-d / -w フラグで検証) |
| チェックをまとめて一括実行する | preflight_check() 関数にまとめて main() の直前に呼ぶ |
| cronで「コマンドが見つからない」を防ぐ | スクリプト先頭で PATH を明示して export する |
| チェック失敗時に処理を止める | 各 require_* 関数内で exit 1 して early return する設計 |
シェルスクリプト講座を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:シェルスクリプトの並列処理設計|&とwaitでバックグラウンドジョブを管理する方法
- この記事の属するカテゴリ:シェルスクリプトへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます