return したら 0 か 1 しか返ってこない……」こんな壁にぶつかったことはありませんか?
bash の
return は終了ステータス(0~255 の整数)を返すだけです。文字列・パス・数値計算の結果を直接返す仕組みはなく、それを知らずにコードを書いても期待どおりには動きません。この記事では、bash 関数 戻り値を実装する3手法——①標準出力+コマンド置換、②nameref変数(
declare -n)参照渡し、③グローバル変数経由——を動作確認済みのコード例で解説します。各手法のサブシェルコスト・bash バージョン要件・呼び出し側の可読性を比較し、「どの場面でどの手法を選ぶか」の判断基準まで整理します。実行環境は RHEL 9.4 / Ubuntu 24.04 LTS(bash 5.2 系)で動作確認済みです。この記事のポイント
・bash の return は終了ステータスのみ。文字列を返せない
・標準出力+$()は最も汎用的だがサブシェルが生まれる
・nameref(declare -n)はサブシェルなしで参照渡しできる
・bash 4.3 未満環境ではグローバル変数規約で代替する
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜ return では値を返せないのか
bash のreturn はシェルの「終了ステータス」専用の構文です。C言語の return と名前が同じでも意味が異なります。#!/bin/bash get_value() { return "hello" # ← エラー。数値しか渡せない } get_value echo $? # → return に渡した数値(0~255)が入る
return に文字列を渡すとエラーになります。整数を渡した場合でも、受け取れるのは $? 経由での「0 か非ゼロか」という成否判定のみです。255 を超える数値は切り捨てられ、247 を返すつもりが別の値になることもあります。関数から任意の値を呼び出し元に渡すには、別の手法が必要です。
方法1 ── 標準出力とコマンド置換で値を受け取る
最もシンプルな手法は、関数内でecho(または printf)を使って値を標準出力に書き出し、呼び出し側が $( ) で受け取るパターンです。1. 基本パターン
#!/bin/bash # パスからファイル名だけ取り出す関数 get_basename() { local filepath="$1" echo "${filepath##*/}" } # $() で標準出力を受け取る result=$(get_basename "/etc/httpd/conf/httpd.conf") echo "ファイル名: ${result}" # → ファイル名: httpd.conf
echo が唯一の「戻り値の出口」になります。printf '%s\n' で改行を制御したり、数値の場合は printf '%d' で書式を明示したりするのも実務でよく使われるパターンです。RHEL 9.4 の検証サーバーで実際に実行すると次のような結果になります。
[miyazaki@rhel94 ~]$ bash test_func.sh ファイル名: httpd.conf [miyazaki@rhel94 ~]$ bash -c ' get_ts() { local -n __r="$1"; __r=$(date "+%Y-%m-%d %H:%M:%S"); } get_ts ts; echo "ts=${ts}" ' ts=2026-09-11 10:30:00
2. サブシェル化のコストと変数スコープへの影響
$() はサブシェルを生成します。サブシェル内の変数変更は親シェルに伝わりません。これが「値は返ってきているのにスクリプトが変な動きをする」原因になりがちです。#!/bin/bash COUNTER=0 increment_and_get() { COUNTER=$(( COUNTER + 1 )) # サブシェル内の変更 echo "${COUNTER}" } val=$(increment_and_get) echo "val = ${val}" # → val = 1 echo "COUNTER = ${COUNTER}" # → COUNTER = 0 ← 変わっていない!
val=$(increment_and_get) の時点で subshell が起動し、その中で COUNTER をインクリメントしても親シェルの変数には影響しません。「関数の中で副作用として変数を書き換えながら、値も返したい」という設計をするとはまります。また、関数内で
exit を呼ぶとサブシェルが終了するだけで親シェルには届かない点も頭に入れておいてください。方法2 ── nameref(declare -n)で参照渡しする
bash 4.3 以降で使えるdeclare -n は、C言語のポインタや他言語の参照渡しに相当する仕組みです。サブシェルを生まず、親シェルの変数を直接書き換えられます。1. 基本パターン
#!/bin/bash get_timestamp() { local -n __retvar="$1" # nameref: bind to caller's variable __retvar=$(date '+%Y-%m-%d %H:%M:%S') } # 変数名 "ts" を文字列で渡す get_timestamp ts echo "現在時刻: ${ts}" # → 現在時刻: 2026-09-11 10:30:00
local -n __retvar="$1" で、呼び出し元が渡した変数名(ここでは "ts")を __retvar にnameref(名前参照)として設定します。__retvar への代入は、実際には ts への代入として処理されます。2. 配列の戻し方
コマンド置換では配列を返せませんが、nameref なら配列もそのまま渡せます。#!/bin/bash list_log_files() { local -n __arr="$1" local dir="$2" __arr=() while IFS= read -r f; do __arr+=("$f") done < <(find "${dir}" -maxdepth 1 -name "*.log" -type f 2>/dev/null) } list_log_files log_files "/var/log" echo "件数: ${#log_files[@]}" for f in "${log_files[@]}"; do echo " ${f}" done
3. 変数名の衝突に注意
nameref の落とし穴は、nameref 変数名と呼び出し元が渡した変数名が衝突したときです。たとえば呼び出し元がget_timestamp retvar と渡し、関数内の nameref も retvar という名前なら、循環参照になってエラーになります。__(ダブルアンダースコア)など、一般変数と被りにくいプレフィックスを関数内 nameref 名に使う慣例があります。bash 関数の戻り値を設計パターンとして体系的に学びたい方は、シェルスクリプト実践講座(Linux Master Pro)もご覧ください。現場で使える設計パターンを体系的に学べます。
方法3 ── グローバル変数(_RESULT 規約)を経由して返す
bash 4.2 以前(RHEL 7 / CentOS 7 の bash 4.2 など)ではdeclare -n が使えません。また、nameref より単純な代替として関数専用のグローバル変数を使う手法もあります。#!/bin/bash _RESULT="" # 関数の戻り値を格納するグローバル変数 get_md5sum() { local file="$1" if [[ ! -f "${file}" ]]; then return 1 fi _RESULT=$(md5sum "${file}" | cut -d' ' -f1) } get_md5sum "/etc/passwd" if [[ $? -eq 0 ]]; then echo "MD5: ${_RESULT}" fi
_RESULT や _RET_ などの規約名を使うことで、通常の変数との混同を防ぎます。ただし、同一スクリプト内で複数の関数が同じ変数名を使い回す場合は上書き順に注意してください。3手法の使い分け基準
| 手法 | サブシェル | bash 要件 | 配列を返せる | 主な用途 |
|---|---|---|---|---|
| 標準出力+$() | あり | 3.x~ | 不可 | 文字列1つ・パイプ連携 |
| nameref(declare -n) | なし | 4.3~ | 可 | 複数値・配列・副作用込み |
| グローバル変数(_RESULT) | なし | 3.x~ | 可 | 互換性重視・小規模スクリプト |
・返す値が文字列1つで、親シェルの変数を変更しないなら → 標準出力+$()
・bash 4.3 以上が保証されていて、配列や複数値を返したいなら → nameref
・古い bash(4.2 以前)との互換性が必要なら → グローバル変数規約
トラブルシュート ── よくあるハマりポイント
1. 「declare: -n: invalid option」が出る
bash 4.3 未満の環境ではdeclare -n は使えません。bash --version で確認し、4.3 未満なら方法3(グローバル変数)に切り替えてください。RHEL 7 / CentOS 7 は bash 4.2 です。2. $() 内で変数を更新したのに呼び出し元で反映されない
サブシェルの仕様です。変数の更新が必要な関数は$() で呼ばないことが原則です。nameref またはグローバル変数を使ってください。3. nameref で「circular name reference」エラーが出る
呼び出し元の変数名と関数内 nameref 変数名が同一のとき発生します。# NG: 呼び出し元が "retvar" を渡し、関数内も "retvar" を使う get_value() { local -n retvar="$1" # $1 == "retvar" → 循環参照! retvar="hello" } get_value retvar # OK: 関数内は __ プレフィックスで衝突を避ける get_value() { local -n __rv="$1" __rv="hello" } get_value retvar
4. 関数内で echo 以外の出力が混ざって値が壊れる
標準出力+$() パターンで、関数内にデバッグ用echo を仕込んだままにすると、その文字列も戻り値に混ざります。デバッグ出力は必ず標準エラー(echo "..." >&2)に向けてください。本記事のまとめ
| やりたいこと | 実装パターン |
|---|---|
| 文字列1つを返す(副作用なし) | echo "${value}" + result=$(func) |
| 配列・複数値を返す(bash 4.3+) | local -n __rv="$1" + func varname |
| 古い bash との互換を保つ | _RESULT=... + 関数呼び出し後に $_RESULT を参照 |
| 関数内からデバッグ出力を出す | echo "debug" >&2(標準エラーへ) |
| nameref の循環参照を避ける | 関数内 nameref 変数名に __ プレフィックスを付ける |
$() のサブシェル特性と nameref の参照渡しの仕組みを把握すれば、状況に応じた判断が迷わずできるようになります。
シェルスクリプト講座を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:シェルスクリプトのmain関数パターン|関数定義と実処理を分けて読み込み時の暴発を防ぐ構成
- この記事の属するカテゴリ:シェルスクリプトへ戻る

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