bash 配列入門 - 配列(array)と連想配列の使い方

bash 配列入門 - 配列(array)と連想配列の使い方

この記事で解決できること

  • bash の インデックス配列連想配列 の違いと使い分けが分かる
  • 宣言・参照・ループ・要素追加/削除の 基本操作 を一通り押さえられる
  • "${arr[@]}" のクォートや sparse 配列など 定番のハマり を回避できる

結論(先に要点)

  • 順番付きの値の集まり → インデックス配列arr=(a b c)
  • キーと値の対応表 → 連想配列declare -A map が必須)
  • ループ・展開では 必ず "${arr[@]}" とダブルクォートする

前提(対象環境)

  • シェル: bash 4.0 以降(連想配列は 4.0+、負の添字は 4.3+)
  • sh / dash では配列は使えない。#!/bin/bash を明示すること

配列とは何か?

結論: 配列は複数の値を 1 つの変数にまとめる仕組み。bash には順序を持つインデックス配列と、キーで引く連想配列の 2 種類がある。

通常のシェル変数は値を 1 つしか持てない。複数のファイル名やユーザー名をまとめて扱いたいとき、空白区切りの 1 文字列で済ませると空白を含む値で破綻する。配列なら各要素を独立した値として安全に保持できる。

bash の配列は 2 種類ある。

  • インデックス配列(indexed array): 0 から始まる整数の添字で要素を管理する
  • 連想配列(associative array): 任意の文字列をキーにして値を引く(ハッシュ/辞書に相当)

インデックス配列はどう使うのか?

結論: arr=(a b c) で宣言し、${arr[0]} で個別要素、"${arr[@]}" で全要素を参照する。添字は省略すると先頭要素を指す。

宣言と代入

# まとめて宣言
fruits=(apple banana cherry)

# 添字を指定して代入
fruits[3]=durian

# 末尾に追加(+= 演算子)
fruits+=(elderberry fig)

参照

echo "${fruits[0]}"      # apple(最初の要素)
echo "${fruits[-1]}"     # fig(末尾、bash 4.3+)
echo "${fruits[@]}"      # 全要素をスペース区切りで
echo "${#fruits[@]}"     # 要素数(5)
echo "${!fruits[@]}"     # 全インデックス(0 1 2 3 4 5)

$fruits のように添字なしで参照すると ${fruits[0]} と同じ(先頭要素のみ)になる。全要素を扱うつもりの取り違えに注意する。

スライス(部分取り出し)

echo "${fruits[@]:1:2}"  # banana cherry(添字1から2個)

連想配列はどう使うのか?

結論: 連想配列は使用前に declare -A で必ず宣言する。宣言を忘れるとインデックス配列扱いになり、キーが 0 に丸められて壊れる。

連想配列は任意の文字列をキーにできる。先に declare -A で宣言することが必須である点がインデックス配列との最大の違い。

declare -A capital          # これを忘れると壊れる
capital[japan]=Tokyo
capital[france]=Paris
capital["united states"]=Washington

echo "${capital[japan]}"    # Tokyo
echo "${!capital[@]}"       # 全キー(順序は不定)
echo "${capital[@]}"        # 全値(順序は不定)
echo "${#capital[@]}"       # 要素数(3)

連想配列は 順序を保持しない。挿入順やキー順での出力は保証されないため、整列したいなら sort に通す。

配列をループで回すには?

結論: 値を回すなら for x in "${arr[@]}"、キー/添字を回すなら for k in "${!arr[@]}"。どちらもダブルクォートが必須。

# 値を順に処理する
for f in "${fruits[@]}"; do
    echo "fruit: $f"
done

# キー(連想配列)を回して値を引く
for country in "${!capital[@]}"; do
    echo "$country -> ${capital[$country]}"
done

クォートを外して for f in ${fruits[@]} と書くと、空白を含む要素が単語分割されるapple pie という 1 要素が applepie の 2 つに割れる。値の安全性のため常に "${arr[@]}" とする。

"${arr[@]}""${arr[*]}" の違いは?

結論: [@] は各要素を別々の単語として展開し、[*] は IFS の先頭文字で連結した 1 つの文字列にする。ループには [@] を使う。

記法 展開結果 主な用途
"${arr[@]}" 要素ごとに独立した単語 ループ・引数渡し
"${arr[*]}" IFS 先頭文字で連結した 1 文字列 表示・文字列化
arr=(one two three)

printf '[%s]\n' "${arr[@]}"   # [one] [two] [three] が別行
printf '[%s]\n' "${arr[*]}"   # [one two three] が1行

# 区切り文字を変えて連結する
IFS=,
echo "${arr[*]}"              # one,two,three
unset IFS

要素の追加・削除はどうするのか?

結論: 追加は arr+=(...)、削除は unset 'arr[2]'。ただし unset したインデックス配列は添字が詰まらず sparse(穴あき)になる。

arr=(a b c d)

arr+=(e)            # 末尾に追加 -> a b c d e
unset 'arr[1]'      # 添字1(b)を削除

echo "${arr[@]}"    # a c d e
echo "${!arr[@]}"   # 0 2 3 4 ← 添字1が欠ける(詰まらない)

unset 後の配列は 連番でなくなる(sparse 配列)。${#arr[@]} の要素数と最大添字が一致しなくなるため、添字を 0..N-1 で決め打ちするループは壊れる。詰め直すには再代入する。

arr=("${arr[@]}")   # 添字を 0,1,2... に振り直す

つまずきやすいポイントは?

結論: 配列は bash 専用、クォート必須、連想配列は declare -A 必須。この 3 点を外すと典型的な事故になる。

コマンド出力を配列に取り込むときは、行単位で安全に読む mapfilereadarray)が便利。

mapfile -t lines < <(ls -1)   # 各行を1要素として lines に格納
echo "${#lines[@]}"           # 行数

-t は各行末尾の改行を取り除くオプション。これを付けないと要素に \n が残る。

まとめ

  • インデックス配列 arr=(a b c) は順序付き、連想配列 declare -A map はキー引き
  • 全要素は "${arr[@]}"、要素数は ${#arr[@]}、添字/キーは ${!arr[@]}
  • ループ・展開では 常にダブルクォートし、単語分割事故を防ぐ
  • 連想配列は declare -A が必須unset 後の配列は sparse になる点に注意

次に読む記事: