はじめに
2022年、"あきまして"おめでとうございます1。
本年もよろしくお願いいたします🎍🐯
年末年始休みで、色々寄り道しながら技術的な調べ物をしています。
その中で、「ターミナルの出力にどうやって色を付けるんだろう」という疑問について調べて分かったことをまとめます。
※2021年はアドカレあんまりできなかったのですが、Python Advent Calendar 2021に空白を見つけたので、時を戻して、Python Advent Calendar 2021 10日目の記事ということにしちゃいます!
目次
- はじめに
- 目次
- TL; DR
- Pythonで知っていることを思い出す
- どうやって出力に色を付けている?
- 色を指定するための文字列
- 色の指定が機能する仕組み
- ターミナルへの出力への色づけ方
- 終わりに
- 動作環境
TL; DR
- エスケープシーケンス:特殊な文字や機能を表すための文字列
- 色を付けたい文字列の前後に 特定のエスケープシーケンス を入れる
- ANSIエスケープシーケンスは
ESC[<色を表す整数>m
で始まる。ESC[m
までの文字列に色が付く
ここにたどり着くまでを以下に記していきます。
Pythonで知っていることを思い出す
「ターミナルの出力にどうやって色を付けるんだろう」という疑問、さっぱり見当が付きませんでした🤯
そこで、まずはなじみ深いPythonで出力の色を変えた経験を思い出します。
私の場合は以下の2つが浮かびました。
- Djangoのカスタムコマンド
rich
https://github.com/willmcgugan/rich
Djangoのカスタムコマンド
django.core.management.base.BaseCommand
を継承して作るカスタムコマンドです。
manage.py my_command
のように独自のコマンドを追加できます。
カスタムコマンドからの出力には色を付けられます。
上のドキュメントの例では、緑色 で出力されます。
self.stdout.write(self.style.SUCCESS('Successfully closed poll "%s"' % poll_id))
rich
色を付けた文字列で思い出したのが rich
というライブラリ。
普段使いしているというより触ってみたいライブラリです。
GitHubのREADMEを参考にしたところ、簡単に出力に色が付きました!
>>> from rich.console import Console >>> console = Console() >>> console.print("Happy", "New", "Year", style="green") # 続く文字は緑色で出力されます Happy New Year
どうやって出力に色を付けている?
では、どのような仕組みで色を付けているのでしょう。
調べて分かったのは、色を指定するための文字列を付与した文字列を出力しているということです。
以下のようなイメージです(文字列1だけ色を変える例):
<ここからこの色に変更>文字列1<色を戻す>文字列2
色を指定するための文字列がどんなものか、先ほど紹介したPythonライブラリの中を覗いてみます。
色を指定するための文字列
Django
django.utils
の下にtermcolors.py
があります2。
その中のcolorize
関数で、色を付与した文字列が返されます。
>>> from django.utils.termcolors import colorize >>> colorize("Happy New Year", fg="green") '\x1b[32mHappy New Year\x1b[0m' >>> print(colorize("Happy New Year", fg="green")) Happy New Year
\x1b[32m
が「ここから32という色(緑)に変更」\x1b[0m
が「色を戻す」
という意味です。
色を付与した文字列をprint
関数で出力すると、色付きの出力となります。
rich
richでも同様の実装をしているように思われます(使用経験はDjangoに比べると著しく少ないので読み違いをしているかもしれません)。
rich.style.Style
クラスのrender
メソッドで、テキストに色の変更や戻す文字列を付与していました3。
f"\x1b[{attrs}m{text}\x1b[0m"
render
メソッド呼び出し例
>>> from rich.color import Color >>> from rich.style import Style >>> green = Color.from_ansi(2) >>> green_style = Style.from_color(green) >>> print(green_style.render("Happy New Year")) Happy New Year
色の指定が機能する仕組み
'\x1b[32mHappy New Year\x1b[0m'
のような文字列が機能する仕組みを見ていきます。
このような文字列にはエスケープシーケンスが含まれていると理解しました。
エスケープシーケンス (escape sequence) とは、コンピュータシステムにおいて、通常の文字列では表せない特殊な文字や機能を、規定された特別な文字の並びにより表したもの。(Wikipedia エスケープシーケンスより)
'\x1b[32mHappy New Year\x1b[0m'
には画面制御のエスケープシーケンスが含まれていて、ANSIエスケープシーケンスと呼ばれるそうです4。
ANSIエスケープシーケンスの構成要素を見ていくと、\x1b
(16進数の1b)は10進では27
です。
文字コードとしてはエスケープを表します。
>>> int("1b", 16) 27 >>> chr(27) '\x1b'
WikipediaのANSI escape code(英語)を参照すると、ANSIエスケープシーケンスの仕様が説明されていました。
文字の色の他に背景色や太字・斜体なども指定できるようです。
CSI、SGRを押さえておくと、ANSI escape code中の表を参照して、気になる指定を試せますね。
ターミナルへの出力への色づけ方
ここまでで、文字列にANSIエスケープシーケンスを付与してターミナルに出力すればよいと分かりました。
bashの例
$ printf '\033[32m%s\033[m\n' 'Happy New Year' Happy New Year
'\033'
(8進数の33)は10進数の27です。
表記が異なりますが、エスケープを表していることに変わりありません
終わりの'\033[m'
ですが、整数の省略は'\033[0m'
として扱われるとWikipediaに記載がありました9。
Pythonの例
"""example.py""" from functools import partial def my_colorize(text: str, color: int): return f"\x1b[{color}m{text}\x1b[0m" colorize_green = partial(my_colorize, color=32) print(colorize_green("Happy New Year"))
$ python example.py # 続く文字は緑色で出力されます Happy New Year
終わりに
「ターミナルの出力にどうやって色を付けるんだろう」がふと気になり、Pythonで知っていることを手がかりに調べ、ANSIエスケープシーケンスというものを知りました。
ターミナルで日頃目にしている色やフォントの変更、白状すると「魔法」でしたが、仕組みの一端が分かり、自分でも少しは使えそうに感じています。
動作環境
-
↩今日は「あきましておめでとう」の1日ですね。
— nikkie にっきー (@ftnext) 2022年1月1日
タイトルにそんな背景が!というのを先生のツイートで知りましたし、
えるたその着物姿、ちらほらお見かけしました
ああそうか、アニメ放送10周年なのか!
劇場オールナイト上映とか期待しちゃいますhttps://t.co/Vatd1Fumaa -
https://github.com/django/django/blob/4.0/django/utils/termcolors.py↩
-
https://github.com/willmcgugan/rich/blob/v10.16.1/rich/style.py#L705↩
-
Wikipedia エスケープシーケンスに具体例として挙がっています↩
-
https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences↩
-
https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters↩
-
SGRの表に「Set foreground color」とあります↩
-
SGRの表に「All attributes off」とあります↩
-
CSIのところで「no parameters at all in ESC[m acts like a 0 reset code」とあります。↩