trコマンドで改行をカンマに変換すると”,”とだけ表示されてびっくりした話

2023-03-27Bash

はじめに

わかってしまえばなんてことないですし、 tr コマンドに明るい方にとっては「よくあることだね」という程度の話だと思います。

先日 MENTA でシェルスクリプトについて支援をさせていただきました。
支援内容の一部に、テキストファイルの各行をカンマで連結して一行にして出力したい、という要望があったので「それならかんたん!」とやり方を提示させていただきました。
ところが、うまくいかずにかっこ悪いところを見られてしまいました。

その時のお話をしたいと思います。

検証環境

$ uname -moi
x86_64 MacBookPro16,1 Darwin

$ bash -version | head -n 1
GNU bash, version 5.1.8(1)-release (x86_64-apple-darwin20.3.0)

テキストファイルの改行コードを任意の文字に置換するには tr コマンドを使う

改行コードを置換する方法はいくつかあります。
sed を使う、 awk を使う、などありますが、一番かんたんなのは tr コマンドを利用する方法です。

早速試してみたいと思います。

テスト用にかんたんなテキストファイルを作成します。
各行に 1〜5 の数値が書かれています。

$ # ファイルを用意
$ seq 1 5 > data.txt

# ファイルの中身を確認
$ cat data.txt
1
2
3
4
5

それではこのファイルの改行をカンマ,に置換して見たいと思います。

$ tr "\n" "," < data.txt
1,2,3,4,5,

各行の改行コードがカンマに変換されました。
5 の行の末尾にも改行コードが含まれているため、 1,2,3,4,5, のように一番うしろにも改行が入っています。

*これを除去したい場合には $ tr "\n" "," < data.txt | sed 's/,$//' のようにしてやればよいでしょう。

tr コマンドを使って改行コードを置換したが思った出力とならない

MENTA で支援させていただいた際の話に戻ります。

この方法を使って改行コードをカンマに変換すれば、かんたんにカンマ区切りの一行にすることができるはずです。
やり方をお伝えして実際にやってみました。すると...

$ tr "\n" "," < data.txt
,$

?????

一瞬パニックになってしまいました。 「あれ?もしかして tr コマンドの使い方、僕はど忘れしてしまっているのか?」

原因は CR/LF

落ち着いて少し考えたところ、原因がわかりました。

というのも、質問者の方の利用している PC が Windows だったため、ピンときました。

file コマンドでデータファイルを確認してみます。

$ file data.txt
data.txt: ASCII text, with CRLF line terminators

このように、ファイルの改行コードが CRLF 形式だったわけですね。

CR が混ざるとなぜおかしな表示になるの?

CR が混ざると何故こんなおかしな表示になってしまうのか?

こんな仕組みです。

例えば 123 という文字を出力してみます。

# -e については後述
$ echo -e "123"
123

これは、 123 のそれぞれの文字を 3 つの工程を踏んで出力されています。

# 1. 出力し、カーソルを後ろにずらす
1|

# 2. 出力し、カーソルを後ろにずらす
12|

# 3. 出力し、カーソルを後ろにずらす
123|

次に 12\r3 という文字を出力してみます。

# -e をつけると、\rのような制御文字が有効となります
$ echo -e "12\r3"
32

これは、 12\r3 のそれぞれの文字を 3 つの工程を踏んで出力されています。

# 1. 出力し、カーソルを後ろにずらす
1|

# 2. 出力し、カーソルを後ろにずらす
12|

# 3. カーソルを先頭に移動
|12

# 3. 出力し、カーソルを後ろにずらす
3|2

結果、 32 という文字が出力されます。

それでは先程の例に戻って。
data.txt の内容は、以下の echo の出力内容と同じになります。

$ echo -e "1\r\n2\r\n3\r\n4\r\n5\r\n"
1
2
3
4
5

\n の部分を , に置き換えて実行してみましょう。

$ echo -e "1\r,2\r,3\r,4\r,5\r,"
,5

echo コマンドは、出力の最後の \n を自動的に出力しますので、 -n オプションを付けてこれを抑制してみます。

$ echo -n -e "1\r,2\r,3\r,4\r,5\r,"
,$

先程の問題と全く同じ現象が発生しました。
カンマの後ろの $ は、プロンプトだったわけですね。

ひとこと

同じような問題が発生する制御文字として \b があります。
もっとも、こちらは意図せず混入することはほぼありません。

使ってみると面白い発見があります。

プログレスバーを作ったりできます。

2023-03-27Bash