Bashで簡単にCSVファイルを読み込んで処理する方法

# はじめに
LinuxのBashのスクリプトでシンプルなCSVファイルを読み込んで処理を行う際に、意外と簡単に記述する事ができるので紹介します。

## ・`cut`コマンドを使用する方法
よく見かけるBashスクリプトでCSVファイルを読み込んで処理を行う方法として、CSVファイルを標準入力から一行ずつ読み込み、`cut`コマンドを使用してカラムを変数に格納する記述があります。
```bash:hoge.shの中身
 #!/bin/bash

while read line
do
  # $lineに読み込んだCSVファイルの一行のテキストが格納され、それをcutコマンドでカラムに分割し変数に格納
  col1=$(echo ${line} | cut -d , -f 1)
  col2=$(echo ${line} | cut -d , -f 2)
  col3=$(echo ${line} | cut -d , -f 3)

  # 処理内容をここに記載

  # $colX で読み込んだCSVファイルのテキストを参照
  echo "col1:$col1 col2:$col2 col3:$col3"
done < $1
```

```bash:csvファイルの中身
$ cat hoge.csv 
a1,a2,a3
b1,b2,b3
c1,c2,c3
$ 
```

```bash:csvファイルを引数に指定してスクリプトを実行
$ ./hoge.sh hoge.csv 
col1:a1 col2:a2 col3:a3
col1:b1 col2:b2 col3:b3
col1:c1 col2:c2 col3:c3
$ 
```

## ・IFSでカラムを変数へ格納する方法
区切り文字の環境変数である`IFS`を`,`に変更しreadコマンドに複数の変数を設定する事で、`cut`コマンドを使用せずにシンプルに記述する事ができます。

```bash:hoge.shの中身
 #!/bin/bash

 # 読み込んだCSVファイルの一行のテキストをカラムに分割し複数の変数に格納
while IFS=, read col1 col2 col3
do
  # 処理内容をここに記載

  # $colX で読み込んだCSVファイルのテキストを参照
  echo "col1:$col1 col2:$col2 col3:$col3"
done < $1
```

## ・IFSでカラムを配列へ格納する方法(●)
また、readコマンドの`-a`オプションを使用する事で分割したカラムを配列に格納する事ができます。
```bash:hoge.shの中身
 #!/bin/bash

 # 読み込んだCSVファイルの一行のテキストをカラムに分割し配列に格納
while IFS=, read -a col
do
  # 処理内容をここに記載

  # ${col[X]}で読み込んだCSVファイルのテキストを参照
  echo "col1:${col[0]} col2:${col[1]} col3:${col[2]}"
done < $1
```
配列になっているため分割したカラムをループさせたり追加・削除・加工がしやすく、また変数展開を使用して柔軟に参照・表示する事ができるため、この方法が一番おすすめです。
```bash:hoge.shの中身
 #!/bin/bash

while IFS=, read -a col
do
  # カラムをループで処理
  for c in ${col[@]}
  do
    # 処理内容をここに記載
    echo "loop:$c"
  done

  # 配列なので配列操作ができる
  ## 3カラム目を削除
  unset col[2]
  ## カラムを追加
  col+=(lastcol)

  # カラムの参照・表示に配列・変数展開が利用できる
  ## 全てのカラムを表示
  echo "${col[@]}"
  ## 2つめ以降の全てのカラムを表示
  echo "${col[@]:1}"
  ## 先頭にcol:をつけて全てのカラムを表示
  echo "${col[@]/#/col:}"
done < $1
```
```bash:csvファイルを引数に指定してスクリプトを実行
$ ./hoge.sh hoge.csv 
loop:a1
loop:a2
loop:a3
a1 a2 lastcol
a2 lastcol
col:a1 col:a2 col:lastcol
loop:b1
loop:b2
loop:b3
b1 b2 lastcol
b2 lastcol
col:b1 col:b2 col:lastcol
loop:c1
loop:c2
loop:c3
c1 c2 lastcol
c2 lastcol
col:c1 col:c2 col:lastcol
$ 
```

## ・スペースかタブ区切りのファイルの場合
ファイルの区切り文字がスペース区切り(SSV)かタブ区切り(TSV)の場合、区切り文字の環境変数であるIFSのデフォルトがスペース・タブ・改行のため、スクリプト内でIFSの指定をせずに処理ができます。
```bash:hoge.shの中身
 #!/bin/bash

while read -a col
do
  echo "col1:${col[0]} col2:${col[1]} col3:${col[2]}"
done < $1
```
ただし、カラムが空の場合は前後の連続したスペースやタブが省略され、変数の場所が詰めて格納されてしまいます。
```bash:ssvファイルの中身(SSV)
$ cat hoge.ssv
a1 a2 a3
b1  b3
  c3
$ 
```
```bash:ssvファイルを引数に指定してスクリプトを実行
$ ./hoge.sh hoge.ssv
col1:a1 col2:a2 col3:a3
col1:b1 col2:b3 col3:
col1:c3 col2: col3:
$ 
```
これを防ぐためには一度スペースやタブをカンマに置換し、カンマ区切りで配列に読み込む必要があります。
```bash:hoge.shの中身(スペース区切り)
 #!/bin/bash

 # 区切り文字をカンマに変更
IFS=,
while read line
do
  # スペースをカンマに置換し、カンマが区切り文字の状態で配列を作成
  col=(${line// /,})
  echo "col1:${col[0]} col2:${col[1]} col3:${col[2]}"
done < $1
```
```bash:hoge.shの中身(タブ区切り)
 #!/bin/bash

 # 区切り文字をカンマに変更
IFS=,
while read line
do
  # タブをカンマに置換し、カンマが区切り文字の状態で配列を作成
  col=(${line//$'\t'/,})
  echo "col1:${col[0]} col2:${col[1]} col3:${col[2]}"
done < $1
```

## ・読み込むCSVファイルを加工する方法
CSVファイルを逆順で読み込みたい、特定の文字を置換したりしてから読み込みたいなど、CSVファイルを読み込む前に加工したい場合は標準入力にプロセス置換 `<()`の結果を渡す事で実現できます。
```bash:hoge.shの中身(CSVファイルを逆順にしてから標準入力に渡す)
 #!/bin/bash

while IFS=, read -a col
do
  echo "col1:${col[0]} col2:${col[1]} col3:${col[2]}"
done < <(tac $1)
```
```bash:csvファイルの中身
$ cat hoge.csv 
a1,a2,a3
b1,b2,b3
c1,c2,c3
$ 
```
```bash:csvファイルを引数に指定してスクリプトを実行
$ ./hoge.sh hoge.csv 
col1:c1 col2:c2 col3:c3
col1:b1 col2:b2 col3:b3
col1:a1 col2:a2 col3:a3
$ 
```

## ・`awk`コマンドを使う方法
CSVファイルの簡単な加工や集計を行う場合は`awk`コマンドで処理する方がシンプルに記述できる場合があります。
`awk`コマンドは引数として指定したファイルを先頭から一行ずつ読み込み、区切り文字で区切られた内容を`$1`,`$2`...という変数に自動的に格納し、一行毎に処理したい内容を記述する事ができます。
なお、CSVファイルを処理する場合は`-F`オプションで区切り文字であるカンマを指定する必要があります。
```bash:awkコマンドを実行する
$ awk -F, '{print "col1:"$1,"col2:"$2,"col3:"$3}' hoge.csv
col1:a1 col2:a2 col3:a3
col1:b1 col2:b2 col3:b3
col1:c1 col2:c2 col3:c3
$ 
```

また、処理内容が複雑になる場合はスクリプトとしてファイルに処理内容を記述する事もできます。
```bash:hoge.awk
{
  # 処理内容をここに記載
  print "col1:"$1,"col2:"$2,"col3:"$3
}
```

```bash:awkコマンドをスクリプトファイルを使用して実行する
$ awk -F, -f hoge.awk hoge.csv 
col1:a1 col2:a2 col3:a3
col1:b1 col2:b2 col3:b3
col1:c1 col2:c2 col3:c3
$ 
```

ファイルの区切り文字がスペース区切り(SSV)かタブ区切り(TSV)の場合、Bashスクリプトの時と同様にオプションを指定せずに読み込む事ができますが、変数の場所が詰めて格納されるため、オプションを指定する方が安全です。
```bash:awkコマンドを実行する(スペース区切り)
$ awk -F'[. ]' '{print "col1:"$1,"col2:"$2,"col3:"$3}' hoge.ssv
col1:a1 col2:a2 col3:a3
col1:b1 col2: col3:b3
col1: col2: col3:c3
$ 
```

```bash:awkコマンドを実行する(タブ区切り)
$ awk -F'[.\t]' '{print "col1:"$1,"col2:"$2,"col3:"$3}' hoge.tsv 
col1:a1 col2:a2 col3:a3
col1:b1 col2: col3:b3
col1: col2: col3:c3
$ 
```

`awk`コマンドも強力なコマンドですが、より直感的で複雑な処理が可能な`perl`コマンドでCSVファイルを処理する方法もあります。
Linuxサーバーにデフォルトで入っているコマンドとしては非常に複雑な処理が可能なので、興味のある方はそちらを利用しても良いかもしれません。