「rootで動かすつもりだったのに一般ユーザーで実行してしまい、権限エラーが連発した」
シェルスクリプトに慣れてくると、こういう凡ミスが意外と痛い失敗につながります。対処は単純で、スクリプトの冒頭で「必要なコマンドがあるか」「適切な権限で動いているか」「ディスクに空きがあるか」をまとめて確認してから本処理に進む——いわゆるpreflight(事前チェック)設計です。
この記事では、RHEL 9.4 / Rocky Linux 9.3で動作確認した事前チェック関数の実装パターンを解説します。
trap によるエラー終了処理との組み合わせ、よくあるつまずきポイントも合わせて紹介します。この記事のポイント
・command -v でコマンド存在を確認し、なければ即時エラー終了できる
・id コマンドと [ -w ] で root権限・書き込み権限をチェックできる
・df -k でディスク空き容量を数値比較し、不足時に処理を止められる
・全チェックを preflight 関数に集約すると本処理との責務が明確に分かれる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
事前チェックを省略すると何が起きるか
シェルスクリプトは「上から順に実行する」という性質上、途中で失敗したときに「ここまでは成功した」状態が残ります。バックアップスクリプトなら古いバックアップが消えた後で新しいバックアップが取れない、デプロイスクリプトなら半分だけ更新されたファイルが本番に残る——どちらも手動で復元するのが厄介な状態です。よくある失敗パターンを3つ挙げます。
・コマンド未インストール: mysqldump や rsync が入っていない環境でスクリプトを流し、バックアップが取れていなかった
・権限の確認漏れ: 一般ユーザーで実行したために /etc 配下の設定ファイルが書けず、中途半端な状態のまま終了した
・ディスクフル: バックアップ先の空きが足りず、途中でディスクがいっぱいになって圧縮ファイルが壊れた
これらは「本処理の前に1行チェックを入れる」だけで防げます。それをまとめて担うのがpreflight関数です。
3つの事前チェック設計パターン
1. command -v でコマンドの存在を確認する
コマンドの存在確認にはcommand -v を使うのが標準的です。which はPATHの通り方によって結果が変わることがあり、type はシェル組み込みコマンドも返すため、外部コマンドの確認には command -v が最も信頼できます。# コマンド存在確認の基本パターン require_command() { local cmd="$1" if ! command -v "$cmd" > /dev/null 2>&1; then echo "[ERROR] 必要なコマンドが見つかりません: ${cmd}" >&2 echo "[ERROR] インストールしてから再実行してください" >&2 exit 1 fi } # 使い方 require_command rsync require_command mysqldump require_command gzip
$ bash backup.sh [ERROR] 必要なコマンドが見つかりません: mysqldump [ERROR] インストールしてから再実行してください
command -v はコマンドのフルパスを標準出力に返します。> /dev/null 2>&1 でその出力を捨てて終了コードだけを判定に使います。! command -v "$cmd" の形で「見つからなかったら」という分岐を書くのがシンプルです。2. id と [ -w ] で実行権限を確認する
「rootで動かす前提のスクリプト」と「特定ディレクトリへの書き込みが必要なスクリプト」では、確認すべき権限が異なります。それぞれの関数を分けておくと使いまわしやすくなります。# root権限チェック(id -u が 0 なら root) require_root() { if [ "$(id -u)" -ne 0 ]; then echo "[ERROR] このスクリプトはroot権限で実行してください" >&2 echo "[ERROR] sudo bash $(basename "$0") で再実行してください" >&2 exit 1 fi } # 書き込み権限チェック require_writable() { local dir="$1" if [ ! -d "$dir" ]; then echo "[ERROR] ディレクトリが存在しません: ${dir}" >&2 exit 1 fi if [ ! -w "$dir" ]; then echo "[ERROR] ディレクトリへの書き込み権限がありません: ${dir}" >&2 exit 1 fi } # 使い方 require_root require_writable /var/backup
[ec2-user@sv01 ~]$ bash deploy.sh [ERROR] このスクリプトはroot権限で実行してください [ERROR] sudo bash deploy.sh で再実行してください
id -u はUIDを数値で返します。rootのUIDは0なので -ne 0(0でなければ)でroot以外を弾けます。[ ! -w "$dir" ] は書き込み権限がない場合にtrueになるテスト式です。3. df で空きディスク容量を確認する
バックアップや大きなファイルを生成するスクリプトでは、ディスクの空き容量を事前に確認しておくと安全です。# ディスク空き容量チェック(単位: MB) require_disk_space() { local dir="$1" local required_mb="$2" # df -Pk: POSIXフォーマット・1024バイトブロック単位で出力 local available_kb available_kb=$(df -Pk "$dir" | awk 'NR==2 {print $4}') local available_mb=$(( available_kb / 1024 )) if [ "$available_mb" -lt "$required_mb" ]; then echo "[ERROR] ディスク容量が不足しています: ${dir}" >&2 echo "[ERROR] 必要: ${required_mb}MB / 利用可能: ${available_mb}MB" >&2 exit 1 fi } # 使い方(/var/backup に 2000MB 以上の空きが必要) require_disk_space /var/backup 2000
df -Pk の -P はPOSIX出力形式(ヘッダ行1行+データ行1行の2行出力を保証)、-k は1024バイト単位です。awk 'NR==2 {print $4}' で2行目の第4フィールド(Available列)を取り出します。この書き方はLinux・macOS・AIXいずれでも動作します。容量不足時の出力例:
[ERROR] ディスク容量が不足しています: /var/backup [ERROR] 必要: 2000MB / 利用可能: 342MB
preflight 関数にまとめる設計パターン
3つのチェック関数ができたら、スクリプト冒頭の1か所から呼び出すpreflight関数にまとめます。本処理を書く前にすべての前提条件を満たしているかを確認するのが目的です。#!/bin/bash # backup-db.sh — MySQLバックアップスクリプト set -euo pipefail BACKUP_DIR="/var/backup/mysql" REQUIRE_MB=3000 # --- チェック関数 --- require_command() { local cmd="$1" if ! command -v "$cmd" > /dev/null 2>&1; then echo "[ERROR] コマンドが見つかりません: ${cmd}" >&2 exit 1 fi } require_root() { if [ "$(id -u)" -ne 0 ]; then echo "[ERROR] root権限が必要です" >&2 exit 1 fi } require_writable() { local dir="$1" if [ ! -d "$dir" ] || [ ! -w "$dir" ]; then echo "[ERROR] 書き込み不可: ${dir}" >&2 exit 1 fi } require_disk_space() { local dir="$1" required_mb="$2" local avail_mb avail_mb=$(( $(df -Pk "$dir" | awk 'NR==2 {print $4}') / 1024 )) if [ "$avail_mb" -lt "$required_mb" ]; then echo "[ERROR] ディスク不足: 必要${required_mb}MB / 利用可能${avail_mb}MB" >&2 exit 1 fi } # --- preflight: ここで全条件を一括検証 --- preflight() { echo "[INFO] 事前チェックを開始します..." require_root require_command mysqldump require_command gzip require_writable "$BACKUP_DIR" require_disk_space "$BACKUP_DIR" "$REQUIRE_MB" echo "[INFO] 事前チェック完了。本処理を開始します" } # --- 本処理 --- main() { preflight local timestamp timestamp=$(date '+%Y%m%d_%H%M%S') local outfile="${BACKUP_DIR}/db_${timestamp}.sql.gz" mysqldump --all-databases | gzip > "$outfile" echo "[INFO] バックアップ完了: ${outfile}" } main "$@"
[root@sv01 ~]# bash backup-db.sh [INFO] 事前チェックを開始します... [INFO] 事前チェック完了。本処理を開始します [INFO] バックアップ完了: /var/backup/mysql/db_20260924_034501.sql.gz
シェルスクリプトによる安全なバックアップ設計のより詳しい解説は、シェルスクリプト実践講座 でもまとめています。
trap と組み合わせた安全な終了処理
preflight設計と相性がよいのが、trap によるエラー終了時の後片付けです。事前チェックをパスしても本処理の途中で失敗することはあります。一時ファイルを使う処理では、trap で確実に後片付けをしておきましょう。#!/bin/bash set -euo pipefail BACKUP_DIR="/var/backup/mysql" TMPFILE="" cleanup() { if [ -n "$TMPFILE" ] && [ -f "$TMPFILE" ]; then rm -f "$TMPFILE" echo "[INFO] 一時ファイルを削除しました: ${TMPFILE}" >&2 fi } # EXIT シグナルで cleanup を必ず呼ぶ(正常終了・異常終了どちらでも) trap cleanup EXIT preflight() { require_root require_command mysqldump require_command gzip require_writable "$BACKUP_DIR" require_disk_space "$BACKUP_DIR" 3000 } main() { preflight TMPFILE=$(mktemp /tmp/db_dump.XXXXXX) mysqldump --all-databases > "$TMPFILE" gzip -c "$TMPFILE" > "${BACKUP_DIR}/db_$(date '+%Y%m%d_%H%M%S').sql.gz" echo "[INFO] バックアップ完了" } main "$@"
trap cleanup EXIT を冒頭に置いておくと、スクリプトが正常終了・エラー終了・Ctrl+C のいずれで終わっても必ず cleanup が実行されます。TMPFILE変数をグローバルに置いておき、mktemp で値を代入した後に cleanup が参照できるようにする点がポイントです。トラブルシュート
「事前チェックをパスしたのに本処理で失敗する」ケース
・rootチェックはパスしたが対象ファイルの所有者が別ユーザー: rootでもSELinux/ACLが有効な環境では書き込みを拒否されることがあります。require_writable でテストファイルを作成してすぐ消す方法(touch "$dir/.preflight_test" && rm -f "$dir/.preflight_test")を使うとより確実です。・コマンドはあるが古いバージョンで動かない:
command -v はバージョンを確認しません。バージョン依存の機能を使う場合は mysqldump --version | awk '{print $3}' 等で確認します。・df -Pk が古いNFSマウントで詰まる: NFSがオフラインの時に
df がタイムアウトまでブロックします。NFSマウント先を確認先にする場合は timeout 5 df -Pk "$dir" としてタイムアウトを設けましょう。「set -euo pipefail と require_command が競合する」ケース
set -e(エラーで即終了)が有効な環境でも、if ! command -v "$cmd" > /dev/null 2>&1 の形にしておけばサブシェルの終了コードを if が受け取るためスクリプトが止まりません。command -v "$cmd" > /dev/null 2>&1 || exit 1 という書き方でも同様です。まとめ
シェルスクリプトのpreflight設計の要点をまとめます。| 確認したいこと | 使う手法 |
|---|---|
| コマンドの存在確認 | command -v コマンド名 > /dev/null 2>&1 |
| root権限の確認 | [ "$(id -u)" -ne 0 ] |
| ディレクトリ書き込み権限の確認 | [ ! -w ディレクトリ ] |
| ディスク空き容量の確認 | df -Pk ディレクトリ | awk 'NR==2 {print $4}' |
| チェックをまとめる場所 | preflight関数に集約してmain冒頭で呼ぶ |
| 後片付けの保証 | trap cleanup EXIT と組み合わせる |
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:シェルスクリプトでAPIキーとパスワードを安全に扱う設計|コマンド履歴・ログ・プロセスリストにシークレットを残さない方法
- この記事の属するカテゴリ:シェルスクリプトへ戻る

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