case/esac徹底解説|ifとの使い分け・パターン設計・落とし穴回避

分岐・反復

case/esac は、長く伸びた if/elif/else を“読みやすいスクリプト”に変えるための道具です。

拡張子ごとの処理、サブコマンドの振り分け、OS 判定のように「値がいくつかの候補に当てはまるだけ」の場面では、case を選ぶだけで可読性と安全性がぐっと上がります。本記事は if との使い分けから入り、パターン設計のコツ、そして 落とし穴の回避までをやさしく解説します。

前提は Bash 5 系、基本は set -euo pipefail。変数は必ずダブルクォートで包むという“お作法”もそのつど確認します。パターン列(a|b|c)やデフォルト節(*)の設計、shopt -s extglob で使える拡張グロブ、;;;&;;& の違いと安全な使い分け、nocasematch による大文字小文字の扱いまで、一歩ずつ進めます。

読み終えるころには、「これは if で書くより case のほうが読みやすい」「デフォルト節で漏れを拾い、ログと終了コードで外に伝える」という判断と手つきが身につきます。既存のスクリプトを置き換えるときにも迷わないよう、短い例と設計の意図をセットで示していきます。

if との使い分けから考える

まずは「いつ case を選ぶか」です。値がいくつかの候補のどれかに“当てはまるだけ”なら、if/elif/else を連ねるより case のほうが読みやすくなります。たとえばファイル拡張子で処理を分けるとき、if [[ "$ext" == "jpg" ]] || [[ "$ext" == "png" ]] … と並べるより、case "$ext" in … esac の形にすると、目で追いやすい地図になります。複雑な数値比較や正規表現を駆使する判定は if[[ ... ]])に譲り、列挙できる“パターンの振り分け”は case に任せる、という住み分けが基本です。

基本形と最小のサンプル

形はとてもシンプルです。変数は必ずダブルクォートで包み、各パターンの末尾は ;; で区切ります。最後の * は「どれにも当てはまらなかったとき」の受け皿です。

set -euo pipefail

handle_ext() {
  local ext="$1"
  case "$ext" in
    jpg|jpeg|png)
      echo "画像として処理します";;
    mp3|wav)
      echo "音声として処理します";;
    *)
      echo "未知の拡張子: $ext" >&2
      return 1;;
  esac
}

handle_ext "jpg"   # => 画像として処理します
handle_ext "mp3"   # => 音声として処理します
handle_ext "pdf"   # => (stderr) 未知の拡張子: pdf / return 1

case 文そのものは、マッチした節のコマンドの終了コードを引き継ぎます。上の例では * 節で return 1 しているので、呼び出し側で if handle_ext "$ext"; then ... のように成否を自然に扱えます。

パターン設計のコツ

case のパターンは “glob(ワイルドカード)” です。* は任意の文字列、? は1文字、[...] は文字クラスを表します。列挙したいときは a|b|c のようにパイプでつなぎます。入力の大小文字が混ざるなら、最初に小文字へ正規化するか、shopt -s nocasematch を一時的に有効化して大文字小文字を無視するのが手堅いです。正規表現のように細かくマッチさせたい場合は、無理せず if [[ "$str" =~ 正規表現 ]] へ切り替えると迷いません。

normalize_lower() {
  # tr はロケールの影響を受けることがあるため LC_ALL=C を添えると安定します
  printf '%s\n' "$1" | LC_ALL=C tr '[:upper:]' '[:lower:]'
}

kind_by_name() {
  local name="$(normalize_lower "$1")"
  case "$name" in
    @(alice|bob|carol))  echo "ユーザー";;
    admin|root)          echo "管理者";;
    *)                   echo "不明"; return 1;;
  esac
}

# 拡張グロブを使うので事前に有効化
shopt -s extglob
kind_by_name "Alice"   # => ユーザー
kind_by_name "ROOT"    # => 管理者

この例では @(alice|bob|carol) のような“拡張グロブ”を使っています。拡張グロブは shopt -s extglob をセットしてから利用します(後述)。

拡張トピック:拡張グロブと “落下” の挙動

Bash には glob を強化する拡張として、?(pat)(0回か1回)、*(pat)(0回以上)、+(pat)(1回以上)、@(pat)(ちょうど1回)、!(pat)(否定)があります。パターンを柔らかく表現できるので、列挙が多い場面ほど読みやすくなります。使う前に shopt -s extglob を忘れないようにしましょう。

もう一つ、case には “節を終えたあとどう振る舞うか” を選ぶ記号が3種類あります。ふだん使うのは停止を意味する ;; です。次の節のコマンドだけを続けて実行したいときは ;&、次の節のパターン再評価をしたいときは ;;& を使います。用途は限られますが、ログだけは共通で出したい、といったときに ;& が便利です。挙動が増えると読みづらくなるので、まずは ;; を基本にして、必要な場面でだけ使うと良いでしょう。

shopt -s extglob
case "$mode" in
  dev)   echo "開発モード";;&      # 次のパターンも“判定”してから実行
  stage) echo "ステージング";;&
  prod)  echo "本番";;&
  *)     echo "共通ログを出すだけ";; # ここまで到達した場合のみ実行
esac

実務でよく使うパターン

拡張子で処理を振り分けるのは定番です。*.{jpg,jpeg,png} のように書きたくなりますが、case のパターンには中括弧展開は使えません。jpg|jpeg|png と列挙するか、拡張グロブの @(jpg|jpeg|png) を使います。

shopt -s extglob

process() {
  local path="$1" base ext
  base="${path##*/}"
  ext="${base##*.}"

  case "${ext,,}" in            # ,,: bash の小文字化
    @(jpg|jpeg|png)) echo "画像: $base";;
    @(mp3|wav|flac)) echo "音声: $base";;
    *)                echo "未対応: $base"; return 2;;
  esac
}

process "photo.JPG"   # 画像
process "music.flac"  # 音声
process "note.txt"    # 未対応(return 2)

CLI の“サブコマンド”も case に向いています。if で比較を連ねるより、コマンドの一覧がひと目で分かります。

usage() { echo "使い方: $0 {add|list|delete} [args...]" >&2; }

main() {
  local sub="${1:-}"
  case "$sub" in
    add)    shift; cmd_add    "$@";;
    list)   shift; cmd_list   "$@";;
    delete) shift; cmd_delete "$@";;
    ""|help|--help|-h) usage; return 2;;
    *) echo "未知のコマンド: $sub" >&2; usage; return 2;;
  esac
}

OS 判定も書いておくと便利です。/etc/os-releaseID を参照し、デフォルト節で“未知”を拾います。

detect_os() {
  local id="unknown"
  if [[ -r /etc/os-release ]]; then
    # shellcheck disable=SC1091
    . /etc/os-release
    id="${ID,,}"
  fi
  case "$id" in
    ubuntu|debian) echo "apt";;
    centos|rhel|rocky|almalinux) echo "yum";;
    arch) echo "pacman";;
    *) echo "unknown"; return 1;;
  esac
}

よくある落とし穴

変数のクォート忘れは、スペースやワイルドカードが含まれる入力で誤判定を招きます。case "$var" in を癖にしてしまいましょう。パターンを大ざっぱにしすぎると、予想外の入力まで * で飲み込みます。具体的なものから先に書き、最後に * を置く順番を守るだけでずいぶん安全になります。

… | while read -r の形で入力を渡すと、環境によっては while がサブシェルで動き、ループ内で更新した変数が外に戻りません。case そのものの問題ではありませんが、よく一緒に使われるので注意です。プロセス置換 < <(...) を使えば、同じシェルの中で安全に処理できます。

count=0
# NG の例(戻らないことがある)
# printf '%s\n' jpg png | while read -r ext; do
#   case "$ext" in jpg|png) ((count++));; esac
# done
# echo "$count"  # 0 のまま…

# OK の例
while read -r ext; do
  case "$ext" in jpg|png) ((count++));; esac
done < <(printf '%s\n' jpg png)
echo "$count"  # 2

また、;&;;& は便利ですが、使いどころを絞らないと“意図しない実行が増える”危険があります。まずは ;; を基本にして、ログの追記など明確な目的があるときだけ ;& を選ぶほうが読みやすさを保てます。

終了コードの設計と、軽いテスト

分岐先で何らかの処理をした結果を、どう外へ伝えるかを決めておきましょう。成功なら 0、未知や入力エラーは非 0、というお約束を崩さないのがコツです。次の例では、既知の拡張子なら 0、未知なら 2 を返しています。スクリプト全体の最後で一度だけ exit する設計にしておくと、後始末の trap などが確実に動きます。

classify() {
  local ext="${1,,}"
  case "$ext" in
    jpg|jpeg|png) echo "image"; return 0;;
    mp3|wav|flac) echo "audio"; return 0;;
    *)            echo "unknown"; return 2;;
  esac
}

if out="$(classify "$1")"; then
  echo "$out"
  exit 0
else
  echo "$out" >&2
  exit $?
fi

テストは難しく考えなくて大丈夫です。いくつかの入力に対して期待する出力と終了コードをメモし、手で実行してみるだけでも十分に効果があります。慣れてきたら bats-core で自動化すると、将来の変更にも強くなります。

まとめ

case/esac は“列挙できる分岐”を読みやすく、安全にまとめる道具です。if と棲み分けをしながら、パターンは具体的なものから書き、最後に * で漏れを拾います。必要なら拡張グロブや小文字化で入力を整え、デフォルト節でログと終了コードを返しておけば、外側の処理も安定します。少しずつ既存の if/elif/elsecase に置き換えるだけでも、コードはぐっと見通しよくなります。

Bash玄

はじめまして!Bash玄です。

エンジニアとしてシステム運用に携わる中で、手作業の多さに限界を感じ、Bashスクリプトを活用して業務を効率化したのがきっかけで、この道に入りました。「手作業は負け」「スクリプトはシンプルに」をモットーに、誰でも実践できるBashスクリプトの書き方を発信しています。

このサイトでは、Bashの基礎から実践的なスクリプト作成まで、初心者でもわかりやすく解説しています。少しでも「Bashって便利だな」と思ってもらえたら嬉しいです!

# 好きなこと
- シンプルなコードを書くこと
- コマンドラインを快適にカスタマイズすること
- 自動化で時間を生み出すこと

# このサイトを読んでほしい人
- Bashに興味があるけど、何から始めればいいかわからない人
- 定型業務を自動化したい人
- 効率よくターミナルを使いこなしたい人

Bashの世界に一歩踏み出して、一緒に「Bash道」を極めていきましょう!

Bash玄をフォローする