この記事の狙い
eval を使わずに動的な変数名を安全に扱い、値の取得・代入・検証を行えるようにします。
Bash の間接展開(${!name})、nameref(declare -n)、printf -v を組み合わせ、配列・連想配列・スコープを壊さない実装指針を身につけます。
前提と対象
- Bash 4.3+ を推奨(
declare -nを利用)。4.2 以前でも一部は可(${!name}は利用可、制約あり)。 - GNU ユーザーランド想定。
set -Eeuo pipefail前提の安全設計に馴染みがある方向け。
TL;DR(最小実装・コピペ可)
eval 禁止。参照は declare -n、代入は printf -v、簡易参照は ${!}。
#!/usr/bin/env bash
set -Eeuo pipefail
# 動的な「変数名」を受け取り、安全に読み書きする
get_by_name() {
local name="$1"
# nameref: name が指す変数を「別名」で参照
declare -n ref="$name" || return 22 # 無効名なら 22(EINVAL) とする
printf '%s\n' "${ref-}" # 未定義は空
}
set_by_name() {
local name="$1" value="$2"
# printf -v は「変数名」を取り、フォーマットして代入する(クォート不要)
printf -v "$name" '%s' "$value"
}
# 例:動的名を作り、読み書き
user_id="42"
key="user_id"
echo "GET:${key} -> $(get_by_name "$key")" # => 42
set_by_name "$key" "100"
echo "SET:${key} -> $(get_by_name "$key")" # => 100
設計の要点(原則)
参照は参照、代入は代入の仕組みを使い分けます。
- 読み取り:
declare -n ref="$name"で nameref を作り、"$ref"を読む。 - 書き込み:
printf -v "$name" '%s' "$value"で フォーマット済み代入。 - 単純参照だけなら
${!name}も有効(ただし配列要素や複雑式には弱い)。 evalは使用しない。意図しない展開・注入・シグナルで壊れやすい。
ステップ実装(分解解説)
段階1:単純な間接参照(${!var})
文字列としての「変数名」を介して値を取る最小手段。読み取り専用に近い使い方。
foo="abc"
name="foo"
printf '%s\n' "${!name}" # -> abc
制約として、配列要素(arr[i])や連想配列要素の参照には向きません。複合名の生成で壊れがちです。
段階2:nameref(declare -n)で強い参照
nameref は左辺にも右辺にも使える参照です。関数引数で“変数そのもの”を扱えるため、スコープ安全でテストもしやすくなります。
# 任意の「変数名」にポインタのように触れる
set_default_if_empty() {
local varname="$1" def="$2"
declare -n ref="$varname"
[[ -n "${ref-}" ]] || ref="$def"
}
msg=""
set_default_if_empty "msg" "hello"
echo "$msg" # -> hello
配列にも効きます。
nums=(1 2 3)
bump_last() {
declare -n a="$1"
local last_index=$(( ${#a[@]} - 1 ))
(( last_index >= 0 )) || return 0
a[$last_index]=$(( a[$last_index] + 1 ))
}
bump_last "nums"
printf '%s\n' "${nums[@]}" # -> 1 2 4
段階3:printf -v で安全代入
printf -v <name> fmt args... は、展開済み文字列をクォート無しで代入できます。
改行・スペース・* などのメタ文字を含んでも壊れません。
varname="path"
value="/tmp/a b*c?.txt"
printf -v "$varname" '%s' "$value"
declare -p path # -> declare -- path="/tmp/a b*c?.txt"
テンプレ文字列の生成にも使えます。
line=""
printf -v line '[%(%Y-%m-%d %H:%M:%S)T] %s' -1 "started"
echo "$line"
段階4:連想配列/ネームスペース風の設計
「変数名を組み立てて管理」より、連想配列で素直に持つほうが安全で読みやすい場面が多いです。
declare -A cfg=(
[host]="db1"
[port]="5432"
)
# 参照は一箇所に寄せる
cfg_get() {
local k="$1"; printf '%s\n' "${cfg[$k]-}"
}
cfg_set() {
local k="$1" v="$2"; cfg[$k]="$v"
}
どうしても “prefix_key” のように動的変数を作る必要があるときだけ、printf -v を採用します。
ns_set() {
local ns="$1" key="$2" val="$3"
local name="${ns}_${key}"
printf -v "$name" '%s' "$val"
}
ns_set "DB" "HOST" "db1"
echo "$DB_HOST" # -> db1
失敗しやすい点(アンチパターン)
eval "$name=value"を使う:展開・グロブ・コマンド置換が混入すれば即事故。
→ 代入はprintf -v、参照はdeclare -nへ置換。${!}で配列要素を無理に読む:"${!arr[i]}"のような記述は壊れやすい。
→ nameref でdeclare -n a="arr"としてa[i]を扱う。- 未定義変数に触れて
set -uで落ちる:
→declare -n ref="$name" || return 22、"${ref-}"のように 未定義ガードを徹底。 - 名前にユーザー入力をそのまま使う:
→ 許可リスト([[ $name =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]])で 識別子検証を入れる。
テストと品質(最小)
入出力と終了コードだけでも十分に効きます。shellcheck は nameref も理解します。
# test/run.sh
#!/usr/bin/env bash
set -Eeuo pipefail
. ./lib.sh # 関数群を分離している想定でも可
x=""
set_default_if_empty "x" "v"
[[ "$x" == "v" ]] || { echo "FAIL default"; exit 1; }
path=""
val='/tmp/a b*c?.txt'
printf -v path '%s' "$val"
[[ "$path" == "$val" ]] || { echo "FAIL printf-v"; exit 1; }
echo "PASS"
運用設計(実務)
- 外部入力から「変数名」を作らない。必要なら、連想配列へ寄せる。
- nameref は寿命(スコープ)に依存するため、関数内で完結させる。返り値は stdout、または
printf -vで“呼び出し元の変数名”に代入する設計が定石。 - ロギングは必ずクォートし、展開結果を出す(名ではなく値)。
互換性と移植性
declare -nは Bash 4.3+。それ以前(やdash)では使えません。${!name}は古い Bash でも動くが、単純変数名の間接参照に限ると割り切る。- macOS の古い
/bin/bashは 3.x 系の場合あり。Homebrew の bash などで 5.x を使うのが無難。
セキュリティと安全設計
- 変数名に使えるのは 識別子のみ。ユーザー入力をそのまま名前化しない。
- 代入時は常に
printf -vを通してクォート不要化。 eval/動的コード生成/コマンド置換を避け、データはデータとして扱う。
パフォーマンスの勘所(短く)
- nameref/
printf -vはオーバーヘッドが小さく、ループ内でも現実的。 - ただし巨大ループのキー・名組み立てはコストになるので、可能なら連想配列に一本化。
参考実装(拡張版・コピペ可)
名の検証を含めた 安全 API の最小セット。
# lib_indirect.sh
#!/usr/bin/env bash
set -Eeuo pipefail
_is_ident() { [[ "${1-}" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; }
indirect_get() {
local name="$1"
_is_ident "$name" || return 22
declare -n ref="$name" || return 22
printf '%s\n' "${ref-}"
}
indirect_set() {
local name="$1" value="$2"
_is_ident "$name" || return 22
printf -v "$name" '%s' "$value"
}
indirect_append_array() {
local arrname="$1" value="$2"
_is_ident "$arrname" || return 22
declare -n a="$arrname" || return 22
a+=("$value")
}
# demo.sh
#!/usr/bin/env bash
set -Eeuo pipefail
. ./lib_indirect.sh
user="alice"
echo "$(indirect_get user)" # alice
indirect_set user "bob"
declare -p user # "bob"
nums=()
indirect_append_array nums "1"
indirect_append_array nums "2"
printf '%s\n' "${nums[@]}" # 1 2
よくある質問(Q&A)
Q. ${!name} と declare -n はどう使い分けますか?
A. 値を“読むだけ”なら ${!name} で軽く済みます。配列・連想配列・書き込みまで踏み込むなら declare -n を選びます。
Q. printf -v と通常の代入 var="$val" の違いは?
A. **左辺が動的(文字列で与えられる)**ときに printf -v <name> が必要です。右辺のフォーマットも同時に行えるのが利点です。
Q. 旧環境(Bash 3.x)では?
A. ${!name} の範囲でしのぐか、設計自体を連想配列やキー→値テーブルへ寄せて“変数の名を増やさない”方針に切り替えます。
