配列・連想配列の基礎|宣言・走査・安全展開

データとパラメータ展開
スポンサーリンク

この記事の狙い

Bash で配列(indexed array)と連想配列(associative array)を安全に扱えるようにします。
宣言・要素追加・走査・長さ取得・存在確認・マージ・削除までをコピペで動く最小実装
とともに解説し、クォートと展開の落とし穴を避ける設計を身につけます。

前提と対象

Bash 4+ を想定します。
macOS の標準 /bin/bash が 3.x の場合は Homebrew などで Bash 5 系を導入してください(連想配列は Bash 4+ で利用可)。
本文は set -Eeuo pipefail 前提の安全設計に馴染みがある方向けです。

TL;DR(最小実装・コピペ可)

配列は "${arr[@]}"、連想配列は "${map[@]}""${!map[@]}" が基本。存在確認は [[ -v 'map[key]' ]] を使います。

#!/usr/bin/env bash
set -Eeuo pipefail

# --- 配列 ---
nums=(10 20 " 30 with space " "" 40)  # 空要素も可
echo "len=${#nums[@]}"

# 安全走査(要素単位)
for x in "${nums[@]}"; do
  printf '[%s]\n' "$x"
done

# 追加・スライス
nums+=("50")
printf 'last=%s\n' "${nums[-1]}"          # Bash 4.2+ の負インデックス
printf 'slice=%s\n' "${nums[@]:1:3}"      # 1 から 3 要素

# --- 連想配列 ---
declare -A cfg=([host]="db1" [port]="5432" [path]="/tmp/a b")

# 参照・存在確認
printf 'host=%s\n' "${cfg[host]}"
if [[ -v 'cfg[path]' ]]; then echo "path exists"; fi

# キー/値の列挙
printf 'keys: %s\n' "${!cfg[@]}"
printf 'vals: %s\n' "${cfg[@]}"

# 追加・上書き・削除
cfg[user]="alice"
unset 'cfg[port]'

# デバッグ:宣言出力
declare -p cfg nums

実行例:

chmod +x script.sh && ./script.sh

設計の要点(原則)

配列は単語の集合です。クォートと展開の選択が品質を左右します。

  • 走査・引き渡しは "${arr[@]}" を必ず使用(要素単位・空要素保持)。
  • 文字列化したいときだけ "${arr[*]}"(IFS による結合)。
  • 連想配列のキー列挙は "${!map[@]}"、存在確認は [[ -v 'map[key]' ]]
  • unset "arr[i]"穴埋め削除unset -v arr配列自体の削除
  • 外部入出力はNUL セーフを意識(printf '%s\0'xargs -0 / readarray -d '')。

ステップ実装(分解解説)

段階1:宣言と初期化

配列は ()、連想配列は declare -A で宣言します。
空要素や空白を含む要素は必ずクォートします。

fruits=("apple" "banana" "" "dragon fruit")
declare -A meta=([name]="project x" [version]="" [path]="/tmp/a b")

段階2:要素アクセスと長さ

インデックスは 0 始まり。長さは ${#arr[@]}、要素の文字数は ${#arr[i]}

echo "${fruits[0]}"        # apple
echo "${#fruits[@]}"       # 要素数
echo "${#fruits[3]}"       # 3番目の「文字数」

段階3:安全な走査と渡し方

要素単位で処理したい場面がほとんどです。常に [@]+クォートが基本形。

for f in "${fruits[@]}"; do
  printf 'item=[%s]\n' "$f"
done

# 別コマンドへ安全に渡す
printf '%s\n' "${fruits[@]}" | sort

配列全体を1 行に連結したいときだけ [*] を使います(IFS 既定値に依存)。

( IFS=','; printf '%s\n' "${fruits[*]}" )   # apple,banana,,dragon fruit

段階4:追加・挿入・スライス

末尾追加は +=()。スライスは ${arr[@]:start:len}

fruits+=("elder berry")
printf '%s\n' "${fruits[@]:1:2}"    # 1 から 2 要素

任意位置に挿入したい場合はスライスで組み立てます。

insert_at() { # name index value
  local -n a="$1"; local i="$2"; local v="$3"
  a=( "${a[@]:0:i}" "$v" "${a[@]:i}" )
}
insert_at fruits 2 "citrus"

段階5:連想配列の定石

キー存在は [[ -v 'map[key]' ]]、キーの列挙は "${!map[@]}"

declare -A cfg=([host]="db1" [port]="5432")
if [[ -v 'cfg[port]' ]]; then echo "port=${cfg[port]}"; fi

for k in "${!cfg[@]}"; do
  printf '%s=%s\n' "$k" "${cfg[$k]}"
done

デフォルト値はパラメータ展開で与えます。

printf '%s\n' "${cfg[user]:-guest}"

段階6:入出力(NUL セーフ)

ファイル名など改行・空白を含む要素の扱いは NUL 終端が堅牢です。

# 配列 → NUL 区切りで出力
printf '%s\0' "${fruits[@]}" | xargs -0 -I{} printf '<%s>\n' "{}"

# NUL 区切り入力 → 配列
mapfile -d '' -t items < <(find . -maxdepth 1 -print0)

段階7:デバッグ/ダンプ

declare -p が最強の友達です。配列の“構造”をそのまま表示します。

declare -p fruits cfg

失敗しやすい点(アンチパターン)

未クォートの展開で要素が分裂・欠落するのが典型です。
"${arr[@]}" を体で覚え、"$var" のクォートを徹底します。

  • for x in ${arr[@]}; do … ×(空要素や空白が壊れる)
    for x in "${arr[@]}"; do … ○
  • "${arr[*]}" を常用 … ×(結合になる)
    "${arr[@]}" を常用、[*]連結が目的の時だけ
  • 連想配列のキー列挙で "${map[@]}" を使う … ×(値が出る)
    "${!map[@]}"キーを取る

テストと品質(最小)

“コピペ可”テストブロックの例。代表的な空要素・空白・存在確認を検証します。

#!/usr/bin/env bash
set -Eeuo pipefail

fruits=("a" "" "b c")
[[ "${#fruits[@]}" -eq 3 ]] || { echo "len"; exit 1; }

joined="$( ( IFS=,; printf '%s' "${fruits[*]}" ) )"
[[ "$joined" == "a,,b c" ]] || { echo "join"; exit 1; }

out="$(for x in "${fruits[@]}"; do printf '<%s>' "$x"; done)"
[[ "$out" == "<a><><b c>" ]] || { echo "iterate"; exit 1; }

declare -A m=([k1]="v1" [k2]="")
[[ -v 'm[k2]' ]] || { echo "exists"; exit 1; }

echo "PASS"

運用設計(実務)

  • 入力リストは最初から配列として受け取り、処理の最後まで配列で持つと安全です。
  • 外部コマンドに渡す直前で printf '%s\0'xargs -0 とするだけで、ファイル名地雷を避けられます。
  • 関数の引数が“可変長”なら、**配列参照(nameref)**で受けると誤爆が減ります。
collect_unique() { # in-array-name out-array-name
  local -n in="$1" out="$2"; declare -A seen=()
  out=()
  for e in "${in[@]}"; do [[ ${seen["$e"]+x} ]] || { out+=("$e"); seen["$e"]=1; }; done
}

互換性と移植性

  • 連想配列は Bash 4+。macOS の古い Bash(3.x)では使えません。
  • mapfilereadarray)は Bash 4+。Bash 3.x 互換が必要なら while IFS= read -r で代替。
  • BusyBox などの簡易シェル環境では挙動が異なるため、シェバンを Bash に固定してください。

セキュリティと安全設計

  • 配列要素をコマンド引数に展開するときは常にクォートcmd "${arr[@]}")。
  • 連想配列のキーを外部入力からそのまま受けない(正規表現で許可集合を作る)。
  • unset -v arr で配列全消去する場面は、意図をコメントに残す(誤ってスコープ外参照を防ぐ)。

パフォーマンスの勘所(短く)

  • 巨大配列のソート・ユニークは外部コマンド(sort, uniq)に任せ、入出力は NUL 終端で橋渡し。
  • ループ内での配列結合(arr=( "${arr[@]}" "$x" ))はコピーが走るため、末尾追加 +=() を使う。

参考実装(拡張版・コピペ可)

代表的な配列ユーティリティをまとめた小さなライブラリです。

# array_utils.sh
#!/usr/bin/env bash
set -Eeuo pipefail

# 加工: ユニーク(順序保持)
array_unique() { # in-array-name out-array-name
  local -n in="$1" out="$2"; declare -A seen=(); out=()
  for e in "${in[@]}"; do [[ ${seen["$e"]+x} ]] || { out+=("$e"); seen["$e"]=1; }; done
}

# 検索: 含む?
array_contains() { # in-array-name needle
  local -n in="$1"; local needle="$2"
  for e in "${in[@]}"; do [[ "$e" == "$needle" ]] && return 0; done; return 1
}

# 変換: 改行区切り→配列(空要素保持)
nl_to_array() { # out-array-name
  local -n out="$1"; mapfile -t out
}

# 出力: 配列→NUL区切り(安全)
array_print0() { # in-array-name
  local -n in="$1"; printf '%s\0' "${in[@]}"
}
# demo.sh
#!/usr/bin/env bash
set -Eeuo pipefail
. ./array_utils.sh

a=("a" "b" "a" " c ")
array_unique a uniq
declare -p uniq

array_contains uniq "b" && echo yes || echo no

printf 'x\ny\n\ny\n' | nl_to_array from_stdin
declare -p from_stdin

array_print0 from_stdin | xargs -0 -I{} printf '<%s>\n' "{}"

よくある質問(Q&A)

Q. "${arr[@]}""${arr[*]}" の違いは?
A. [@]要素単位(空要素も保持)。[*]1 個に結合(IFS 区切り)。通常は [@] を使い、結合が目的の時だけ [*]

Q. 連想配列で空文字 "" と未定義の違いを判定したい。
A. [[ -v 'map[key]' ]] が存在確認。存在して値が空かは [[ ${map[key]+x} ]] と分けて判定できます。

Q. ファイルリストを安全に渡すには?
A. mapfile -d '' -t files < <(find . -print0) で配列化し、cmd "${files[@]}"。もしくは printf '%s\0' | xargs -0

参考リンク

スポンサーリンク
Bash玄

はじめまして!Bash玄です。

エンジニアとしてシステム運用に携わる中で、手作業の多さに限界を感じ、Bashスクリプトを活用して業務を効率化したのがきっかけで、この道に入りました。「手作業は負け」「スクリプトはシンプルに」をモットーに、誰でも実践できるBashスクリプトの書き方を発信しています。

このサイトでは、Bashの基礎から実践的なスクリプト作成まで、初心者でもわかりやすく解説しています。少しでも「Bashって便利だな」と思ってもらえたら嬉しいです!

# 好きなこと
- シンプルなコードを書くこと
- コマンドラインを快適にカスタマイズすること
- 自動化で時間を生み出すこと

# このサイトを読んでほしい人
- Bashに興味があるけど、何から始めればいいかわからない人
- 定型業務を自動化したい人
- 効率よくターミナルを使いこなしたい人

Bashの世界に一歩踏み出して、一緒に「Bash道」を極めていきましょう!

Bash玄をフォローする