2026年9月3日
2026年9月3日
bashのnameref(declare -n)と間接展開(${!var})の使い方|違い・使い分け・注意点
bashで「変数名を文字列で受け取り、その名前の変数を読み書きしたい」ときに使う方法は2つあります。読むだけなら間接展開 ${!name}、読み書きの両方や配列を関数に渡したいなら nameref(declare -n / local -n)です。この記事では、それぞれの書き方と違い、関数で配列を受け渡す実用例、名前の衝突による不具合、外部入力を変数名に使うときの危険性、eval を避けるための代替手段までを説明します。
nameref は bash 4.3 で追加された機能です。(2026年10月現在)主要な Linux ディストリビューションの bash は 5 系なので問題なく使えますが、macOS に標準で入っている bash 3.2 では使えません。bash --version で確認してください。間接展開 ${!name} は古い bash でも使えます。
間接展開 ${!name}:変数名から値を読む
${!name} は、「変数 name の値」を変数名とみなし、その変数の値に展開します。
#!/usr/bin/env bash
DB_HOST_STAGING="10.0.0.5"
DB_HOST_PRODUCTION="10.0.0.10"
env="production"
var="DB_HOST_${env^^}" # DB_HOST_PRODUCTION(^^ は大文字化、bash 4.0 以降)
echo "${!var}" # 10.0.0.10
環境名や項目名から変数名を組み立てて値を取り出す、という使い方が典型です。配列の要素も、[@] を含めた文字列を変数に入れれば間接的に展開できます。
servers_web=(web01 web02)
group="web"
ref="servers_${group}[@]"
for s in "${!ref}"; do echo "$s"; done # web01, web02
${!name} では代入できない
間接展開は読み取り専用です。${!var}=value のような書き方はできません。変数名を文字列で指定して代入したい場合は、次のどちらかを使います。
target="LAST_RESULT"
printf -v "$target" '%s' "ok" # 現在のスコープに代入する
declare -g "$target=ok" # 関数内からグローバル変数に代入する(bash 4.2 以降)
printf -v は書式付きで代入できるため、数値の桁揃えなどにも便利です。
${!prefix*} と ${!arr[@]} は別の機能
! を使う展開には、間接展開とは別の意味を持つものがあり、混同しやすいので整理しておきます。
${!name}:間接展開(nameの値を変数名として展開)${!prefix*}/${!prefix@}:prefixで始まる変数名の一覧${!arr[@]}:配列arrの添字(連想配列ならキー)の一覧
DB_HOST=localhost DB_NAME=wordpress DB_USER=wp
echo "${!DB_@}" # DB_HOST DB_NAME DB_USER
nameref:変数の別名を作る
declare -n ref=target と書くと、ref は target の別名(名前参照)になります。ref を読めば target の値が、ref に代入すれば target に代入されます。
target="before"
declare -n ref=target
echo "$ref" # before
ref="after"
echo "$target" # after
関数の中では local -n を使います。nameref が最も役に立つのは、配列を関数に渡したり、関数から配列を返したりする場面です。bash の関数は配列を値として受け取れないため、配列名を渡して nameref で参照します。
#!/usr/bin/env bash
# 第1引数: 結果を入れる配列の名前 / 第2引数: カンマ区切りの文字列
split_csv() {
local -n _split_out=$1
IFS=',' read -r -a _split_out <<< "$2"
}
# 第1引数: 合計を入れる変数の名前 / 第2引数: 数値の配列の名前
sum_array() {
local -n _sum_result=$1 _sum_values=$2
local v
_sum_result=0
for v in "${_sum_values[@]}"; do
_sum_result=$(( _sum_result + v ))
done
}
split_csv nums "10,20,30"
declare -p nums # declare -a nums=([0]="10" [1]="20" [2]="30")
sum_array total nums
echo "$total" # 60
コマンド置換 $(func) で値を返す方法と比べると、サブシェルを起動しないため速く、配列や複数の値をそのまま返せる点が利点です。
nameref 自体を操作する
nameref に対する一部の操作は、参照先ではなく nameref そのものに作用させる指定が必要です。
declare -n ref=target
unset ref # 参照先の target が削除される
unset -n ref # nameref の ref そのものが削除される
echo "${!ref}" # nameref に ${!} を使うと、参照先の変数名 "target" が返る
unset ref で nameref を消したつもりが参照先の変数を消してしまう、というのはよくある誤りです。また、declare -n は配列に付けることはできず、nameref から配列を参照することはできても、nameref 自体を配列にすることはできません。
名前の衝突に注意する
nameref は「名前」で参照先を探すため、関数内の変数名と呼び出し側の変数名が重なると問題が起きます。
同じ名前を渡すと循環参照になる
f() {
local -n arr=$1
arr=(x y)
}
arr=()
f arr
# warning: arr: circular name reference
関数内の nameref の名前と、渡された変数名がどちらも arr なので、arr が自分自身を参照する形になります。bash 5 系では警告が出たうえで処理が続きますが、意図どおりに動く保証はありません。
関数内のローカル変数が優先される
もう1つは警告が出ないため気付きにくい問題です。
f() {
local tmp="inside"
local -n out=$1
out="f からの値"
}
tmp="caller"
f tmp
echo "$tmp" # caller のまま
bash は変数を動的スコープで探すため、out の参照先 tmp は、呼び出し元ではなく関数 f 自身のローカル変数 tmp に解決されます。値は関数内の tmp に入り、関数を抜けると消えてしまいます。
どちらの問題も、関数内の nameref とローカル変数の名前に、関数名を含む接頭辞を付けると避けられます。前述の split_csv で _split_out のような名前にしているのはこのためです。ライブラリとして複数のスクリプトから使う関数では、特に意識してください。
外部入力を変数名に使わない
${!name} も nameref も、参照先の名前に arr[...] のような配列の添字を含めることができ、その添字は算術式として評価されます。このため、変数名として渡された文字列に x[$(コマンド)] のような内容が含まれていると、そのコマンドが実行されてしまいます。
# 悪い例: 外部から受け取った値をそのまま変数名として使う
name="$1"
echo "${!name}"
コマンドライン引数、設定ファイル、Web フックの入力など、内容を制御できない文字列を変数名として使う場合は、変数名として正しい形かを先に確認します。
name="$1"
if [[ ! $name =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
echo "エラー: 変数名として使えない文字が含まれています" >&2
exit 2
fi
echo "${!name}"
さらに安全にするなら、許可する名前を case で列挙するか、連想配列のキーとして扱う設計に変えます。値を名前で引き当てたいだけなら、多くの場合は連想配列の方が素直です。
declare -A db_host=( [staging]="10.0.0.5" [production]="10.0.0.10" )
env="production"
echo "${db_host[$env]}"
eval を使わずに書く
nameref や間接展開を知らないと、eval "echo \$$name" や eval "$name=\$value" のように eval で書きがちです。eval は文字列全体をコマンドとして再解釈するため、値に ; や $( ) が含まれていると任意のコマンドが実行されます。
目的ごとの置き換えは次のとおりです。
- 名前から値を読む:
${!name} - 名前を指定して代入する:
printf -v "$name" '%s' "$value" - 読み書きの両方や配列の受け渡し:
local -n ref=$name - 名前で値を引き当てる:連想配列
これらで書けない場面はほとんどありません。どうしても eval が必要なときは、前述の正規表現などで入力を厳しく絞ってから使ってください。
まとめ
- 変数名から値を読むだけなら間接展開
${!name}を使う。代入はできないのでprintf -vかdeclare -gを使う - 読み書きや配列の受け渡しには nameref(
declare -n/local -n、bash 4.3 以降)を使う unset refは参照先を消す。nameref 自体を消すならunset -n ref- 関数内の nameref とローカル変数には接頭辞を付け、呼び出し側の名前との衝突を避ける
- 外部入力を変数名に使う前に正規表現で形式を確認する。可能なら連想配列で設計する
evalは避け、目的に合った展開や nameref で置き換える