改行コードには気を付ける

# はじめに
windows端末で作成されたファイルをLinux環境に転送して処理をさせようとして想定外の挙動が発生してしまう。
初歩的なミスですが、あるあるなのかなと思うので記事に残しておこうと思います。

改行コードとはなんぞや、という部分については今回は省略します。
詳しく知りたい方は「改行コード」と調べていただければたくさん記事が出てくると思います。

今回はUnix系OS(Linux,macOS)で使用される`LF`と、WindowsOSで使用される`CRLF`の差によって発生した事象を紹介していきます。

# 再現
やろうとしていたことは、windwos端末で作成したテキストファイルを仮想マシン（VirtualBox）に転送し、シェルスクリプトを使用して必要な情報を抜き出す、という作業です。
この辺は本題とはあまり関係がないので流していただいて問題ないです。

具体的には以下の順で作業を実施していました。

### テキストファイルを作成
```:コマンドプロンプト
C:\Users\User\デスクトップ\dir>find /v /c "" * > ..\rows.txt
```
ファイルの中身は以下のようになっています。

```:コマンドプロンプト
C:\Users\User\OneDrive\デスクトップ\dir>type ..\rows.txt

---------- TEST1.TXT: 3

---------- TEST2.TXT: 4

---------- TEST3.TXT: 5

---------- TEST4.HTML: 3

---------- TEST5.TSV: 6
```

なお、実際のフォルダ内のイメージは以下です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3581035/ab176ad6-89fb-b272-e6d4-576f0261c04e.png)

デスクトップ上に作成されたrows.txtをteraterm等を使用して仮想マシンに転送しておきます。
```
$ ls 
rows.txt
```


### シェルスクリプトで処理をさせる
テキストファイルを設置したディレクトリ内に以下シェルスクリプトを配置しキック。
やりたいことは、テキストファイルから拡張子が`.txt`であるファイルの行数のみを抽出しcsv形式で出力させたい、というものです。

```shell:trans_csv.sh
#/bin/bash

in_file=rows.txt
out_file=rows.csv

# 拡張子が.txtであるものを抽出
tr 'A-Z' 'a-z' < ${in_file} | grep ".txt$" > target_${in_file}

# 必要な情報のみ抜き出してcsv形式で出力
awk 'BEGIN{FS = " "} {print $2,$3}' target_${in_file} | sed "s/: /,/g" > ${out_file}

rm ./target_${in_file}
```

勘のいい方だとお気づきかもしれませんが、これだとシェルスクリプトを実行しても空のrows.csvが出力されるだけです。

# 問題点を探す

```diff_shell:trans_csv.sh
#/bin/bash

in_file=rows.txt
out_file=rows.csv

+ set -x

# 拡張子が.txtであるものを抽出
tr 'A-Z' 'a-z' < ${in_file} | grep ".txt$" > target_${in_file}
+ # デバッグ -------------------------------
+ tr 'A-Z' 'a-z' < ${in_file} | grep ".txt$"
+ echo $?
+ #-----------------------------------------

# 必要な情報のみ抜き出してcsv形式で出力
grep " [4-9]$" target_${in_file} | awk 'BEGIN{FS = " "} {print $2,$3}' | sed "s/: /,/g" > ${out_file}
+ # デバッグ -----------------------------------------------------------
+ grep " [4-9]$" target_${in_file} | awk 'BEGIN{FS = " "} {print $2,$3}' | cat | wc -l
+ #---------------------------------------------------------------------


rm ./target_${in_file}
```
`set -x`を前に入れることでデバッグが可能です。
さらにそれぞれのコマンドでどのような結果が出力されたのかも確認できるようにしておきます。

その実行結果が以下です。

```shell:
$ bash trans_csv.sh
+ grep .txt
+ tr A-Z a-z
+ grep .txt
+ tr A-Z a-z
---------- test1.txt: 3
---------- test2.txt: 4
---------- test3.txt: 5
+ echo 0
0
+ sed 's/: /,/g'
+ awk 'BEGIN{FS = " "} {print $2,$3}'
+ grep " [4-9]$" target_rows.txt
+ wc -l
+ cat
+ awk 'BEGIN{FS = " "} {print $2,$3}'
+ grep ' [4-9]$ ' target_rows.txt
0
+ rm ./target_rows.txt
```

ポイントは出力内の17行目のwcの結果です。
grepとawkの結果を出力させて行数をカウントした結果が0となっています。

つまりこのgrepの段階で想定通りの挙動がなされていない（何もヒットしなかった）ということになります。

以下コマンドで制御コードも含めてすべて出力してみます。
```shell:
$ tr 'A-Z' 'a-z' < rows.txt | cat -v
^M
---------- test1.txt: 3^M
^M
---------- test2.txt: 4^M
^M
---------- test3.txt: 5^M
^M
---------- test4.html: 3^M
^M
---------- test5.tsv: 3^M
```
・・・改行部分が`^M`になっています。

`grep " [4-9]$"`で検索しているため、行末の`^M`のせいでヒットしなくなっているようです。

なぜ改行部分（の制御コード）が`^M`となってしまうのかは、以下の記事がわかりやすかったです。
[改行コードCRはなぜ（^M）で\rなのか - 自分の仕事を憎むには人生は余りにも短い](https://garapon.hatenablog.com/entry/2017/05/26/)

# 改善
以下のようにスクリプトを修正します。
※当然デバッグ部分は後程削除します

改行コードを置換してあげることで解決するはずです。

```diff_shell:trans_csv.sh
#/bin/bash

in_file=rows.txt
out_file=rows.csv

+ sed -i 's/\r//g' ${in_file}

set -x

# 拡張子が.txtであるものを抽出
tr 'A-Z' 'a-z' < ${in_file} | grep ".txt" > target_${in_file}
# デバッグ -------------------------------
tr 'A-Z' 'a-z' < ${in_file} | grep ".txt"
echo $?
#-----------------------------------------

# 必要な情報のみ抜き出してcsv形式で出力
grep " [4-9]$" target_${in_file} | awk 'BEGIN{FS = " "} {print $2,$3}' | sed "s/: /,/g" > ${out_file}
# デバッグ -----------------------------------------------------------
grep " [4-9]$" target_${in_file} | awk 'BEGIN{FS = " "} {print $2,$3}' | cat | wc -l
#---------------------------------------------------------------------

rm ./target_${in_file}
```

```shell:
$ bash trans_csv.sh
+ grep .txt
+ tr A-Z a-z
+ grep .txt
+ tr A-Z a-z
---------- test1.txt: 3
---------- test2.txt: 4
---------- test3.txt: 5
+ echo 0
0
+ sed 's/: /,/g'
+ awk 'BEGIN{FS = " "} {print $2,$3}'
+ grep ' [4-9]$' target_rows.txt
+ wc -l
+ cat
+ awk 'BEGIN{FS = " "} {print $2,$3}'
+ grep ' [4-9]$' target_rows.txt
2
$ cat rows.csv
test2.txt,4
test3.txt,5
```
正しく実行されました。


# さいごに
そもそも今回問題となったgrepでは行数部分を検索しているのですが、awkで以下のように抽出するほうが効率的にもいいですし、改行コードの影響も受けません。

```shell:
awk 'BEGIN{FS = " "} $3 >= 4 {print $2,$3}' target_${in_file} | sed "s/: /,/g" > ${out_file}
```

なぜ先に気が付かなかったんでしょうか。

ですが、おかげで身をもって改行コードトラップを踏むことができました。
巨大なスクリプトから探す羽目にならなくてよかったです。
皆さんもお気をつけて。