nikkie-ftnextの日記

イベントレポートや読書メモを発信

色づくターミナル出力の仕組みから

はじめに

2022年、"あきまして"おめでとうございます1
本年もよろしくお願いいたします🎍🐯

年末年始休みで、色々寄り道しながら技術的な調べ物をしています。
その中で、「ターミナルの出力にどうやって色を付けるんだろう」という疑問について調べて分かったことをまとめます。

※2021年はアドカレあんまりできなかったのですが、Python Advent Calendar 2021に空白を見つけたので、時を戻して、Python Advent Calendar 2021 10日目の記事ということにしちゃいます!

目次

TL; DR

  • エスケープシーケンス:特殊な文字や機能を表すための文字列
  • 色を付けたい文字列の前後に 特定のエスケープシーケンス を入れる
    • ANSIエスケープシーケンスとも言われる
  • ANSIエスケープシーケンスはESC[<色を表す整数>mで始まる。ESC[mまでの文字列に色が付く

ここにたどり着くまでを以下に記していきます。

Pythonで知っていることを思い出す

「ターミナルの出力にどうやって色を付けるんだろう」という疑問、さっぱり見当が付きませんでした🤯
そこで、まずはなじみ深いPythonで出力の色を変えた経験を思い出します。
私の場合は以下の2つが浮かびました。

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'

WikipediaANSI escape code(英語)を参照すると、ANSIエスケープシーケンスの仕様が説明されていました。

  • ESC[の部分が CSI (Control Sequence Introducer)5
    • '\x1b['の部分のこと
  • CSI<整数>mSGR (Select Graphic Rendition)6
    • 画面の属性を設定する
    • 例えば、30-37の整数を指定すると、文字の色を指定したことになる7
    • '\x1b[32m'は32で緑色を指定
    • '\x1b[0m'は属性をオフにする指定8

文字の色の他に背景色や太字・斜体なども指定できるようです。
CSI、SGRを押さえておくと、ANSI escape code中の表を参照して、気になる指定を試せますね。

ターミナルへの出力への色づけ方

ここまでで、文字列にANSIエスケープシーケンスを付与してターミナルに出力すればよいと分かりました。

bashの例

参考:https://qiita.com/ko1nksm/items/095bdb8f0eca6d327233#1-echo-%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%8F-printf-%E3%82%92%E4%BD%BF%E3%81%86

$ 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. https://github.com/django/django/blob/4.0/django/utils/termcolors.py

  2. https://github.com/willmcgugan/rich/blob/v10.16.1/rich/style.py#L705

  3. Wikipedia エスケープシーケンスに具体例として挙がっています

  4. https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences

  5. https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters

  6. SGRの表に「Set foreground color」とあります

  7. SGRの表に「All attributes off」とあります

  8. CSIのところで「no parameters at all in ESC[m acts like a 0 reset code」とあります。