設定を読み込む Hubot スクリプトをつくろう

これは [Hubot Advent Calendar 2014][hubot-adventar-2014] の 13 日目、のつもりで書いた 16 日目の記事です。真の 13 日目は mako09 さんが 『[hubot-slack アダプタ v2 から v3 へ][hubot-adventar-2014-13]』を書いてくださっているようです。

また、今回は @bouzuya の Hubot 連載の第 10 回です。[目次は、第 1 回の記事にある][hubot-adventar-2014-bouzuya-index]ので、そちらをどうぞ。

ちなみに昨日は @hiconyan さんの [研究室でHubot使ってる話][hubot-adventar-2014-11] でした。 @hiconyan さんとこの Huboco ちゃんはアイコンなど、きちんとしていてうらやましいですね。Hubot を定期的なジョブ実行に使う事例はよく見ますね。

# 前回まで、そして今回は

さて、前回は [Hubot スクリプトの読み込み処理を読んでみよう][hubot-adventar-2014-12] ということで、Hubot の起動処理を追いかけて、Hubot スクリプトがどんな風に読み込まれているのかを確認し、Node.js のプログラムとして単純に `require(script)(robot)` されているだけだということが分かりました。

今回は、Hubot スクリプトをつくる際に欠かせないテクニックとして設定を読み込む方法を紹介します。

(ちなみに前日の @hiconyan さんの記事の中で当然のように使われていますが、気にしない方向で)

# hubot-hello-world ふたたび

今回は設定を読み込む、ということで、Hubot スクリプトでは一般的な環境変数から値を読み込む処理をつくってみます。

元にする Hubot スクリプトは [第4回][hubot-adventar-2014-4] でつくった [bouzuya/hubot-hello-world][] で、それと類似の、**設定可能**な Hubot スクリプトをつくろうと思います。

# で、さっそく

`yo hubot:script` でつくってみました。`hubot-hello-xxx` という適当な名前で。

```coffeescript:src/hello-xxx.coffee
# Description
#   A Hubot script that responds XXX
#
# Configuration:
#   HUBOT_HELLO_XXX_MESSAGE
#   HUBOT_HELLO_XXX_MESSAGES
#
# Commands:
#   hubot hello - respond XXX
#   hubot hello! - respond XXX
#
# Author:
#   bouzuya <m@bouzuya.net>

config =
  message: process.env.HUBOT_HELLO_XXX_MESSAGE
  messages: JSON.parse(process.env.HUBOT_HELLO_XXX_MESSAGES ? '[]')

module.exports = (robot) ->
  unless config.message?
    robot.logger.error 'process.env.HUBOT_HELLO_XXX_MESSAGE is not defined'
    return

  robot.respond /hello$/, (msg) ->
    msg.send "#{config.message}, #{msg.envelope.user.name}!"

  robot.respond /hello!$/, (msg) ->
    message = msg.random config.messages
    msg.send message if message?
```

# 動かしてみましょう

解説の前に動かしてみましょう。

## 設定(環境変数)なし

```
$ HUBOT_SHELL_USER_NAME='bouzuya' PATH="./node_modules/hubot/node_modules/.bin:$PATH" $(npm bin)/hubot -a shell -n hubot -r src                               
[Sat Dec 13 2014 23:52:40 GMT+0900 (JST)] ERROR process.env.HUBOT_HELLO_XXX_MESSAGE is not defined
hubot> 
```

## 設定(環境変数)あり

```
$ HUBOT_HELLO_XXX_MESSAGES='["あびゃー","チョリーッス"]' HUBOT_HELLO_XXX_MESSAGE='うるせえ' HUBOT_SHELL_USER_NAME='bouzuya' PATH="./node_modules/hubot/node_modules/.bin:$PATH" $(npm bin)/hubot -a shell -n hubot -r src
hubot> hubot hello
うるせえ, bouzuya!
hubot> hubot hello!
あびゃー
hubot> 
```


# 解説

簡単な解説をつけます。

## `process.env.XXX`

環境変数からの読み込みは Node.js ユーザーからすればおなじみの `process.env.XXX` です。

Hubot は Heroku にデプロイできるようにデザインされているため、設定情報の読み込みは基本的に環境変数です。もちろん、外部から取得するなどの工夫は自由にできますが、一般的には環境変数の値が使われます。

## `HUBOT_HELLO_XXX_MESSAGE`

### 設定にはプリフィックスを

Hubot の設定に利用する環境変数の名前付けにはルールがあります。

それは先頭に `HUBOT_<スクリプト名>_` をつけるというものです。

ただ、あくまでも慣習であって、ここから外れるとどうなるというものではありません。合わせておくのが無難だとは思います。

### 設定はコメントに記載

また、読み込む環境変数の一覧はコメントの `Configuration:` 欄に書いておきます。ここに書くときには `HUBOT_` を削る派閥もあるようですが、個人的には、何を設定するのか分かりづらいので `HUBOT_` つきで書くことにしています。

## `config.XXX` 

今回のスクリプトがそうですが、設定項目を `config` などにまとめておく人も居ます。

ぼくは普段ほとんどしないのですが、試してみました。まあ、わかりやすいかもしれません。

## `JSON.parse ...`

複数の値を設定値として受け取る方法はいろいろあります。一般的なのはカンマ区切りだと思いますが、個人的にはエスケープなどを検討するのも面倒なので、 JSON を受け取るようにしています。

CoffeeScript であれば、 `undefined` や `null` の場合のデフォルト値を `process.env.XXX ? 'default'` のように提供しておくと良いかもしれません。

## `unless config.message`

設定がなかったときの挙動です。

### `robot.logger.error ...`

設定がなかったときにエラーログを残すようにしています。

ログについてはまた別に回をとって紹介したいと思います。

### `return` か `process.exit(1)` か

設定がなかったときの挙動はスクリプトごとにまちまちなのですが、必須の設定がなかった場合にものによっては `process.exit(1)` しているものもあります。

個人的にはログを残しつつ、動作しないようにするのが好きです。

# まとめ

Hubot スクリプトから設定(環境変数)を読み込む処理について紹介しました。

# 最後に

個人用のスクリプトなら自由にやればいいんじゃないですかね(適当)。

[getting-started-with-hubot]: https://github.com/github/hubot/tree/v2.9.3/docs#getting-started-with-hubot
[bouzuya/hubot-hello-world]: https://github.com/bouzuya/hubot-hello-world/
[bouzuya/hubot-choice]: https://github.com/bouzuya/hubot-choice/
[bouzuya/hubot-adventar]: https://github.com/bouzuya/hubot-adventar/
[bouzuya/hubot-homepage]: https://github.com/bouzuya/hubot-homepage/
[bouzuya/hubot-script-gist]: https://github.com/bouzuya/hubot-script-gist/
[hubot-script-per-day-61]: http://bouzuya.hatenablog.com/entry/2014/09/12/224215
[github/hubot]: https://github.com/github/hubot/
[github/generator-hubot]: https://github.com/github/generator-hubot/
[ops-for-everyone]: https://speakerdeck.com/johndbritton/ops-for-everyone
[adventar]: http://www.adventar.org/
[express-reference]: http://expressjs.com/3x/api.html
[hubot.github.com]: https://hubot.github.com/
[hubot-adventar-2014]: http://www.adventar.org/calendars/384
[hubot-adventar-2014-bouzuya-index]: http://qiita.com/bouzuya/items/c7d0ad80c357aab6b696
[hubot-adventar-2014-1]: http://qiita.com/bouzuya/items/c7d0ad80c357aab6b696
[hubot-adventar-2014-2]: http://qiita.com/bouzuya/items/11c0c6da2b3ad54b827f
[hubot-adventar-2014-3]: http://qiita.com/bouzuya/items/2a200c9e8a45e2478bc2
[hubot-adventar-2014-4]: http://qiita.com/bouzuya/items/4c0206d72ff22ade9339
[hubot-adventar-2014-5]: http://pasero.net/~mako/blog/s/685
[hubot-adventar-2014-6]: http://qiita.com/bouzuya/items/4e051f68f8d68a2944b0
[hubot-adventar-2014-7]: http://qiita.com/bouzuya/items/2e935c99647b93d8ee1d
[hubot-adventar-2014-8]: http://udzura.hatenablog.jp/entry/2014/12/08/182744
[hubot-adventar-2014-9]: http://qiita.com/bouzuya/items/587c4e920151342e6f1d
[hubot-adventar-2014-10]: http://qiita.com/bouzuya/items/8e8ee8e2e8f83513ad35
[hubot-adventar-2014-11]: http://qiita.com/bouzuya/items/3e239bc530e7c2c3a26c
[hubot-adventar-2014-12]: http://qiita.com/hiconyan/items/3e5481a61657890624dc
[hubot-adventar-2014-13]: http://pasero.net/~mako/blog/s/689
[hubot-adventar-2014-16]: http://qiita.com/bouzuya/items/3e239bc530e7c2c3a26c