Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
56
Help us understand the problem. What is going on with this article?
@ko1nksm

シェルスクリプトの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 コマンドはシェルによって実装がバラバラで互換性がありません。POSIX ではどのように規定されているのか見てみると以下のように書かれています。

If the first operand is -n, or if any of the operands contain a <backslash> character, the results are implementation-defined.

「最初のオペランド(引数)が -n またはオペランドにバックスラッシュが含まれている場合、その結果は実装による」ということなのでバックスラッシュの解釈はどの実装も POSIX 違反にはなりません。一応その下の行に追加の仕様が書いてあり、エスケープシーケンスを解釈するのが正しいと思うかもしれませんが、これは XSI (X/Open System Interfaces) の拡張機能なので、POSIX 準拠システムとしては必ず実装すべきというものではありません。

[XSI] On XSI-conformant systems, if the first operand is -n, it shall be treated as a string, not an option. The following character sequences shall be recognized on XSI-conformant systems within any of the arguments:

ちなみに XSI 拡張では、最初の引数が -n の場合はそのまま文字列として出力されるとあります。殆どのシェルで -n は改行を行わないというオプションであり、私が知る限り XSI 拡張に準拠している echo は POSIX モード (/bin/sh) として実行した Apple 版(パッチを当てている)の bash だけです。(なんで POSIX はそんな仕様にしたんだ?と思うかもしれませんが、そもそも先にそのようなシェルが存在しており POSIX は既存の実装を元に取りまとめたものだからです。)

ではどのようにすれば、問題なく色をつけられるでしょうか?一番簡単な方法は 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[0m (色も含めて全てリセットされます)
  • 対応している属性は使用している端末(ターミナルソフト)によって異なります
  • () の中は明るい色
属性 属性 前景色 (前景色 指定例) 背景色
0 リセット 30 黒 (90) \033[30m BLACK \033[m 40 黒 (100)
1 太字 21 二重下線 31 赤 (91) \033[31m RED \033[m 41 赤 (101)
2 低輝度 22 太字・低輝度解除 32 緑 (92) \033[32m GREEN \033[m 42 緑 (102)
3 イタリック 23 イタリック解除 33 黄 (93) \033[33m YELLOW \033[m 43 黄 (103)
4 下線 24 下線解除 34 青 (94) \033[34m BLUE \033[m 44 青 (104)
5 点滅 25 点滅解除 35 紫 (95) \033[35m MAGENTA \033[m 45 紫 (105)
6 高速点滅 36 水 (96) \033[36m CYAN \033[m 46 水 (106)
7 反転 27 反転解除 37 白 (97) \033[37m WHITE \033[m 47 白 (107)
8 非表示 28 非表示解除 38 拡張色 256 色 または 24bit カラー(下記参照) 48 拡張色
9 取り消し線 29 取り消し線解除 39 標準色 49 標準色

16色について

こちらによると、多くの端末では「太字」を実際にフォントを太くするのではなく「明るい色」として実装したそうです。そのためこれらの端末では太字にすることで 16 色表示を行うことができます。(「太字」を本当に太字で表示する端末もあります。)また、のちにコード 90-97 に明るい前景色、コード 100-107 に明るい背景色が追加されました。

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

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

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 に変わる仕組みを再発明すれば十分だと考えています。アプリで使用するエスケープシーケンスだけ対応すればいいので必要な作業はそう多くないでしょう。そういうのが必要な環境って今どれだけあるのでしょうね?

56
Help us understand the problem. What is going on with this article?
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
ko1nksm
おそらくウェブアプリエンジニア。フロントやったりサーバーやったりたまにインフラ。好きなもの:シンプルで無駄のないコード、リファクタリング。嫌いなもの:技術的負債、レガシーコード

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
56
Help us understand the problem. What is going on with this article?