Help us understand the problem. What is going on with this article?

シェルスクリプトのechoで”問題なく”色をつける(bash他対応)

解説

echo に色をつけるという記事はあちこちにありますが、なぜ今更こんな記事を書くのかというと、世の中に多く出回ってるコードでは問題があるからです。例えばよくこのようなコードが紹介されています。

echo -e "\e[31m RED \e[m"

ですがこれ、シェルによっては意図したとおりに出力されません。bash では問題なく出力されますが、例えば dash では余計な -e が出力されたりします。(みんな bash しか使ってないんだろうか? Debian / Ubuntu の /bin/sh は dash なんですが…)

各シェル(Ubuntu 18.04でパッケージからインストール)で echo -e "\e[31m RED \e[m" を実行した結果は以下のとおりです。(各シェルのデフォルトの挙動です。一部のシェルは set, setopt, ECHO_STYLE 変数で挙動を変更できます。)

シェル 出力 シェル 出力
dash -e RED mksh RED
bash RED yash -e \e[31mRED\e[m
zsh RED posh -e \e[31mRED\e[m
ksh \e[31mRED\e[m busybox ash RED

色がつかないものが存在しますね。では echo -e "\033[31mRED\033[m" ではどうでしょうか?

シェル 出力 シェル 出力
dash -e RED mksh RED
bash RED yash -e RED
zsh RED posh -e RED
ksh RED busybox ash RED

全て色が付きました。(実は debian 3.1 時代の古い ksh 93q では echo -e "\033[31mRED\033[m" とそのまま表示されました。)しかし余計な -e がついているものがあります。ならば -e をとって echo "\033[31mRED\033[m" としてみましょう。

シェル 出力 シェル 出力
dash RED mksh RED
bash \033[31mRED\033[m yash RED
zsh RED posh RED
ksh \033[31mRED\033[m busybox ash \033[31mRED\033[m

残念。というか当然というか。-e なしでは \033 がエスケープ文字と解釈されず色がつかないものがあります。

このように echo コマンドはシェルによって実装がバラバラで互換性がありません。ではどのようにすれば、問題なく色をつけられるでしょうか?一番簡単な方法は echo を使うのをやめ printf を使う方法です。printf '\033[31m%s\033[m\n' 'RED' とすればどのシェルでも色を付けることが出来ます。(この記事は echo の話では? それじゃ echo じゃないじゃんって声が聞こえてきそうです。)

根本的な問題は echo がバックスラッシュによるエスケープを解釈したりしなかったり -e が必要だったりするからです。なので(解釈済みの)エスケープ文字を変数に入れて使えば良いのです。

ESC=$(printf '\033')
echo "${ESC}[31mRED${ESC}[m"

注意点がいくつかあります。

  • $'\e' という書き方に対応してないシェルがあるので printf でエスケープ文字を変数に入れます
  • \e, \x1b, \33 に対応してない printf があるため \033 を使用します
  • ${ESC} ではなく $ESC と書くと zsh で invalid subscript のエラーになります
  • 文字列にバックスラッシュが含まれる場合 echo が対応しているシーケンス(例 \n 等)はシェル依存です

別解

echoが -e に対応しているかどうかで処理を分岐させ、必ずバックスラッシュによるエスケープを解釈するようにします。

[ "${BASH_VERSION:-}" ] && shopt -s expand_aliases # bashでシェルスクリプトでaliasを使えるようにするため
[ "$(echo -e)" ] || alias echo='echo -e' # aliasを使わずに関数でラップしても良いです
echo '\033[31mRED\033[m'

この方法であれば、文字列中のバックスラッシュは必ず解釈されるため \n は必ず改行として扱われるでしょう。ただしシェルによって echo が対応しているシーケンスに違いがあり、例えば bash では \x41 という文字は A と表示されますが dash では \x41 と表示されてしまうので注意が必要です。

今回のテーマは echo に色をつけるという点なのでシェルによる挙動の違いには目をつぶる事とします。固定の文字列を表示するなど文字列中に意図しないバックスラッシュが入ることがないのであれば特に問題は起きないでしょう。

結論

1. echo ではなく printf を使う

※ 通常のおすすめ

ESC=$(printf '\033') # \e や \x1b または $'\e' は使用しない
printf "${ESC}[31m%s${ESC}[m\n" 'RED'
printf '\033[31m%s\033[m\n' 'RED'
  • エスケープ文字は変数に入れても直接書いても良い
  • 一番手っ取り早いが printf がビルトインでない場合(下記のシェル)は遅くなる
    • zsh 4.0 以前
    • ksh88
    • mksh(コンパイルオプションによる。例 Debian 4~10 の中で 6・7 のみビルトイン)
    • OpenBSD ksh (loksh, oksh も含む)
    • posh

2. echo-e なしで使う

※ printf が遅くて困った人用

ESC=$(printf '\033') # \e や \x1b または $'\e' は使用しない
echo "${ESC}[31mRED${ESC}[m"
  • エスケープ文字は変数に入れて使う
  • 注意 echo が対応しているシーケンスに違いがあるためシェルによって出力が異なる場合がある

3. echo を必ずバックスラッシュを解釈するようにする

※ おすすめしない

ESC=$(printf '\033') # \e や \x1b または $'\e' は使用しない
[ "${BASH_VERSION:-}" ] && shopt -s expand_aliases
[ "$(echo -e)" ] || alias echo='echo -e' # aliasではなくラッパー関数を作っても良い
echo "${ESC}[31mRED${ESC}[m"
echo '\033[31mRED\033[m'
  • エスケープ文字は変数に入れても直接書いても良い
  • 注意 echo が対応しているシーケンスに違いがあるためシェルによって出力が異なる場合がある
  • 以下の理由でおすすめしない
    • bash だと expand_aliases を変更しないと alias が使えない。
    • マイナーだが alias に対応してないシェル (posh) がある。
    • ラッパー関数を作るとすべての echo を書き換える必要がある
    • どうせ書き換えるなら 4. を使ったほうが良い
    • そこまでする必要がない場合は、一般的な 1. の printf の方が良い

4. バックスラッシュを解釈しない putsn 関数を使う

※ 完璧を求める人用

そもそも echo コマンドがオプションやバックスラッシュやシーケンスへの対応がまちまちでシェルによって挙動が違うのがいけないわけです。そこでこれらを全く解釈せずどのシェルでも同じように動作する putsn 関数を作りました。(実装はこちら

ESC=$(printf '\033') # \e や \x1b または $'\e' は使用しない
putsn "${ESC}[31mRED${ESC}[m"
  • エスケープ文字は変数に入れて使う
  • バックスラッシュやオプションをそのまま表示することを保証しているのでどのシェルでも挙動が同じ
  • 各シェルへのワークアラウンドを行ってるので printf がビルトインでなくとも遅くならない
  • この putsn 関数 を使ってビルトインの echo を置き換えることが可能です

おまけ1 色指定方法 一覧

8色(16色)

ついでによく使われる属性と色の一覧を載せておきます。よく使うであろう前景色はコピペできるように色付きで指定方法を書いておきます。複数同時に指定する場合は値をセミコロンで区切ります。

  • \033[値m (値は下記の属性、前景色、背景色のこと)
  • \033[属性;前景色;背景色m (実際に値はどの順番でも構いません。またいくつでも構いません)
  • リセットは \033[m または \033[0m (色も含めて全てリセットされます)
  • 下記の 8 色に「太字」の属性を組み合わせることで 16 色になります。(詳細は下の方で説明)
属性 属性 前景色 前景色 指定例 背景色
0 リセット 30 黒 \033[30m BLACK \033[m 40 黒
1 太字 21 二重下線 31 赤 \033[31m RED \033[m 41 赤
2 低輝度 22 太字・低輝度解除 32 緑 \033[32m GREEN \033[m 42 緑
3 イタリック 23 イタリック解除 33 黄 \033[33m YELLOW \033[m 43 黄
4 下線 24 下線解除 34 青 \033[34m BLUE \033[m 44 青
5 点滅 25 点滅解除 35 紫 \033[35m MAGENTA \033[m 45 紫
6 高速点滅 36 水 \033[36m CYAN \033[m 46 水
7 反転 27 反転解除 37 白 \033[37m WHITE \033[m 47 白
8 非表示 28 非表示解除 38 拡張色 256 色 または 24bit カラー(下記参照) 48 拡張色
9 取り消し線 29 取り消し線解除 39 標準色 49 標準色

※ 一部の属性は正しく表示されないことがあります。対応している属性は使用している端末(ターミナルソフト)によって異なります。

# 8色 + 太字 + 低輝度 の組み合わせを表示するワンライナー
seq 30 37 | xargs -I {} printf '\033[{}m{}\033[m \033[1;{}m1;{}\033[m \033[2;{}m2;{}\033[m\n'

16色対応について

こちらによると、多くのシェルでは「太字」を実際にフォントを太くするのではなく「明るい色」として実装したそうです。そのため本来は 8 色ですが事実上は 16 色相当となっているようです。またのちにコード 90-97 で明るい前景色、コード 100-107 で明るい背景色を直接指定できるようにしたので対応しているシェルでは「太字コード 1+色コード 30-37」の代わりに「色コード 90-97」でも同じ色で表示されるはずです。

256色

指定方法(色番号 は 0-255)

  • 前景色 \033[38;5;色番号m
  • 背景色 \033[48;5;色番号m

色番号について

  • 0-7:標準色 8色(\033[30m-\033[37m と同じ)
  • 8-15:明るい色 8色(\033[90m-\033[97m と同じ)
  • 16-231:各色(赤・緑・青) 6 段階、6×6×6=216色
  • 232-255:グレースケール、24色
# 256色を表示するワンライナー
seq 0 255 | xargs -I {} printf '\033[38;5;{}m{}\033[m '

1677万色(24bit カラー)

赤・緑・青 は 0-255

  • 前景色 \033[38;2;赤;緑;青m
  • 背景色 \033[48;2;赤;緑;青m

参考 https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters

おまけ2 色変数定義

結局の所、実際に使う時はみんな変数に入れて使いますよね?ということで用意しておきました。

# eval "$(printf "TAB='\\011' LF='\\012' ESC='\\033'")" 下記参照
ESC=$(printf '\033') RESET="${ESC}[0m"

BOLD="${ESC}[1m"        FAINT="${ESC}[2m"       ITALIC="${ESC}[3m"
UNDERLINE="${ESC}[4m"   BLINK="${ESC}[5m"       FAST_BLINK="${ESC}[6m"
REVERSE="${ESC}[7m"     CONCEAL="${ESC}[8m"     STRIKE="${ESC}[9m"

GOTHIC="${ESC}[20m"     DOUBLE_UNDERLINE="${ESC}[21m" NORMAL="${ESC}[22m"
NO_ITALIC="${ESC}[23m"  NO_UNDERLINE="${ESC}[24m"     NO_BLINK="${ESC}[25m"
NO_REVERSE="${ESC}[27m" NO_CONCEAL="${ESC}[28m"       NO_STRIKE="${ESC}[29m"

BLACK="${ESC}[30m"      RED="${ESC}[31m"        GREEN="${ESC}[32m"
YELLOW="${ESC}[33m"     BLUE="${ESC}[34m"       MAGENTA="${ESC}[35m"
CYAN="${ESC}[36m"       WHITE="${ESC}[37m"      DEFAULT="${ESC}[39m"

BG_BLACK="${ESC}[40m"   BG_RED="${ESC}[41m"     BG_GREEN="${ESC}[42m"
BG_YELLOW="${ESC}[43m"  BG_BLUE="${ESC}[44m"    BG_MAGENTA="${ESC}[45m"
BG_CYAN="${ESC}[46m"    BG_WHITE="${ESC}[47m"   BG_DEFAULT="${ESC}[49m"

エスケープ・改行・タブの一括変数代入

この記事ではエスケープ文字を変数に代入して使用する方法を提示してきましたが、改行やタブなども変数に入れると使いやすくなるでしょう。

# LF=$(printf '\012') 単純なコマンド置換だと末尾の改行が消えるのでこれではだめ。
LF='
'
LF=$(printf '\012_'); LF=${LF%_} # 末尾に文字を追加しておきそれを削除する。よく見かける。
LF=$'\n' # すべてのシェルで対応しているわけではない。

TAB=$(printf '\011')
ESC=$(printf '\033')

何度も printf がでてきてイヤですね? この printf を一度使用するだけで書くやり方がこちらです。

eval "$(printf "TAB='\\011' LF='\\012' ESC='\\033'")"

おまけ3 tput について

この記事で扱ってる通り、色を付けるためにはエスケープシーケンスを使用するわけですが、実は出力されたエスケープシーケンスをどのように表示するかは、使用している端末(ターミナルソフト)によって異なります。もしかしたらここで書いたエスケープシーケンスに対応しておらず別のエスケープシーケンスによって色をつける端末が存在するかもしれません。

tput はその違いを吸収しユーザーが使用している端末(xterm や puttyなど)を TERM 環境変数より判定し、端末ごとに適切なエスケープシーケンスを出力します。(それを実現するために各端末でどのようなエスケープシーケンスを使用するかというデータベースを持っています。)

tput を実行すると以下のように先程のエスケープシーケンスと同じものが出力されます。

# 前景色を赤にするエスケープシーケンス
#(odコマンドで表示可能文字はそのままそれ以外を8進数で表示しています)
$ tput setaf 1 | od -An -tc 
 033   [   3   1   m

tput は POSIX で規定されており (https://pubs.opengroup.org/onlinepubs/9699919799/utilities/tput.html) 可搬性を求めるのであれば tput を使用したほうが良い・・・ということになっているはずですが私は以下の理由で使用していません。

  • tput がインストールされておらず使えない場合がある(例 Docker の Alpine Linuxイメージ)
  • 外部コマンド呼び出しとなるので遅い
  • 太字+赤など二回コマンド呼び出しが必要になる(GNUでは -S オプションがあるが POSIX 準拠ではない)
  • どちらにしろ色番号指定なのでわかりやすくなるわけではない
  • 一般的なエスケープシーケンス以外を使う端末はもう殆ど使われてない(はず)
  • すでにエスケープシーケンスを直接指定する方法が広く使われており、それによる問題が話題になることは少ない

現実の話としてエスケープシーケンスを直接使用しても問題が確認できず、遅かったり使えない環境があるというデメリットがあります。昔は互換性がない端末の種類が多かったから必要だったのだとは思いますが、今は逆に tput を使用した方が問題が発生することが多いと思います。

もしマイナーな環境やエスケープシーケンスで必要なことがあれば、独自で TERM 環境変数を見て出力を変えるなど tput に変わる仕組みを再発明すれば十分だと考えています。アプリで使用するエスケープシーケンスだけ対応すればいいので必要な作業はそう多くないでしょう。そういうのが必要な環境って今どれだけあるのでしょうね?

ko1nksm
おそらくウェブアプリエンジニア。フロントやったりサーバーやったりたまにインフラ。好きなもの:シンプルで無駄のないコード、リファクタリング。嫌いなもの:技術的負債、レガシーコード
https://blog.nksm.name/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした