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

- **[色指定方法 一覧](#おまけ1-色指定方法-一覧)** はこちら （さくっと出力したい人用）
- 手っ取り早く **[結論](#結論)** を見たい人はこちら

## 解説

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

```sh
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 <font color="red">RED</font> |     | **mksh**        | <font color="red">RED</font> |
| **bash** | <font color="red">RED</font>    |     | **yash**        | -e \e[31mRED\e[m             |
| **zsh**  | <font color="red">RED</font>    |     | **posh**        | -e \e[31mRED\e[m             |
| **ksh**  | \e[31mRED\e[m                   |     | **busybox ash** | <font color="red">RED</font> |

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

| シェル   | 出力                            |     | シェル          | 出力                            |
| -------- | ------------------------------- | --- | --------------- | ------------------------------- |
| **dash** | -e <font color="red">RED</font> |     | **mksh**        | <font color="red">RED</font>    |
| **bash** | <font color="red">RED</font>    |     | **yash**        | -e <font color="red">RED</font> |
| **zsh**  | <font color="red">RED</font>    |     | **posh**        | -e <font color="red">RED</font> |
| **ksh**  | <font color="red">RED</font>    |     | **busybox ash** | <font color="red">RED</font>    |

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

| シェル   | 出力                         |     | シェル          | 出力                         |
| -------- | ---------------------------- | --- | --------------- | ---------------------------- |
| **dash** | <font color="red">RED</font> |     | **mksh**        | <font color="red">RED</font> |
| **bash** | \033[31mRED\033[m            |     | **yash**        | <font color="red">RED</font> |
| **zsh**  | <font color="red">RED</font> |     | **posh**        | <font color="red">RED</font> |
| **ksh**  | \033[31mRED\033[m            |     | **busybox ash** | \033[31mRED\033[m            |

残念。というか当然というか。`-e` なしでは `\033` がエスケープ文字と解釈されず色がつかないものがあります。このように `echo` コマンドはシェルによって実装がバラバラで互換性がありません。[POSIX](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/echo.html) ではどのように規定されているのか見てみると以下のように書かれています。

> 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` が必要だったりするからです。なので（解釈済みの）エスケープ文字を変数に入れて使えば良いのです。

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

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

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

**別解**

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

```sh
[ "${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` を使う

**※ 通常のおすすめ**

```sh
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 が遅くて困った人用**

```sh
ESC=$(printf '\033') # \e や \x1b または $'\e' は使用しない
echo "${ESC}[31mRED${ESC}[m"
```

- エスケープ文字は変数に入れて使う
- **注意** `echo` が対応しているシーケンスに違いがあるためシェルによって出力が異なる場合がある

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

**※ おすすめしない**

```sh
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` 関数を作りました。（実装は[こちら](https://qiita.com/ko1nksm/items/d0b066268cda42ff24eb)）

```sh
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[**30**m <font color="black">**BLACK**</font> \033[m     | 40 黒 (100) |
| **1 太字**      | 21 二重下線             | 31 赤 (91) | \033[**31**m <font color="red">**RED**</font> \033[m         | 41 赤 (101) |
| 2 低輝度        | **22 太字・低輝度解除** | 32 緑 (92) | \033[**32**m <font color="green">**GREEN**</font> \033[m     | 42 緑 (102) |
| 3 イタリック    | 23 イタリック解除       | 33 黄 (93) | \033[**33**m <font color="yellow">**YELLOW**</font> \033[m   | 43 黄 (103) |
| **4 下線**      | **24 下線解除**         | 34 青 (94) | \033[**34**m <font color="blue">**BLUE**</font> \033[m       | 44 青 (104) |
| 5 点滅          | 25 点滅解除             | 35 紫 (95) | \033[**35**m <font color="magenta">**MAGENTA**</font> \033[m | 45 紫 (105) |
| 6 高速点滅      |                         | 36 水 (96) | \033[**36**m <font color="cyan">**CYAN**</font> \033[m       | 46 水 (106) |
| **7 反転**      | **27 反転解除**         | 37 白 (97) | \033[**37**m <font color="white">**WHITE**</font> \033[m     | 47 白 (107) |
| 8 非表示        | 28 非表示解除           | 38 拡張色  | 256 色 または 24bit カラー                                   | 48 拡張色   |
| 9 取り消し線    | 29 取り消し線解除       | 39 標準色  |                                                              | 49 標準色   |

#### 16色について

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

```sh
# 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色

```sh
# 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 色変数定義

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

```sh
# 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"
```

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

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

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

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

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

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

## おまけ3 `tput` について

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

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

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

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