シェルスクリプトのreadarrayでファイルを配列に読み込む設計|mapfileとの違い・サブシェル問題・実践パターンを解説

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)シェルスクリプト > シェルスクリプトのreadarrayでファイルを配列に読み込む設計|mapfileとの違い・サブシェル問題・実践パターンを解説
「while readでファイルを1行ずつ処理していたら、前の行と比較したい場面が出てきた。全行を配列に入れてからまとめて処理したい」
「ログファイルの特定の行番号範囲だけ取り出したいが、while readだと途中でスキップする方法が面倒だ」

こうした場面で役立つのが readarray(別名 mapfile)コマンドです。ファイルや標準入力の全行を一括で配列に読み込むことで、インデックスによるランダムアクセス・スライシング・前後の行の参照が自由にできるようになります。

この記事では、readarray コマンドの使い方と実践的な設計パターンを、RHEL 9.4 / Ubuntu 24.04 LTS で動作確認したコードで解説します。while read との使い分け、CSVパース、サブシェル問題と回避策まで体系的に説明します。

この記事のポイント

readarray -t arr < file でファイル全行を配列に格納できる(-t は必須)
・while read がストリーム処理なのに対し、readarray は一括読み込みでランダムアクセスが前提
・パイプで渡すと配列が消える — プロセス置換 < <(cmd) で回避する
・bash 4.0以降が必要。RHEL 7以降・Ubuntu 14.04以降は標準で対応済み


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

while read と readarray を使い分ける理由

シェルスクリプトでファイルを読む方法として最もよく使われるのが while read ループです。1行ずつ処理するストリーム方式なので、大きなファイルでもメモリ使用量を最小限に抑えられます。ただし、「一度通り過ぎた行には戻れない」という制約があります。

以下のような要件が出てきたとき、while read では対応が難しくなります。

・前の行と現在の行を比べて差分を検出したい
・行番号で指定した範囲だけを取り出したい
・行数が数千行程度と分かっているファイルを何度も参照したい
・配列操作(スライシング・並べ替え・重複削除)を適用したい

こうしたケースには、readarray で全行を配列に読み込んでからインデックスで操作する設計が適しています。
比較項目 while read readarray
処理方式 ストリーミング(1行ずつ) 一括読み込み(全行を配列へ)
メモリ使用 少ない(行1つ分) ファイルサイズ分確保
ランダムアクセス 不可 可(インデックスで参照)
適したファイル規模 数百MB以上の大容量 小~中(数万行程度まで)
bash バージョン要件 どのバージョンでも動作 bash 4.0以降
大容量のアクセスログを集計するなら while read、設定ファイルや数千行のサーバーリストをランダムアクセスして処理するなら readarray、と使い分けるのが設計の基本です。

readarray の基本的な使い方

1. ファイルを配列に読み込む(基本形)

最もシンプルな使い方は、リダイレクトでファイルを渡すだけです。

# 基本形: /etc/hosts の全行を lines 配列に読み込む readarray -t lines < /etc/hosts # 行数の確認 echo "行数: ${#lines[@]}" # 最初の行を参照 echo "${lines[0]}" # ループで全行を処理 for line in "${lines[@]}"; do echo "$line" done

実機での確認結果(RHEL 9.4):

[tomohiro@server01 ~]$ readarray -t lines < /etc/hosts [tomohiro@server01 ~]$ echo "行数: ${#lines[@]}" 行数: 6 [tomohiro@server01 ~]$ echo "${lines[0]}" 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 [tomohiro@server01 ~]$ echo "${lines[1]}" ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6

-t オプションはほぼ必須と考えてください。付けないと各要素の末尾に改行文字(\n)が含まれたまま格納されます。後続の文字列比較や出力で「改行が混入している」バグの温床になるため、常に -t を付ける習慣をつけてください。

2. コマンド出力を配列に読み込む(プロセス置換)

コマンドの出力を配列に読み込むときは、プロセス置換(< <(コマンド))を使います。

# nginxプロセスの一覧を配列に読み込む readarray -t procs < <(ps aux | grep nginx | grep -v grep) echo "nginx プロセス数: ${#procs[@]}" # find の結果を配列に読み込む readarray -t logfiles < <(find /var/log -name '*.log' -mtime -7) echo "直近7日のログファイル数: ${#logfiles[@]}" for f in "${logfiles[@]}"; do echo " $f" done

なぜパイプ(|)ではなくプロセス置換を使うのかは、後半のトラブルシュートセクションで詳しく説明します。

3. -n・-s・-O オプションで読み込みを細かく制御する

# ヘッダ行(1行目)をスキップして2行目以降を読み込む readarray -t -s 1 data < servers.csv # 先頭から最大10行だけ読み込む readarray -t -n 10 first_ten < access.log # 21行目から10行分(21~30行目)を取得 readarray -t -s 20 -n 10 chunk < bigfile.txt # 複数ファイルの内容を1つの配列に追記結合(-O で開始インデックスを指定) declare -a all_lines for f in /var/log/app/app-202609*.log; do readarray -t -O "${#all_lines[@]}" all_lines < <(grep 'ERROR' "$f") done echo "エラー合計: ${#all_lines[@]} 件"

オプション 意味 使う場面
readarray -t arr < file 末尾の改行を削除して読み込む 通常は必ず付ける
readarray -t -n N arr < file 先頭N行だけ読み込む 大きなログの先頭だけ確認
readarray -t -s N arr < file 先頭N行をスキップ CSVのヘッダ行スキップ
readarray -t -O N arr < file インデックスN番目から追記 複数ファイルを1配列に結合
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、20年以上の運用経験を持つ現役エンジニアが基礎から教えます。
シェルスクリプト講座を見る >>

実務設計パターン

1. CSVを行ごとにフィールド分解して処理する

readarray は行単位で読み込みます。1行をカンマ区切りのフィールドに分解するには、IFS 分割(IFS=',' read -ra fields <<< "$row")を組み合わせます。

#!/bin/bash set -eu # servers.csv の内容例: # hostname,ip,role # server01,192.168.1.10,web # server02,192.168.1.11,db CSVFILE="servers.csv" # ヘッダ行(1行目)をスキップして読み込む readarray -t -s 1 rows < "$CSVFILE" for row in "${rows[@]}"; do # カンマで区切ってフィールドに分解 IFS=',' read -ra fields <<< "$row" hostname="${fields[0]}" ip="${fields[1]}" role="${fields[2]}" echo "ホスト: $hostname IP: $ip 役割: $role" done

実機での出力結果:

[tomohiro@server01 ~]$ bash parse_csv.sh ホスト: server01 IP: 192.168.1.10 役割: web ホスト: server02 IP: 192.168.1.11 役割: db

ここで使っている <<< "$row"(ヒアストリング)はコマンドに文字列を直接入力として渡す構文です。サブシェルを作らないため、内部で変数を書き換えても親シェルに反映されます。

2. 配列スライシングで必要な行範囲だけ処理する

bash の配列は ${arr[@]:start:count} でスライシングができます。readarray で全行を読んだ後、必要な範囲だけを取り出せます。

#!/bin/bash set -eu readarray -t lines < /var/log/messages total=${#lines[@]} echo "全行数: $total" # 最後の10行を取得(tail コマンド相当) echo "--- 末尾10行 ---" for line in "${lines[@]: -10}"; do echo "$line" done # 特定インデックス: 101~200行目を取得 echo "--- 101~200行目 ---" for line in "${lines[@]:100:100}"; do echo "$line" done # 前の行と現在の行を比較して差分行を検出 prev="" for line in "${lines[@]}"; do if [[ "$line" != "$prev" ]]; then echo "変化: $line" fi prev="$line" done

スライシングの構文まとめ:

${arr[@]:2} : インデックス2番目以降を全部取得
${arr[@]:2:5} : インデックス2番目から5個を取得
${arr[@]: -10} : 末尾10要素を取得(マイナスに空白が必要)

3. 複数ファイルをまとめて1配列に結合する

#!/bin/bash set -eu declare -a all_errors=() # 直近7日のアプリログから ERROR 行を1つの配列に集約 for logfile in /var/log/app/app-2026*.log; do [[ -f "$logfile" ]] || continue # -O で現在の配列サイズを開始インデックスに指定 readarray -t -O "${#all_errors[@]}" all_errors < <(grep 'ERROR' "$logfile" 2>/dev/null || true) done echo "ERRORログ合計: ${#all_errors[@]} 件" # エラー種別ごとに集計 declare -A error_counts=() for line in "${all_errors[@]}"; do # "ERROR [カテゴリ]" の形式からカテゴリを抽出 if [[ "$line" =~ ERROR\ \[([^]]+)\] ]]; then category="${BASH_REMATCH[1]}" error_counts["$category"]=$(( ${error_counts["$category"]:-0} + 1 )) fi done for category in "${!error_counts[@]}"; do echo " $category: ${error_counts[$category]} 件" done

よくある落とし穴とトラブルシュート

1. パイプで渡すと配列が空になる(サブシェル問題)

# NG: パイプの右辺はサブシェルで実行される cat /etc/hosts | readarray -t lines echo "${#lines[@]}" # → 0(空になる!) # OK: プロセス置換を使う readarray -t lines < <(cat /etc/hosts) echo "${#lines[@]}" # → 正常な行数が表示される

bash のパイプ(|)でつないだコマンドは子プロセス(サブシェル)で実行されます。サブシェルは親シェルとは独立した変数空間を持つため、サブシェル内で作成した配列は親シェルには引き継がれません。パイプの右辺で readarray を実行すると、配列が格納されるのはサブシェルの中だけで、パイプが終わった時点で消えます。

プロセス置換(< <(コマンド))はコマンドを子プロセスで実行しますが、その出力を親シェルのリダイレクト入力として渡すため、配列は親シェルに正常に格納されます。コマンド出力を配列に読み込む場合は、常にこの形を使ってください。

なお、bash 4.2以降では lastpipe オプションを有効にすることでパイプ最後のコマンドを親シェルで実行できますが、互換性の観点からプロセス置換の方がお勧めです。

2. macOS のデフォルト bash は readarray に対応していない

# bash バージョンの確認 bash --version # GNU bash, version 5.2.26(1)-release (x86_64-redhat-linux-gnu) # RHEL 9.4 / Ubuntu 24.04 LTS は bash 5.x が標準 # → readarray は問題なく使える

macOS 10.14 Mojave 以前のデフォルト bash はApple配布の bash 3.2(GPLv2 ライセンス問題から更新されない)で、readarray に対応していません。開発機が macOS の場合は bash --version でバージョンを確認し、必要に応じて brew install bash でbash 5.x をインストールしてください。

本番サーバーが RHEL 7以降・Ubuntu 14.04以降であれば bash 4.3以上が標準インストールされているため、readarray の利用に問題はありません。

3. 空ファイルや存在しないファイルへの対処

#!/bin/bash set -eu DATAFILE="input.txt" # ファイル存在確認 if [[ ! -f "$DATAFILE" ]]; then echo "エラー: $DATAFILE が見つかりません" >&2 exit 1 fi readarray -t lines < "$DATAFILE" # 空ファイルへの対処(行数0の場合) if [[ ${#lines[@]} -eq 0 ]]; then echo "ファイルが空です" >&2 exit 0 fi echo "読み込んだ行数: ${#lines[@]}"

set -eu を有効にしている場合、空ファイルを readarray に渡しても処理は続行します(readarray のエラーではないため)。ただし ${#lines[@]} が 0 になるため、後続処理の前に行数チェックを入れておくのが安全です。

4. -d オプション(NUL区切り)は bash 4.4以降が必要

# bash 4.4以降: NUL文字区切りで読み込む(find -print0 と組み合わせ) readarray -t -d '' files < <(find /var/log -name '*.log' -print0) # bash 4.3以前(RHEL 7など)での代替手段 while IFS= read -r -d '' f; do files+=("$f") done < <(find /var/log -name '*.log' -print0)

スペースを含むファイル名を安全に扱う場合は NUL 区切りの -d '' が便利ですが、RHEL 7(bash 4.2)では使えません。対象環境の bash バージョンを確認した上で使用してください。

本記事のまとめ

やりたいこと コマンド
ファイル全行を配列に読む readarray -t arr < file
コマンド出力を配列に読む readarray -t arr < <(cmd)
ヘッダ行をスキップして読む readarray -t -s 1 arr < file
先頭N行だけ読む readarray -t -n N arr < file
配列の行数を確認 echo "${#arr[@]}"
配列の末尾10行を参照 "${arr[@]: -10}"
複数ファイルを1配列に追記結合 readarray -t -O "${#arr[@]}" arr < <(cmd)
CSV1行をフィールドに分解 IFS=',' read -ra fields <<< "$row"
readarray はwhile readのような「1行ずつストリームで処理」ではなく、「まとめて読んで何度も参照する」場面の道具です。ランダムアクセスが必要な設定ファイルの処理、スライシングで行範囲を切り出す操作、複数ファイルのエラーログ集約などに積極的に活用してください。大容量ファイルのストリーム処理では while read と、処理方式に応じて使い分けることがシェルスクリプト設計の品質を上げます。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、20年以上の運用経験を持つ現役エンジニアが基礎から教えます。
シェルスクリプト講座を見る >>

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。