bash の関数では、
return と exit の使い分けを誤ると意図しない動作を引き起こす。さらに set -e(errexit)を設定していても、関数をif文の条件式や&&・||リストに組み込んだ場合はその保護が無効化されるというbash固有の挙動がある。この記事では、bash 関数における
return と exit の動作の違い、set -eが効かなくなる具体的なケース、落とし穴を踏まない設計パターンを実機出力とともに解説する。動作確認はRHEL 9.4 / Ubuntu 24.04 LTSのbash 5系で実施している。この記事のポイント
・bash関数内のreturnは関数だけを終了し、exitはスクリプト全体を終了する
・if文の条件式や&&・||リスト内の関数呼び出しではset -eが無効になる
・sourceで読み込んだライブラリ関数にexitを書くと呼び出し元シェルが終了する
・die()関数でexit呼び出しを一本化し、return 1と組み合わせるのが安全設計の基本
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
returnとexitの動作を実機で比較する
まず基本を確認しよう。return と exit は見た目が似ているが、動作が根本的に異なる。1. returnは関数だけを終了する
return は現在実行中の関数を終了し、呼び出し元に制御を戻す。数値を引数に渡すと、それが関数の終了ステータス($?)になる。#!/bin/bash greet() { echo "こんにちは" return 0 # 関数だけを終了して呼び出し元に戻る echo "ここは実行されない" } greet echo "関数呼び出し後の処理" # ← ちゃんと実行される
$ bash test_return.sh こんにちは 関数呼び出し後の処理
return の後の処理は関数内では飛ばされるが、呼び出し元のスクリプトは継続する。2. exitはスクリプト全体を終了する
exit は関数の中で呼び出しても、シェルプロセス全体を終了させる。引数の数値が終了コードになる。#!/bin/bash check_config() { if [ ! -f /etc/myapp/config.conf ]; then echo "設定ファイルがない" >&2 exit 1 # 関数内でも、スクリプト全体が終了する fi return 0 } check_config echo "この行は実行されるか?" # exitが呼ばれると実行されない
$ bash test_exit.sh 設定ファイルがない $ echo $? 1
exit 1 でシェルごと終了したためだ。意図してスクリプトを止めたいなら正しい動作だが、ライブラリ的に使う関数に書いてしまうと問題になる。3. sourceで読み込んだライブラリ内でexitを呼ぶと危険
複数のスクリプトから共通処理をsource(または .)で読み込む構成では、ライブラリ側の関数で exit を使うと呼び出し元のシェルまで終了してしまう。# lib.sh(共通ライブラリ) cleanup() { rm -f /tmp/work.$$ exit 0 # 危険:sourceした呼び出し元シェルも終了する }
# main.sh source lib.sh cleanup echo "この行は絶対に実行されない" # exitで終了している
lib.sh 内の cleanup() に return 0 と書くべきところを exit 0 にしてしまうと、main.sh の後続処理が全て飛んでしまう。ライブラリ関数内では必ず return を使うことが鉄則だ。set -e(errexit)が関数内で効かない条件
set -e は「コマンドが失敗(終了コード非ゼロ)したらスクリプトを即座に終了する」オプションで、多くのスクリプトの冒頭に置かれる。しかしbashの仕様上、特定の文脈では set -e が無効化される。関数呼び出しがその文脈に入ると、関数内部のエラーも保護されなくなる点が見落としやすい。1. if文の条件式に関数を置いた場合
bashのmanページには「if文のテスト部分で失敗したコマンドがあってもシェルは終了しない」という仕様が明記されている。関数呼び出し自体がテスト部分に置かれると、その関数内のすべてのコマンドについてもset -e が一時的に無効化される。#!/bin/bash set -e check_func() { echo "関数開始" false # set -eが有効なら即終了のはずだが…… echo "falseの後も実行されてしまう" } echo "--- if条件内の呼び出し ---" if check_func; then echo "成功" else echo "失敗" fi echo "スクリプト継続"
$ bash test_errexit.sh --- if条件内の呼び出し --- 関数開始 falseの後も実行されてしまう 失敗 スクリプト継続
false が失敗しても set -e はスクリプトを止めていない。if check_func; then と書いた時点で check_func 全体がif文の条件評価対象となり、内部での set -e 保護が外れる。2. &&・||リストの一部に関数を置いた場合
&& や || リストの中のコマンドも同様に set -e の保護外になる。#!/bin/bash set -e validate() { echo "バリデーション開始" false # set -eが効かない echo "falseの後も続く" return 0 } # &&の左辺に関数を置くケース validate && echo "バリデーション成功" echo "ここも実行される"
$ bash test_and.sh バリデーション開始 falseの後も続く バリデーション成功 ここも実行される
validate() の中の false はスクリプトを止めない。さらに return 0 で成功を返しているため、&& echo "バリデーション成功" も実行される。バリデーションが実質ノーチェックになっている状態だ。3. !で返り値を反転した場合
! で関数の返り値を反転しても同様に set -e が効かなくなる。#!/bin/bash set -e is_root() { false # set -eが効かない [ "$(id -u)" -eq 0 ] } if ! is_root; then echo "rootではない" fi
set -e の保護外になる。この挙動はbashの仕様であり、バグではない。スクリプト設計の前提として覚えておくべきポイントだ。落とし穴を踏まない設計パターン
1. 関数内ではreturnを使い、exitはdie()に集約する
ライブラリ関数の中ではreturn だけを使い、スクリプトを強制終了したいケースは専用の die() 関数に任せる設計が安全だ。エラーメッセージの書式と終了コードを一箇所で管理できる。#!/bin/bash set -e # スクリプト強制終了専用関数 die() { echo "ERROR: $*" >&2 exit 1 } # ライブラリ関数:returnだけを使う check_config() { local file="$1" if [ ! -f "$file" ]; then return 1 # 呼び出し元に失敗を伝えるだけ fi return 0 } # 呼び出し側でdie()と組み合わせる check_config /etc/myapp/config.conf || die "設定ファイルが見つかりません: /etc/myapp/config.conf" echo "設定ファイル確認完了"
|| の右辺の die() は最後のコマンドとして実行されるため、set -e の保護対象外にはならず確実に動作する。2. set -eに頼らず明示的にエラーチェックする
if文の条件部分に関数を使う必要がある場合は、set -e の保護を期待せず、関数内でエラーを明示的に処理して return で返す設計にする。#!/bin/bash set -e parse_config() { local file="$1" # set -eが無効になる文脈を想定して、明示的にエラー処理する if [ ! -f "$file" ]; then echo "ERROR: $file が存在しない" >&2 return 1 fi grep -q "^enabled=true" "$file" || { echo "ERROR: enabled=trueの設定がない" >&2 return 1 } return 0 } # 関数をif条件に使う場合は、関数内でエラー処理を完結させる if parse_config /etc/myapp/config.conf; then echo "設定読み込み成功" else echo "設定の読み込みに失敗" >&2 exit 1 fi
3. 終了ステータスを必ず明示的に返す
関数の最後にreturn 0 または return 1 を明示する習慣をつける。何も書かないと、最後に実行されたコマンドの終了コードがそのまま関数の終了コードになるため、意図せず失敗を返すケースが生まれる。# 曖昧な例(最後のコマンドの結果に依存する) setup_dir() { mkdir -p /var/myapp/logs chmod 750 /var/myapp/logs # これが失敗すると関数が1を返す # return の明示なし } # 明示的な例 setup_dir_safe() { mkdir -p /var/myapp/logs || return 1 chmod 750 /var/myapp/logs || return 1 return 0 }
|| return 1 を各コマンドの末尾に付けることで、途中の失敗を確実に呼び出し元に伝えられる。トラブルシュート
Q: set -eを設定しているのにスクリプトが途中で止まらないA: 失敗したコマンドがif文・&&・||・!の文脈にないか確認する。関数呼び出し自体がそれらの文脈に置かれていないかも確認する。
bash -x スクリプト名 でトレースしながら確認するのが確実だ。Q: sourceしたライブラリを呼び出すとスクリプトが終了してしまう
A: ライブラリ内の関数に
exit が含まれている可能性が高い。grep -n 'exit' lib.sh で確認し、return に置き換える。Q: 関数から文字列や複数の値を返したい
A:
return で返せるのは0~255の整数のみ。文字列を返したい場合は、echo してコマンド置換(result=$(my_func))で受け取る方法か、グローバル変数に書き込む方法を使う。bash 4.3以降であればnameref変数(declare -n)も選択肢になる。Q: 関数内でreturnを書いたのにexit codeが期待通りにならない
A:
return の引数が0~255の範囲を超えていないか確認する。また、return $? とした場合に $? が正しく設定されているか、直前のコマンドで意図せず書き換わっていないかを確認する。本記事のまとめ
bashの関数でreturnとexitを使い分ける際のポイントをまとめる。| 確認したいこと | ポイント・対処 |
|---|---|
| 関数が終わっても処理を続けたい | return を使う(exit は使わない) |
| sourceするライブラリ関数内 | 必ず return を使う(exit は呼び出し元シェルも終了する) |
| エラー時にスクリプトを終了したい | die() 関数に exit を集約し、呼び出し側から || die "メッセージ" で使う |
| set -eが機能するか確認したい | if文・&&・||・!の文脈では set -e が無効になることを前提に設計する |
| if条件で関数を使うとき | 関数内で明示的にエラー処理を完結させ、set -eに頼らない |
| 関数の終了コードを確実に制御する | 末尾に return 0 または return 1 を必ず明示する |
set -e はシェルスクリプトのエラー検知に役立つが、万能ではない。関数をif文・&&・||に組み込む場合は保護が外れる——この仕様を理解して設計することが、信頼性の高いシェルスクリプトへの第一歩だ。設計パターンをより体系的に学びたい方は、次のシェルスクリプト講座を参照してほしい。
シェルスクリプト講座を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:シェルスクリプトに複数の機能を持たせるサブコマンド設計|引数から関数を安全に呼び分けて1本にまとめる構成
- この記事の属するカテゴリ:シェルスクリプトへ戻る

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