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 とプロセス置換で読み込み、パイプの先での代入は避ける

お気軽にご相談ください

お見積りへ お問い合わせへ
▲