「ログファイルの特定の行番号範囲だけ取り出したいが、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以降は標準で対応済み
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
while read と readarray を使い分ける理由
シェルスクリプトでファイルを読む方法として最もよく使われるのが while read ループです。1行ずつ処理するストリーム方式なので、大きなファイルでもメモリ使用量を最小限に抑えられます。ただし、「一度通り過ぎた行には戻れない」という制約があります。以下のような要件が出てきたとき、while read では対応が難しくなります。
・前の行と現在の行を比べて差分を検出したい
・行番号で指定した範囲だけを取り出したい
・行数が数千行程度と分かっているファイルを何度も参照したい
・配列操作(スライシング・並べ替え・重複削除)を適用したい
こうしたケースには、readarray で全行を配列に読み込んでからインデックスで操作する設計が適しています。
| 比較項目 | while read | readarray |
|---|---|---|
| 処理方式 | ストリーミング(1行ずつ) | 一括読み込み(全行を配列へ) |
| メモリ使用 | 少ない(行1つ分) | ファイルサイズ分確保 |
| ランダムアクセス | 不可 | 可(インデックスで参照) |
| 適したファイル規模 | 数百MB以上の大容量 | 小~中(数万行程度まで) |
| bash バージョン要件 | どのバージョンでも動作 | bash 4.0以降 |
readarray の基本的な使い方
1. ファイルを配列に読み込む(基本形)
最もシンプルな使い方は、リダイレクトでファイルを渡すだけです。# 基本形: /etc/hosts の全行を lines 配列に読み込む readarray -t lines < /etc/hosts # 行数の確認 echo "行数: ${#lines[@]}" # 最初の行を参照 echo "${lines[0]}" # ループで全行を処理 for line in "${lines[@]}"; do echo "$line" done
[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
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配列に結合 |
シェルスクリプト講座を見る >>
実務設計パターン
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[@]}" # → 正常な行数が表示される
|)でつないだコマンドは子プロセス(サブシェル)で実行されます。サブシェルは親シェルとは独立した変数空間を持つため、サブシェル内で作成した配列は親シェルには引き継がれません。パイプの右辺で 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 は問題なく使える
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)
本記事のまとめ
| やりたいこと | コマンド |
|---|---|
| ファイル全行を配列に読む | 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 と、処理方式に応じて使い分けることがシェルスクリプト設計の品質を上げます。シェルスクリプト講座を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 次のページへ:シェルスクリプトで設定ファイルを安全に更新する設計|バックアップ・原子的置換・trapロールバックの実装パターン
- 前のページへ:シェルスクリプトで設定値を3層優先順位(CLIオプション・環境変数・設定ファイル)で管理する設計
- この記事の属するカテゴリ:シェルスクリプトへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます