シェルスクリプトの画面出力を考えてみる

この記事は [チームスピリット Advent Calendar 2017](https://adventar.org/calendars/2207) の7日目です。
僕はシェルスクリプトはあまり詳しくありませんが、~~クソツールをみんなに押し付ける~~自動化するのが大好きです!

# ユーザーの都合を知る

まずユーザーの都合について知っておこう、というわけでユーザーが引数で画面出力について指定できるようにする。

## --trace

処理の中で実行するコマンドの標準出力を全て表示する。実行シェルが `bash` や `zsh` ならシェルのトレースも有効にする。

## --no-color

データなどを出力する場合は、ANSIカラーの出力を無効にしてパイプライン処理を行いやすくする。(といっても、大抵のコマンドは自動判別してくれる。)

# ログレベル

大抵のアプリケーションは **TRACE**, **DEBUG**, **INFO**, **WARN**, **ERROR** だが、ツールとしてのシェルスクリプトを書くという前提で使いやすいように再定義してみる。

* **WAIT** - 時間のかかる処理が始まったことを通知するメッセージ。
* **DONE** - 時間のかかる処理が終了したことを通知するメッセージ。
* **WARN** - 処理の続行は出来るが好ましくない状況が発生したことを通知するメッセージ。
* **FAIL** - 処理が失敗したことを通知するメッセージ。

# テキストフォーマット

* 画面分割で処理することも考慮して、一行を80文字以内で収めることが好ましい。
    エディターがVimであれば、行末に `# vi: et sw=2 cc=80` と記述しておくと分かりやすい。

* 処理の間に発生したメッセージは、行頭にインデントとして半角スペース2つを挿入する。
* 処理の間に発生したメッセージの後は空行を1行入れる。

# メッセージカラー

![GatsbyJS](https://qiita-image-store.s3.amazonaws.com/0/102243/4bb2e2b4-d065-d0fa-e4c5-22153b8b572d.png)

* **WAIT** - 青
* **DONE** - 緑
* **WARN** - 黄色
* **FAIL** - 赤
* 強調 (処理対象のファイルパスなど) - 白
* コマンドのサジェスト - 水色(シアン)

ここらへんはFacebookのCLIツールのパクリです。センスがないので丸パクリ。

# 実装してみる

## ロガー

`zsh` でのロガーの実装例です。

```zsh
autoload -Uz colors
colors

__chalk-internal() {
  local prefix="$1"
  local suffix="$2"
  local text="$3"
  echo "${prefix}${text}${suffix}"
}

chalk-em() {
  local text="$1"
  __chalk-internal "${fg[white]}" "${reset_color}" "$text"
}

chalk-cmd() {
  local text="$1"
  __chalk-internal "${fg[cyan]}" "${reset_color}" "$text"
}

__log-internal() {
  local label_prefix="$1"
  local label_suffix="$2"
  local text_prefix="$3"
  local text_suffix="$4"
  local label="$5"
  local text="$6"
  echo "${label_prefix} ${label} ${label_suffix} ${text_prefix}${text}${text_suffix}"
}

log-wait() {
  local text="$1"
  __log-internal \
      "${bg[blue]}${fg[black]}" \
      "${reset_color}" \
      "${fg[blue]}" \
      "${reset_color}" \
      "WAIT" \
      "$text"
}

log-done() {
  local text="$1"
  __log-internal \
      "${bg[green]}${fg[black]}" \
      "${reset_color}" \
      "${fg[green]}" \
      "${reset_color}" \
      "DONE" \
      "$text"
}

log-warn() {
  local text="$1"
  __log-internal \
      "${bg[yellow]}${fg[black]}" \
      "${reset_color}" \
      "${fg[yellow]}" \
      "${reset_color}" \
      "WARN" \
      "$text"
}

log-fail() {
  local text="$1"
  __log-internal \
      "${bg[red]}${fg[black]}" \
      "${reset_color}" \
      "${fg[red]}" \
      "${reset_color}" \
      "FAIL" \
      "$text"
}
```

## ロガーを使ったスクリプト

```zsh
cat <<EOM
This is an article of the 7th day of $(chalk-em "TeamSpirit Advent Calendar 2017")!
You're still not using Vim? Calm down and run $(chalk-cmd "vimtutor").

EOM

log-wait 'Checking $EDITOR ...'

log-warn "\$EDITOR is not 'vim' nor 'nvim'. That's weird..."

log-fail "$EDITOR is 'emacs'!"

cat <<EOM
  Don't worry, the problem has been fixed!
  We've just added a line $(chalk-cmd "alias emacs='vim'") to your $(chalk-em ".zshrc") :D

EOM

log-done 'Finish successfully!'
```

## 実行結果

![./logger.zsh](https://qiita-image-store.s3.amazonaws.com/0/102243/2f948ff3-c794-5ffe-5bc6-ab8dafb61272.png)

# オマケ

僕がシェルスクリプトでツールを作る際に心掛けていることです。

* 処理内容が作業ディレクトリに依存しない。(ヘルパーの参照など)
* 処理が冪等である。複数回実行や途中中断しても整合性が保たれているようにする。(必要に応じて `trap` を使う)
* 環境や依存コマンドのチェックなどは最初に済ませておき、処理の途中でエラーになることを極力防ぐ。(可能ならロールバックする。)
* 処理が複数ある場合は実行引数で特定の処理に限定することが出来る。