シェルスクリプトの関数から値を受け取る設計|標準出力とコマンド置換・nameref変数で結果を返す

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)シェルスクリプト > シェルスクリプトの関数から値を受け取る設計|標準出力とコマンド置換・nameref変数で結果を返す
「bash の関数で計算した値を呼び出し元に渡したい。でも 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 未満環境ではグローバル変数規約で代替する


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は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")を __retvarnameref(名前参照)として設定します。__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 変数名に __ プレフィックスを付ける
bash 関数の戻り値は「どの手法を選ぶか」より「なぜその手法でないといけないか」を理解することが重要です。$() のサブシェル特性と nameref の参照渡しの仕組みを把握すれば、状況に応じた判断が迷わずできるようになります。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、20年以上の運用経験を持つ現役エンジニアが基礎から教えます。
シェルスクリプト講座を見る >>

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

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

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

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

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

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

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

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

この記事を書いた人

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

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

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