この記事の狙い
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)では使えません。
mapfile(readarray)は 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。
