Linux Command Line & Bash Scripting Mastery · レッスン

関数間での配列と連想マップの受け渡し

namerefと間接展開を使い、複雑なデータ構造を関数へ安全に渡し、関数から受け取ります。

レッスン 4/413 ステップ

「関数間での配列と連想マップの受け渡し」はCoddyKit上の無料Linux Command Line & Bash Scripting Masteryレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはLinux Command Line & Bash Scripting Mastery学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Linux Command Line & Bash Scripting Masteryコースには全4レッスンが含まれています。

配列を関数に渡すのが難しい理由

Bashでは、配列と連想配列は第一級の値ではありません。配列を関数に渡すとき、単純に$my_arrayと書くことはできません。これは最初の要素だけを展開するためです。

次の例は正しく動作しません。

my_func "${my_array[@]}"

関数が受け取るのは、単なる単語のリストです。どこで要素が終わり、次の要素が始まるのかを知る方法がないため、配列の構造が完全に失われます。

Bashには、複雑なデータ構造を渡すための信頼できる仕組みが2つあります。

  • Nameref(declare -n)— 名前によって別の変数を参照する仕組み(Bash 4.3以降)
  • 間接展開(${!varname[@]})— 別の変数に格納された名前の変数へアクセスする仕組み(古いBash)

このレッスンでは両方の方法を扱い、配列やマップを扱える堅牢で再利用可能な関数を書けるようにします。

Namerefを使って配列を名前で渡す

現在のBashで最もすっきりした方法はnamerefです。配列の名前を文字列として渡し、関数内でその配列への参照となるローカル変数を宣言します。

基本的な構文は次のとおりです。

  • declare -n ref="$1" — refは、最初の引数として渡された変数名のエイリアスになります
  • refは、元の配列とまったく同じように読み取り、反復処理、変更ができます

配列全体をコピーする必要がないため、サイズを問わず配列を扱えます。

#!/usr/bin/env bash

print_array() {
    declare -n arr="$1"   # nameref: arr -> caller's variable
    echo "Array has ${#arr[@]} elements:"
    for elem in "${arr[@]}"; do
        echo "  - $elem"
    done
}

fruits=(apple banana cherry)
print_array fruits

関数内で呼び出し元の配列を変更する

Namerefは真のエイリアスなので、namerefを通じて書き込むと、呼び出し元の元の変数が変更されます。値をechoで返したり、グローバル変数を使ったりする必要はありません。

このため、namerefは配列をその場で変換する関数に適しています。たとえば、文字列の正規化、要素のフィルタリング、並べ替えなどに使えます。

#!/usr/bin/env bash

to_uppercase() {
    declare -n _arr="$1"
    for i in "${!_arr[@]}"; do
        _arr[$i]="${_arr[$i]^^}"   # ^^ = uppercase in Bash 4+
    done
}

words=(hello world bash)
echo "Before: ${words[@]}"
to_uppercase words
echo "After:  ${words[@]}"

Namerefを通じて新しい配列を返す

関数は、出力パラメーターとして呼び出し元から渡された配列の名前を受け取り、その配列に書き込むこともできます。呼び出し元が空の配列を作成して名前を渡し、関数がそこに値を格納します。

このパターンは、結果用バッファーへのポインターを渡すC言語の古典的なイディオムに似ています。ただし、ここではすべてシェル内で行います。

#!/usr/bin/env bash

get_even_numbers() {
    local -a source=("$@")
    # Last argument is the output array name — pop it off
    local out_name="${source[-1]}"
    unset 'source[-1]'

    declare -n _out="$out_name"
    _out=()   # clear
    for n in "${source[@]}"; do
        (( n % 2 == 0 )) && _out+=("$n")
    done
}

declare -a evens
get_even_numbers 1 2 3 4 5 6 7 8 evens
echo "Evens: ${evens[@]}"

Namerefの名前衝突に注意する

Namerefには重要な落とし穴が1つあります。ローカルのnameref変数が呼び出し元の変数と同じ名前だと、Bashは無限の自己参照ループに入り、スクリプトが失敗します。

たとえば、呼び出し元の配列名がarrで、関数がdeclare -n arr="$1"を実行すると、namerefのarrは自分自身を指してしまいます。

これを避けるための慣例: 内部のnameref変数にはアンダースコアを付けます(例: _arr、_ref、_out)。このような名前を呼び出し元が使うことはほとんどないため、衝突が起きる可能性を大幅に減らせます。

#!/usr/bin/env bash

# DANGER: caller uses variable named 'arr'
arr=(one two three)

bad_func() {
    declare -n arr="$1"   # collision! arr refers to itself
    echo "${arr[@]}"
}

good_func() {
    declare -n _arr="$1"  # underscore prefix avoids collision
    echo "${_arr[@]}"
}

# bad_func arr   # would hang / error
good_func arr

連想配列(マップ)を名前で渡す

連想配列(declare -A)もnamerefで同じように扱えます。マップの名前を文字列として渡し、関数内でnamerefを宣言すると、すべてのキーと値のペアを完全に読み書きできます。

これは、ライブラリ関数間で共有する設定マップ、オプションパーサー、ルックアップテーブルに非常に役立ちます。

#!/usr/bin/env bash

print_map() {
    declare -n _map="$1"
    echo "Map contents:"
    for key in "${!_map[@]}"; do
        printf '  %-15s => %s\n' "$key" "${_map[$key]}"
    done
}

declare -A config
config[host]="localhost"
config[port]="5432"
config[db]="myapp"

print_map config

Namerefを使って2つのマップを結合する

Namerefによって関数に有効なエイリアスが与えられるため、マップの結合のようなユーティリティをきれいに実装できます。宛先マップと1つ以上のソースマップを名前で渡し、関数がソースのキーを反復処理して宛先に書き込みます。

Namerefを使えば、スクリプト内の任意の連想配列で動作する汎用的なmerge_map関数を1つ書けます。

#!/usr/bin/env bash

merge_map() {
    # merge_map dest src
    declare -n _dest="$1"
    declare -n _src="$2"
    for key in "${!_src[@]}"; do
        _dest["$key"]="${_src[$key]}"
    done
}

declare -A defaults=([timeout]=30 [retries]=3 [verbose]=false)
declare -A overrides=([retries]=5 [log_level]=debug)

merge_map defaults overrides

for k in "${!defaults[@]}"; do
    echo "$k = ${defaults[$k]}"
done

間接展開: 古いBashとの互換性

Bash 4.0〜4.2(または古いmacOSのデフォルトシェル)をサポートする必要がある場合、declare -nは利用できません。その場合の代替手段が、${!varname}を使った間接展開です。

間接展開は、varnameの値を変数名として評価し、その変数の値を返します。配列の場合は、特別な構文と組み合わせてすべての要素を取得します。

Namerefと比較した場合の制限:

  • 実質的に読み取り専用です(書き戻しにはevalが必要ですが、インジェクションのリスクがあります)
  • evalなしでは、連想配列のキーを安全に反復処理できません
  • 可能な限りnamerefを使い、必要な場合だけ間接展開に切り替えてください
#!/usr/bin/env bash

sum_array() {
    local arr_name="$1"
    # Indirect: build a temporary copy by expanding arr_name[@]
    local -a items
    eval "items=( \"\${${arr_name}[@]}\" )"
    local total=0
    for n in "${items[@]}"; do
        (( total += n ))
    done
    echo "$total"
}

numbers=(10 20 30 40)
result=$(sum_array numbers)
echo "Sum: $result"

モジュール化されたスクリプトライブラリを構築する

プロフェッショナルなBashプロジェクトでは、ユーティリティ関数をライブラリファイル(例: lib/array_utils.sh)に保存し、必要なスクリプトからsourceします。

基本的なパターンは次のとおりです。

  • 各ライブラリファイルでは関数だけを定義し、トップレベルで副作用を発生させません
  • センチネル変数で二重読み込みを防ぎます: [[ -n $__LIB_LOADED ]] && return; __LIB_LOADED=1
  • 利用側のスクリプトはsourceまたは.を使ってライブラリを読み込みます
  • 関数間でnamerefを使って複雑なデータをやり取りし、ロジックを自己完結させます

これにより、一度きりのシェルの小技ではなく、テスト可能で再利用しやすく、保守しやすいスクリプトを作成できます。

実践例: マップを返すオプションパーサー

実際の用途として、呼び出し元から渡された連想配列に値を格納するコマンドラインオプションパーサーを考えてみましょう。呼び出し元は最初の引数に配列名を渡し、その後に実際の"$@"引数を続けます。

このパターンを使えば、コードをコピーしたりグローバル変数に依存したりせず、どのスクリプトからでも同じパーサーを再利用できます。

#!/usr/bin/env bash

parse_opts() {
    declare -n _opts="$1"
    shift
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --host)   _opts[host]="$2";   shift 2 ;;
            --port)   _opts[port]="$2";   shift 2 ;;
            --user)   _opts[user]="$2";   shift 2 ;;
            *)        echo "Unknown option: $1" >&2; shift ;;
        esac
    done
}

declare -A opts=([host]=localhost [port]=5432 [user]=admin)
parse_opts opts --host db.example.com --port 3306

echo "Connecting to ${opts[host]}:${opts[port]} as ${opts[user]}"

1つの関数で入力と出力のNamerefを組み合わせる

関数は複数のnamerefを同時に保持できます。1つを入力用、もう1つを出力用にします。これにより、グローバル状態に触れずに、ソース構造から読み取り、結果を計算し、宛先構造に書き込む純粋な変換関数を作成できます。

それぞれのnamerefに異なるアンダースコア付きの名前を使えば、呼び出し元の変数が単純な名前であっても衝突を確実に防げます。

#!/usr/bin/env bash

filter_map_by_key_prefix() {
    # Usage: filter_map_by_key_prefix src_map dest_map prefix
    declare -n _src="$1"
    declare -n _dst="$2"
    local prefix="$3"

    _dst=()
    for key in "${!_src[@]}"; do
        if [[ $key == ${prefix}* ]]; then
            _dst["$key"]="${_src[$key]}"
        fi
    done
}

declare -A settings=(
    [db_host]=localhost [db_port]=5432
    [cache_ttl]=300     [cache_driver]=redis
    [app_debug]=true
)
declare -A db_settings

filter_map_by_key_prefix settings db_settings db_

for k in "${!db_settings[@]}"; do
    echo "$k = ${db_settings[$k]}"
done

理解度チェック: Namerefと配列の受け渡し

Bashで関数間に配列やマップを渡す方法について、理解度を確認しましょう。

まとめ: Bashで複雑なデータ構造を渡す

このレッスンで学んだことをまとめます。

  • 配列は値渡しできません。代わりに変数の名前を文字列として渡します。
  • declare -n ref="$varname"はnamerefを作成します。これは呼び出し元の変数への有効なエイリアスです。読み取りも書き込みも元の変数に反映されます。Bash 4.3以降が必要です。
  • Namerefの衝突は、内部変数と外部変数の名前が同じときに発生します。内部のnamerefに_arrや_mapなどの接頭辞を付けて防ぎます。
  • 出力パラメーターは、空の結果配列またはマップの名前を渡して使用します。関数はnamerefを通じてそこに値を格納します。
  • 1つの関数で複数のnamerefを使うと、グローバルな副作用なしで入力から出力への純粋な変換を実行できます。
  • 間接展開(${!varname[@]} + eval)はBash < 4.3向けの古い代替手段ですが、インジェクションのリスクがあり、実質的に読み取り専用です。
  • これらの手法をsourceするライブラリファイルと組み合わせることで、モジュール化され、再利用しやすく、テスト可能なBashコードベースを構築できます。
無料で開始

AI チューターと学ぶ Bash — 無料

ブラウザでリアルコードを書いて実行し、24/7 の AI チューターから瞬時にサポートを受け、ウェブまたはアプリで続きから学習できます。

コース
22
レッスン
88

よくある質問

「関数間での配列と連想マップの受け渡し」レッスンは無料ですか?

はい。「関数間での配列と連想マップの受け渡し」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Linux Command Line & Bash Scripting Masteryコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Linux Command Line & Bash Scripting Masteryコースには全4レッスンが含まれています。

「関数間での配列と連想マップの受け渡し」で何を学びますか?

namerefと間接展開を使い、複雑なデータ構造を関数へ安全に渡し、関数から受け取ります。 ブラウザで直接実行するハンズオンコードでLinux Command Line & Bash Scripting Masteryを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Linux Command Line & Bash Scripting Masteryを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのLinux Command Line & Bash Scripting Masteryは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。

「関数間での配列と連想マップの受け渡し」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このLinux Command Line & Bash Scripting Masteryレッスンでコードを書いて実行できますか?

はい。すべてのLinux Command Line & Bash Scripting Masteryレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. ローカルスコープと終了コードを使った関数設計
  2. 再利用可能なBashライブラリの構築と読み込み
  3. getoptsによるフラグと引数の解析
  4. 関数間での配列と連想マップの受け渡し
← Linux Command Line & Bash Scripting Masteryに戻る