「条件が5つを超えてから自分でも把握しきれなくなり、バグが出るたびに頭を抱えている」
こういった状況は、シェルスクリプトを書き始めて少し経った頃に必ずぶつかる壁です。
この記事では、bash の
case 文を使った分岐設計を解説します。if-elif を使うべき場面と case 文を使うべき場面の違いから、コマンド引数の解析・OS種別の切り分け・ファイル拡張子別処理という現場で頻出する3つのパターン、そしてクォートや * (ワイルドカード) に関するハマりやすい落とし穴まで、RHEL 9.4 / Ubuntu 24.04 LTS の実機で動作確認したコードで解説します。この記事のポイント
・case文は「1変数 vs 複数の固定値」の分岐でif-elifより圧倒的に読みやすい
・コマンド引数解析・OS種別判定・拡張子別処理の3パターンが頻出ユースケース
・*)をデフォルト節として必ず末尾に用意し、想定外入力をエラー終了させる設計が堅牢
・変数は必ず "$var" とクォートして空文字・スペース混入によるマッチ事故を防ぐ
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜcase文を使うのか|if-elif地獄からの脱出
まず if-elif による引数処理を見てください。引数 $1 によって処理を切り替える典型的なシェルスクリプトです。#!/bin/bash # if-elif による引数処理(読みにくい例) ACTION="$1" if [ "$ACTION" = "start" ]; then echo "Starting..." elif [ "$ACTION" = "stop" ]; then echo "Stopping..." elif [ "$ACTION" = "restart" ]; then echo "Restarting..." elif [ "$ACTION" = "status" ]; then echo "Checking status..." elif [ "$ACTION" = "reload" ]; then echo "Reloading..." else echo "Usage: $0 {start|stop|restart|status|reload}" exit 1 fi
case 文で書き直すと次のようになります。#!/bin/bash # case 文による引数処理(読みやすい例) ACTION="$1" case "$ACTION" in start) echo "Starting..." ;; stop) echo "Stopping..." ;; restart) echo "Restarting..." ;; status) echo "Checking status..." ;; reload) echo "Reloading..." ;; *) echo "Usage: $0 {start|stop|restart|status|reload}" exit 1 ;; esac
[ "$ACTION" = "start" ] とアクション名 "start" が離れて書かれますが、case文では start) の直後に処理が続くため、視線が上から下に素直に流れます。case文を使うべき判断基準:
・1つの変数・式に対して「値が何であるか」で分岐する → case文
・複数の変数を組み合わせた複雑な条件式(&&, ||)→ if文
・数値の大小比較(-gt, -lt)→ if文
case文の基本構文と書き方
1. 基本形:マッチ → 処理 → ;;
case文の基本構文は次のとおりです。case 式 in パターン1) 処理1 ;; パターン2) 処理2 ;; *) デフォルト処理 ;; esac
・
case の後に評価する式(変数・コマンド置換など)を書く・
in の後にパターンと処理ブロックを列挙する・各処理ブロックは
;; で終わる(終端記号。これを忘れると次のパターンの処理に流れる)・
esac(case を逆に綴ったもの)で全体を閉じる・
*) はどのパターンにもマッチしなかった場合のデフォルト節2. 複数パターンのORマッチ(|区切り)
| で区切ることで、1つの処理ブロックに複数のパターンを対応させられます。#!/bin/bash # 複数パターンのORマッチ ANSWER="$1" case "$ANSWER" in yes|Yes|YES|y|Y) echo "続行します" ;; no|No|NO|n|N) echo "中断します" exit 0 ;; *) echo "yes または no で入力してください" exit 1 ;; esac
3. グロブパターン(*・?・[...])の活用
case文のパターンにはシェルのグロブ(ワイルドカード)が使えます。#!/bin/bash # グロブパターンの例(ファイル名切り分け) FILENAME="$1" case "$FILENAME" in *.tar.gz|*.tgz) echo "gzip 圧縮 tar アーカイブです" ;; *.tar.bz2) echo "bzip2 圧縮 tar アーカイブです" ;; *.zip) echo "ZIP アーカイブです" ;; /var/*) echo "/var 以下のパスが指定されました" ;; [0-9]*) echo "数字で始まるファイル名です" ;; *) echo "認識できない形式です: $FILENAME" exit 1 ;; esac
*(任意の文字列)・?(任意の1文字)・[...](文字クラス)がそのまま使えます。正規表現ではないので、+ や {n} などの拡張正規表現は使えない点に注意してください。コマンド引数解析にcase文を使う設計パターン
1. $1 でアクションを切り分ける基本パターン
サービス管理スクリプトやメンテナンススクリプトでは、第1引数で「何をするか」を切り替える設計が定番です。次の例は Apache(httpd)を管理するスクリプトです。RHEL 9.4(hostname: web01.example.com)で動作確認した出力例を合わせて示します。#!/bin/bash # httpd 管理スクリプト(/usr/local/bin/httpd-ctl.sh) # 使用方法: ./httpd-ctl.sh {start|stop|restart|status} set -euo pipefail SERVICE="httpd" LOG_PREFIX="[$(date '+%Y-%m-%d %H:%M:%S')]" case "${1:-}" in start) echo "${LOG_PREFIX} INFO: ${SERVICE} を起動します" systemctl start "${SERVICE}" echo "${LOG_PREFIX} INFO: 起動完了" ;; stop) echo "${LOG_PREFIX} INFO: ${SERVICE} を停止します" systemctl stop "${SERVICE}" echo "${LOG_PREFIX} INFO: 停止完了" ;; restart) echo "${LOG_PREFIX} INFO: ${SERVICE} を再起動します" systemctl restart "${SERVICE}" echo "${LOG_PREFIX} INFO: 再起動完了" ;; status) systemctl status "${SERVICE}" --no-pager ;; "") echo "エラー: アクションが指定されていません" >&2 echo "使い方: $0 {start|stop|restart|status}" >&2 exit 1 ;; *) echo "エラー: 不明なアクション '${1}'" >&2 echo "使い方: $0 {start|stop|restart|status}" >&2 exit 1 ;; esac
# httpd の状態確認 $ sudo ./httpd-ctl.sh status * httpd.service - The Apache HTTP Server Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; preset: disabled) Active: active (running) since Mon 2026-08-04 10:23:38 JST; 3h 45min ago Main PID: 1355 (httpd) Status: "Total requests: 12482; Idle/Busy workers 100/0;..." # 不明なアクションを渡した場合 $ sudo ./httpd-ctl.sh deploy エラー: 不明なアクション 'deploy' 使い方: ./httpd-ctl.sh {start|stop|restart|status} # 引数なしで実行した場合 $ sudo ./httpd-ctl.sh エラー: アクションが指定されていません 使い方: ./httpd-ctl.sh {start|stop|restart|status}
・
${1:-} と書くことで引数なし時に空文字になり、set -u でエラーにならない・空文字(
"")パターンを専用節として設け、「引数ゼロ」と「不正な引数」を別メッセージで区別する・エラーメッセージは標準エラー(
>&2)に出力し、exit 1 で異常終了させる2. whileループとcase文を組み合わせたオプション解析
getopts を使わずに複数オプションを処理したい場合、while ループと case 文を組み合わせるパターンが有効です。--verbose のようなロングオプションに対応できる点が getopts との違いです。#!/bin/bash # while + case によるオプション解析 set -euo pipefail VERBOSE=0 DRY_RUN=0 TARGET="" while [[ $# -gt 0 ]]; do case "$1" in -v|--verbose) VERBOSE=1 shift ;; -n|--dry-run) DRY_RUN=1 shift ;; -t|--target) if [[ -z "${2:-}" ]]; then echo "エラー: --target には値が必要です" >&2 exit 1 fi TARGET="$2" shift 2 ;; --) shift break ;; -*) echo "エラー: 不明なオプション: $1" >&2 exit 1 ;; *) # オプション以外の引数(ファイル名など) break ;; esac done echo "VERBOSE=${VERBOSE}, DRY_RUN=${DRY_RUN}, TARGET=${TARGET}"
shift で処理済みの引数を取り除き、次のループで残りの引数を処理する設計です。-t|--target のように値を伴うオプションは shift 2 で引数を2つ消化します。OS種別・環境切り分けのcase文設計
1. uname -s でOS判定する
サーバー管理スクリプトを複数OS環境(RHEL系・Debian系・macOS など)で共用したい場合、uname -s の出力を case文で切り分けるパターンが定番です。#!/bin/bash # OS種別によるパッケージマネージャの自動選択 OS_TYPE="$(uname -s)" case "${OS_TYPE}" in Linux) # Linux の場合はディストリビューション判定を追加する if [ -f /etc/redhat-release ]; then PKG_MGR="dnf" elif [ -f /etc/debian_version ]; then PKG_MGR="apt-get" else PKG_MGR="unknown" fi ;; Darwin) PKG_MGR="brew" ;; FreeBSD) PKG_MGR="pkg" ;; *) echo "エラー: 未対応のOS: ${OS_TYPE}" >&2 exit 1 ;; esac echo "使用するパッケージマネージャ: ${PKG_MGR}"
$ uname -s Linux $ cat /etc/redhat-release Red Hat Enterprise Linux release 9.4 (Plow) $ bash detect-os.sh 使用するパッケージマネージャ: dnf
2. /etc/os-release でRHEL系クローンを区別する
同じ Linux でも RHEL・Rocky Linux・AlmaLinux を区別したい場合は、/etc/os-release の ID フィールドを case文に渡します。現場では RHEL 本家とクローンではサブスクリプション管理や証明書の扱いが異なるため、この切り分けが必要になります。#!/bin/bash # RHEL クローン種別の判定 if [ ! -f /etc/os-release ]; then echo "エラー: /etc/os-release が見つかりません" >&2 exit 1 fi # /etc/os-release を source して ID 変数を取り出す # ID=rhel / rocky / almalinux / centos / ubuntu など OS_ID="$(. /etc/os-release && echo "${ID}")" case "${OS_ID}" in rhel) echo "Red Hat Enterprise Linux です" SUBSCRIPTION_NEEDED=1 ;; rocky) echo "Rocky Linux です" SUBSCRIPTION_NEEDED=0 ;; almalinux) echo "AlmaLinux です" SUBSCRIPTION_NEEDED=0 ;; centos) echo "CentOS です(EOL 確認を推奨します)" SUBSCRIPTION_NEEDED=0 ;; ubuntu|debian) echo "Debian 系 Linux です" SUBSCRIPTION_NEEDED=0 ;; *) echo "未知のディストリビューション: ${OS_ID}" >&2 exit 1 ;; esac echo "サブスクリプション要否: ${SUBSCRIPTION_NEEDED}"
. /etc/os-release && echo "${ID}" はサブシェル内で os-release を source して ID 変数を取り出す慣用句です。grep + cut より堅牢で、引用符やスペースを含む値にも対応できます。ファイル拡張子別処理の自動化
1. ファイル名の末尾パターンで展開コマンドを切り分ける
バックアップファイルの展開スクリプトや、複数形式の入力を統一的に処理するスクリプトでは、ファイル名の末尾パターンを case文で切り分けるのが定番です。次のスクリプトは実際に現場で使っている形式判定&展開スクリプトです。#!/bin/bash # ファイル形式を自動判定して展開するスクリプト(extract.sh) # 使い方: ./extract.sh <ファイル名> set -euo pipefail FILE="${1:-}" if [ -z "${FILE}" ]; then echo "エラー: ファイル名を指定してください" >&2 echo "使い方: $0 <ファイル名>" >&2 exit 1 fi if [ ! -f "${FILE}" ]; then echo "エラー: ファイルが見つかりません: ${FILE}" >&2 exit 1 fi case "${FILE}" in *.tar.gz|*.tgz) echo "tar.gz 形式として展開します: ${FILE}" tar -xzf "${FILE}" ;; *.tar.bz2) echo "tar.bz2 形式として展開します: ${FILE}" tar -xjf "${FILE}" ;; *.tar.xz) echo "tar.xz 形式として展開します: ${FILE}" tar -xJf "${FILE}" ;; *.gz) echo "gzip 形式として展開します: ${FILE}" gunzip "${FILE}" ;; *.zip) echo "ZIP 形式として展開します: ${FILE}" unzip "${FILE}" ;; *.bz2) echo "bzip2 形式として展開します: ${FILE}" bunzip2 "${FILE}" ;; *.xz) echo "xz 形式として展開します: ${FILE}" unxz "${FILE}" ;; *) echo "エラー: 未対応の形式です: ${FILE}" >&2 echo "対応形式: .tar.gz .tgz .tar.bz2 .tar.xz .gz .zip .bz2 .xz" >&2 exit 1 ;; esac echo "完了しました"
$ ls -lh -rw-r--r--. 1 admin admin 2.4M Aug 4 09:00 backup-20260804.tar.gz -rw-r--r--. 1 admin admin 1.1M Aug 3 09:00 config-20260803.zip -rw-r--r--. 1 admin admin 512K Aug 2 09:00 logs-20260802.tar.bz2 $ bash extract.sh backup-20260804.tar.gz tar.gz 形式として展開します: backup-20260804.tar.gz 完了しました $ bash extract.sh config-20260803.zip ZIP 形式として展開します: config-20260803.zip 完了しました $ bash extract.sh unknown.7z エラー: 未対応の形式です: unknown.7z 対応形式: .tar.gz .tgz .tar.bz2 .tar.xz .gz .zip .bz2 .xz
2. 大文字・小文字の両方をマッチさせる
ユーザーが手動でファイル名を入力する場面では、大文字拡張子(.ZIP, .TAR.GZ)も想定すべきです。case文では| で両方を列挙します。case "${FILE}" in *.tar.gz|*.tgz|*.TAR.GZ|*.TGZ) tar -xzf "${FILE}" ;; *.zip|*.ZIP) unzip "${FILE}" ;; *) echo "未対応の形式: ${FILE}" >&2 exit 1 ;; esac
shopt -s nocasematch を使うと case文のマッチを大文字小文字区別なしにできますが、設定を元に戻し忘れると同スクリプト内の他の case文にも影響します。明示的に列挙する方が副作用がなく安全です。トラブルシュート|よくある落とし穴と対処法
1. 変数のクォート忘れでスペース・グロブが誤マッチする
case文のパターンマッチには「変数のクォート」が重要です。# NG例:クォートなしで変数を渡すと予期しない動作になる FILE_NAME="my archive.tar.gz" case $FILE_NAME in # スペースで単語分割が起きて "my" と "archive.tar.gz" になる *.tar.gz) echo "tar.gz です" # マッチしない ;; esac # OK例:クォートで囲む case "$FILE_NAME" in *.tar.gz) echo "tar.gz です" # 正しくマッチする ;; esac
$FILE_NAME に my archive.tar.gz を渡すと、my と archive.tar.gz に単語分割されてマッチが失敗します。変数は常に "$var" とクォートするのが鉄則です。2. *)デフォルト節を途中に置くとその後のパターンが到達不能になる
*) は「すべてにマッチする」ため、先頭や途中に置くと後続のパターンが永遠に到達しなくなります。# NG例:*)を先頭に置くとすべて *)節が実行される case "$ACTION" in *) echo "不明なアクション" exit 1 ;; start) # 到達不能 echo "Starting..." ;; esac # OK例:*)は必ず最後に置く case "$ACTION" in start) echo "Starting..." ;; stop) echo "Stopping..." ;; *) echo "不明なアクション: ${ACTION}" >&2 exit 1 ;; esac
3. set -u 環境で $1 が未定義のままだとエラーになる
引数なしでスクリプトを呼び出した場合、set -u を使っているとき $1 がエラーになります。${1:-}(未定義なら空文字)を使い、空文字パターンで専用のエラーメッセージを返す設計が堅牢です。#!/bin/bash set -euo pipefail # set -u 環境でも安全な書き方 case "${1:-}" in start) echo "Starting..." ;; "") # 引数なし専用エラーメッセージ echo "エラー: アクションを指定してください" >&2 echo "使い方: $0 {start|stop|restart}" >&2 exit 1 ;; *) echo "エラー: 不明なアクション '${1:-}'" >&2 exit 1 ;; esac
${1:-} と書くことで、引数なし時は空文字扱いとなり "") 節でハンドリングできます。4. ;; の付け忘れでフォールスルーが起きる
bash の case文では、;; を省略すると次のパターンの処理に「フォールスルー」します。C言語の switch文と同様の挙動で、意図しない場合はバグの原因になります。# NG例:;; を忘れると次の処理も実行される(意図しないフォールスルー) case "$STATUS" in running) echo "実行中です" # ;; を書き忘れると stopped 節の処理も実行される stopped) echo "停止中です" ;; esac # OK例:必ず ;; で終わる case "$STATUS" in running) echo "実行中です" ;; stopped) echo "停止中です" ;; esac # bash 4.0 以降で意図的なフォールスルーをする場合は ;& を明示的に使う case "$LEVEL" in error) echo "[ERROR]" >&2 ;& # 意図的なフォールスルー(コメントで必ず理由を書く) warning) echo "ログに記録します" ;; esac
;& を使い、コメントで意図を明記してください。誰が見ても「意図的な設計」と分かるようにすることが保守性を高めます。本記事のまとめ
| ユースケース | case文のパターン例 | ポイント |
|---|---|---|
| コマンド引数解析 | case "${1:-}" in start|stop|restart) |
${1:-} で set -u 対応 |
| オプション解析(while組み合わせ) | case "$1" in -v|--verbose) shift ;; |
ロングオプション対応可 |
| OS種別切り分け | case "$(uname -s)" in Linux|Darwin) |
/etc/os-release との併用も可 |
| ファイル拡張子別処理 | case "$FILE" in *.tar.gz|*.tgz) |
OR マッチで大文字も対応 |
| Yes/No 入力処理 | case "$ANSWER" in yes|Yes|y|Y) |
表記揺れをまとめて処理 |
・変数は必ず
"$var" とクォートする(スペース・グロブによる誤マッチを防ぐ)・
*)(デフォルト節)は必ず末尾に配置し、想定外入力を exit 1 で終了させる・
${1:-} で set -u 環境でも安全に引数を受け取る・
;; の書き忘れによるフォールスルーに注意する・意図的なフォールスルーは
;& を使いコメントで理由を明記するcase文を使いこなすと「1変数 × 複数パターン」の分岐をすっきり設計できます。if-elif が5行以上連続したら case文への書き換えを検討してください。コードの可読性と保守性が大幅に向上します。
シェルスクリプト講座を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:シェルスクリプトの起動前チェック設計|依存コマンド・環境変数・ディレクトリを自動検証する実装パターン
- この記事の属するカテゴリ:シェルスクリプトへ戻る

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