適当おじさんの適当ブログ

技術のことやゲーム開発のことやゲームのことなど自由に雑多に書き連ねます

ターミナルのテキストの色を変える方法を crayons から読み解いたメモ

はじめに

crayons はPythonのターミナルに色をついたテキストを表示するためのモジュールです。どのようにしてターミナルのテキストの色を変更しているのかが気になったので、crayonsのコードを読みつつ調べたときのメモです。

github.com

使い方

crayons を読み解く前に、簡単に使い方について触れておきます。色のついたテキストを表示するには、その色に対応した関数を呼び出すだけです。bold=True とすることで太字にすることもできます。

import crayons

print(crayons.red("red text"))
print(crayons.blue("blue text", bold=True))

f:id:subarunari:20190119231152p:plain

実行すると、上図のように色のついたテキストを表示できます。他にも、赤、緑、黄、青、黒、橙、青緑、白を指定できます。crayons.disable() とすることで各種関数で色がつかないようにすることもできます。以上のように、非常にシンプルに使えるライブラリとなっています。

該当箇所のコードを読む

crayons.pyredblue といった関数を見ると、ColoredStringクラスのインスタンスを作成していることがわかります。

def red(string, always=False, bold=False):
    return ColoredString('RED', string, always_color=always, bold=bold)

作成されたColoredStringインスタンスを print で表示すると、色のついたテキストが表示されます。print時にColoredStringクラスに定義されている __unicode__ が呼び出されるためです。この関数の中では、color_str 関数が呼び出されています。

def __unicode__(self):
    value = self.color_str
    if isinstance(value, bytes):
        return value.decode('utf8')
    return value

__unicode__ から呼ばれている color_str 関数にテキストの色を変更するコードが含まれています。以下は、 color_str の色を変更している部分を抜粋したものです。% 演算子は古くからあるPythonの文字列フォーマットです。

c = '%s%s%s%s%s' % (getattr(colorama.Fore, self.color),
                    getattr(colorama.Style, style),
                    self.s,
                    colorama.Fore.RESET,
                    getattr(colorama.Style, 'NORMAL'))

self.s が変換対象の文字列で、その前後が colorama というモジュールの値で挟まれていることがわかります。ターミナルに色付きの文字を表示する鍵は colorama にありそうです。

colorama

coloramaは、ANSI escape codeのショートハンドを提供するライブラリです。crayonsは、colorama経由で ANSI escape code を使い、色付きのテキストを表示しています。

github.com

ANSI escape code とは、ターミナル操作のための特殊文字列のようなものです(参考)。テキストに色をつけるだけではなく、カーソルの位置を移動したり、スクロールしたり、テキストの背景色を変更したりもできます。

coloramaのコードを読む

colorama/ansi.py に ANSI escape codeに該当する部分がありました。ここを読んでいきます。

エスケープコード

エスケープコードは、ESC[ ではじまるフォーマットで書かれます。ただし、ESC はエスケープを表すものであり、実際にこの文字列を書くわけではありません。エスケープに対応するASCIIコードを書きます。colorama では、ソースコードの冒頭でエスケープのための文字列が定義されています。

CSI = '\033['

エスケープはASCIIコードの「27」に該当します。これを8進数で表すと 33 となります。ASCIIコードは10進数で指定できないため、このように8進数で定義しています。16進数で指定することもでき、その場合は x1b となります。8進数でも16進数でも結果は同じです。Pythonはエスケープシーケンスを\ で表現するので、組み合わせて \033 と表現しています。もちろん、\x1b でも構いません。

テキストの色やスタイルを設定するには、それらに対応したコード<code>\033m で挟んだ以下のフォーマットで書くようです。<code> には数値が入ります(参考)。

'\033[m`

カラーコードの定義

colorama/ansi.py の中には、AnsiCursor AnsiFore AnsiBack AnsiStyle といったクラスが定義されており、それぞれのクラスにはターミナルの操作に応じたコードが指定されています。テキストの色に関するコードは、AnsiForeクラスに定義されています。

class AnsiFore(AnsiCodes):
    BLACK           = 30
    RED             = 31
    GREEN           = 32
    YELLOW          = 33
    BLUE            = 34
    MAGENTA         = 35
    CYAN            = 36
    WHITE           = 37
    RESET           = 39

AnsiCodesというクラスを継承しているようなので、こちらを見てみましょう。見やすさのためにコメントを省略しています。

class AnsiCodes(object):
    def __init__(self):
        for name in dir(self):
            if not name.startswith('_'):
                value = getattr(self, name)
                setattr(self, name, code_to_chars(value))

def code_to_chars(code):
    return CSI + str(code) + 'm'

dir でクラスに関する情報をすべて取得しています。_ ではじまる名前のものは処理しないようにしています。これは、__xxx__ のような関数はPythonの特別な関数であるため処理しないようにしています。AnsiCodesを継承するクラスのインスタンスが作成されると、__init__ が実行されて、BLACKやREDなどの属性にcode_to_chars をかませています。

code_to_chars は色を表す値に対して、先頭にエスケープコード、末尾に m をつけています。code_to_chars によって作られた文字列は、setattr でインスタンス変数と設定されます。同じ名前のクラス変数とインスタンス変数がある場合は、インスタンス変数が優先されます。実際に値を取得してみると、以下のようになります。

>>> form colorama import Fore
>>> Fore.BLACK
'\x1b[30m'
>>> Fore.RED
'\x1b[31m'
>>> Fore.GREEN
'\x1b[32m'
>>> Fore.RESET
'\x1b[39m'

Pythonのインタプリタでは、\033\x1b へと自動的に変換されて表示されるようです。このコードを print 等で実行すれば、以降のテキストの文字色が変わることが確認できると思います。

リセットコードについて

変更内容のリセットは明示的にする必要があり、リセットに対応したエスケープコードをつけます。Fore.RESETの値がリセットに対応するコードです。ターミナルを操作する場合は、リセットも意識する必要があります。たとえば、一度青色にして、元の色に戻したい場合は以下のようにします。

'\033[34mblue color text\033[39m default color text'   # printなどで出力する

f:id:subarunari:20190121004255p:plain

crayons の color_str では、対象文字列の末尾にリセットコードをつけているので、自分でリセットする必要はありません。

複数のエスケープシーケンスを指定する

先ほど見た crayons のコードをもう一度確認すると、「テキストの色」と「テキストのスタイル」のために複数のコードを連続で指定しているのがわかります。

c = '%s%s%s%s%s' % (getattr(colorama.Fore, self.color),
                    getattr(colorama.Style, style),
                    self.s,
                    colorama.Fore.RESET,
                    getattr(colorama.Style, 'NORMAL'))

正確には、「文字色のコード + スタイルのコード + テキスト + スタイルのリセットコード + 文字色のリセットコード」で構成されています。たとえば、赤色+太字を指定した場合、cの値は以下のようになります。1 が太字、31 が赤色のためのコードです。039 はそれぞれスタイルと文字色のリセットコードです。

'\033[1m\033[31mred bold text\033[39m\033[0m'

別々に指定するのではなく、;区切りで一度に指定することもできます。

'\033[1;31mred bold text\033[0;39m'

背景色の指定

ついでなので背景色も指定してみます。ボールドにする値「1」、背景色を赤色にするための値「41」、テキストをシアンにする値「36」を指定してみます。

'\033[1;41;36mred bold text\033[0;49;39m'

f:id:subarunari:20190121011607p:plain

なお、crayons には背景色を指定する方法がありません。もし背景色を変えたいなら、自分で対処する必要があります。

さいごに

以上、ターミナルのテキストの色を変える方法を crayons と colorama から理解したときのメモでした。ANSI escape codeを指定することでターミナルに対する操作が行え、その操作の中に色を変更する操作があるということがわかりました。基本的にライブラリを使うので、直接これらのコードを使う機会はあまりありませんが、知っておいて損はないかと思います。

自分の知らない仕様をコードから理解するのも面白いなぁと思い、OSSのコードと絡めた記事にしてみました。

参考