【Bash】シェルスクリプトでの配列の使い方まとめ|宣言・追加・ループ処理・要素数を徹底解説

スクリプト設計

シェルスクリプト(Bash)で複数のファイル名、URLリスト、設定項目などを一括管理・処理したいときに必須となるのが配列(Array)です。

Bashの配列は、宣言(array=("val1" "val2"))、要素の追加(array+=("val3"))、要素数の取得(${#array[@]})、全要素の展開("${array[@]}")など、他のプログラミング言語とは少し異なるシェル特有の記法を持っています。正しく理解していないと、「スペースを含むファイル名が勝手に分割される」「要素を削除したらループ処理が壊れた」といったトラブルに直面しがちです。

この記事では、Bash/シェルスクリプトにおける配列の基本操作から、実務で頻出する for ループ処理、mapfile を用いたコマンド出力の安全な配列化、連想配列(Key-Value)の扱い方まで、即コピペして使える実例とともに網羅的に解説します。

  1. 【早見表】Bash配列の基本構文チートシート
  2. 1. 配列の宣言と初期化
    1. 基本的な宣言と初期化
    2. スペースを含む要素の扱い(クォート必須)
  3. 2. 要素の参照と要素数(長さ)の取得
    1. インデックス指定での要素参照
    2. 要素数(配列の長さ)の取得
    3. インデックス番号(キー)の一覧取得
  4. 3. 要素の追加・上書き・削除
    1. 末尾に要素を追加する(+= 演算子)
    2. 指定したインデックスの値の上書き
    3. 要素の削除(unset)と「疎配列」の注意点
    4. 配列全体のクリア
  5. 4. 配列をループ(for文)で処理する3つの方法
    1. パターン1: 全要素を直接走査(最推奨・安全)
    2. パターン2: インデックス(キー)番号で走査
    3. パターン3: C言語スタイルのカウントループ
  6. 5. 【比較】「"${arr[@]}"」と「"${arr[*]}"」の違い
    1. 実証:スペース入り要素でのループ挙動の違い
    2. 応用:配列の要素をカンマ区切りで結合する(IFS活用)
  7. 6. 配列のスライス(部分切り出し)と検索・置換
    1. スライス(部分切り出し)構文
    2. 配列内の一括文字列置換
  8. 7. コマンド出力やファイルを配列に読み込む(mapfile / readarray)
    1. 【推奨】mapfile(readarray)を使った安全な読み込み
    2. 注意:array=($(command)) はなぜ非推奨なのか?
  9. 8. 連想配列(Key-Valueマップ)の使い方
    1. 連想配列の宣言と初期化(declare -A 必須)
    2. 連想配列のキーと値をループ処理する
  10. 9. 実務でよくある落とし穴とアンチパターン
    1. 落とし穴1: set -u 有効時に空配列を展開するとエラーになる
    2. 落とし穴2: 関数へ配列を渡す・戻す際の注意
  11. 10. 実務で即コピペできる実践スクリプト例
    1. 実践例1: 複数ホストへの一括死活監視・ヘルスチェック
    2. 実践例2: 対象ファイルの一括バックアップとリネーム
  12. まとめ
  13. 関連おすすめ記事
  14. 関連記事

【早見表】Bash配列の基本構文チートシート

よく使う配列操作の構文と実行例の早見表です。日常の開発やスクリプト作成時のリファレンスとしてご活用ください。

操作内容 構文・記法 実行例 / 説明
配列の初期化 arr=("a" "b" "c") 空白区切りで要素を並べる
空配列の宣言 arr=() / declare -a arr 要素が0個の空配列を作成
要素の参照 ${arr[0]} 0始まりのインデックスで参照(波括弧 {} 必須)
末尾要素の参照 ${arr[-1]} 末尾の要素を取得(Bash 4.2+)
全要素の展開(推奨) "${arr[@]}" 各要素を独立してクォート展開(空白を保護)
全要素の連結文字列 "${arr[*]}" 全要素をIFS(空白など)で結合した1つの文字列
要素数(長さ)の取得 ${#arr[@]} 配列に含まれる要素の総数を取得
要素の文字数取得 ${#arr[0]} 指定インデックスの要素の文字数を取得
インデックス一覧 "${!arr[@]}" 割り当てられているインデックス番号を一覧展開
要素の末尾追加 arr+=("d") 既存配列の末尾に新しい要素を追加
要素の上書き arr[1]="new" 指定したインデックスの値を更新
要素の削除 unset arr[1] 指定インデックスの要素を削除(歯抜けになる)
配列の全削除 unset arr または arr=() 配列全体を破棄または初期化
スライス(部分取得) "${arr[@]:1:2}" 開始位置と要素数を指定して部分切り出し
連想配列の宣言 declare -A map Key-Value形式の連想配列を宣言(Bash 4+)

1. 配列の宣言と初期化

Bashの通常の配列(インデックス配列)は、丸括弧 () の中にスペース区切りで値を並べて初期化します。

基本的な宣言と初期化

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

# 要素を指定して初期化
fruits=("apple" "banana" "orange")

# 空の配列を作成
empty_list=()
# または明示的に宣言
declare -a servers=()

スペースを含む要素の扱い(クォート必須)

要素の中にスペースやタブなどの空白文字が含まれる場合は、必ずダブルクォートまたはシングルクォートで囲む必要があります。

# OK: 空白を含む文字列を1要素として保持
files=("app.log" "access combined.log" "error.log")

# NG: "access" と "combined.log" の2要素に分裂してしまう
files=(app.log access combined.log error.log)

注意(カンマ区切りはNG):
JavaScriptやPythonなどの感覚で fruits=("apple", "banana", "orange") とカンマを入れると、カンマ自体が文字列の一部として扱われてしまいます(例: "apple,")。必ずスペースで区切ってください。

2. 要素の参照と要素数(長さ)の取得

配列の要素を参照するときは、必ず 波括弧 ${} を使用します。波括弧を省略して $fruits[0] と書くと、$fruits(=先頭要素)に文字列 "[0]" が結合された結果になってしまいます。

インデックス指定での要素参照

インデックスは 0 から始まります。

fruits=("apple" "banana" "cherry" "durian")

echo "先頭要素: ${fruits[0]}"      # apple
echo "2番目の要素: ${fruits[1]}"    # banana
echo "末尾の要素: ${fruits[-1]}"     # durian (Bash 4.2以降)
echo "末尾から2番目: ${fruits[-2]}"  # cherry

要素数(配列の長さ)の取得

配列全体の要素数を取得するには、変数名の先頭に # を付け、インデックス部分に @ を指定します(${#配列名[@]})。

fruits=("apple" "banana" "cherry")

echo "要素数: ${#fruits[@]}"       # 3

なお、インデックス番号を指定して ${#fruits[0]} と書いた場合は、その要素の文字列長(文字数)が返されます。

echo "fruits[0]の文字数: ${#fruits[0]}"  # 'apple' なので 5

インデックス番号(キー)の一覧取得

配列に割り当てられているすべてのインデックス番号を取得するには、変数名の先頭に ! を付けます(${!配列名[@]})。

fruits=("apple" "banana" "cherry")

echo "インデックス一覧: ${!fruits[@]}"  # 0 1 2

3. 要素の追加・上書き・削除

作成済みの配列に対して、要素を後から追加したり、特定の値を変更・削除することができます。

末尾に要素を追加する(+= 演算子)

配列に要素を追加する最も一般的な方法は += 演算子を使うことです。丸括弧で囲んで追加します。

fruits=("apple" "banana")

# 1要素を追加
fruits+=("orange")

# 複数要素をまとめて追加
fruits+=("grape" "mango")

echo "全要素: ${fruits[@]}"  # apple banana orange grape mango

指定したインデックスの値の上書き

特定のインデックスに直接代入することで値を上書きできます。また、まだ存在しないインデックスに直接代入することも可能です。

fruits[1]="blueberry"  # 'banana' を 'blueberry' に上書き
fruits[10]="kiwi"      # インデックス10に直接セット(疎配列になる)

要素の削除(unset)と「疎配列」の注意点

特定の要素を削除するには unset コマンドを使用します。

fruits=("apple" "banana" "cherry")

# インデックス1(banana)を削除
unset 'fruits[1]'

echo "要素数: ${#fruits[@]}"       # 2
echo "インデックス一覧: ${!fruits[@]}"  # 0 2 (1が抜けている!)

重要:インデックスの歯抜け(疎配列 / Sparse Array)
unset で要素を削除しても、後ろの要素は自動的に前詰めされません。インデックスが 0, 2 のように歯抜け(疎配列)になります。
そのため、後述の for ((i=0; i<len; i++)) のような連番ループを使うと、削除されたインデックスを参照して空文字を処理してしまう原因になります。要素走査には必ず for item in "${fruits[@]}" または for i in "${!fruits[@]}" を使用しましょう。

配列全体のクリア

配列全体を空にしたい場合は、空括弧を代入するか unset を実行します。

# 空にする(変数定義自体は残す)
fruits=()

# 変数ごと完全に破棄する
unset fruits

4. 配列をループ(for文)で処理する3つの方法

配列に格納した各要素を順次処理するパターンを3つ解説します。実務ではパターン1が最も安全で多用されます。

パターン1: 全要素を直接走査(最推奨・安全)

"${配列名[@]}" をダブルクォートで囲んで for 文に渡します。空白や特殊文字が含まれる要素も1つずつ安全に取り出すことができます。

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

servers=("web-01.example.com" "web-02.example.com" "db-01.example.com")

for server in "${servers[@]}"; do
    echo "Processing server: ${server}"
    # ping -c 1 "$server" 等の処理
done

パターン2: インデックス(キー)番号で走査

"${!配列名[@]}" を使うことで、配列に存在する実際のインデックス番号だけを走査できます。要素を削除してインデックスが歯抜けになっている場合でも安全に動作します。

fruits=("apple" "banana" "cherry")
unset 'fruits[1]'  # bananaを削除(インデックスは 0 と 2)

for i in "${!fruits[@]}"; do
    echo "Index ${i}: ${fruits[i]}"
done

実行結果:

Index 0: apple
Index 2: cherry

パターン3: C言語スタイルのカウントループ

インデックス番号 0 から 要素数-1 までインクリメントして処理します。連続したインデックスであることが保証されている場合に有効です。

items=("Step 1" "Step 2" "Step 3")
total=${#items[@]}

for ((i=0; i

5. 【比較】「"${arr[@]}"」と「"${arr[*]}"」の違い

配列の全要素を展開する際、@* の違いを正しく理解することはシェルスクリプトのバグを防ぐ上で極めて重要です。

記法 展開の挙動 スペース入り要素の扱い 実務での推奨用途
"${arr[@]}"
(ダブルクォートあり)
各要素を個別にクォートして展開
"item1" "item2" "item 3"
安全(空白が維持され要素が分裂しない) ループ処理・コマンド引き渡しの標準(最推奨)
"${arr[*]}"
(ダブルクォートあり)
全要素をIFS(通常は空白)で連結した1つの巨大な文字列に合体
"item1 item2 item 3"
1つの文字列になる ログ出力やカンマ区切り文字列の生成
${arr[@]} / ${arr[*]}
(クォートなし)
単語分割(Word Splitting)が発生 危険(空白で要素が勝手に分割される) 非推奨

実証:スペース入り要素でのループ挙動の違い

#!/usr/bin/env bash

list=("Tokyo Station" "Shinjuku Station" "Shibuya Station")

echo "=== 【推奨】 \"\${list[@]}\" の場合 ==="
for item in "${list[@]}"; do
    echo "要素: [${item}]"
done

echo ""
echo "=== \"\${list[*]}\" の場合 ==="
for item in "${list[*]}"; do
    echo "要素: [${item}]"
done

実行結果:

=== 【推奨】 "${list[@]}" の場合 ===
要素: [Tokyo Station]
要素: [Shinjuku Station]
要素: [Shibuya Station]

=== "${list[*]}" の場合 ===
要素: [Tokyo Station Shinjuku Station Shibuya Station]

このように、"${list[@]}" は3回正しくループしますが、"${list[*]}" は1つの文字列として連結されるため1回しかループしません。

応用:配列の要素をカンマ区切りで結合する(IFS活用)

"${arr[*]}" の特性を利用し、特殊変数 IFS(内部フィールド区切り文字)を一時的に変更することで、配列要素をカンマやハイフン区切りで簡単に結合できます。

members=("Alice" "Bob" "Charlie")

# サブシェル内でIFSをカンマに変更して連結
joined=$(IFS=, ; echo "${members[*]}")
echo "CSV形式: ${joined}"  # Alice,Bob,Charlie

6. 配列のスライス(部分切り出し)と検索・置換

Bashの配列では、部分的な要素の切り出しや文字列置換を簡単に行うことができます。

スライス(部分切り出し)構文

構文: "${配列名[@]:開始インデックス:要素数}"

letters=("A" "B" "C" "D" "E" "F")

# インデックス1から3個の要素を取り出す
echo "${letters[@]:1:3}"   # B C D

# インデックス3から末尾まですべて取り出す(要素数を省略)
echo "${letters[@]:3}"     # D E F

配列内の一括文字列置換

構文: "${配列名[@]/置換前パターン/置換後文字列}"

files=("image1.png" "image2.png" "photo.jpg")

# 全要素の .png を .webp に置換して展開
new_files=("${files[@]/.png/.webp}")

echo "${new_files[@]}"  # image1.webp image2.webp photo.jpg

7. コマンド出力やファイルを配列に読み込む(mapfile / readarray)

実務で非常によくあるのが、「findls、テキストファイルの行一覧を配列に読み込みたい」というケースです。

【推奨】mapfile(readarray)を使った安全な読み込み

Bash組み込みの mapfile(別名 readarray)を使うと、改行区切りの入力を1行=1要素として安全かつ高速に配列へ読み込めます。末尾の改行コードを取り除くため、必ず -t オプションを付けます。

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

# コマンドの出力を配列に格納(プロセス置換を使用)
mapfile -t conf_files < <(find /etc -maxdepth 1 -name "*.conf")

echo "見つかった設定ファイル数: ${#conf_files[@]}"
for f in "${conf_files[@]}"; do
    echo "File: $f"
done

テキストファイルを読み込む場合もリダイレクトで簡単に書けます:

# ファイルから1行ずつ配列に読み込む
mapfile -t targets < /path/to/servers.txt

注意:array=($(command)) はなぜ非推奨なのか?

古いスクリプトでよく見かける files=($(ls *.txt)) のような書き方は重大な危険を孕んでいます。

  • ファイル名にスペースが含まれている場合、そこで意図せず分割されて配列の要素が壊れる
  • ファイル名にワイルドカード文字(*?)が含まれる場合、パス名展開(Globbing)が暴発する

コマンド出力を配列に取り込む際は、常に mapfile -t または while IFS= read -r line; do array+=("$line"); done を使用してください。

8. 連想配列(Key-Valueマップ)の使い方

Bash 4.0以降では、文字列をキーにして値を保持できる連想配列(Associative Array)がサポートされています。

連想配列の宣言と初期化(declare -A 必須)

連想配列を使うには、事前に declare -A での明示的な宣言が必須です。

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

# 連想配列の宣言と初期化
declare -A user_roles=(
    ["alice"]="admin"
    ["bob"]="editor"
    ["charlie"]="viewer"
)

# 要素の参照
echo "Alice's role: ${user_roles["alice"]}"  # admin

# 新しいキーと値の追加・更新
user_roles["dave"]="guest"

# キーの存在確認(Bash 4.2+ の [[ -v ]])
if [[ -v user_roles["alice"] ]]; then
    echo "Alice exists in user_roles"
fi

連想配列のキーと値をループ処理する

キー一覧を取得する "${!user_roles[@]}" をループで回し、そのキーを使って値を取り出します。

for user in "${!user_roles[@]}"; do
    role="${user_roles[$user]}"
    echo "User: ${user} -> Role: ${role}"
done

9. 実務でよくある落とし穴とアンチパターン

Bash配列を扱う際に引っかかりやすい代表的な落とし穴と、その回避策をまとめました。

落とし穴1: set -u 有効時に空配列を展開するとエラーになる

Bashスクリプトで安全のために set -u(未定義変数の参照でエラー終了)を設定している場合、空の配列に対して "${empty_arr[@]}" を展開すると、古いBashバージョン(Bash 4.3以前)では「unbound variable」エラーが発生します。

対策:
Bash 4.4以降では解消されていますが、下位互換性を保つ必要がある場合は以下のようなデフォルト値構文を用います。

# 空配列でも set -u で安全に展開するイディオム
for item in ${empty_arr[@]+"${empty_arr[@]}"}; do
    echo "$item"
done

落とし穴2: 関数へ配列を渡す・戻す際の注意

Bashの関数には、配列をそのまま値として渡すことができません(渡すと全要素がフラットな引数 $1, $2... に分解されます)。

対策(Bash 4.3以降の「参照渡し」 local -n):

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

# 名前参照(nameref)を使って配列を受け取る関数
print_elements() {
    local -n arr_ref="$1"  # 第1引数に渡された変数名を参照
    for item in "${arr_ref[@]}"; do
        echo "Item: $item"
    done
}

my_data=("alpha" "beta" "gamma")
print_elements my_data

10. 実務で即コピペできる実践スクリプト例

現場でそのまま使える実用的なスクリプトテンプレートを紹介します。

実践例1: 複数ホストへの一括死活監視・ヘルスチェック

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

# 監視対象サーバーの配列
TARGET_HOSTS=(
    "192.168.1.10"
    "192.168.1.11"
    "192.168.1.20"
    "gateway.local"
)

SUCCESS_COUNT=0
FAILED_HOSTS=()

echo "=== サーバー死活監視開始 ==="

for host in "${TARGET_HOSTS[@]}"; do
    if ping -c 1 -W 2 "$host" > /dev/null 2>&1; then
        echo "[OK] $host は正常に応答しています"
        ((SUCCESS_COUNT++))
    else
        echo "[NG] $host への疎通に失敗しました" >&2
        FAILED_HOSTS+=("$host")
    fi
done

echo "-----------------------------------"
echo "結果: ${SUCCESS_COUNT}/${#TARGET_HOSTS[@]} 成功"

if [ "${#FAILED_HOSTS[@]}" -gt 0 ]; then
    echo "エラー発生ホスト一覧: ${FAILED_HOSTS[*]}" >&2
    exit 1
fi

echo "すべてのホストが正常です。"

実践例2: 対象ファイルの一括バックアップとリネーム

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

BACKUP_DIR="./backups_$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"

# ログファイル一覧を安全に配列へ取得
mapfile -t LOG_FILES < <(find . -maxdepth 1 -name "*.log" -type f)

if [ "${#LOG_FILES[@]}" -eq 0 ]; then
    echo "対象の .log ファイルが存在しません。"
    exit 0
fi

echo "バックアップ対象ファイル数: ${#LOG_FILES[@]}"

for file in "${LOG_FILES[@]}"; do
    base_name=$(basename "$file")
    dest="${BACKUP_DIR}/${base_name}.bak"
    cp -p "$file" "$dest"
    echo "Backed up: $file -> $dest"
done

echo "バックアップ完了: $BACKUP_DIR"

まとめ

Bash/シェルスクリプトにおける配列操作の重要ポイントをまとめます。

  • 宣言・追加: arr=("a" "b") で初期化し、arr+=("c") で末尾追加する
  • 要素数の取得: ${#arr[@]} で配列の長さ、${#arr[0]} で要素の文字数を取得する
  • ループ展開: 空白を含む要素の破壊を防ぐため、常に "${arr[@]}"(ダブルクォート付き) を使用する
  • コマンド出力の格納: mapfile -t arr < <(command) を使って安全かつ高速に行を配列化する
  • インデックス走査: 要素削除(unset)後の歯抜けにも対応できる for i in "${!arr[@]}" が安全
  • 連想配列: declare -A で明示宣言し、Key-Valueのマップ構造を活用する

配列を使いこなせるようになると、シェルスクリプトで扱えるデータの幅が広がり、複雑なバッチ処理や自動化タスクも安全かつ簡潔に記述できるようになります。本記事のチートシートとテンプレートをぜひ日々のスクリプト作成に役立ててください。

関連おすすめ記事

Bash玄

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

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

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

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

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

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

Bash玄をフォローする

コメント