2026年9月4日
2026年9月4日
bashの配列とハッシュ(連想配列)の使い方|宣言・ループ・存在確認まで
bashには、番号で要素を管理する「配列(インデックス配列)」と、文字列のキーで値を管理する「連想配列(ハッシュ)」があります。サーバー名の一覧をループしたり、ログのステータスコードを数えたりと、シェルスクリプトで頻繁に使う機能です。この記事では、それぞれの宣言・追加・取り出し・ループの書き方と、引用符の付け方、キーの存在確認、コマンド出力を配列に入れる方法、そして「パイプの先で代入した値が消える」などのつまずきをまとめます。
前提は bash 4.0 以降です。(2026年10月現在)主要な Linux ディストリビューションでは bash 5 系が標準です。macOS に標準で入っている bash は 3.2 系のため、連想配列は使えません。bash --version で確認してください。
インデックス配列の基本
丸括弧で囲んで空白区切りで並べると配列になります。添字は0から始まります。
#!/usr/bin/env bash
servers=(web01 web02 db01)
echo "${servers[0]}" # web01
echo "${servers[@]}" # web01 web02 db01(全要素)
echo "${#servers[@]}" # 3(要素数)
echo "${!servers[@]}" # 0 1 2(添字の一覧)
servers+=(cache01) # 末尾に追加
servers[10]=backup01 # 添字を飛ばして代入することもできる
echo "${!servers[@]}" # 0 1 2 3 10
配列名だけを $servers と書くと、先頭要素 ${servers[0]} だけが展開されます。全要素のつもりで $servers と書いてしまうのは、よくある見落としです。
削除すると添字に穴が空く
unset で要素を消しても、後ろの要素の添字は詰められません。
servers=(web01 web02 db01)
unset 'servers[1]'
echo "${!servers[@]}" # 0 2
echo "${#servers[@]}" # 2
このため、for ((i=0; i<${#servers[@]}; i++)) のように添字を数字で回すループは、穴があると要素を取りこぼします。添字が必要な場合は "${!servers[@]}" でループし、要素だけが必要なら "${servers[@]}" でループするのが安全です。詰め直したいときは servers=("${servers[@]}") と再代入します。unset の引数を引用符で囲むのは、servers[1] がファイル名のパターンとして展開されるのを防ぐためです。
スライス
${配列[@]:開始:個数} で一部を取り出せます。
nums=(10 20 30 40 50)
echo "${nums[@]:1:3}" # 20 30 40
echo "${nums[@]: -2}" # 40 50(負の値の前には空白が必要)
“${arr[@]}” と “${arr[*]}” の違い
配列を展開するときは、ほとんどの場面で二重引用符付きの "${arr[@]}" を使います。
"${arr[@]}":各要素を別々の単語として展開する。要素に空白が含まれていても1要素のまま"${arr[*]}":全要素を IFS の先頭文字(既定は空白)でつないだ1つの文字列にする- 引用符なしの
${arr[@]}:単語分割とファイル名展開がかかり、空白を含む要素が分解される
files=("access log.txt" "error.log")
for f in "${files[@]}"; do echo "[$f]"; done
# [access log.txt]
# [error.log]
for f in ${files[@]}; do echo "[$f]"; done
# [access] [log.txt] [error.log] と分解されてしまう
(IFS=,; echo "${files[*]}") # access log.txt,error.log(区切り文字でつなぐ)
コマンドに引数として渡すときも同じで、rsync "${opts[@]}" src/ dst/ のように書けば、オプションを配列で組み立てて安全に渡せます。
連想配列(ハッシュ)
連想配列は declare -A で宣言してから使います。宣言を省くと、通常の配列として扱われてキーが数値に変換され、意図しない結果になります。
declare -A port=(
[http]=80
[https]=443
[mysql]=3306
)
port[redis]=6379
echo "${port[https]}" # 443
echo "${#port[@]}" # 4(要素数)
for key in "${!port[@]}"; do
printf '%-6s %s\n' "$key" "${port[$key]}"
done
キーの順序は保証されません。表示順をそろえたい場合は、キーを並べ替えてからループします。
while IFS= read -r key; do
printf '%-6s %s\n' "$key" "${port[$key]}"
done < <(printf '%s\n' "${!port[@]}" | sort)
関数の中で declare -A すると、その配列は関数内のローカル変数になります。関数の外でも使いたい場合は declare -gA を使います。
キーが存在するか確認する
値が空文字の場合と、キー自体が無い場合を区別するには、-v を使います(bash 4.3以降)。
if [[ -v port[mysql] ]]; then
echo "mysql の設定あり"
fi
# 古い bash でも使える書き方
if [[ -n "${port[mysql]+set}" ]]; then
echo "mysql の設定あり"
fi
[[ -n "${port[mysql]}" ]] だと、値が空文字のキーも「無い」と判定されます。キーの削除は unset 'port[mysql]' です。
実例:ステータスコードを数える
アクセスログの9列目(一般的な combined 形式の場合)を数える例です。
#!/usr/bin/env bash
set -euo pipefail
declare -A count=()
while read -r _ _ _ _ _ _ _ _ status _; do
[[ $status =~ ^[0-9]{3}$ ]] || continue
count[$status]=$(( ${count[$status]:-0} + 1 ))
done < /var/log/nginx/access.log
for code in "${!count[@]}"; do
echo "$code ${count[$code]}"
done | sort -k2,2nr
${count[$status]:-0} で未登録のキーを0として扱っているため、set -u を有効にしていてもエラーになりません。ログの行数が数百万を超えるような規模では bash のループは遅くなるため、awk で集計した方が現実的です。
コマンドの出力を配列に入れる
1行を1要素として読み込むには mapfile(別名 readarray)を使います。
mapfile -t users < <(cut -d: -f1 /etc/passwd)
echo "ユーザー数: ${#users[@]}"
-t は各行末の改行を取り除く指定です。よく見かける users=( $(command) ) は、空白で分割されるうえにワイルドカードがファイル名に展開されるため、空白や * を含むデータで壊れます。
1行の文字列を区切り文字で分割するなら read -ra を使います。
line="web01,web02,db01"
IFS=, read -ra hosts <<< "$line"
echo "${hosts[1]}" # web02
よくあるつまずき
パイプの先で代入した値が消える
command | while read ...; do arr+=(...); done のように書くと、パイプの各段はサブシェルで実行されるため、ループを抜けると配列の変更が失われます。上の例のように done < <(command)(プロセス置換)か mapfile を使ってください。
set -u で空配列がエラーになる
bash 4.3以前では、要素が0個の配列を "${arr[@]}" で展開すると、set -u 環境下で「unbound variable」エラーになります。4.4以降では解消されていますが、古いサーバーで動かす可能性がある場合は ${arr[@]+"${arr[@]}"} という書き方で回避できます。
中身を確認したい
デバッグには declare -p 配列名 が便利です。添字やキーと値の対応が、そのまま再利用できる形で表示されます。
関数に配列を渡す
bash の関数は配列を値として受け取れません。要素を "${arr[@]}" で引数として渡すか、配列名を渡して declare -n(名前参照、bash 4.3以降)で参照します。
まとめ
- インデックス配列は
arr=(...)、連想配列はdeclare -Aで宣言してから使う - 展開は原則
"${arr[@]}"。引用符なしや$arrは取りこぼしや分解の原因になる unsetで添字に穴が空くため、ループは"${!arr[@]}"か"${arr[@]}"で回す- キーの有無は
[[ -v arr[key] ]]、連想配列の順序は保証されないので必要ならsortする - コマンド出力は
mapfile -tとプロセス置換で読み込み、パイプの先での代入は避ける