時間がかかるコマンドの実行結果をSlackに通知する

## なに？
こういうやつ．
![image.png (43.2 kB)](https://img.esa.io/uploads/production/attachments/2063/2016/04/24/3140/c0beebbd-dc13-4754-bdab-24d76371b01b.png)

![image.png (43.6 kB)](https://img.esa.io/uploads/production/attachments/2063/2016/04/24/3140/0324ce63-4ef2-4a53-b03d-b34a9949adfc.png)


## モチベーション
`docker build`や機械学習，データ解析 ，画像処理等の「時間がかかるコマンド」について，いちいち終わったかなーみたいに確認しに行くのめんどくさいし頭のリソースのムダになる．実行完了/失敗したらSlackに通知したい．

通知先についてはどこでもいいんだけど，自分はbotとふたり暮らしのteam持ってるのでそこに流している．分報channel持ってるならそっちでもいいかもしれない．LINEやFBメッセンジャーに送るのもおもしろそうではある．

## zsh hookを利用してSlack通知
### アウトライン

```zsh
function notify_preexec {
  # TODO: 開始時間と実行コマンドを取得
}

function notify_precmd {
  # TODO: ステータスや実行環境からペイロード生成
  # TODO: Slackその他に送りつける
}

add-zsh-hook preexec notify_preexec
add-zsh-hook precmd notify_precmd
```

### 「時間がかかるコマンド」？
`$TTYIDLE`を見ればいい（サンプルはbeepを鳴らす）．

> ```zsh
> precmd() {
>   [ $TTYIDLE -gt 10 ] && echo -n ^G
> }
> ```
>
> <cite>[時間がかかるコマンドが終了したときに Beep! - f99aq8oveのブログ][beep]</cite>

### 実行したコマンドは？
`preexec`の引数見ればいいらしい．ついでに開始時間もとっておく？

```zsh
function notify_preexec {
  notif_prev_executed_at=`date`
  notif_prev_command=$2
}
```

### 実行結果のsuccess/fail判定は？
`$?`を見る．precmdの先頭で隔離しておかないとifの条件式で上書きされてしまうので注意．

```zsh
  notif_status=$?
  if [ -n "${SLACK_WEBHOOK_URL+x}" ] && [ -n "${SLACK_USER_NAME+x}" ] && [ $TTYIDLE -gt ${SLACK_NOTIF_THRESHOLD:-180} ]; then
    notif_title=$([ $notif_status -eq 0 ] && echo "Command succeeded :ok_woman:" || echo "Command failed :no_good:")
    notif_color=$([ $notif_status -eq 0 ] && echo "good" || echo "danger")
    notif_icon=$([ $notif_status -eq 0 ] && echo ":angel:" || echo ":smiling_imp:")

    # snip.
  fi
}
```

なお，このままだと`Ctrl-z`や`Ctrl-c`でコマンドを強制終了させた場合にも通知が飛んでしまう．Jupyter NotebookやRails，その他WAFのサーバプロセスを殺した場合や，Gulp等タスクランナーのファイル監視プロセスを殺した場合にも通知が出ると厄介なので，そこはstatusみて回避する．

```zsh
 if [ -n "${SLACK_WEBHOOK_URL+x}" ] && [ -n "${SLACK_USER_NAME+x}" ] && [ $TTYIDLE -gt ${SLACK_NOTIF_THRESHOLD:-180} ] && [ $notif_status -ne 130 ] && [ $notif_status -ne 146 ]; then
  # snip.
fi
```

### 送りつけるJSONは？
JSONをべちゃっと1行で書くのはちょっと遠慮したい．
bash/zsh等のヒアドキュメントは標準入力に流し込まれるので，`cat`して変数に突っ込んでおく．

Slackのメッセージのフォーマットは[公式のAPIドキュメント][slack-attachments]見てよしなに…．

```zsh
# ここではJSONの中身は省略してます
payload=`cat << EOS
{
  "attachments": [
    {
      "color": "$notif_color",
      "title": "$notif_title",
      "fields": [
        {
          "title": "command",
          "value": "$notif_prev_command",
          "short": false
        },
        {
          "title": "elapsed time",
          "value": "$TTYIDLE seconds",
          "short": true
        }
      ]
    }
  ]
}
EOS
`
```

Markdown（のようなもの）にする場合，`mrkdwn`に`true`をセットしてあげる．`attachment`内でMarkdown（のようなもの）を書きたい場合は`mrkdwn_in`に対象となる属性名を配列で渡してあげる．なお，コードブロックを含めたい場合，うまくエスケープしてあげないと死を招くので注意する．

```zsh
payload=`cat << EOS
{
  "attachments": [
    {
      "mrkdwn_in": ["fields"],
      "fields": [
        {
          "title": "command",
          "value": "\\\`$notif_prev_command\\\`",
          "short": false
        }
      ]
    }
  ]
}
EOS
````

バッククォートを含める場合，「バックスラッシュをエスケープしたもの」と「バッククォートをエスケープしたもの」を並べて書くと`payload`に「バックスラッシュ+バッククォート」が入る．これでこの後の`curl`でも適切にエスケープしてくれる．なに言ってるかわかんなくなってきた．

### どんなリクエスト投げる？
こんな感じ．bodyに改行あったら厄介なので`tr`で潰す．ついでにスペースも潰す．

```zsh
curl --request POST \
  --header 'Content-type: application/json' \
  --data "$(echo "$payload" | tr '\n' ' ' | tr -s ' ')" \
  $SLACK_WEBHOOK_URL
```

## 完成形
実行コマンド，`pwd`，`hostname`，`whoami`，開始時間，実行時間 [sec]を投げてます．
`SLACK_WEBHOOK_URL`と`SLACK_USER_NAME`は各自いい感じにどうぞ．

```zsh
if [ -z "${SLACK_WEBHOOK_URL+x}" ]; then
  echo "SLACK_WEBHOOK_URL is empty !!!"
fi

if [ -z "${SLACK_USER_NAME+x}" ]; then
  echo "SLACK_USER_NAME is empty !!!"
fi

function notify_preexec {
  notif_prev_executed_at=`date`
  notif_prev_command=$2
}

function notify_precmd {
  notif_status=$?
  if [ -n "${SLACK_WEBHOOK_URL+x}" ] && [ -n "${SLACK_USER_NAME+x}" ] && [ $TTYIDLE -gt ${SLACK_NOTIF_THRESHOLD:-180} ] && [ $notif_status -ne 130 ] && [ $notif_status -ne 146 ]; then
    notif_title=$([ $notif_status -eq 0 ] && echo "Command succeeded :ok_woman:" || echo "Command failed :no_good:")
    notif_color=$([ $notif_status -eq 0 ] && echo "good" || echo "danger")
    notif_icon=$([ $notif_status -eq 0 ] && echo ":angel:" || echo ":smiling_imp:")
    payload=`cat << EOS
{
  "username": "command result",
  "icon_emoji": "$notif_icon",
  "text": "<@$SLACK_USER_NAME>",
  "attachments": [
    {
      "color": "$notif_color",
      "title": "$notif_title",
      "mrkdwn_in": ["fields"],
      "fields": [
        {
          "title": "command",
          "value": "\\\`$notif_prev_command\\\`",
          "short": false
        },
        {
          "title": "directory",
          "value": "\\\`$(pwd)\\\`",
          "short": false
        },
        {
          "title": "hostname",
          "value": "$(hostname)",
          "short": true
        },
        {
          "title": "user",
          "value": "$(whoami)",
          "short": true
        },
        {
          "title": "executed at",
          "value": "$notif_prev_executed_at",
          "short": true
        },
        {
          "title": "elapsed time",
          "value": "$TTYIDLE seconds",
          "short": true
        }
      ]
    }
  ]
}
EOS
`
    curl --request POST \
      --header 'Content-type: application/json' \
      --data "$(echo "$payload" | tr '\n' ' ' | tr -s ' ')" \
      $SLACK_WEBHOOK_URL
  fi
}

add-zsh-hook preexec notify_preexec
add-zsh-hook precmd notify_precmd
```

## References
- [時間がかかるコマンドを実行した後に通知する - handlename's blog][notif]
- [時間がかかるコマンドが終了したときに Beep! - f99aq8oveのブログ][beep]
- [Message Attachments | Slack][slack-attachments]

[notif]: http://handlename.hatenablog.jp/entry/2013/02/02/190720
[beep]: http://f99aq.hateblo.jp/entry/20080101/1199196416
[slack-attachments]:https://api.slack.com/docs/attachments