詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

Fork me on GitHub

任意の入力を human-readable にする humr ってツールを書いた

Web サーバのアクセスログを tail してながめている時にパーセントエンコーディングされた文字列をデコードしたり、コマンドラインで数字を sort -n したあとにでかい数字を丸めて表示したり("1234567" を "1.2M" に、とか)したくなることが多かったので、その辺を汎用的におこなってくれる humr というツールを書きました。

npm でインストールできます:

npm install -g humr

簡単に動きを確認するには、以下のコマンドを試してみてください:

% echo '/search?q=%E6%97%A5%E6%9C%AC%E8%AA%9E 1234567' | humr

f:id:motemen:20151225104506p:plain

入力のうち読みづらいところが色付きで変換されて出力されたことが分かります。冒頭に書いたようにログなどの入力をパイプしてご利用ください。

仕組み

humr は入力を

  1. Parser によって各行ごとにフィールドに分解し、さらに
  2. 各フィールドを Formatter によって人間向けの表現にする、

ということを行っています。Parser は1つしか指定できませんが Formatter は複数存在しえて、可能なものを順番に試していってフィールドを解釈できた場合、その結果に色付けされて出力されます。解釈できなかった場合は入力そのまま。

それぞれ、

  • Parser
    • regexp: 行を正規表現(デフォルトでは s+)で区切られたフィールドの列とみなす
    • ltsv: 入力を LTSV として解析し、それぞれの値をフィールドとする
    • apache 入力を Apache Common Log フォーマットのログとみなす
  • Formatter
    • si: SI 接頭辞(k、M など)で数値を略記する
    • url: 文字列中のパーセントエンコーディングをデコードする
    • date: 日付っぽい文字列を解釈して現在のロケールに合わせて表現する
    • emoji: :+1: みたいな文字列を絵文字に変換する

ってのを同梱して提供しています。

オプション

オプションなしだと Parser には regexp、Formatter には上記すべてが利用されるのですが、もちろんオプションでこれを変更できます。

humr -p <parser>[=<arg>] -f [<label>:]<formatter>

-p は Parser を指定します。例えば -p ltsv とすると入力は LTSV として解釈され、

size:1234567[tab]path:/search?q=%E6%97%A5%E6%9C%AC%E8%AA%9E

な入力のうち、"1234567" と "/search?..." がフォーマット対象となります。

-f で Formatter を指定できます(複数指定可)。基本的にすべてのフィールドにすべての Formatter が試みられますが、<label> を指定することで Formatter を特定のフィールドのみに適用できます。上の例だと

humr -p ltsv -f size:si -f path:url

とすることができます。またフィールドの指定は LTSV のように名前か、単純に1オリジンのインデックスを使えます。

humr -p ltsv -f 1:si -f 2:url

カスタマイズ

humr は起動時に ~/.config/humr/*.js にあるスクリプトを評価するようになっているので、自前の Parser/Formatter を定義できます。

+function () {
  'use strict';

  let formatters = require('humr/formatter').registry;

  class JapaneseNumbersFormatter {
    format (text, hl) {
      const NUMBERS = '〇一二三四五六七八九';
      return text.replace(/[0-9]/g, (n) => hl(NUMBERS[n]));
    }
  }

  formatters.register('japanese_numbers', JapaneseNumbersFormatter);
} ();

以上のような内容のファイルを ~/.config/humr/japanese_numbers.js (ファイル名は .js で終わっていればなんでもよいです)として配置しておけば、Formatter として "japanese_numbers" を利用できます。

humr -p ltsv -f size:japanese_numbers

Parser, Formatter としてどんなクラスが期待されているかはそれぞれの定義を見るのが手っ取り早いです: ParserFormatter

Parser としては行を受け取り、フィールドを { label?: string; text: string } に、それ以外の文字列を string に分解する parse メソッドを持ったクラスを登録します。

Formatter としてはフィールドの文字列とハイライト関数を受け取り、フォーマット可能であればフォーマットした文字列をハイライト関数に渡したうえで文字列を返す format メソッドを持ったクラスを登録します。

以上

昔 Ruby で書いてたやつなんだけど、拡張のしやすさ考えると JavaScript で書きたいなってことで Node で書きなおした。役立ててくださいね。日付の解釈に moment を使ってるのが良い選択だったか気になってます。

goiferr で Go のエラー処理コードを自動挿入する

Go 言語には例外機構が備わっておらず、関数や手続きのエラー的な状況を表すには、返り値を多値にして本来興味ある結果とともに error インターフェイスを返す、というのが一般的です。例をあげるまでもないですが、ファイルを開くという(失敗する可能性のある)処理を行うならこういう感じ:

f, err := os.Open(filename)
if err != nil {
    // handle err
}

何ごとも明に書き下すことを求める Go らしい仕様ですね。文句ある人も多そうですが、呼び出し側が異常系を意識せざるを得なくなるので、よい効果も大きいと思います。

先の例のように、エラーを発生させるような処理を呼び出したあとは err をチェックする、というのは最初に学ぶイディオムと言ってもよいくらいよく書くことになるコードですが、ほんとうに何度も書くことになるのでこれは面倒。

go-iferr

そこでもちろん、プログラムにコードを書かせる試み。

https://github.com/motemen/go-iferr

goiferr は何らかのエラーを受け取る処理の直後に、そのエラーの処理を行う「いつもの」コードをよしなに挿入してくれるツールです。go get でインストールできます。

go get -u github.com/motemen/go-iferr/cmd/goiferr

例を見るのが手っ取り早いでしょう:

package eg

import "os/exec"

func GoVersion() (string, error) {
    path, err := exec.LookPath("go")

    cmd := exec.Command(path, "version")
    b, err := cmd.Output()

    return string(b), nil
}

例えばこのような "go version" コマンドの出力を文字列として返すコードを書いたとします。あ、エラーチェックをサボっていて中途半端ですね。というかこのままではコンパイルも通らないのでは。

このソースコードを a.go という名前で保存し、goiferr に処理させます。

% goiferr a.go > b.go
main.go:31: error (ignored): a.go:6:8: err declared but not used
=== a.go
% diff -u a.go b.go
--- a.go        2015-12-18 13:06:27.000000000 +0900
+++ b.go        2015-12-18 13:06:43.000000000 +0900
@@ -4,9 +4,15 @@

 func GoVersion() (string, error) {
        path, err := exec.LookPath("go")
+       if err != nil {
+               return "", err
+       }

        cmd := exec.Command(path, "version")
        b, err := cmd.Output()
+       if err != nil {
+               return "", err
+       }

        return string(b), nil
 }

あら便利、err のチェックをよしなにおこなってくれました! よく見るエラー処理が追加されて、安心感がありますね。

引数にディレクトリやパッケージ名を与えると複数のファイルを対象に同じ処理を行ないます。-w オプションを与えるとファイルを直接上書きしますので、普通はこれを利用するのが便利です。

細かい挙動

エラー処理が挿入される箇所

goiferr は、error 型をもつ変数が左辺に登場する代入文それぞれに対して、代入文とその次の文との間が1行以上空いている場合、エラー処理を挿入します。つまり

x, err := getX()
x.Foo()

のようなソースには何も変更を加えませんが、

x, err := getX()

x.Foo()

となっているとエラー処理を挿入します。

エラー処理の内容

エラー処理は

if err != nil {
    ...
}

という形で、コンテキストによって if 文の内容が変わります。

まず当該の文が error を返り値に関数の中にある場合、err を返すような return 文になります。これは先ほどの GoVersion 関数の例にあるとおり。

また、スコープから log パッケージを参照できる場合には log.Fatalf(err) します。

 import "log"

 func main() {
        _, err := GoVersion()
+       if err != nil {
+               log.Fatal(err)
+       }
+
 }

t という *testing.T 型をもつ変数を参照できる場合にはテスト関数の中だとみなして、t.Fatalf(err) します。

 import "testing"

 func TestFoo(t *testing.T) {
        _, err := GoVersion()
+       if err != nil {
+               t.Fatal(err)
+       }
+
 }

以上のどの条件にも合致しなければ、panic(err.Error()) します。

 func main() {
        _, err := GoVersion()
+       if err != nil {
+               panic(err.Error())
+       }
+
 }

よくできてますね。


https://github.com/motemen/go-iferr

以上、Go を書く上で面倒なところを自動化する goiferr の紹介でした! 今のところ便利ですが、「とりあえず作ってみた」レベルなので以下のようにまだまだやるべきことは残っています。コンパイルの通らないコードを生成することもあるかと思いますが、おいおい直していくつもりです。

TODO

  • 関数の返り値が名前付きの場合の対処
  • err 以外の変数名を許可する
  • エラー処理のコードをカスタマイズ可能にする

Slack のログを自動で Google Spreadsheet に保存する

2020-05-12 22:50 追記

2020-05-05 より、Slack のトークンは作れなくなってるので、このエントリの方法ではストレートに実現できなくなっています。トークンの代替方法についてはサポートしかねる(というか知らない)ので、各自がんばりましょう!

2015-11-13 16:40 追記

以下のスクリプトの利用が Slack の TOS に触れるのではないか……という指摘をいただきました。

No Other Storing. You may not copy or store any Data or capture or store any information expressed by the Data (such as hashed or transferred data), except to the extent permitted by this API TOS.

全体を読んでみると OAuth を利用してサービスを提供する場合を想定しているようなので、今回の利用を Slack が積極的に禁じているわけではないと思いますが(管理画面でアーカイブの zip を提供していることもあるし)、スクリプトを利用される際はご自身で TOS をいま一度ご確認ください。


Slack はたいへん安易に利用できるので重宝するけれど、無料プランではログが10000件までしか保存されず、過去の発言が消えてしまうのは何だか勿体ないというか、不便だし辛い。あと金もない。

ありがたいことに Slack は API がかなり整備されていて、これを丁寧に使うことで各チャンネルのログを取得することはできそうである。しかし定期的に API を叩く機構を用意することや、ログを保存する場所のことを考えるとすこし億劫にもなる。

そこで、今回は貧者の cron であるところの Google Apps Script(GAS) と貧者のデータベースであるところの Google Spreadsheet を使って Slack のログを保存する方法を紹介する。

準備

Slack API を使用するので、まずは https://api.slack.com/custom-integrations/legacy-tokens でチームのトークンを生成しておく。

Google Apps Script のコードは https://github.com/motemen/gas-slack-log-spreadsheet/blob/master/app.js にある。これを https://script.google.com/ から作成する新規プロジェクトのコードとしてコピペする。

ちなみにソースコードは TypeScript で書いているので、修正したい場合はそちらをいじるとよろしい。

github.com

「無題のプロジェクト」と表示されているところをクリックして、プロジェクト名を保存する。自分は「Slacklog (<チーム名>)」などとした。

メニューから [ファイル] → [プロジェクトのプロパティ] → [スクリプトのプロパティ] から行を追加する。

  • プロパティ: slack_api_token
  • 値: 先ほど生成したSlack API のトークン

f:id:motemen:20151113130302p:plain

準備は以上で完了。

初回の実行

メニューの [実行] → [StoreLogsDelta] から初回の実行をおこなう。このとき Google Drive、Spreadsheet などへのアクセス権限を求められるので許可する(以降は不要)。

f:id:motemen:20151113130321p:plain

この初回の実行ですべての過去ログがスプレッドシートに保存されるはず。GAS の実行はあまりに時間がかかると強制的に中断されるが、無料枠の 10000 件であればすぐに終わるはず。

結果の確認

Google Drive の Slack Logs/<チーム名> フォルダ以下に、yyyy-MM 形式のファイル名で月ごとのログがスプレッドシートとして保存される。

f:id:motemen:20151113130339p:plain

スプレッドシート中にはチャンネルごとにシートが作られていて、メッセージが古い順に格納されている。D カラムは Slack API における JSON を保持しているので、さらにリッチな表示をしたければこの値を利用して何かしらできるようになっている。

定期実行の設定

スクリプトを次に実行するときは、差分だけを取得して追記という形になる。1時間毎くらいの頻度でおこなっておけば問題なさそう。

メニューの [編集] → [現在のプロジェクトのトリガー] から、StoreLogsDelta を毎時実行するように設定する。

f:id:motemen:20151113130353p:plain

ひと晩くらいしか動かしてないけど、大丈夫そう。ちなみに月またぎは経験していないので何か落とし穴があるかもしれません。

……という感じです。Google Drive のフォルダ単位でチームメンバーに共有することを考えているので、プライベートグループのログの保存は行っていない。やるなら別フォルダを用意することになるだろう。

無課金勢のみなさまにおかれましては、どうぞご利用ください。

はてなで一緒に働きませんか?