この記事では、bash 関数呼び出しを活用したサブコマンド設計を解説します。第1引数をそのまま関数名に解決するディスパッチ関数を実装し、未定義のコマンドは即座にエラーで拒否する安全弁もセットで組み込みます。git のように `./backup.sh start` や `./backup.sh restore` と呼び分けられる構成を、1本のスクリプトで実現する方法を実例付きで説明します。
この記事のポイント
・bash 関数呼び出しは関数名を変数に入れて `"$func" "$@"` の形で動的実行できる
・`declare -f 関数名` で関数の存在を確認してから呼ぶのが安全な設計
・サブコマンドと関数を `cmd_start` 形式でプレフィックス対応させる
・未定義サブコマンドは `declare -f` で検出し、明示的にエラーを返す
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜ「サブコマンド設計」が必要なのか
スクリプトが1機能だけであれば問題ありません。しかし現場では「バックアップスクリプトに、状態確認・復元・クリーンアップも欲しい」という要求が後から積み重なります。素朴に対応するとこうなりがちです:
・`backup.sh`(バックアップ本体)
・`backup_status.sh`(状態確認)
・`backup_restore.sh`(復元)
・`backup_cleanup.sh`(古いバックアップの削除)
4ファイルに分散すると、共通の設定値(バックアップ先パスなど)の定義が重複し、変更時の更新漏れが起きやすくなります。かといって、1ファイルに if-elif の連鎖で詰め込む方法もセクションが長くなってメンテナンスが辛い。
サブコマンド設計は、この問題をきれいに解決します:
$ ./backup.sh start # バックアップを開始する $ ./backup.sh status # 状態を確認する $ ./backup.sh restore 日付 # 指定日時から復元する $ ./backup.sh cleanup # 古いバックアップを削除する
bash 関数呼び出しによるサブコマンド設計の基本
1. 機能を関数として定義する
最初のステップは、各サブコマンドの処理を独立した関数として定義することです。命名規則は「`cmd_`+サブコマンド名」に統一します。これが後のディスパッチ処理で重要になります。cmd_start() { local dest="${BACKUP_DIR}/$(date '+%Y%m%d_%H%M%S')" mkdir -p "$dest" rsync -av /var/data/ "$dest/" echo "完了: $dest" } cmd_status() { echo "=== 最近のバックアップ(5件)===" ls -lh "${BACKUP_DIR}" | tail -5 } cmd_restore() { local target="${1:-}" if [[ -z "$target" ]]; then echo "使い方: $(basename "$0") restore <バックアップ名>" >&2 exit 1 fi rsync -av "${BACKUP_DIR}/${target}/" /var/data/ echo "復元完了: $target" } cmd_cleanup() { find "${BACKUP_DIR}" -maxdepth 1 -type d -mtime +30 -exec rm -rf {} + echo "30日超えのバックアップを削除しました" }
2. ディスパッチ関数で第1引数を関数名に解決する
次に、第1引数(サブコマンド)を受け取り、対応する関数を呼び出すディスパッチ関数を作ります。bash 関数呼び出しの重要なポイントは、関数名を変数に入れて動的に実行できることです。dispatch() { local subcmd="${1:-}" shift # $1(サブコマンド)を消費して残りの引数を $@ に残す local func="cmd_${subcmd}" # declare -f で関数の存在を確認してから呼ぶ(安全なbash 関数呼び出し) if declare -f "$func" > /dev/null 2>&1; then "$func" "$@" else echo "エラー: 不明なサブコマンド '${subcmd}'" >&2 usage exit 1 fi }
`shift` で `$1`(サブコマンド)を消費した後、`"$@"` で残りの引数を各関数に渡します。`./backup.sh restore 20260901_120000` と呼べば、`cmd_restore` に `20260901_120000` が `$1` として渡ります。
3. 未定義コマンドを安全に拒否する設計
サブコマンドを省略したときや、タイポがあったときに安全に弾く設計を加えます。「空のサブコマンド」と「未定義のサブコマンド」を分けて判定するのがポイントです。dispatch() { local subcmd="${1:-}" # 引数なしで呼ばれた場合 if [[ -z "$subcmd" ]]; then echo "エラー: サブコマンドを指定してください" >&2 usage exit 1 fi shift # 定義されていない関数名は弾く local func="cmd_${subcmd}" if ! declare -f "$func" > /dev/null 2>&1; then echo "エラー: 不明なサブコマンド '${subcmd}'" >&2 usage exit 1 fi "$func" "$@" }
実践例:バックアップスクリプトをサブコマンド設計で実装する
サブコマンド設計を体系的に身につけたい方は、シェルスクリプト実践ガイドも合わせて参照してください。現場で使える設計パターンを体系的に学べます。1. スクリプト全体の構成
ここまでの要素を1本に組み立てた完成形です。#!/bin/bash set -euo pipefail # ==== 設定 ==== BACKUP_DIR="/var/backup/data" # ==== サブコマンド関数 ==== cmd_start() { local dest="${BACKUP_DIR}/$(date '+%Y%m%d_%H%M%S')" mkdir -p "$dest" rsync -av /var/data/ "$dest/" echo "完了: $dest" } cmd_status() { echo "=== 最近のバックアップ(5件)===" ls -lh "${BACKUP_DIR}" | tail -5 } cmd_restore() { local target="${1:-}" if [[ -z "$target" ]]; then echo "使い方: $(basename "$0") restore <バックアップ名>" >&2 exit 1 fi rsync -av "${BACKUP_DIR}/${target}/" /var/data/ echo "復元完了: $target" } cmd_cleanup() { find "${BACKUP_DIR}" -maxdepth 1 -type d -mtime +30 -exec rm -rf {} + echo "30日超えのバックアップを削除しました" } # ==== ヘルプ ==== usage() { cat <<'HELP' 使い方: backup.sh <サブコマンド> [オプション] サブコマンド: start バックアップを開始する status バックアップ一覧を表示する restore <バックアップ名> 指定バックアップから復元する cleanup 30日超えのバックアップを削除する HELP } # ==== ディスパッチ ==== dispatch() { local subcmd="${1:-}" if [[ -z "$subcmd" ]]; then echo "エラー: サブコマンドを指定してください" >&2 usage exit 1 fi shift local func="cmd_${subcmd}" if ! declare -f "$func" > /dev/null 2>&1; then echo "エラー: 不明なサブコマンド '${subcmd}'" >&2 usage exit 1 fi "$func" "$@" } # ==== エントリーポイント ==== dispatch "$@"
2. 実行例で動作を確認する
実際のサーバーで実行した結果を示します。# バックアップ開始 $ ./backup.sh start sending incremental file list ./ config/ config/app.conf data/ data/records.db sent 4,280,192 bytes received 84 bytes 1,712,110.40 bytes/sec total size is 4,280,064 speedup is 1.00 完了: /var/backup/data/20260912_143022 # 状態確認 $ ./backup.sh status === 最近のバックアップ(5件)=== drwxr-xr-x 4 root root 4.0K Sep 10 08:15 20260910_081512 drwxr-xr-x 4 root root 4.0K Sep 11 08:15 20260911_081503 drwxr-xr-x 4 root root 4.0K Sep 12 14:30 20260912_143022 # 存在しないサブコマンドを指定した場合 $ ./backup.sh upgrade エラー: 不明なサブコマンド 'upgrade' 使い方: backup.sh <サブコマンド> [オプション] サブコマンド: start バックアップを開始する status バックアップ一覧を表示する restore <バックアップ名> 指定バックアップから復元する cleanup 30日超えのバックアップを削除する # サブコマンドなしで呼んだ場合 $ ./backup.sh エラー: サブコマンドを指定してください 使い方: backup.sh <サブコマンド> [オプション] ...
トラブルシュート:サブコマンド設計のよくあるミスと対処
eval による動的呼び出しは使わない`eval "$func $@"` のように eval を使った実装を見かけることがありますが、サブコマンド名にシェルのメタ文字が混入するとコマンドインジェクションになるリスクがあります。`declare -f` で存在確認してから `"$func" "$@"` で呼ぶ方法が安全です。
`shift` のタイミングに注意する
dispatch 関数の中で `$1`(サブコマンド)を使った後に `shift` しないまま `"$func" "$@"` を呼ぶと、サブコマンド名が各関数にも `$1` として渡ってしまいます。`local subcmd="${1:-}"` でサブコマンドを退避してから `shift` し、その後に `"$func" "$@"` と呼ぶ順序を守ってください。
`set -e` と `declare -f` の組み合わせに注意する
`set -e`(エラー時即時終了)が有効な環境で `declare -f "$func"` が失敗(未定義)すると、if の条件評価前にスクリプトが終了してしまいます。`if ! declare -f "$func" > /dev/null 2>&1; then` の形にすることで、`set -e` の影響を受けずに条件判定できます。
関数プレフィックスはプロジェクト内で統一する
今回は `cmd_` を使いましたが、プレフィックスを統一していれば `_subcmd_` でも `do_` でも構いません。重要なのは「サブコマンド名と関数名のマッピングルールが1箇所のディスパッチ関数に集約されていること」です。ルールが複数箇所に散らばると、サブコマンドを追加するたびに複数の場所を変更しなければならなくなります。
本記事のまとめ
bash 関数呼び出しを活用したサブコマンド設計の実装パターンを解説しました。要点を以下にまとめます。
| やりたいこと | 設計・実装のポイント |
|---|---|
| 第1引数でサブコマンドを選択する | dispatch "$@" でエントリーポイントを1箇所に集約する |
| 関数名を動的に決定して呼ぶ | local func="cmd_${subcmd}" && "$func" "$@" で動的に呼び出す |
| 関数の存在を安全に確認する | declare -f "$func" > /dev/null 2>&1 で定義済みかチェックする |
| 未定義サブコマンドを拒否する | ! declare -f "$func" で条件判定し exit 1 で終了する |
| 残りの引数を各関数に渡す | ディスパッチ内で shift 後に "$@" を使う |
| eval を使わず安全に実装する | 関数名変数を直接コマンドとして実行する "$func" "$@" 形式にする |
シェルスクリプト講座を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:シェルスクリプトの関数から値を受け取る設計|標準出力とコマンド置換・nameref変数で結果を返す
- この記事の属するカテゴリ:シェルスクリプトへ戻る

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