コマンドの出力結果からANSIエスケープシーケンスのカラーリングを取り除く方法

2023-03-27Bash,CentOS,Linux,SSH,Ubuntu,Zsh

はじめに

ターミナル操作時、コマンドの実行結果が色付けされていることがあります。

僕がプログラミングを始めた頃はもう少し淡白なコマンドの出力結果ばかりだったのですが、最近のツールは出力結果がカラフルなものも多いです。

ターミナル上の出力結果をカラフルに色付けしてくれるための制御文字は ANSI エスケープシーケンス と呼ばれます。
( 厳密に言うと、ANSI エスケープシーケンス には色付けだけでなく、カーソル位置、フォントスタイル、その他のオプションなどの制御文字が含まれているそうです。 )

コマンドによっては、出力結果をパイプで繋いだ場合にカラーリングの ANSI エスケープシーケンスを除去してくれ、パイプ先で文字列操作する際に大変便利です。

例えば、 ls コマンドは ls -l を実行すると色付けされますが、 ls -l | cat のようにパイプでつなぐと制御文字が除去されます。
cut で切り取る場合は制御文字が邪魔になるので大変便利な挙動です。
しかし、コマンドによってはこのような色付けの制御文字を除去しません。

そんなときに ANSI エスケープシーケンスのカラーリング制御文字を取り除く方法について紹介します。

検証環境

$ uname -moi
aarch64 aarch64 GNU/Linux

$ head -n 3 /etc/os-release
PRETTY_NAME="Ubuntu Kinetic Kudu (development branch)"
NAME="Ubuntu"
VERSION_ID="22.10"

$ bash -version | head -n 1
GNU bash, バージョン 5.2.0(1)-release (aarch64-unknown-linux-gnu)

$ sed --version | head -n 2
sed (GNU sed) 4.8
パッケージ作成者: Debian

[...] で囲まれた部分の文字列だけを取り出したいが ANSI エスケープシーケンスが邪魔をする場合

JavaScript 関連のツールを起動した際の文言として、以下のようなものがあります。

$ echo -e "hoge (pid  12321) is starting...\u001b[60G[\u001b[0;42m  OK  \u001b[0;39m]"
hoge (pid  12321) is starting...                           [  OK  ]

$ echo -e "hoge (pid  12321) is starting...\u001b[60G[\u001b[0;41m  NG  \u001b[0;39m]"
hoge (pid  12321) is starting...                           [  NG  ]

文字列だけだとわかりにくいので、スクリーンショットを貼っておきます。

echo コマンドは -e オプション付きで実行すると、引数に指定された文字列内の ANSI エスケープシーケンスを使ってターミナル上の表示を変更してくれます。

ここで、 [ OK ] または [ NG ] の部分だけを抽出したいとします。

grep で抽出

grep で抽出してみます。 -oオプションをつけることで一致した部分のみを出力できます。

$ echo -e "hoge (pid  12321) is starting...\u001b[60G[\u001b[0;42m  OK  \u001b[0;39m]" | grep -o "\[.*\]"
[60G[  OK  ]

想定していた文字列以外にも、おまけが付いてきました。 [60G の部分は想定していない文字列です。

sed で抽出

sed で抽出してみます。[...] 以外の部分を置換し取り除いてみます。

$ echo -e "hoge (pid  12321) is starting...\u001b[60G[\u001b[0;42m  OK  \u001b[0;39m]" | sed "s/.*\(\[.*\]\).*/\1/"
[0;39m]

grep のときよりも悪化しました。

sed で ANSI エスケープシーケンスを取り除く

今回は echo に引き渡す文字列を変更すれば解決はしますが、問題になるのは echo で出力している部分が他のツールによって出力されているケースです。

自分ではツールに手を加えることができない場合は、 ANSI エスケープシーケンスの制御文字を取り除いてしまいましょう。

sed -E "s/\x1b\[([0-9]{1,3}((;[0-9]{1,3})*)?)?[mGK]//g"

という呪文のような sed コマンドを通すことで、制御文字が除去できます。

$ echo -e "hoge (pid  12321) is starting...\u001b[60G[\u001b[0;42m  OK  \u001b[0;39m]" | sed -E "s/\x1b\[([0-9]{1,3}((;[0-9]{1,3})*)?)?[mGK]//g"
hoge (pid  12321) is starting...[  OK  ]

やはり文字だけではわかりにくいのでスクリーンショットを貼っておきます。
先程のスクリーンショットとは異なり、[ OK ] の部分が色付けされていません。

除去したあとの文字列に対して、 grep あるいは sed を適用してみます。

grep で抽出

$ echo -e "hoge (pid  12321) is starting...\u001b[60G[\u001b[0;42m  OK  \u001b[0;39m]" | sed -E "s/\x1b\[([0-9]{1,3}((;[0-9]{1,3})*)?)?[mGK]//g" | \grep -o "\[.*\]"
[  OK  ]

sed で抽出

$ echo -e "hoge (pid  12321) is starting...\u001b[60G[\u001b[0;42m  OK  \u001b[0;39m]" | sed -E "s/\x1b\[([0-9]{1,3}((;[0-9]{1,3})*)?)?[mGK]//g" | sed "s/.*\(\[.*\]\).*/\1/"
[  OK  ]

呪文のような sed について

呪文のような sed について。

sed -E "s/\x1b\[([0-9]{1,3}((;[0-9]{1,3})*)?)?[mGK]//g"

\x1b\[\x1B\[ と大文字でも問題有りません。これはエスケープシーケンスの始まりを表します。
[sed 内では特殊な意味を持つため、 \[ としてバックスラッシュでエスケープしています。
つまり、 \x1b[ はエスペープシーケンスの始まりを表現しています。

ちなみに echo でエスペープシーケンスの始まりを指定する場合は、 \u001b[\033[\e[ のいずれかが利用でき、いずれも同じ意味になります。

$ echo -e '\u001b[38;5;123mHELLO'
HELLO

$ echo -e '\e[38;5;123mHELLO'
HELLO

$ echo -e '\033[38;5;123mHELLO'
HELLO

この後に 0;3338;5;123 のように、 : 区切りの数字が 2 つ以上並んだ後、最後に m が続きます。

$ echo -e '\033[0;33mHELLO'
HELLO

$ echo -e '\033[38;5;123mHELLO'
HELLO

カラーリングの制御だけであれば最終の文字が m であるケースを除去すれば十分なのですが、 [mGK] という置換条件が書かれており、 GK も対象としています。
いずれもカーソル移動を制御するための文字列で、プログレスバーの制御で利用されることがあり、念の為除去しています。

コマンドラインツール ansifilter を使うと簡単

sed で頑張る方法を紹介してきたわけですが、実は便利なツールも提供されています。

ジャストフィットなツール名ですね。

macOS 環境の場合はインストールも brew install ansifilter を実行するだけです。

こちらを使うとこねくり回していた呪文のような sed 命令から開放されます。

(スクリーンショットを貼るのが面倒になってきたので) cat -A を実行することで、ANSI シーケンス文字列が視覚化できるため、これを利用します。

# $ は行末の改行を表す制御文字です
$ echo -e '\u001b[38;5;123mHELLO' | cat -A
^[[38;5;123mHELLO$

ansifilter というツールを通した結果を確認します。

$ echo -e '\u001b[38;5;123mHELLO' | ansifilter | cat -A
HELLO$

行末の改行を表す $ 以外は消えていることがわかります。

ただし、いくつかのコマンドのカラーリングされた出力を ansifilter でフィルタリングしてみましたが、完全に ANSI シーケンスが除去しきれないケースがあり、 sed 方式のほうが確実かと思いました

ひとこと

改めて、入力あるいは出力がパイプされているかどうかの判定が行われているツールは素晴らしい。

2023-03-27Bash,CentOS,Linux,SSH,Ubuntu,Zsh