シェルスクリプト（Bash）で作るTwitterクライアント

<!-- シェルスクリプト（Bash）で作るTwitterクライアント -->

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

[Linux Advent Calendarの方にGUIアプリのスクショを定期的にSlackに流すシェルスクリプトの話でエントリーした](http://qiita.com/piroor/items/543da3fba81cbc3ed7e4)のですが、Shell Script Advent Calendar的にはそれをこっちを投稿した方が良かったかもと今更思いつつ、今日は別の話題です。

シェルスクリプト製Twitterクライアントには[恐怖！小鳥男](https://github.com/ShellShoccar-jpn/kotoriotoko)や[tweet.sh（同名の別実装）](https://github.com/rcknr/tweet.sh)などいくつか実装例がありますが、自分も2015年末頃から[tweet.sh][]という汎用のTwitterクライアントを開発しています。この記事ではtweet.shをネタに、以下の事を解説します。

 * Bashスクリプト製Twitterクライアント [tweet.sh][]の簡単な使い方の解説
 * tweet.shの実装の解説
   * 文字列をBashと`nkf`といくつかの一般的なコマンドでURLエンコードする
   * たくさんあるパラメータを見やすい形で定義する
   * 改行区切りのパラメータのリストを`キー1=値1,キー2=値2,...`の形に変換する
   * OAuth 1.0認証によるWeb APIアクセスの、ほぼ0からの実装手順


## 開発の動機

そもそも先行実装があるのになぜ新たに自分でBashでTwitterクライアントを書くのか？という事なんですが、端的に言うと*技術力のデモ*です。

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

[^appeal]: ……というのは半分くらい冗談です。実の所は、Web上での広報活動を自動化して楽をできないか？というのが発端で、どうせなら自前で賄えばネタになるかなと軽い考えで開発をスタートしたのでした。

とはいえただ単に作ってみただけでなく、[Twitterの宣伝用アカウント][みんとちゃんbot]の自動投稿やリモート制御にこの1年間使ってきているので、実用性もそれなりにあるのではないかと思っています。
![mintbot.png](https://qiita-image-store.s3.amazonaws.com/0/61387/6ffb237d-e8db-1846-5f47-f432a42f1155.png)

なお、実際の運用はこのTwitterクライアントに皮をかぶせた形の[tweetbot.shというbotスクリプト][tweetbot.sh]を使っています。こちらについてご興味のある方は、[チャットボットAdvent Calendarの方に書いた解説][tweetbot.sh-解説]を併せてご覧下さい。

## つかいかた

### インストール

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

 * `curl`
 * `jq`
 * `nkf`
 * `openssl`

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

次に、[tweet.shのリポジトリ][tweet.sh]をcloneするか、[メインの実装である同名のシェルスクリプト][tweet.sh-tweet.sh]をダウンロードします。このクライアントは単一のスクリプトで完結していますので、実装である`tweet.sh`（ファイル）に実行権限を与えて適当な所（`~/bin/`など）に設置してください。

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

最後に、認証情報を定義します。以下の内容で`~/.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
...
~~~

スクリーンネームと言語は、ストリーミングの受信やツイートの検索、取得したツイートの種類の判別のために使われます。単にRESTで投稿するだけの場合には、これらの情報が無くても一応動きます（多分）。

### 使えるコマンドの一覧

これで準備完了です。後は以下のようにすればツイートできます。

~~~bash
$ tweet.sh post こんにちは！ █
~~~

スクリプト名の直後にサブコマンドを指定し、その後に引数を指定するスタイルです。本稿執筆時点[^本稿執筆時点]での代表的なコマンドは以下の物があります。

 * 読み込み系コマンド
   * `fetch` (`get`, `show`): ツイートのURLまたはIDを指定してJSONを取得
   * `search`: クエリを指定してツイートを検索
   * `watch-mentions` (`watch`): メンションその他をストリーミングで受信
   * `type`: `fetch`や`search`、`watch`で取得した個々のJSONを元に、それがRTなのか引用RTなのかといった識別を行う
 * 書き込み系コマンド
   * `post`　(`tweet`, `tw`): ツイートを投稿する
   * `reply`: リプライする
   * `upload`: ファイルをアップロードする（`post`または`reply`との併用前提）
   * `delete` (`del`, `remove`, `rm`): ツイートを削除する
   * `favorite` (`fav`) / `unfavorite` (`unfav`): ツイートをファボる/ファボを取り消す
   * `retweet` (`rt`) / `unretweet` (`unrt`): ツイートをRTする/RTを取り消す
   * `follow` / `unfollow`: ユーザをフォローする/フォローを解除する
 * DM操作系コマンド
   * `fetch-direct-messages` (`fetch-dm`, `get-direct-messages`, `get-dm`): 最近のDMを取得する
   * `direct-message` (`dm`): DMを送る

[^本稿執筆時点]: 2016年12月5日現在。

一般用途向けのTwitterクライアントと言うよりは、他のコマンドやスクリプトと組み合わせてライブラリ的に使う事を想定しているため、各サブコマンドの戻り値はTwitterのWeb APIが返すJSON文字列そのものとなっています。どういうJSON文字列が帰ってくるのかといった情報や、各サブコマンドの詳しい使い方については、[README][tweet.sh-README]をご覧下さい。

### ストリーミングの受信

コマンドの中でストリーミング形式に対応している`search`と`watch-mentions`について、補足説明です。

#### ハンドラを使わないストリーミング受信

`search`は`tweet.sh -q "検索クエリ" -w`という風に`-w`オプションを付けるとストリーミングモードになります。`watch-mentions`はストリーミングのみの対応なので、特に何も指定しなくてもストリーミングモードで動作します。

ストリーミング受信時には、これらのコマンドは新しいイベントやツイートが流れてくる度にそのJSON文字列を1件＝1行で標準出力に出力します。以下のように`while read`を使えばそれぞれの結果を取り出して処理できます。

~~~bash
$ ./tweet.sh search -q "シェルスクリプト" -w |
    while read -r tweet; do echo "検索条件にマッチ: $tweet"; done
~~~

~~~bash
$ ./tweet.sh watch-mentions |
    while read -r event; do echo "イベント受信: $event"; done
~~~

このとき、`read`に`-r`オプションを指定していることに注意して下さい。`-r`は標準入力の中のバックスラッシュをエスケープ文字ではなくバックスラッシュという文字としてそのまま受け取るオプションです。これを指定しておかないと、ツイートの中にある改行の`\n`までもがイベントの区切りの改行として解釈されてしまい、1行1イベント（ツイート）という構造が崩れてしまいます。

#### ハンドラを使ったストリーミング受信

`search`は`-h`オプション、`watch-mentions`は`-r`, `-t`, `-q`, `-f`, `-d`, `-s`の各オプションにハンドラとなるコマンド列を指定できます。この場合も、これらのコマンドはストリーミングモードとして動作します。

この状態では、新しいイベントやツイートが流れてくる度にハンドラのコマンド列が実行されます。この時、流れてきたイベントやツイートのJSON文字列は標準入力経由で受け取る事ができます。複雑なことをしたければ、コマンド列を直接書かずにシェルスクリプトを指定するのがオススメです。

~~~bash
$ ./tweet.sh search -q "シェルスクリプト" -h 'echo "イベント受信: $event"'
~~~

~~~bash
$ ./tweet.sh watch-mentions \
    -k "キーワード1,キーワード2,..." \
    -r "echo 'リプライまたはメンション'; cat" \
    -t "echo 'RT'; cat" \
    -q "echo '引用RT'; cat" \
    -f "echo 'フォローされた'; cat" \
    -d "echo 'DMを受信した'; cat" \
    -s "echo 'それ以外にマッチしたツイート'; cat" █
~~~

`watch-mentions`にハンドラを指定した場合には、tweet.sh自身がイベントやツイートの種類を自動的に判別した上で適切なハンドラが実行されます。ハンドラが指定されなかった種類のイベントやツイートは、通常通り標準出力に1行ずつ出力されます。


## tweet.shの実装の解説

### ポイント

Twitterクライアントを作るにあたってのポイントは、以下の点でした。

 1. APIをどう叩く？
 2. APIの返す情報をどう取り扱う？
 3. 認証は？  
 4. ストリーミングの受信は？  

Twitterは[REST形式のWeb API](https://dev.twitter.com/rest/public)を提供しているので、`curl`でリクエストを送ればAPIを叩けます。また、Web APIのレスポンスはJSONなので、`jq`を使えば簡単に情報を取り出せます。

となると、残るは認証とストリーミングの受信です。

でもその前にちょっと寄り道として、いくつか文字列の変換処理の解説を先にやっておこうと思います。


### 文字列をBashと`nkf`といくつかの一般的なコマンドでURLエンコードする

Web APIへのリクエストを組み立てる時は、文字列をURLエンコード（[パーセントエンコーディング](https://dev.twitter.com/oauth/overview/percent-encoding-parameters)）する処理がよく出てきます。

なので、`tweet.sh`内でも`url_encode`というシェル関数を以下のように定義しています。

~~~bash
url_encode() {
  while read -r line
  do
    echo "$line" |
      nkf -W8MQ |
      sed 's/=$//' |
      tr '=' '%' |
      paste -s -d '\0' - |
      sed -e 's/%7E/~/g' \
          -e 's/%5F/_/g' \
          -e 's/%2D/-/g' \
          -e 's/%2E/./g'
  done
}
~~~

`sed`や`tr`等のコマンドと同じ感覚で使えるように、この関数では内部で`while read`でループを回して、複数行を取り扱えるようにしています。入力の1行の中に`\n`のような文字があった時に行の区切りとして解釈されてしまう事を防ぐために、`read`には`-r`オプションを指定しています。

`nkf -W8MQ`は、UTF-8の文字列として標準入力から渡された内容をQuoted-Printable形式で出力する指定です。Quoted-Printableというのは、`こんにちは`という文字列が`=E3=81=93=E3=82=93=E3=81=AB=E3=81=A1=E3=81=AF`みたいになるエンコーディング形式[^Quoted-Printable]です。

[^Quoted-Printable]: 入力を8bitごとに分けて、それぞれの16進数表記の前に`=`を付けた物。

Quoted-Printableエンコーディングでは出力の1行あたりの最大文字数が76文字と定められていて、それより長くなる場合は行末に`=`を付けて改行された結果が出力されます[^nkf-output]。つまり*この時点で、1行の入力が複数行になり得ます*。

[^nkf-output]: nkfの場合は72文字で区切られます。

複数行になった出力は後で1行に繋げ直すのですが、その時「行末に付けられた`=`」が邪魔になるので、これを`sed 's/=$//'`で取り除きます。

で、`tr '=' '%'`でQuoted-Printableの文字列の`=`を`%`に置換すると、`%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF`という見慣れた形式の文字列になります。

その後、nkfによって複数行に分けられた出力について、`paste -s -d '\0' -`で元の1行に連結[^paste-join]してから、「Quoted-Printableではエンコード対象になるがURLエンコードでは予約語なのでエンコードしなくてよい文字」を`sed -e 's/%7E/~/g' -e 's/%5F/_/g' -e 's/%2D/-/g' -e 's/%2E/./g'`で元に戻しています。

[^paste-join]: `paste`は複数の入力をTSV形式の各列として連結するためのコマンドですが、`-s`を指定すると、1列を1行に変換するという動作になります。ここでさらに区切り文字を`-d '\0'`と指定することで、一般的なプログラミング言語の配列操作で`array.join('')`とするのと同等の結果を得ています。なお、Linuxの`paste`やFreeBSDの`paste`では`paste -s -d ''`だけで期待通りの結果を得られるのですが、macOS（OS X）標準のBSD pasteでは`paste -s -d '\0' -`という具合に区切り文字に明示的に`\0`（null文字＝文字無しを意味するコード）を指定し、入力ファイルにも明示的に`-`（標準入力）を指定する必要がありました。macOSでも動作する書き方はLinuxでもFreeBSDでも元と同じ結果を得られたので、ここではその書き方に揃えています。

~~~bash:実行例
$ echo "こんにちは" | url_encode
%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF
$ █
~~~

### たくさんあるパラメータを見やすい形で定義する

Web APIを呼ぶ時は、複数のパラメータを指定する事が多いです。しかし、

~~~bash
  local params="q=$query&lang=$MY_LANGUAGE&locale=$locale..."
~~~

のように普通に横に並べて書いてしまうと、

 * どこに何が書かれているのか読みにくいし探しにくい。
 * パラメータ名（キー）と値を個別に取り扱いにくい。
 * （OAuth 1.0認証のように）パラメータを名前順でソートしないといけない場面で、データを取り扱いにくい。

といった問題に悩まされる事になります。

シェルスクリプトで利用できる文字列操作用のコマンドは大抵はデータが行区切りになっている事を期待していますので、パラメータの一覧を加工しやすくするためには、1パラメータを1行として定義しておいた方が容易です。これは、例えば文字列の定義時に改行を`\`でエスケープすることでも実現できます。

~~~bash
  local params="\
q $query\
lang $MY_LANGUAGE\
locale $locale\
result_type recent\
count $count\
$since_id\
"
~~~

この方法の欠点は、各行の末尾に`\`という見た目上余計な文字が必要だという点です。また、エスケープし忘れるとエラーになってしまいます。なので、`tweet.sh`内の各所では以下のようにしてパラメータを行ごとに定義しています。


~~~bash
  local params="$(cat << FIN
q $query
lang $MY_LANGUAGE
locale $locale
result_type recent
count $count
$since_id
FIN
  )"
~~~

`cat << XXX`はヒアドキュメントの書式で、そこから`XXX`と書かれた行までをファイルの内容として`cat`に渡し、出力結果（＝入力そのまま）を文字列置換で変数の値として格納しています。前後に余計なコードが増えるのは残念ですが、各行はクリーンな形に保たれるため、編集時の取り扱いが容易になります。


### 改行区切りのパラメータのリストを`キー1=エンコード済みの値1,キー2=エンコード済みの値2,...`の形に変換する

上記のように、途中段階では改行区切りの形式でパラメータを定義しておくことで文字列処理での取り扱いが容易になりますが、最終的に`curl`でWeb APIに送信する時には`q=%E3%82%AF%E3%82%A8%E3%83%AA&lang=ja&locale=ja...`のような形に変換する必要があります。

このために、`tweet.sh`では`to_encoded_list`という関数を以下のように定義しています。

~~~bash
to_encoded_list() {
  local delimiter="$1"
  [ "$delimiter" = '' ] && delimiter='\&'
  local transformed="$( \
    sort -k 1 -t ' ' |
    grep -v '^\s*$' |
    url_encode |
    sed 's/%20/=/' |
    paste -s -d "$delimiter" - |
    tr -d '\n')"
  echo "$transformed"
  log "to_encoded_list: $transformed"
}
~~~

この関数の標準入力には

~~~
キー1 値1
キー2 値2
...
~~~

という改行区切りの文字列が渡ってくることを期待していて、これをまずキー名でソートし（`sort -k 1 -t ' '`）、空行を除去し（`grep -v '^\s*$'`）、行をまるごとURLエンコードして（`url_encode`）、その結果の最初の`%20`を`=`に戻して（`sed 's/%20/=/'`）、`paste`で任意の区切り文字（省略時は`&`）で1行に連結しています。これで、上記のような入力から`キー1=エンコード済みの値1&キー2=エンコード済みの値2&...`という出力を得られます。また、このとき最後に改行文字が付与されるので、`tr -d '\n'`で取り除いています。

区切り文字を変えられるようにしているのは、Web APIのURLパラメータ等で渡す時と、後述するOAuth 1.0の認証情報のヘッダに含める情報とで各パラメータの区切り文字が変わるからです。

~~~bash:実行例
$ cat params.txt 
q 検索クエリ
result_type recent
lang ja
$ cat params.txt | to_encoded_list
lang=ja&q=%E6%A4%9C%E7%B4%A2%E3%82%AF%E3%82%A8%E3%83%AA&result_type=recent
$ cat params.txt | to_encoded_list ','
lang=ja,q=%E6%A4%9C%E7%B4%A2%E3%82%AF%E3%82%A8%E3%83%AA,result_type=recent
$ █
~~~

### Twitter APIのOAuth 1.0認証

前置きが長くなりました。ここからやっとOAuth 1.0の話に入っていきます。

TwitterのWeb APIは、ストリーミングAPIも含めてすべて[OAuth 1.0での認証](https://dev.twitter.com/oauth/overview)を必要とします。

実は、Twitter公式の[twurl](https://github.com/twitter/twurl)というコマンドを使うと、OAuth 1.0認証の実装をまるっと省略できてしまいます。が、[USP友の会のTwitter bot用スクリプト](https://github.com/ryuichiueda/TomoTool)の実装を見ると案外簡単そうだったので
[^why-oauth]、せっかくだからこれも自前でやってみることにしました[^from-usp-bot]。

[^why-oauth]: 単に、その時`twurl`の存在を知らなかったからというのもあります。というか最近になってやっと存在を知りました。

[^from-usp-bot]: ということで、`tweet.sh`におけるOAuth 1.0での認証の実装は[USP友の会のTwitter bot用スクリプト](https://github.com/ryuichiueda/TomoTool)の実装を色々と改変する形で作られています。[`tweet.sh`のソース][tweet.sh-tweet.sh]のライセンス表記にTomoToolの名があるのはこういう経緯のためです。スクラッチしたと言っておきながら肝心な所は自作ではなかったのでした。

OAuth 1.0での認証情報をHTTPリクエストに付与するには、以下の2段階を経る必要があります。

 * [署名を作る](https://dev.twitter.com/oauth/overview/creating-signatures)
 * [署名を使って認証情報のヘッダを作る](https://dev.twitter.com/oauth/overview/authorizing-requests)

#### 署名を作る

ここでいう「署名」というのは平たく言うと、APIに送る情報（APIのURL、投稿するツイートの内容や、リプライ先のIDといったパラメータなど）を1つに繋げて<del>秘密鍵で暗号化した</del><ins>[共通鍵で符号化した](https://ja.wikipedia.org/wiki/HMAC)</ins>ものです[^signature]。[OAuth1.0の署名(Signature)を作成する方法](https://syncer.jp/how-to-make-signature-of-oauth-1 "OAuth1.0の署名(Signature)を作成する方法")という記事で詳細が分かりやすく解説されているので、是非ご一読ください。以下は、その記事で解説されている内容を`tweet.sh`でどのように実装しているかの説明です。

[^signature]: なので、これは正しくは「（電子）署名」ではなく「メッセージ認証符号」と呼ばれる物です。電子署名との大きな違いは、電子署名が公開鍵と秘密鍵の鍵ペアを使って「情報の送り手だけがそれを作れる」という性質を有しているのに対し、メッセージ認証符号は共通鍵を使うので「送り手も受け手もどちらも同じ物を作れる」という点です。[TwitterのAPIドキュメントで「signature（署名）」「sign（署名する）」という用語が使われている](https://developer.twitter.com/en/docs/basics/authentication/guides/authorizing-a-request)ため、この記事全体はそれに倣って「署名」と表記している、ということになります。

##### 手順1: キーの作成

これは`tweet.sh`内で定義しているシェル関数`generate_signature`の中の以下の箇所が該当します。

~~~bash
  local signature_key="${CONSUMER_SECRET}&${ACCESS_TOKEN_SECRET}"
~~~

##### 手順2: データの作成

これはシェル関数`generate_signature`の以下の箇所が該当します。

~~~bash
  local encoded_url="$(echo "$url" | url_encode)"
  local signature_source="${method}&${encoded_url}&$( \
    to_encoded_list |
    url_encode |
    # Remove last extra line-break
    tr -d '\n')"
~~~

先の記事の「パラメータの組み立て」と「パラメータをエンコード」は、この文字列置換の中にある`to_encoded_list`と`url_encode`でやっています。`to_encoded_list`は何も入力を取っていないように見えますが、内部で`read`を実行しているので、結果的には`generate_signature`に渡された標準入力をそのまま受け取っている形となります。また、最後には末尾に付け加えられる余計な改行文字を`tr -d '\n'`で取り除いています。

その結果を文字列置換として埋め込む事で、これ全体で「パラメータの組み立て」「パラメータをエンコード」「3つの要素を`&`で繋ぐ」の3ステップを行っていることになります。

なお、一旦この処理の結果を`transformed`というローカル変数に格納しているのは、デバッグ情報を出力しやすくするためですので、本質的な処理とは無関係です。


##### 手順3: キーとデータを署名に変換

最後に、データとキーを使って署名を作ります。これは`generate_signature`の以下の箇所が該当します。

~~~bash
  local signature=$(echo -n "$signature_source" |
    openssl sha1 -hmac $signature_key -binary |
    openssl base64 |
    tr -d '\n')
~~~

<ins>署名の役割を担う情報として、</ins>`openssl`コマンドでHMAC-SHA1の<del>ハッシュ値</del><ins>メッセージ認証符号</ins>を得て、それをBase64エンコードしています。また、ここでも改行文字が付与されるので、`tr -d '\n'`で取り除いています。

#### 署名を使って認証情報のヘッダを作る

署名を作るだけではOAuth 1.0認証はできません。これをHTTPの`Authorization`ヘッダで送る必要があります。

`Authorization`ヘッダの内容は、シェル関数`call_api`の以下の箇所で求めています。

~~~bash
  local params=''
  if [ ! -t 0 ]
  then
    params="$(cat)"
  fi

  local oauth="$(echo "$params" | generate_oauth_header "$method" "$url")"
  local headers="Authorization: OAuth $oauth"
~~~

標準入力で渡ってきたパラメータ（先の改行区切りのデータです）を後で複数回使うため、一旦`params`という変数に格納しています。`call_api`自体に標準入力が渡されなかった時に`cat`を実行すると期待に反した結果になるため、`[ ! -t 0 ]`で標準入力の有無を確認してから`cat`しています。それを`generate_oauth_header`という別のシェル関数に渡して、OAuth 1.0の認証用ヘッダ文字列を生成しています。

`generate_oauth_header`は以下のように定義しています。

~~~bash
generate_oauth_header() {
  local method=$1
  local url=$2

  local common_params="$(common_params)"

  local signature=$(cat - <(echo "$common_params") | generate_signature "$method" "$url")
  local header=$(cat <(echo "$common_params") <(echo "oauth_signature $signature") |
    to_encoded_list ',' |
    tr -d '\n')

  echo -n "$header"
  log "HEADER: $header"
}
~~~

`common_params`は、OAuth認証のヘッダを作る時に複数回登場する`oauth_consumer_key`などのパラメータを改行区切りで出力するシェル関数で、以下のように定義しています。

~~~bash
common_params() {
  cat << FIN
oauth_consumer_key $CONSUMER_KEY
oauth_nonce $(date +%s%N)
oauth_signature_method HMAC-SHA1
oauth_timestamp $(date +%s)
oauth_token $ACCESS_TOKEN
oauth_version 1.0
FIN
}
~~~

`oauth_nonce`はリクエストごとにユニークな文字列ということなので、ここでは現在時刻をナノ秒単位で取得して使用しています。この関数の出力を

~~~bash
  local common_params="$(common_params)"
~~~

で同名の変数に代入して「インスタンス化」しています。というのも、上記のシェル関数をそのまま何度も呼ぶと`oauth_nonce`や`oauth_timestamp`の値が変化してしまうので、同じ値を何度も使い回せるように一旦静的な文字列にしているというわけです。

その上で、それを使って

~~~bash
  local signature=$(cat - <(echo "$common_params") | generate_signature "$method" "$url")
~~~

ここで「この関数自体の標準入力（`-`)＝ツイート内容などのパラメータ」と「`echo "$common_params"`の出力結果（`<(echo "$common_params")`[^process-substitution]）＝OAuthヘッダ生成用の共通パラメータ」を`cat`に渡して連結したものを前述の`generate_signature`に渡し、「署名」を作っています。そしてそれを

[^process-substitution]: この書き方は[「プロセス置換（Process Substitution）」という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)です。

~~~bash
  local header=$(cat <(echo "$common_params") <(echo "oauth_signature $signature") |
    to_encoded_list ',' |
    tr -d '\n')

  echo -n "$header"
~~~

ここで`oauth_signature`という名前の新たなパラメータとして共通パラメータに連結し、改行区切りのリストから`キー1=値1,キー2=値2,...`というカンマ区切りのURLエンコード済みリストに変換しています。なお、ここでも一旦その結果を`header`というローカル変数に格納していますが、これもデバッグ用で、本質的な処理とは無関係です。

……という処理の結果を使って、

~~~bash
  local oauth="$(echo "$params" | generate_oauth_header "$method" "$url")"
  local headers="Authorization: OAuth $oauth"
~~~

ここで`Authorization: OAuth oauth_consumer_key=xxx,oauth_nonce=xxx,oauth_signature=xxx,oauth_signature_method=HMAC-SHA1,oauth_timestamp=xxx,oauth_token=xxx,oauth_version=1.0`というヘッダを作っているというわけなのでした。

#### OAuth 1.0の認証ヘッダを付けてリクエストを送る

後はもう、ここまでで作ったヘッダを付けてリクエストを送るだけです。

~~~bash
curl --header "$headers" \
     --data "$(echo "$params" | to_encoded_list)" \
     $url
~~~

（実際には他にもオプションを指定していますが、ここでは省略しています。）

という感じでBashベースでOAuth 1.0認証してみたわけですが、最初に書いた通り`twurl`を使えばこのあたりのことはすっかり`twurl`に任せてしまえます。よっぽど酔狂な人以外は得をしない解説にお付き合い頂いた皆さん、お疲れさまでした……


### ストリーミングの受信

TwitterのWeb APIにはストリーミングAPIがいくつかあります。

例えばtweet.shの`watch-mentions`では、[User streams](https://dev.twitter.com/streaming/userstreams)を使っています。これは自分へのメンションやフォローされた・RTされたといったイベントが通知されるAPIで、`track`というパラメータでキーワードを指定しておくと、そのキーワードにマッチするツイートも（フォローしていないユーザのツイートも）流れてくるというものです。

「流れてくる」というのはどういう事かというと、平たく言えば、*APIが返すHTTPレスポンスのBODYの長さが不定の状態で、1行あたり1イベントとして少しずつレスポンスが返ってくる*という事です。`curl`なら、標準出力にレスポンスが1行ずつ出力されるという結果になります。

という説明からもうお分かりかと思いますが、tweet.shでもこれを`while read`で読み取って、`jq`で情報を取り出して種類を判別した後適切なハンドラを実行するというような事をしています。

~~~bash
curl --get ... https://userstream.twitter.com/1.1/user.json | while read -r event
do
  # eventにはツイートやイベントを表すJSON文字列が格納されている。
  # 必要な情報はjqを使って取り出す。
done
~~~

実際には、

~~~bash
  call_api GET https://userstream.twitter.com/1.1/user.json |
    handle_mentions "$user_screen_name" "$@"
~~~

といった具合にcurlの結果をパイプラインでシェル関数に渡して、受け取った側の関数`handle_mentions`の中で`while`ループを回しています。



### その他のこと

[`tweet.sh`][tweet.sh-tweet.sh]は本稿執筆時点[^本稿執筆時点]で1400行ほどありますが、外部から受け取るのは（設定ファイルの認証情報以外は）標準入力と引数だけですし、出力も標準入力と`curl`によるWeb APIへのアクセスのみで、それ以外のほとんどの箇所は文字列の加工か条件分岐です。また、処理効率よりもロジックの読み取りやすさを優先しているために、そんなに凝ったテクニックは使っていない比較的素直な（愚直な）Bashスクリプトになっています。なるべく細かい単位で関数を分けるようにしているので、その処理が一体何をする物なのかという事については比較的容易に読み取れるのではないかと思います。

答え合わせをする感じで[スクリプト本体][tweet.sh-tweet.sh]を読んでみて、「なるほど、これをやるにはこうすればいいのか」という事を読み取って頂き、ご自身でシェルスクリプトを書かれる際に知見として活かして頂ければ幸いです！
![thumbsup.png](https://qiita-image-store.s3.amazonaws.com/0/61387/102129cc-ff07-40b7-f862-fc4bed52fb9f.png)

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

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

自分は2011年から[日経Linux][]誌上で「シス管系女子」というケーススタディ形式の解説マンガ記事を連載させて頂いています。

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

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

もし良かったら、お近くの大きめの書店で[シス管系女子][]の本や[掲載誌][日経Linux]を手に取ってチラ見して頂けましたら幸いです。

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

[^イラスト]: [これまでのイラスト一覧](http://system-admin-girl.com/comic/#illust)



以上、Shell Script Advent Calendarの5日目でした。6日目は[Yoshihiko Takeuchiさん](http://qiita.com/YoshihikoTakeuchi)による記事です。お楽しみに！


[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
[tweetbot.sh]:https://github.com/piroor/tweetbot.sh
[tweetbot.sh-解説]:http://qiita.com/piroor/items/5f0d96ca001b06a7fa4d
[シス管系女子]:http://system-admin-girl.com/
[みんとちゃんbot]:https://twitter.com/sysadgirl_mint
[日経Linux]:http://itpro.nikkeibp.co.jp/linux/