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

2021-03-11Bash

はじめに

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

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

$ echo ${VARS[0]}
1

$ echo ${VARS[1]}
3

$ echo ${VARS[2]}
5

"縦に区切り変数に入れる" というイメージになりますが、今回取り上げるのは "横に区切り変数に入れる" 、 "改行で区切り変数に入れる" ためのコマンドである mapfiles ( エイリアスとして readarrays ) を取り上げます。

検証環境

$ 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
")'

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

変数名を指定しなかった場合には、 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

-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")'

こちらのほうがより使い勝手が良さそうですね。

-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 LINESe -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エラーになりますしね。

2021-03-11Bash