SIer だけど技術やりたいブログ

ターミナルの色付き文字の仕組み

linux

ターミナルを操作してると、ものによっては色が付くけど、コレがどうやって実現されてるかを調べた。

色付きのターミナルの例

https://en.wikipedia.org/wiki/ANSI_escape_code で制御文字が定義されていて(ASCII の 0x1B)、ターミナルがこの制御文字を読み込んで色を付けているっぽい。なので利用しているターミナル(Teraterm とか Windows PowerShell とか)に依存した技術っぽい。

たとえば、PowerShell から Python で print('\N{ESC}[31;4mhello\u001b[mworld') を実行しても赤線+下線が付けられるし、

python3 print の例

Windows PowerShell から ssh した Bash で printf "\u1b[31;4mhello\033[mworld\n" を実行しても赤色+下線が付けられる。

printf の例

うーん、でも ls -al で出力されるテキストを less で読んでも普通のテキストにしか見えないんだけど…

$ ls -al /tmp/ | less

total 0
drwxrwxrwt. 11 root root 220 Aug  5 13:31 .
dr-xr-xr-x. 18 root root 237 Jul 25 22:30 ..
drwxrwxrwt.  2 root root  40 Aug  5 12:54 .ICE-unix
drwxrwxrwt.  2 root root  40 Aug  5 12:54 .X11-unix
drwxrwxrwt.  2 root root  40 Aug  5 12:54 .XIM-unix
drwxrwxrwt.  2 root root  40 Aug  5 12:54 .font-unix
drwx------.  3 root root  60 Aug  5 12:54 systemd-private-42b4409a4786411fad91dc5d123c51cb-chronyd.service-gzz3N3
drwx------.  3 root root  60 Aug  5 12:54 systemd-private-42b4409a4786411fad91dc5d123c51cb-dbus-broker.service-Y0wOSE
drwx------.  3 root root  60 Aug  5 12:54 systemd-private-42b4409a4786411fad91dc5d123c51cb-policy-routes@enX0.service-RfRofl
drwx------.  3 root root  60 Aug  5 12:54 systemd-private-42b4409a4786411fad91dc5d123c51cb-systemd-logind.service-QoGcYH
drwx------.  3 root root  60 Aug  5 12:54 systemd-private-42b4409a4786411fad91dc5d123c51cb-systemd-resolved.service-UE8UVV

と思ったけど、これは ls コマンドのデフォルトの挙動が、標準出力がターミナルでなければ色を消す設定になっているからだった。 --color=always を設定すれば、色の制御コードを常に出力してくれる。

—color[=WHEN]
colorize the output; WHEN can be ‘always’ (default if omitted), ‘auto’, or ‘never’; more info below
… Using color to distinguish file types is disabled both by default and with —color=never. With —color=auto, ls emits color codes only when
standard output is connected to a terminal. The LS_COLORS environment variable can change the settings. Use the dircolors command to set it.
ls の man から引用

たしかに --color=always を使うと制御コードが表示される。

ls -al /tmp --color=always | less
total 0
drwxrwxrwt. 11 root root 220 Aug  5 13:33 ESC[0mESC[30;42m.ESC[0m
dr-xr-xr-x. 18 root root 237 Jul 25 22:30 ESC[01;34m..ESC[0m
drwxrwxrwt.  2 root root  40 Aug  5 12:54 ESC[30;42m.ICE-unixESC[0m
drwxrwxrwt.  2 root root  40 Aug  5 12:54 ESC[30;42m.X11-unixESC[0m
drwxrwxrwt.  2 root root  40 Aug  5 12:54 ESC[30;42m.XIM-unixESC[0m
drwxrwxrwt.  2 root root  40 Aug  5 12:54 ESC[30;42m.font-unixESC[0m
drwx------.  3 root root  60 Aug  5 12:54 ESC[01;34msystemd-private-42b4409a4786411fad91dc5d123c51cb-chronyd.service-gzz3N3ESC[0mESC[K
drwx------.  3 root root  60 Aug  5 12:54 ESC[01;34msystemd-private-42b4409a4786411fad91dc5d123c51cb-dbus-broker.service-Y0wOSEESC[0mESC[K
drwx------.  3 root root  60 Aug  5 12:54 ESC[01;34msystemd-private-42b4409a4786411fad91dc5d123c51cb-policy-routes@enX0.service-RfRoflESC[0mESC[K
drwx------.  3 root root  60 Aug  5 12:54 ESC[01;34msystemd-private-42b4409a4786411fad91dc5d123c51cb-systemd-logind.service-QoGcYHESC[0mESC[K
drwx------.  3 root root  60 Aug  5 12:54 ESC[01;34msystemd-private-42b4409a4786411fad91dc5d123c51cb-systemd-resolved.service-UE8UVVESC[0mESC[K

なお、less はデフォルトだと制御コードをエスケープするが、 -R オプションをつけると色が付けられる。

ls -al /tmp --color=always | less -R 

色付きの less の例

grep とかにも ls と同様のオプションがあるようなので、これで grep の一致した部分の色を消さずに less できますね。やったぜ。

grep -r "Amazon Linux" --color=always | less -R

複数 grep をかけたとき(grep A | grep Bで A と B に色付ける的な)も色を保てるっぽい。

色付きの grep の例

ただ --color=always を付けたときの注意点としては、目には見えないけど制御文字は付いてるので、思ってる文字列ではない。 とくにパイプの途中で grep してるとコーナーケースではつまずきそうなのと、テキストに出力するなら邪魔でしかないので、使うかというと微妙そう。

// Amazon Linux があるが
$ grep -r "Amazon" /etc/os-release --color=always
NAME="Amazon Linux"
// --color=always をつけると Amazon の前後に見えない制御文字があるので Amazon Linux に一致しなくなる
PRETTY_NAME="Amazon Linux 2023"
$ grep -r "Amazon" /etc/os-release --color=always | grep "Amazon Linux"

参考

ところで ls コマンドは標準出力がターミナルでなければ色を消すといってるが、そんなの判別する方法あるんだっけ…と思って coreutils のコード見たところ(upstream だけど)、isatty(3) という API があるらしい。

ためしにサンプルコード作ってみたが、まあそうなんだ、というかんじ。

$ cat main.c
#include <unistd.h>
#include <stdio.h>
void main() {
       if (isatty(1)) {
               printf("stdout is a tty\n");
       } else {
               printf("stdout is not a tty\n");
       }
}

$ gcc main.c
$ ./a.out
stdout is a tty

$ ./a.out | cat
stdout is not a tty