ShellScript
Bash
shell

シェルスクリプトのechoで”問題なく”色をつける

解説はいらないって方は結論をクリック


解説

なんで今更こんな記事を書くのかというと、世の中に多く出回ってるコードでは問題があるからです。

例えばよくこのようなコードが紹介されています。

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

ですがこれ、シェルによっては意図したとおりに出力されません。

bashでは問題なく出力されますが、例えば dashでは余計な -e が出力されたりします。(みんなbashしか使ってないんだろうか?)

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

シェル
出力

dash
-e RED

bash
RED

zsh
RED

ksh
\e[31mRED\e[m

mksh
RED

yash
-e \e[31mRED\e[m

posh
-e \e[31mRED\e[m

busybox ash
RED

色がつかないものが存在しますね。

では echo -e "\033[31mRED\033[m" ではどうでしょうか?

シェル
出力

dash
-e RED

bash
RED

zsh
RED

ksh
RED

mksh
RED

yash
-e RED

posh
-e RED

busybox ash
RED

全て色が付きました。(実はdebian 3.1時代の古いksh 93qでは色がつきませんでした)

しかし余計な -e がついているものがあります。ならば -e をとって、 echo "\033[31mRED\033[m" としてみましょう。

シェル
出力

dash
RED

bash
\033[31mRED\033[m

zsh
RED

ksh
\033[31mRED\033[m

mksh
RED

yash
RED

posh
RED

busybox ash
\033[31mRED\033[m

残念。-eなしではエスケープシーケンスが解釈されず色がつかないものがあります。

このようにechoコマンドはシェルによって実装がバラバラで互換性がありません。

ではどのようにすれば、問題なく色をつけられるでしょうか?

一番簡単な方法は、echoを使うのをやめ、printfを使う方法です。

printf '\033[31m%s\033[m\n' 'RED' とすればどのシェルでも色を付けることが出来ます。

(でもそれじゃechoじゃないって声が聞こえてきそうです。)

根本的な問題はechoがエスケープシーケンスが解釈したりしなかったり解釈するのに -e が必要だったりするからです。なのでエスケープコードを変数に入れて使えば良いのです。

ESC=$(printf '\033')

echo "${ESC}[31mRED${ESC}[m"

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


  • エスケープシーケンスには \033 を使用します。 \e\x1b は対応してないシェルがあります


  • \033 ではなく \33 と書くと古いシェルで動かない場合があります


  • ${ESC} ではなく $ESC と書くとzshでinvalid subscriptのエラーがでます

  • 文字列の中にエスケープシーケンス(例 \n 等)が含まれる場合、解釈されるかはシェルに依存します。

別解

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

[ "${BASH_VERSION:-}" ] && shopt -s expand_aliases # bashでシェルスクリプト内でaliasを使えるようにするため

[ $(echo -e) ] || alias echo='echo -e'
echo '\033[31mRED\033[m'

# aliasを使わずに関数でラップしても良いです。

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

今回のテーマはechoに色をつけるという点なので、echoのシェルによる挙動の違いには目をつぶる事とします。文字列中にエスケープシーケンスが入ることがなければ特に問題は起きないでしょう。


結論

# 1. printf を使う。

# エスケープコードは変数に入れても直接書いても良い。
# (シェル依存がなく一番手っ取り早いが、printfがビルトインでない場合は遅い)
printf '\033[31m%s\033[m\n' 'RED'
printf "${ESC}[31m%s${ESC}[m\n" 'RED'

# 2. echoを-eなしで使う。そのためシェルによってはエスケープシーケンスが解釈されない。
# エスケープコードは変数に入れて使う。
# (シェルによって文字列中のエスケープシーケンスを解釈するかどうかの違いがあるので注意)
ESC=$(printf '\033') # \e や \x1b は使用しない
echo "${ESC}[31mRED${ESC}[m"

# 3. echoを関数で必ずエスケープシーケンスを解釈させる。
# エスケープコードは変数に入れても直接書いても良い。
# (シェルによってechoが対応しているエスケープシーケンスに違いがあるので注意)
[ "${BASH_VERSION:-}" ] && shopt -s expand_aliases
[ $(echo -e) ] || alias echo='echo -e' # aliasではなく関数でラップしても良い
echo '\033[31mRED\033[m'
echo "${ESC}[31mRED${ESC}[m"

# 4. エスケープシーケンスを解釈しない関数を作成して使う。
# エスケープコードは変数に入れて使う。
# (シェル依存がなく、printfがビルトインでなくとも遅くならない)
ESC=$(printf '\033') # \e や \x1b は使用しない
putsn "${ESC}[31mRED${ESC}[m"
# putsn関数の実装に関しては以下の記事を参照してください

※ putsn関数の実装 https://qiita.com/ko1nksm/items/d0b066268cda42ff24eb


  1. 2. はシェル依存がなく2.は1.の速度改良版。2.と3.は注意点はあるもののechoを使う場合の実装です。

おまけ1

ついでによく使われる属性と色の一覧を載せておきます。

複数同時に指定の仕方は値をセミコロンで区切るだけですね。 例 \033[値;値;値m

属性
前景色
背景色

0 属性なし
30 黒
40 黒

1 太字
31 赤
41 赤

2 薄
32 緑
42 緑

3 イタリック
33 黄
43 黄

4 下線
34 青
44 青

5 点滅
35 紫
45 紫

6 高速点滅
36 水
46 水

7 反転
37 白
47 白

8 非表示
38 拡張
48 拡張

9 取り消し線
39 標準色
49 標準色

※一部の属性に対応してない端末もあります。

256色表示 (色番号 は 0-255)


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

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

24bitカラー (赤・緑・青 は 0-255)


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

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

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

おまけ2

同じ理由でエスケープコードだけでなく改行やタブなども変数に入れて使用したほうが良いでしょう。

TAB=$(printf '\011')

# LF=$(printf '\012') 終端の改行コードが消されるのでこれではだめ
LF='
'

ESC=$(printf '\033')

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

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