mapfile(readarray)コマンドを使ってファイルや標準入力の内容を配列変数に読み込む

2023-03-27Bash

はじめに

Bash シェルスクリプトで標準入力からの情報を取り扱う read コマンドと declare コマンドを組み合わせると便利 | ゲンゾウ用ポストイット で取り上げましたが、 read -a 変数名 とすることで標準入力から文字列を読み込み 空白で分割し変数に配列として代入することができます

$ read -a VARS <<<"1 3 5"

$ echo ${VARS[0]}
1

$ echo ${VARS[1]}
3

$ echo ${VARS[2]}
5

read -a は "列で区切って変数に入れる" ためのコマンドです。

今回は "行で区切って変数に入れる" ためのコマンドである mapfiles ( エイリアスとして readarray が用意されている ) を紹介します。

検証環境

$ uname -moi
x86_64 x86_64 GNU/Linux

$ bash -version | head -n 1
GNU bash, version 4.2.46(2)-release (x86_64-redhat-linux-gnu)

mapfile コマンドの使い方

mapfile コマンドの使い方を見ていきますが、 挙動を確認するため適当なデータファイル data.txt を作成しておきます。

$ seq 1 20 | xargs -n 5 echo > data.txt

$ cat data.txt
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20

配列変数に代入

まずはオプション無しで使う場合について。

指定された名前で変数を作成します。
その後、 標準入力から読み込んだ情報を改行で区切り、配列として変数に代入します。

$ mapfile LINES < data.txt

$ echo "${LINES[0]}"
1 2 3 4 5

$ echo "${LINES[1]}"
6 7 8 9 10

$ echo "${LINES[3]}"
16 17 18 19 20

# 要素外の配列変数は空
$ echo "${LINES[4]}"

よく見ると、 echo した後に随分行が空いているのがわかります。
declare -p で配列の中身を確認してみると、各要素 ( 4 つの要素がありますね ) には行末に改行が含まれている事がわかります。

改行で分割してくれますが、区切り文字である改行は取り除かれない点に注意が必要です

$ declare -p LINES
declare -a LINES='([0]="1 2 3 4 5
" [1]="6 7 8 9 10
" [2]="11 12 13 14 15
" [3]="16 17 18 19 20
")'

-t オプションを付与して改行を除去する

-t オプションを付与すると配列要素の行末から改行が除去されます。

$ mapfile -t LINES < data.txt

$ echo "${MAPFILE[0]}"
1 2 3 4 5

$ echo "${MAPFILE[1]}"
6 7 8 9 10

$ declare': declare -p LINES
declare -a LINES='([0]="1 2 3 4 5" [1]="6 7 8 9 10" [2]="11 12 13 14 15" [3]="16 17 18 19 20")'

こちらのほうが使い勝手はよいです。
-t オプションは常につけておいて良いでしょう。

名前を指定せずに配列変数に代入

変数名を指定しなかった場合には、 MAPFILE 変数に代入されます。

$ mapfile < data.txt

$ echo "${MAPFILE[2]}"
11 12 13 14 15

$ echo "${MAPFILE[3]}"
16 17 18 19 20

MAPFILE 変数のように特定のコマンドを実行した結果、固定名の変数に値が代入されるケースはいくつかあります。

Bash シェルスクリプト上での read コマンドの便利な使い方いろいろ | ゲンゾウ用ポストイット で紹介していますが、 read コマンドにおける REPLY 変数のようなものです。

$ cat data.txt| while read -r; do echo "* ${REPLY}"; done
* 1 2 3 4 5
* 6 7 8 9 10
* 11 12 13 14 15
* 16 17 18 19 20

-s オプションを付与して読み込むデータの開始位置を指定

-s オプションを付与すると、読み込むデータの開始位置を指定できます。(先頭から始める場合は 0 )

$ mapfile -s 2 LINES < data.txt

$ declare -p LINES
declare -a LINES='([0]="11 12 13 14 15
" [1]="16 17 18 19 20
")'

tails コマンドと組み合わせられれば以下と同じです。

( 長いので -s オプションを使ったほうがいいですね。 )

$ mapfile LINES < <(tail -n +3 data.txt)

$ declare -p LINES
declare -a LINES='([0]="11 12 13 14 15
" [1]="16 17 18 19 20
")'

-n オプションを付与して読み込むデータの終了位置を指定

-s とは逆に、 -n を指定して読み込むデータの終了位置を指定できます。

$ mapfile -n 2 LINES < data.txt

$ declare -p LINES
declare -a LINES='([0]="1 2 3 4 5
" [1]="6 7 8 9 10
")'

-C オプションでコールバック関数を呼び出し代入前にデータ加工する

Bash コマンド または コールバック関数を使って、読み込みデータの各行に対して処理を行います。

コマンド、コールバック関数いずれの場合でも 第 0 引数に行番号第 1 引数にデータファイルのデータ が格納されます。

$ callback () { echo "$1 : $2"; }

$ mapfile -t -C callback -c 1 LINES < data.txt
0 : 1 2 3 4 5
1 : 6 7 8 9 10
2 : 11 12 13 14 15
3 : 16 17 18 19 20

$ declare -p LINES
declare -a LINES='([0]="1 2 3 4 5" [1]="6 7 8 9 10" [2]="11 12 13 14 15" [3]="16 17 18 19 20")'

ひとこと

便利そうな気もしなくないですが、シェルスクリプトで配列にデータを格納して操作するケースがあまり多くはないですし、まして行数の多いテキストデータは sedawk に活躍してもらうことが多いです。

メモリに読み込むと OOM エラーになりますしね。

2023-03-27Bash