シェルスクリプトに複数の機能を持たせるサブコマンド設計|引数から関数を安全に呼び分けて1本にまとめる構成

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)シェルスクリプト > シェルスクリプトに複数の機能を持たせるサブコマンド設計|引数から関数を安全に呼び分けて1本にまとめる構成
「バックアップスクリプトに状態確認や復元機能も追加したくなったが、ファイルを分けると共通変数の定義が重複して管理が辛い」——現場でよく起きる問題です。かといって if-elif を積み重ねた1ファイルも可読性が下がる。

この記事では、bash 関数呼び出しを活用したサブコマンド設計を解説します。第1引数をそのまま関数名に解決するディスパッチ関数を実装し、未定義のコマンドは即座にエラーで拒否する安全弁もセットで組み込みます。git のように `./backup.sh start` や `./backup.sh restore` と呼び分けられる構成を、1本のスクリプトで実現する方法を実例付きで説明します。

この記事のポイント

・bash 関数呼び出しは関数名を変数に入れて `"$func" "$@"` の形で動的実行できる
・`declare -f 関数名` で関数の存在を確認してから呼ぶのが安全な設計
・サブコマンドと関数を `cmd_start` 形式でプレフィックス対応させる
・未定義サブコマンドは `declare -f` で検出し、明示的にエラーを返す


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

なぜ「サブコマンド設計」が必要なのか

スクリプトが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 # 古いバックアップを削除する

1本のスクリプトにまとめながら、各機能は独立した関数として整理できます。

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 }

`declare -f 関数名` は、指定した名前の関数が定義済みかどうかを確認するbashの組み込みコマンドです。定義済みなら終了コード0を返し、未定義なら1を返します。これにより、eval を使わずに安全な動的呼び出しが実現できます。

`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" "$@" }

どちらのケースも `usage` を呼んでサブコマンド一覧を表示してから `exit 1` で終了します。`exit 1` があるので、呼び出し元のスクリプトや cron でも失敗を正しく検出できます。

実践例:バックアップスクリプトをサブコマンド設計で実装する

サブコマンド設計を体系的に身につけたい方は、シェルスクリプト実践ガイドも合わせて参照してください。現場で使える設計パターンを体系的に学べます。

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 "$@"

スクリプトの構造は「設定 → 各機能関数 → ヘルプ → ディスパッチ → エントリーポイント」の順に並べると、新しいサブコマンドを追加するときに `cmd_xxx` 関数を1つ足すだけで済みます。ディスパッチ関数やエントリーポイントは一切変更不要です。

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 <サブコマンド> [オプション] ...

`upgrade` という存在しないサブコマンドを渡すと、`declare -f cmd_upgrade` が1を返すため、ディスパッチ関数が即座にエラーを出して終了します。タイポや誤操作を確実に検出できます。

トラブルシュート:サブコマンド設計のよくあるミスと対処

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" "$@" 形式にする
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、20年以上の運用経験を持つ現役エンジニアが基礎から教えます。
シェルスクリプト講座を見る >>

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。