【mapfile】bash/シェルスクリプトマニアックコマンドあれこれ13

mapfile(マップファイル)

bash シェルの mapfile コマンドは、読み取り配列としてよく知られています。
主な目的は、標準入力行を読み取り、それらをインデックス付き配列変数に格納することです。
mapfile は、パイプではなく置換 (<) から読み取る必要があります。
さらに、読み取りループと比較して、mapfile ははるかに高速で便利なソリューションです。
コマンドの実行が成功した場合は 1 を返し、失敗した場合は 0 を返します。
配列名を指定しない場合、mapfile 変数がデフォルトの配列変数となります。

データテキストの準備

ここで簡単なデータテキスト用意します。

One
Two
Three

普通のやり方 while read パターン

このテキストを読み込んで配列にデータを格納したいと思います。
多少冗長ではありますが通常は以下のような感じになります。

#!/usr/bin/bash

DATAFILE="data.txt";  # データファイル
declare -a aLine;     # 配列の宣言
declare -i COUNT=0;   # カウンターの宣言
IFS=$'\n';            # 区切り文字を改行コードに指定
 
while read line;do
  # 1行ずつ読み込んだ内容 $line を配列に代入
  aLine[$COUNT]="$line";
  ((COUNT++));    # インクリメント
done<$DATAFILE    # ファイルの入力

echo "配列の内容すべてを表示"
echo ${aLine[@]}; # One Two Three
echo "添字の0を表示"
echo ${aLine[0]}; # One
echo "添字の1を表示"
echo ${aLine[1]}; # Two
echo "添字の2を表示"
echo ${aLine[2]}; # Three

実行結果は以下のとおりです。

bash-3.2$ bash array01.sh
配列の内容すべてを表示
One Two Three
添字の0を表示
One
添字の1を表示
Two
添字の2を表示
Three
bash-3.2$

普通のやり方 fileコマンドパターン

ファイルの読み込みを以下のようにすることもできますね。

#!/usr/bin/bash

DATAFILE="data.txt";  # データファイル
declare -a aLine;     # 配列の宣言
declare -i COUNT=0;   # カウンターの宣言
IFS=$'\n';            # 区切り文字を改行コードに指定
 
# ファイルを配列に読み込む
file=(`cat "$DATAFILE"`)
 
# 行ごとに繰り返し処理を実行
for line in "${file[@]}"; do
  # 1行ずつ読み込んだ内容 $line を配列に代入
  aLine[$COUNT]="$line";
  ((COUNT++));    # インクリメント
done


echo "配列の内容すべてを表示"
echo ${aLine[@]}; # One Two Three
echo "添字の0を表示"
echo ${aLine[0]}; # One
echo "添字の1を表示"
echo ${aLine[1]}; # Two
echo "添字の2を表示"
echo ${aLine[2]}; # Three

実行結果は以下のとおりです。

bash-3.2$ bash array02.sh
配列の内容すべてを表示
One Two Three
添字の0を表示
One
添字の1を表示
Two
添字の2を表示
Three
bash-3.2$

登場! mapfile を使う

なんと、ファイル読み込みや配列への代入にあれこれやっていましたが、mapfileを使うと1行で住みます。
COUNT変数といったカウンターや IFSといった定義も不要です。

One Two Three
Four Five Six
Seven Eight Nine
Ten
#!/usr/bin/bash

DATAFILE="data.txt";  # データファイル
declare -a aLine;     # 配列の宣言
 
# -t は行末の改行を除去
mapfile -t aLine < "$DATAFILE";

echo "配列の内容すべてを表示"
echo ${aLine[@]}; # One Two Three
echo "添字の0を表示"
echo ${aLine[0]}; # One
echo "添字の1を表示"
echo ${aLine[1]}; # Two
echo "添字の2を表示"
echo ${aLine[2]}; # Three

実行結果はいずれも同じですが以下のとおりです。

bash-3.2$ bash array03.sh
配列の内容すべてを表示
One Two Three
添字の0を表示
One
添字の1を表示
Two
添字の2を表示
Three
bash-3.2$

すごいですね。
配列に入れるだけならmapfileで十分です。しかも読み込み専用ということもあり、読み込み速度は通常の数十倍高速です。

列の代入

ここで余談ですが、これまでは行の読み込みを行い、行を単位に配列に格納してきました。
列の中で空白区切りで値が入っている場合の配列への代入はどうしましょう?

こうなります。

#!/usr/bin/bash

# 1行に3つの値が空白区切りで並んでいます
read -a aLine <<< "One Two Three"

echo "配列の内容すべてを表示"
echo ${aLine[@]}; # One Two Three
echo "添字の0を表示"
echo ${aLine[0]}; # One
echo "添字の1を表示"
echo ${aLine[1]}; # Two
echo "添字の2を表示"
echo ${aLine[2]}; # Three

実行結果は以下のとおりです。

bash-3.2$ bash col.sh
配列の内容すべてを表示
One Two Three
添字の0を表示
One
添字の1を表示
Two
添字の2を表示
Three
bash-3.2$

データファイルからの入力

では、データファイルの構造を少し複雑にしてみます。

One Two Three
Four Five Six
Seven Eight Nine
Ten

mapfileコマンドで行の内容を配列に入れる方法は説明しましたが、今回は、行の中で空白区切りの値が3つあります。
こうしたデータ構造をmapfileに加えてreadコマンドを使って効率的に、かつ高速に読み込んでみます。

#!/usr/bin/bash

DATAFILE="data02.txt";
declare -a aLine;

# データファイルを読み込みます。
mapfile -t aLine<"$DATAFILE";

for((i=0;i<4;i++));do

  # 行の内容を読み込み、空白区切りで配列に格納します
  read -a var <<< "${aLine[$i]}";

  echo "varの中身は以下の通り";
  echo "${var[@]}";
  echo "添字の0を表示"
  echo ${var[0]}; # One
  echo "添字の1を表示"
  echo ${var[1]}; # Two
  echo "添字の2を表示"
  echo ${var[2]}; # Three
done

実行結果は以下のとおりです。

bash-3.2$ bash colArray.sh
varの中身は以下の通り
One Two Three
添字の0を表示
One
添字の1を表示
Two
添字の2を表示
Three
varの中身は以下の通り
Four Five Six
添字の0を表示
Four
添字の1を表示
Five
添字の2を表示
Six
varの中身は以下の通り
Seven Eight Nine
添字の0を表示
Seven
添字の1を表示
Eight
添字の2を表示
Nine
varの中身は以下の通り
Ten
添字の0を表示
Ten
添字の1を表示

添字の2を表示

bash-3.2$

最後の行は値が一つしかありません(Ten)
必要であれば値がない場合は出力しないなどの処理をすれば良さそうです。
(配列的には別に値がなくても問題はないと思いますが)

mapfilereadコマンドを上手に使って、効率的に配列に代入してください。

書籍の紹介

BASHシェルスクリプトで「キー入力待ち」処理を作ってみよう

BASHシェルスクリプトで「キー入力待ち」処理を作ってみよう

【ちょいと便利な】シェルスクリプトワンライナー特集2【一行完結】

【ちょいと便利な】シェルスクリプトワンライナー特集2【一行完結】