シェルスクリプト（Bash）で作るTwitter bot

2018年9月27日追記：[シェルスクリプトマガジン Vol.56](https://www.amazon.co.jp/gp/product/4904807537/ref=as_li_ss_tl?ie=UTF8&qid=1538010510&sr=8-1&keywords=%E3%82%B7%E3%82%A7%E3%83%AB%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%83%9E%E3%82%AC%E3%82%B8%E3%83%B3&linkCode=ll1&tag=outsiderrefle-22&linkId=1d8f7478120e4944ccd61ae3bb3bc21d&language=ja_JP)に、この記事が再掲されております。お手元においておきたい方はどうぞ（？）。

[チャットボット Advent Calendar](http://qiita.com/advent-calendar/2016/chatbot)をご覧の皆様、初めまして。Piroといいます。普段はJavaScriptやRubyでのソフトウェア開発やFirefoxの法人向け技術サポートなどをしつつ、[日経Linux誌][日経Linux]で[シス管系女子][]という連載マンガを描かせて頂いております。

このアドベントカレンダーをご覧の皆さんはフレームワークやニューラルネットでディープラーニングなAIといった最新のbot事情に関心の強い方が多いと思うのですが、今日の記事は時代に逆行しまくって、Bashスクリプトで昔ながらの人工無脳botを作りましたというお話です。

この記事では以下のことを書いています。

 * Bashスクリプト製Twitter bot [tweetbot.sh][]の使い方説明
 * tweetbot.shの実装の解説
   * マルチプロセス設計
   * 親プロセスが停止させられたら子孫プロセスまでまとめて停止する
   * 一定間隔でREST APIを呼ぶ（ポーリング）
   * 独り言や返事の発言内容の選択
   * Twitter以外の他のサービスへの応用


## 開発の動機

実際の所、Bash製のTwitter botにも先行実装はいくつかあります[^bash-twitter-bot]。じゃあなんで自作したのかという話なんですが、端的に言うと*技術力のデモ*です。

[^bash-twitter-bot]: そもそも[tweetbot.sh][]の核である[Bashスクリプト製Twitterクライアント][tweet.sh]自体も[USP友の会のTwitter bot用スクリプト](https://github.com/ryuichiueda/TomoTool)の改造に端を発しています

というのも、自分が執筆している[シス管系女子][]はLinuxのコマンド操作やシェルスクリプトの事をマンガ形式で解説しているのですが、「ロクに技術的な知識も無い絵描きが適当なマンガ描きちらかしてんだろwwwww」と思われてはシャクだったので、「Twitter botをスクラッチで書けるくらいの技術力はあるぞｺﾞﾗｧ!!」と示しておきたかったのです[^demo]。

[^demo]: ……というのは半分くらい冗談です。元々、Twitter上で最新情報を時間を変えてツイートしたり言及を拾ったりという地道な広報活動をbotに任せられれば原稿制作の作業に集中できるかな？と思ったのがきっかけだったのですが、既製のbotを使えばいいのにを何をトチ狂ったのか「どうせやるならbotも自作した方が面白いんじゃね？」などと考えてしまい、軽い気持ちで始めたというのが正味の話なのでした。

とはいえ作って放置というわけではなく、実際に2015年末〜2016年頭頃から[Twitterの宣伝用アカウント][みんとちゃんbot]の運用に活用しています。
![mintbot.png](https://qiita-image-store.s3.amazonaws.com/0/61387/6ffb237d-e8db-1846-5f47-f432a42f1155.png)
時々動作が怪しくなったりもしますが、概ね安定して動作しているのでそこそこ実用的と言えるのではないでしょうか。

## できること

このbotはこんなことができます。

 * キーワードでの検索結果に反応する。
 * メンション、リプライなどに返事をする。
 * フォローされたらフォローし返す。（※2018年8月以降、UserStream API廃止に伴い動作しなくなります）
 * DMでリモート制御する。

Twitterという公開の場に置く以上、イタズラで変な言葉を覚えさせられると困るので、いわゆる学習は行いません[^no-learning]。

[^no-learning]: 単に、筆者に機械学習とその結果を活用するだけの知識が無いからという理由もありますが……

## つかいかた

### インストール

tweetbot.shは以下のコマンドに依存しています。

 * `curl`
 * `jq`
 * `nkf`
 * `openssl`
 * `git`（インストールに使用）

まず`apt`や`yum`などで各々のパッケージをインストールし、これらのコマンドを使える状態にしておいてください。

次に、データ類を置くためのディレクトリを用意します。ここでは仮に、`~/tweetbot/`を使うとしましょう。

~~~bash
$ mkdir ~/tweetbot
$ cd tweetbot
█
~~~

ディレクトリができたら、そこに[tweetbot.shのリポジトリ][tweetbot.sh]をcloneします。

~~~bash
$ git clone --recursive https://github.com/piroor/tweetbot.sh.git
█
~~~

`--recursive`オプションを付けて再帰的にcloneするか、cloneした後で`git submodule update --init`してサブモジュールもすべてダウンロードしておいて下さい。

次は、認証用に[apps.twitter.comでアプリケーションを作成](http://apps.twitter.com/)します[^twitter-developer-account]。Webサービスではないので、URLやコールバックは特に指定しなくても大丈夫です。アプリケーションには投稿のための書き込み権限を与えておいてください。また、DMを扱いたい場合はDMの読み書きの権限も必要です。

[^twitter-developer-account]: 2018年8月10日現在、APIキーの発行のためには開発者アカウントの作成が必要で、その際は300文字以上の文章で目的を説明する必要があるようです。結果がどうなるかは分かりませんが、とりあえず自分は「I'm trying to develop chatbot based on bash shellscripts. "tweet.sh" https://github.com/piroor/tweet.sh/ is a custom client for Twitter APIs, and "tweetbot.sh" https://github.com/piroor/tweetbot.sh/ is a chatbot based on tweet.sh. tweetbot.sh provides ability to respond to replies, mentions, and quoted tweets (RTs with quotation). It also does RT and giving "like" to tweets matched to preliminarily specified keywords. And, it do automatic tweets randomly based on topic database.」や「I'm developing a chatbot based on bash shellscripts "tweet.sh" https://github.com/piroor/tweet.sh/ and "tweetbot.sh" https://github.com/piroor/tweetbot.sh/ . This account aims to promote a book "System Admin Girl, a collection of case-studies of Linux commands and shellscripts" I issued at Japan. Because it is a rare case a cartoon-style technological book, I need to demonstrate my technical background to consumers as "not just a cartoon artist, but an IT developer". And, I hope the chatbot to communicate with consumers and help to grow trust between me and consumers.」といった説明で申請してみている所です。結果が出たらまた追記します。

アプリケーションができたら、認証情報を定義します。以下の内容で`~/tweetbot/tweet.client.key`の位置にファイルを作成し、作成したアプリケーションのコンシューマキーとシークレット、アクセストークンとシークレットを記入して下さい。

~~~bash
MY_SCREEN_NAME=投稿に使うアカウントのスクリーンネーム
MY_LANGUAGE=投稿に使うアカウントの言語
CONSUMER_KEY=xxxxxxxxxxxxxxxxxxx
CONSUMER_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ACCESS_TOKEN_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
~~~

例えば、スクリーンネームが`example`で言語が日本語なら以下のようになります。

~~~bash
MY_SCREEN_NAME=example
MY_LANGUAGE=ja
...
~~~

スクリーンネームと言語は、ストリーミングの受信やツイートの検索、取得したツイートの種類の判別のために使われます。これを忘れると、自分でやったRTを延々RTし続けるみたいなことになってしまうので注意して下さい。

認証情報のファイルを設置したら、今度はbotの挙動を定義するファイルを`~/tweetbot/personality.txt`の位置に作成します。これは`source`で読み込む前提のファイルで、Bashスクリプトの記法で環境変数として各種の設定を記述します。以下は基本の設定例です。

~~~bash:~/tweetbot/personality.txt
# 管理者として扱うユーザ。
# これらのユーザからのDMはコマンドとして受け付ける。
ADMINISTRATORS="piro_or, sysadgirl_mint"
# 反応キーワード。
# これらのキーワードへの言及を検出したら反応する。
WATCH_KEYWORDS='sysadgirl, system-admin-girl, シス管系女子, #シス管系女子'

# 言及への応答のキューを処理する間隔。
PROCESS_QUEUE_INTERVALL_MINUTES=20
# 言及への応答のキューを処理する時間帯。
ACTIVE_TIME_RANGE="11:40-15:00,17:00-24:00"

# 独り言をする時間帯。
MONOLOGUE_ACTIVE_TIME_RANGE="00:00-00:30,06:00-24:00"
# 独り言の自動投稿の間隔。
MONOLOGUE_INTERVAL_MINUTES=90

# メンションに連続して返事をする最大の回数。
# 延々と同じ返事ばかりを返さないようにする安全装置。
MAX_MENTIONS_IN_PERIOD=10
# 連続して返事をする最大の回数の制限を反映する時間。
# この指定の場合、120分の間に最大で10回までしか返事をしないということ。
MENTION_LIMIT_PERIOD_MIN=120

# フォローする基準：フォローされたらフォローし返す、言及されたらフォローする（RTは除く）。
FOLLOW_ON_FOLLOWED=true
FOLLOW_ON_MENTIONED=true
FOLLOW_ON_QUOTED=true
FOLLOW_ON_RETWEETED=false

# ファボる基準：言及されたらファボる。
FAVORITE_MENTIONS=true
FAVORITE_QUOTATIONS=true
FAVORITE_SEARCH_RESULTS=true

# RT基準：メンション以外はRTする。
RETWEET_MENTIONS=false
RETWEET_QUOTATIONS=true
RETWEET_SEARCH_RESULTS=true

# 言及への言及：リプライかメンションには応答、それ以外は言及しない
RESPOND_TO_MENTIONS=true
RESPOND_TO_QUOTATIONS=false
RESPOND_TO_SEARCH_RESULTS=false
~~~

他にも色々と細かい設定が可能です。詳しくは[README][tweetbot.sh-README]をご覧下さい。

### 起動

設定ファイルを置いた`~/tweetbot`をカレントディレクトリにした状態で、`~/tweetbot/tweetbot.sh/watch.sh`を実行して下さい。

~~~bash
$ cd ~/tweetbot
$ tweetbot.sh/watch.sh
█
~~~

これでスクリプトが起動し、その後は`Ctrl-C`などで停止するまでスクリプトが動き続ける状態になります。

ただ、この方法だと自分がログアウトできません[^terminal-multiplexer]。また、サーバーを再起動したらbotは停止したままになってしまいます。[init.dを使う](http://kazmax.zpp.jp/linux_beginner/self-made_service_chkconfig.html "（CentOS6まで）自作のサービスをchkconfigで登録する")なり[systemdを使う](http://qiita.com/DQNEO/items/0b5d0bc5d3cf407cb7ff "Systemdを使ってさくっと自作コマンドをサービス化してみる - Qiita")なりして、サービスとして自動起動するようにしておくと便利でしょう。構成ファイルの中にはsystemdのユニットをインストールするためのインストールスクリプトが含まれており、`sudo ~/tweetbot/tweetbot.sh/systemd/install.sh`のように実行するとウィザードが始まるようになっています。

[^terminal-multiplexer]: botを動かしたままログアウトするには、`screen`や`tmux`といったいわゆるターミナルマルチプレクサを使う必要があります。

### 発言データの準備

これだけでもファボやRTはできるのですが、話しかけても全く応答しないbotになってしまうので、できれば応答や発言を用意しておきたい所です。

#### 自動発言（独り言）のデータ

独り言用の発言は、`~/tweetbot/monologues`ディレクトリの下に置かれたテキストファイル（エンコーディングはUTF-8、改行コードはLF）の1行を1発言としてランダムに選択します。特に重複の除外処理は行っていないため、十分な数の発言データが無いと同じ発言ばかり繰り返してしまいますので、なるべくたくさん用意してあげましょう。例えばこんな感じです。

~~~bash:~/tweetbot/monologues/バレンタイン.txt
# バレンタイン
# date: *.02.01-*.02.14

バレンタインってなんかワクワクしません？✨おいしいチョコがいっぱい食べれる季節～！😁
この時期、スーパーとかコンビニとか行くとバレンタインソングがすっごいかかってますよねー💦聞きすぎたせいで歌詞覚えちゃいましたよ……😖
~~~

見ての通り、Unicode絵文字も使えます。行頭に`#`がある行は発言にはならずコメントのように扱われますが、DMで発言を追加する際の目印として使う事ができます。

また、特定の範囲の日においてのみ投稿して欲しい独り言のデータも定義できます。日付の範囲はファイルごとに`# date: YYYY.MM.DD-YYYY.MM.DD`の書式で指定し、ワイルドカードも受け付けます。この例であれば、毎年2月の上旬にだけ流れる発言となります。


#### 自動応答（返事）のデータ

メンションやリプライに対する応答の発言は、`~/tweetbot/responses`ディレクトリの下に置かれたテキストファイル（エンコーディングはUTF-8、改行コードはLF）の1行を1発言として選択します。このとき、どの発言ファイルを使うかは受け取ったメンションの本文に対するパターンマッチングで判断されます。

~~~bash:~/tweetbot/responses/おはよう.txt
# good morning
# おはよう
# お早う
# ぐっど? *もーにん
# グッド? *モーニン

おはようございまーす！🙌
おはようございまーす！🙌 今日も一日がんばりましょう🎵
~~~

`#`で始まる行はコメントのように扱われますが、同時に正規表現によるマッチングパターンとしても解釈されます。いずれかのパターンにマッチすると、そのファイルの中に書かれた発言の中の1つが返事として投稿されます。

マッチングパターンだけを定義して発言を定義しない場合、それらのパターンにマッチしたメンションには無反応になります。いわゆるNGワードとして使えます。

~~~bash:~/tweetbot/responses/NG.txt
# 嫌い
# 黙れ
~~~

返事の定義ファイルは名前順にマッチングが試行されるため、NGワードを定義するファイルは`000NG.txt`のように名前順で先頭に来るようにしておくと良いでしょう。

メンションの本文がいずれのマッチングパターンにもマッチしなかった場合、

 * `_pong.txt`（相槌）
 * `_connectors.txt`（接続）
 * `_developments.txt`（会話の継続）
 * `_topics.txt`（新しい話題）

の4つの特別な返事定義ファイルの内容に基づいて、それっぽい応答がランダムに生成されて返されます。これは、以下のような内容で用意します。

~~~bash:~/tweetbot/responses/_pong.txt
# 相槌

おおっ！
へえー！
ふ～ん！
~~~

~~~bash:~/tweetbot/responses/_connectors.txt
# 接続

そういえば
ところで
それはそうと
~~~

~~~bash:~/tweetbot/responses/_developments.txt
# 会話の継続

いいですね！
すごい！
~~~

~~~bash:~/tweetbot/responses/_topics.txt
# 新しい話題

何かオススメの本ってあります？？📖
今日は何をしてるんですか？😃
~~~

返事の元になったメンションの内容は考慮されないので、*空気を読まない・人の話を聞かない・適当に相槌を打つ*、という相当アホの子な返事しか返せません。まあ会話してるっぽい雰囲気だけの古式ゆかしい人工無脳ということで、あしからずご了承ください。

発言ファイルには他にもいくつか機能があります。詳しくは[README][tweetbot.sh-README]をご覧下さい。

### DMでのリモート制御

設定ファイルで管理者として登録したアカウントからのDMは、リモート操作用のコマンドとして解釈され、コマンドの実行結果がDMの返事として返却されます。以下は代表的なコマンドです。

 * `echo 任意のテキスト`: 指定されたテキストをそのままDMで返却します。死活確認に使えます。
 * `+res キー 返事の内容`: 返事のパターンを`キー.txt`のファイルに追加します。その名前のファイルがない場合は、`# キー`というコメントがあるファイルに追加します。どちらの場合でもファイルが見つからない場合、新しくファイルを作成します。最初の`+`を`-`にすると、返事のパターンが削除されます。
 * `+キー 独り言の内容`: 独り言のパターンを`キー.txt`のファイルに追加します。その名前のファイルがない場合は、`# キー`というコメントがあるファイルに追加します。どちらの場合でもファイルが見つからない場合、新しくファイルを作成します。最初の`+`を`-`にすると、独り言のパターンが削除されます。
 * `del ツイートのID`: IDで指定したツイートを削除します。

各コマンドの詳細やこれら以外のコマンドについては、[README][tweetbot.sh-README]をご覧下さい。

……というのがtweetbot.shの大まかな使い方です。



## tweetbot.shの実装の解説

では、ここからは実装の解説に入ります。

ただ、全部を解説するとキリが無いので、*長時間動き続けるBashスクリプト*という形でbotを作る時のキモになりそうな部分だけ解説することにします。

 * tweetbot.shでツイートやRTといった事を行うのには、Twitterクライアントであるtweet.shの機能をほぼそのまま使っています。BashでTwitterのWeb APIを呼ぶ方法の詳細やストリーミングAPIの待ち受け方については、[Shell Script Advent Calendarの方に書いたtweet.shの解説][tweet.sh-解説]を併せてご覧下さい。
 * tweetbot.shで行っている[ランダムに何かをやるという事についての解説](http://qiita.com/piroor/items/6300e665bc8ac314211b "シェルスクリプトでランダムにアレをやる - Qiita")や[「だいたい1時間」のようなルーズな判断で何かをやるという事についての解説](http://qiita.com/piroor/items/8682faddd4b0c7d397a1 "シェルスクリプトでだいたい1時間の間隔であれをやる - Qiita")もあります。

### マルチプロセス設計

TwitterのストリーミングAPIのうち[User streams](https://dev.twitter.com/streaming/userstreams)は完全性が保証されておらず、見落としが発生することが度々あります。なので見落としを防ぐために普通のツイート検索も並行して実行しておきたくなるのですが、普通にやるとストリーミングAPIのレスポンスを待ち受けている間は他のことができません。

また、複雑な処理や外部（Twitter API）と通信する処理は、予期せぬエラーが起こって全体が停止してしまうリスクがあります。

これらの問題を回避するために、tweetbot.shは`watch.sh`を実行するプロセスをマスタープロセスとして、その配下にストリーミングの待ち受け用やポーリング用などのサブプロセスを従える設計としています。これにより、複数の処理を並行して実行できる上に、サブプロセス側でエラーが発生してプロセスが終了してしまっても、マスタープロセスが生きている間は自動的に処理を復帰できるようになっています。

これは、以下の要領で実装しています。

~~~bash:watch.sh
#!/usr/bin/env bash

watch_mentions() {
  while true
  do
    # メンション待ち受け処理。正常に動いていればこの行で処理がブロックするため、この先に進むことはない。
    # 異常終了した場合、10秒待ってからまた同じ処理を実行し直す。
    sleep 10
  done
}
watch_mentions &

periodical_search() {
  # 検索APIのポーリング用の処理。
}
periodical_search &

periodical_fetch_direct_messages() {
  # DMのAPIのポーリング用の処理。
}
periodical_fetch_direct_messages &

periodical_monologue() {
  # 定期的な自動発言用の処理。
}
periodical_monologue &

...

wait
~~~

通常、関数やコマンドを実行すると実行が終わるまで待ってから次の行に処理が進みますが、コマンド（関数）名の後に`&`を付けると、子プロセスが作られて関数が非同期に実行されるようになります。この仕組みを使って、並行して動かしたいそれぞれの処理を関数にした上で子プロセスで実行しています。

最後の行に`wait`と書いておくのがポイントで、これを書かないと、子プロセスがまだ動いているのにマスタープロセスの方だけ先に最終行に到達してそのまま終了してしまいます。`wait`を実行すると、子プロセスがすべて終了するまでマスタープロセスの処理がそこで一時停止します。

### 親プロセスが停止させられたら子孫プロセスまでまとめて停止する

マルチプロセス化とセットでやっておきたいのが、親プロセスの終了と同時に子孫プロセスを自動的に終了させるという事です。これを忘れると、マスタープロセスを`Ctrl-C`で終了させた後も子孫プロセスが動き続けてしまいます。

これを防ぐには、マスタープロセスが停止した時（正確には、停止要求のシグナルを受け取った時）の処理を`trap`コマンドで定義してやります。具体的には以下のようにしています。

~~~bash:watch.sh
...

kill_descendants() {
  local target_pid=$1
  local children=$(ps --no-heading --ppid $target_pid -o pid)
  for child in $children
  do
    kill_descendants $child
  done
  if [ $target_pid != $$ ]
  then
    kill $target_pid 2>&1 > /dev/null
  fi
}
self_pid=$$
trap 'kill_descendants $self_pid; clear_all_lock; exit 0' HUP INT QUIT KILL TERM

...
~~~

コールバックに指定する関数`kill_descendants`は「自分の直接の子プロセスを強制停止する処理」を再帰的に実行するようになっていて、これによってマスタープロセスの終了時には末端の子孫プロセスから順番に終了されていきます[^why-recursive]。

[^why-recursive]: プロセスツリーの末端から終了するのは、祖先側から終了すると、万が一親プロセスだけ終了してしまったという場合に子孫プロセスがトラッキング不能な形で取り残されてしまうと思ったからです。


### 一定間隔でREST APIを呼ぶ（ポーリング）

前述したようにストリーミングAPIの監視だけでは監視漏れが発生するため、tweetbot.shではそれとは別にツイートの検索やDMの取得のためのポーリング（定期的なAPI呼び出し）も行っています[^why-polling]。

[^why-polling]: ツイート検索専用のストリーミングAPIという物もあり、tweet.shもそれに対応しているのですが、同一IPから複数のストリーミングAPIを同時に使用することは禁止されているため、tweetbot.shでは通常のREST APIを使ってポーリングしています。


~~~bash:watch.sh
periodical_search() {
  ...
  local last_id_file="$status_dir/last_search_result"
  local last_id=''
  [ -f "$last_id_file" ] && last_id="$(cat "$last_id_file")"
  ...

  while true
  do
    debug "Processing results of REST search API (newer than $last_id)..."
    while read -r tweet
    do
      [ "$tweet" = '' ] && continue
      id="$(echo "$tweet" | jq -r .id_str)"
      ...
      if [ $id -gt $last_id ]
      then
        last_id="$id"
        echo "$last_id" > "$last_id_file"
      fi
      ...
      # 検索結果として得たツイートをここで処理する。
      ...
    done < <("$tools_dir/tweet.sh/tweet.sh" search \
                -q "$query" \
                -c "$count" \
                -s "$last_id" |
                jq -c '.statuses[]' |
                tac)

    if [ "$last_id" != '' ]
    then
      # increment "since id" to bypass cached search results
      last_id="$(($last_id + 1))"
      echo "$last_id" > "$last_id_file"
    fi
    sleep 3m
  done
}

...

if [ "$query" != '' ]
then
  log "Tracking search results with the query \"$query\"..."
  periodical_search &
  periodical_process_queue &
else
  log "No search queriy."
fi
~~~

この時大事なのは、*前回取得した結果よりも新しい結果だけを取得するようにリクエストする*ということです。さもないと、同じツイートや同じDMを何度も処理してしまうことになります。

ここでは`while`ループを二重にしていて、外側が「前回の検索終了時から3分待って次の検索を行う」というポーリングのためのループ、内側が「検索結果として得られたツイート1つ1つを処理する」ためのループです。検索結果の中で最も新しいツイート（＝次の検索を行う際に「これより新しい結果のみを返す」という条件に使用するツイート）のIDを保持する変数`last_id`の値を外側のループと共有したかったので、内側は[プロセス置換を使った`while`ループ](https://fumiyas.github.io/2013/12/10/lastpipe.sh-advent-calendar.html)としています。

tweet.shの`search`コマンドはREST APIで得た検索結果を出力しますが、検索結果は新しいツイートの方が先に返ってきます。しかしストリーミングAPIの補完として使うには古いツイートから処理したいので、そのJSON文字列から`jq -c '.statuses[]'`でツイートの配列を取り出して`tac`で逆順に並べ替えています。その出力結果を`<(コマンド列)`という書き方（[Bashのプロセス置換機能](http://sechiro.hatenablog.com/entry/2013/08/15/bash%E3%81%AE%E3%83%97%E3%83%AD%E3%82%BB%E3%82%B9%E7%BD%AE%E6%8F%9B%E6%A9%9F%E8%83%BD%E3%82%92%E6%B4%BB%E7%94%A8%E3%81%97%E3%81%A6%E3%80%81%E3%82%B7%E3%82%A7%E3%83%AB%E4%BD%9C%E6%A5%AD%E3%82%84%E3%82%B9)）で擬似的なファイルとして扱って、リダイレクトの`<`で内側の`while`の標準入力に流し込んでいます。これにより、内側の`while`ループが外側の`while`ループと同じプロセスで実行され、変数`last_id`の値が共有されるようになります[^share-value]。

[^share-value]: 内側の`while`ループ内で一時ファイルに値を書き出して、それを外側の`while`ループで読み込む、という風にすればべつにプロセス置換を使うまでもないのですが、既に値を保持した変数があるのにそれを使えないのは何となく気持ち悪かったので、このようにしました。

内側のループが終了したら、次の検索で「これより新しい結果のみを返す」という条件に使用するツイートのIDを`last_id="$(($last_id + 1))"`でインクリメントしています。これは、検索結果が1件も見つからなかった場合に`last_id`が変化しないと、次の検索リクエストの内容が前回と同じになってしまってキャッシュされたレスポンスが返ってきてしまう可能性があるためです。


### 独り言や返事の発言内容の選択

発言を種類毎に管理できるよう、発言のデータは細かくファイルを分けられるようになっていますし、コメント行でマッチングパターンの定義や投稿可能日時の範囲の指定もできるようになっています。しかし、実際に発言をする時になってこれらを一気に適切に取り扱うのは結構大変です。

そこで、tweetbot.shでは`watch.sh`の起動時（および、DMのコマンドで動的に発言データが編集されたとき）に発言データファイルを全スキャンし、発言の選択ロジックそのものをシェルスクリプトとして組み立てて、以後はそれを使うようにしています。具体的には、独り言用は[generate_monologue_selector.sh](https://github.com/piroor/tweetbot.sh/blob/master/generate_monologue_selector.sh)が`~/tweetbot/monologue_selector.sh`というファイルを生成し、返事用は[generate_responder.sh](https://github.com/piroor/tweetbot.sh/blob/master/generate_responder.sh)が`~/tweetbot/responder.sh`というファイルを生成します。

自動生成された`~/tweetbot/responder.sh`は、例えば以下のような内容になります。


~~~bash:~/tweetbot/responder.sh
...

if echo "$input" | egrep -i "good morning|おはよう|お早う|ぐっど? *もーにん|グッド? *モーニン" > /dev/null
then
  [ "$DEBUG" != '' ] && echo "Matched to \"good morning|おはよう|お早う|ぐっど? *もーにん|グッド? *モーニン\", from \"$base_dir/./responses/おはよう.txt\"" 1>&2
  extract_response "$base_dir/./responses/おはよう.txt"
  exit $?
fi

if echo "$input" | egrep -i "^(hello|hey|yo|hi)[ \.,!]|good afternoon|こんに?ちは|ハロー|はろー|ヘイ|やあ|よ[うお]" > /dev/null
then
  [ "$DEBUG" != '' ] && echo "Matched to \"^(hello|hey|yo|hi)[ \.,!]|good afternoon|こんに?ちは|ハロー|はろー|ヘイ|やあ|よ[うお]\", from \"$base_dir/./responses/こんにちは.txt\"" 1>&2
  extract_response "$base_dir/./responses/こんにちは.txt"
  exit $?
fi

...
~~~

標準入力で与えられたメンションの本文に対してどのようにマッチングを行いどのファイルから発言を抽出するのか、スクリプトを見れば一目で分かりますし、動作試験も以下のようにスクリプトを実行するだけなので簡単です。常駐プロセスの側をいちいち止めたり再起動したりする必要はありません。

~~~bash
$ cd ~/tweetbot
$ echo "こんにちは" | ./responder.sh
こんにちは～！
$ █
~~~

また、今回は実装を見送りましたが、話しかけられた内容を学習したり文脈に応じて会話をしたりといった高度な応答に対応したくなった場合でも、この設計であれば`responder.sh`や`monologue_selector.sh`を差し替えるだけで済むはずです。

### Twitter以外の他のサービスへの応用

[tweetbot.sh][]はTwitter用botですが、入出力の部分を入れ替えれば、同様のやり方で他の様々なサービス用のbotを作る事もできます。

 * [`nc`コマンドを使ってシェルスクリプトでHTTPサーバーを立てる例](https://github.com/ma2saka/websh)を参考にすると、GitHubのService Hookを受け取って何か応答を返しつつSlackのチャンネルやTwitterのタイムラインに通知を流す、というbotを作れそうです。
 * [slackcat](https://github.com/rlister/slackcat)を使えばSlackのチャンネルに常駐するタイプのbotも作れそうな気がします。

いずれの場合も、ここで解説したような常駐型スクリプトを作る時の技術が役に立つでしょう。

シェルスクリプトの良い所は、どんなコマンドラインツールも容易に部品として組み込めるという点です。自分の得意な言語用にライブラリが提供されていない場合でも、コマンドラインツールがあればシェルスクリプトで利用できます。また、自分が何かツールを作る時も、コマンドラインツールとして標準入力・標準出力でデータをやりとりできるように設計しておけば、それもまた部品として再利用できます。

様々なコマンドを連携させるグルー（糊）としてシェルスクリプトを活用し、皆さんも普段のちょっとした不満や面倒を解消してみて下さい！
![thumbsup.png](https://qiita-image-store.s3.amazonaws.com/0/61387/102129cc-ff07-40b7-f862-fc4bed52fb9f.png)


## 「シス管系女子」について

最後に[シス管系女子][]の事も改めてご紹介します。

自分は2011年から[日経Linux][]誌上で「シス管系女子」というケーススタディ形式の解説マンガ記事を連載させて頂いています。この記事でやっているような「Bashでガッツリプログラミング」という物ではなく、*「コマンド操作怖い……」レベルの人が自力でコマンド列を組み立てられるようになるくらい*を目指した内容です。

ブログ等で紹介されているコマンド列をコピペ実行はできても、そのコマンド列をゼロから自分で組み立てられないのでは、ちょっと前提が変わっただけでお手上げになってしまいます。また、理解が怪しい状態では、Webサービスの開発や運用でサーバーにSSHでログインしてコマンドで操作しないといけないという場面でも、何が起こるかよく分からなくて物怖じしてしまうのは致し方ないでしょう（筆者もそうでした）。

なので「シス管系女子」では、*「個々のパーツのはたらきや仕組みをちゃんと理解して、必要に応じて手順や要素などのパーツを入れ換えたり調整したりといった応用ができる」*というレベルになる事を意識して、*コマンドやオプションの動作を絵解きで説明*することを心がけています。上記のような不安を持っていた方から「実際に参考になった」という感想を頂くこともあり、また、上司や先輩の方が新人や後輩のための1冊としてご紹介して下さっている事例も結構あると伺っています。

コマンド一覧を丸暗記しようとして挫折してしまった人、先輩にGUI禁止令を出されて途方に暮れてしまった新人さん、サーバーのトラブルでSSH越しに操作しないといけないのにお手上げになってしまったGUI派の人など、コマンド操作の勘所が分からずお悩みのすべての方にぜひ見て頂きたい内容なので、もし良かったら、お近くの大きめの書店で[シス管系女子][]の本や[掲載誌][日経Linux]を手に取ってチラ見して頂けましたら幸いです。

それ以外にも、[Twitterのみんとちゃんbotアカウント][みんとちゃんbot]でイラスト[^イラスト]や本編に入りきらなかった小ネタを流したり、Webサイトの方にも[連載や本では扱わなかったもっと基礎的な話の特別編](http://system-admin-girl.com/comic/#begins)を置いていたりします。あと、[「シス管系女子」をテーマにしたAdvent Calendar](http://www.adventar.org/calendars/1692)も公開中です。

[^本稿執筆時点]: 2016年12月5日現在。
[^イラスト]: [これまでのイラスト一覧](http://system-admin-girl.com/comic/#illust)

ということで、チャットボットAdvent Calendarの5日目でした。6日目は[kashira2339さん](http://qiita.com/kashira2339)による、会社でリアルに良い反響のチャットボットの機能(hubot + GitHub webhook)のお話です。お楽しみに！


[tweet.sh]:https://github.com/piroor/tweet.sh
[tweet.sh-tweet.sh]:https://github.com/piroor/tweet.sh/blob/master/tweet.sh
[tweet.sh-README]:https://github.com/piroor/tweet.sh/blob/master/README.md
[tweet.sh-解説]:http://qiita.com/piroor/items/a999eeef9a67283b1e56
[tweetbot.sh]:https://github.com/piroor/tweetbot.sh
[tweetbot.sh-README]:https://github.com/piroor/tweetbot.sh/blob/master/README.md
[シス管系女子]:http://system-admin-girl.com/
[みんとちゃんbot]:https://twitter.com/sysadgirl_mint
[日経Linux]:http://itpro.nikkeibp.co.jp/linux/