SHOYAN BLOG

I am a pragmatic programmer.

超高速grep「The Silver Searcher(ag)」からhighwayに乗り換えた

プログラマーにとって,grepコマンドはなくてはならない存在です。
基本的かつ古典的なユーティリティであるgrepですが,使いにくい面もあります。

2013年のはじめころから,grepに取って代わるコマンドとして「The Silver Searcher」(以下「ag」)が注目されはじめました。
そして最近、highwayというagライクで高速なgrepツールがリリースされました。

The Silver Searcher(ag)とは?

highwayの紹介の前にagとagが開発された背景について説明したいと思います。

プログラムを書いていると,ソースコード全域にわたって文字列を検索したい,ということはよくあります。
そのようなときにgrepコマンドが活躍するわけですが,ソースコードのディレクトリには検索したくないファイルが往々としてあるわけです。

たとえばバージョン管理システムが使っている「.git」ディレクトリは,検索対象に含めたくありません。
これをgrepで実現しようとすると,オプションやパイプを組み合わせて,少々面倒なワンライナーを書く必要があります。

こういった問題を解決するため,ackというプログラムが作られました。
ackは以下のような特徴を持つ,開発者向けの賢いgrepツールです。

  • デフォルトでディレクトリツリーを再帰検索
  • .gitなどを暗黙的に除外
  • 検索するファイルタイプをオプションで指定可能(–perl,など)
  • 拡張子だけでなく,shebangを見てファイルタイプを判別可能
  • 検索結果をファイル単位でわかりやすくまとめて表示

ackはgrepと比べて使い勝手がよいのですが,Perlで実装されていることもあり,動作速度に難があります。
その動作速度の問題を改善するためにagが開発されました。

agは開発者が「A code searching tool similar to ack, with a focus on speed.」と紹介しているとおり,「高速なack」を目指して開発されたプログラムです。
「ackに比べて3から5倍高速」を謳っており,そして実際非常に高速です。

The Silver Searcher(ag)の問題点

紹介した通り、使い勝手がよく、性能もよいagなのですが、以下の問題点があります。

  • EUC-JPやShift_JISなどの日本語に使われるマルチバイト文字列が検索できない
  • 検索結果の出力順が検索する度に異なる

全てのコードがUTF-8であれば問題ありませんが、日本語圏で使われるファイルでは、なかなかそうもいきません。
検索結果の出力順が検索する度に異なるのも少し使いにくいと感じます。

その点、highwayはEUC-JPやShift_JISをサポートしており、検索結果の表示順の問題もありません。

highwayのメリット

agの問題点であげた点をhighwayはクリアしています。
また、速度もagよりも高速です。

ベンチマークについては以下の記事をご覧ください。
http://tkengo.github.io/blog/2015/10/19/release-highway/

highwayのインストール

For OS X

homebrewでインストールできます。

1
2
$ brew tap tkengo/highway
$ brew install highway

For Fedora Core

1
2
3
4
5
6
7
8
$ sudo vi /etc/yum.repos.d/highway.repo
[repos.highway]
name=highway
baseurl=http://tkengo.github.io/highway/fedora
enabled=0
gpgcheck=0

$ sudo yum install highway --enablerepo="repos.highway"

使い方

1
2
3
4
5
6
7
8
# カレントディレクトリをhogeをいう文字列で再帰的に検索
$ hw hoge

# public_html以下のディレクトリからhogeという文字列を検索し、マッチした行から10行目までを表示
$ hw -A 10 hoge public_html

# オプションは以下のコマンドで参照できます
$ hw -h

unite.vimと連携する

unite.vimのgrepにhighwayを使うようにします。

1
2
3
4
5
6
7
8
9
" grep検索
nnoremap <silent> ,g  :<C-u>Unite grep:. -buffer-name=search-buffer<CR>

" unite grepにhw(highway)を使う
if executable('hw')
  let g:unite_source_grep_command = 'hw'
  let g:unite_source_grep_default_opts = '--no-group --no-color'
  let g:unite_source_grep_recursive_opt = ''
endif

おわりに

grepツールは完全にhighwayに乗り換えましたが、速度面、機能面に関しては問題ありません。
まだgrepで頑張っている人はもちろん,agをすでに使っている人であっても,highwayの利用を検討する価値は充分にあると思います。

C言語でHello World

たくさんのソフトウェアがC言語で作られており、自分もそういうソフトウェアの仕組みを知ったり自分で作れるようになりたいなと思って、C言語の勉強を始めました。

とりあえず、最初はC言語でHello worldをしてみます。
環境はMac OSXです。
X Codeをいれたら、gccが入るのでC言語はすぐ動くようでした。

gccがインストールされているかの確認

gcc -v と入力して、以下のように表示されたらOKです。

1
2
3
4
5
⇒  gcc -v
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 7.0.0 (clang-700.0.72)
Target: x86_64-apple-darwin14.5.0
Thread model: posix

Hello Worldする

hello.cを作成して以下のように記入します。

1
2
3
4
5
6
#include <stdio.h>
int main(int argc, char **argv)
{
    printf("hello world\n");
    return 0;
}

コンパイルします。

1
$ gcc hello.c -o hello

helloという実行ファイルができるので、実行します。

1
2
$ ./hello
hello world

天気予報をSlackに通知する on Heroku

@keita_kawamotoが天気予報を見ずに出社して、途中で雨に降られて困っていたので天気予報通知をつくってみました。
Herokuでスケジューラーに登録してSlackに通知するようにしています。

weather-nitify-slack

ソースコードは公開しているので参考にどうぞ。

天気情報を取得する

天気情報の取得はweather_hacksのAPIを利用しました。
APIはjsonでレスポンスが返されます。

以下、サンプルコードです。

slack-weather-notifier.rb

1
2
3
4
5
6
7
8
9
10
11
require 'json'
require 'open-uri'

uri = 'http://weather.livedoor.com/forecast/webservice/json/v1?city=400010'

res     = JSON.load(open(uri).read)
title   = res['title']
link    = res['link']
weather = res['forecasts'].first
message = "[#{weather['date']}#{title}](#{link})は「#{weather['telop']}」です。"
puts message

ターミナルでruby slack-weather-notifier.rb と実行すると、今日の天気の情報が表示されます。

Slackに通知する

天気情報の取得ができたので、次はSlackに通知します。
Slackの通知にはIncoming WebHooksを使います。
Incoming WebHooksを使うには、Webhook URLの発行が必要です。

Slackの設定画面にアクセスします。
https://example.slack.com/services/new/incoming-webhook
example.slack.comは自分のSlack Teamのドメインをいれてください。

通知先のチャンネルを選んで、Add Incoming Webhooks Integrationのボタンを押すと発行されます。

slack-setting-example.png

設定画面でWebhook URLが確認できます。
このURLにPOSTすると、Slackに通知できるようになります。

Customize NameやCustomize Iconを変更すると、通知するbotの名前やアイコンが変更できます。

slack-setting-example2.png

では、通知をしてみましょう。
通知はslack-incoming-webhooksというgemを使うと簡単にできるので、今回はそれを使います。

slack-incoming-webhooks をインストール

1
$ gem install slack-incoming-webhooks

先ほどのスクリプトにslack通知の設定を追加します。

1
2
3
4
5
6
7
8
9
10
11
12
13
require 'json'
require 'open-uri'
require 'slack/incoming/webhooks'

uri = 'http://weather.livedoor.com/forecast/webservice/json/v1?city=400010'

res     = JSON.load(open(uri).read)
title   = res['title']
link    = res['link']
weather = res['forecasts'].first

slack = Slack::Incoming::Webhooks.new "webhook_url"
slack.post "<#{link}|#{weather['date']}#{title}>は「#{weather['telop']}」です。"

ターミナルでruby slack-weather-notifier.rb と実行してみましょう。
Slackに通知されれば成功です。

HerokuでAppを作成

さて、天気情報とSlack通知ができるようになりました。
これが定期的に実行できれば便利ですね。

Herokuを使えば無料でスクリプトを定期実行できます。
Herokuでアカウントを作成します。
https://signup.heroku.com/

アカウント登録をしたら、Toolbeltをインストールします。
Toolbeltは、Herokuをコマンドラインから利用できるようになるツールです。
https://toolbelt.heroku.com/

次に認証を行います。
ターミナルにheroku loginとコマンドを入力します。
メールアドレスとパスワードが聞かれるので、先ほどHerokuで登録したメールアドレスとパスワードを入力してください。

アプリを作成

アプリケーションを登録しましょう。

Herokuにデプロイするには、Gitを使ってリモートリポジトリへプッシュする必要があるので、Gitの登録を行います。

ここでは、slack-weather-notifierというディレクトリを作成し、そこに先ほどのファイルを作成し、Gitで管理します。

また、Herokuでslack-incoming-webhooks gemを使うためにGemfileでgemを管理します。
slack-weather-notifierディレクトリの直下にGemfileを作成します。

Gemfile

1
2
3
source 'https://rubygems.org'

gem 'slack-incoming-webhooks'

ターミナルで bundle installと実行すると、gemがインストールされ、Gemfile.lockファイルが作成されます。

もし、bundlerをインストールしていない場合は、 gem install bundler でインストールしてください。

以下のような構成になります。

1
2
3
4
slack-weather-notifier
├── Gemfile
├── Gemfile.lock
└── slack-weather-notifier.rb

Webhook Urlは外部に公開すべきではないので、環境変数に登録して、それを使うようにします。

1
2
3
4
5
6
7
8
9
10
11
12
13
require 'json'
require 'open-uri'
require 'slack-incoming-webhooks'

uri = 'http://weather.livedoor.com/forecast/webservice/json/v1?city=400010'

res     = JSON.load(open(uri).read)
title   = res['title']
link    = res['link']
weather = res['forecasts'].first

slack = Slack::Incoming::Webhooks.new ENV['WEBHOOK_URL']
slack.post "<#{link}|#{weather['date']}#{title}>は「#{weather['telop']}」です。"

Gitに登録しましょう。

1
2
3
$ git init
$ git add .
$ git commit -m "first commit"

Herokuにpushします。

1
$ git push heroku master

WEBHOOK URLを登録します。

1
heroku config:set WEBHOOK_URL=ここにwebhook_urlを入力

スケジューラーを登録

カード登録

スケジューラーを利用するにはカード登録が必要です。
無料利用枠を超過した場合は料金が発生しますが、数秒で終わるスクリプトなので大丈夫です。

以下でカードを登録できます。
https://dashboard.heroku.com/account/billing

スケジューラーを登録

カード登録ができたらAdd-onsにスケジューラーを追加します。

以下のコマンドでコンソールから登録できます。

1
heroku addons:add scheduler:standard

またはaddonページから登録してください。
https://addons.heroku.com/scheduler

スケジュール管理画面を開く

https://heroku-scheduler.herokuapp.com/dashboard

コンソールからも開けます。

1
$ heroku addons:open scheduler

スケジューラーの設定

Select an appで登録したアプリケーションを選び ‘Add Standard for free’ のボタンをクリックすると登録できます。
登録したら、ダッシュボードのアプリケーションのページのAdd-onsにHeroku Schedulerが表示されています。

クリックすると、設定ページに飛ぶので、コマンドと時間を登録します。
ちなみに時間はUTCなので気をつけてください。
0:00で登録すると日本時間の9:00に通知されます。

heroku-scheduler

ソースコードは公開しているので参考にどうぞ。

RubyでSlack通知をする

Slackは使っていますか?
僕は会社やプライベートはもっぱらSlackを使っています。

SlackにはAPIが備わっていて、APIを使えばSlack通知が簡単にできます。

今回はRubyでSlackに通知する方法を紹介します。

RubyでSlack通知をするには、以下の作業が必要です。

  1. webhook urlの発行
  2. webhook urlに対してpostする

1. webhook urlの発行

まずは、webhook urlを発行します。
https://yourteam.slack.com/services/new/incoming-webhook のページで発行できます。
Post先のチャンネルを選んで、Add Incoming Webhooks Integrationのボタンを押すと発行されます。

slack-setting-example

channelや通知するbotの名前を決めれるので、適当に決めます。

slack-setting-example2

これでSlack側の準備は完了です。

2. webhook urlに対してpostする

webhook urlの発行ができたら、Slackに通知をしてみましょう。

今回、Slack通知にはslack-incoming-webhooksというgemを使います。

インストール

インストールは以下のコマンドでできます。

1
gem install slack-incoming-webhooks

Slackへ通知

使い方はシンプルです。

1
2
3
4
require 'slack/incoming/webhooks'

slack = Slack::Incoming::Webhooks.new "WEBHOOK_URL"
slack.post "Hello World"

もし、通知先のチャンネルや通知するユーザーネームを変更したいときは指定できます。

1
2
3
4
slack = Slack::Incoming::Webhooks.new "WEBHOOK_URL", channel: '#other-channel', username: 'monkey-bot'

# Direct message
slack = Slack::Incoming::Webhooks.new "WEBHOOK_URL", channel: '@shoyan'

アクセサメソッドも用意されています。

1
2
slack.channel = '#other-channel'
slack.icon_emoji = ':ghost:'

Attachmentsを使う

さて、単純な通知ができたら次はもっとリッチなフォーマットで通知をしてみましょう。
それにはattachmentsを使います。

Example1

slack-example

1
2
3
4
5
6
7
attachments = [{
  title: "Ticket #1943: Can't reset my password",
  title_link: "https://groove.hq/path/to/ticket/1943",
  text: "Help! I tried to reset my password but nothing happened!",
  color: "#7CD197"
}]
slack.post "New ticket from Andrea Lee", attachments: attachments

Example2

slack-example2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
attachments = [{
  text: "<https://honeybadger.io/path/to/event/|ReferenceError> - UI is not defined",
  fields: [
    {
      title: "Project",
      value: "Awesome Project",
      short: true
    },
    {
      title: "Environment",
      value: "production",
      short: true
    }
  ],
  color: "#F35A00"
}]
slack.post "", attachments: attachments

Example3

slack-example3

1
2
3
4
5
6
7
8
attachments = [{
  title: "Network traffic (kb/s)",
  title_link: "https://datadog.com/path/to/event",
  text: "How does this look? @slack-ops - Sent by Julie Dodd",
  image_url: "https://api.slack.com/img/api/attachment_example_datadog.png",
  color: "#764FA5"
}]
slack.post "", attachments: attachments

いかがでしたでしょうか。
今回紹介できなかったオプションもあるので、詳細はSlackのattachmentsのドキュメントを参考にしてください。

では、よいSlack Lifeを!

相関サブクエリを使って次回契約を取得する

相関サブクエリを使って次回契約を取得します。

Contract table

| id | account_id | start_date | end_date |
| — | — | — | — |
| 1 | 1 | 20140101 | 20141231 |
| 2 | 1 | 20150101 | 20151231 |
| 3 | 1 | 20160101 | 20161231 |
| 4 | 1 | 20170101 | 20171231 |
| 5 | 2 | 20150101 | 20151231 |
| 6 | 2 | 20160101 | 20161231 |

上記のようなaccount_idと開始日、終了日の登録してあるテーブルがあるとします。
現在の契約を取得するのは簡単ですね。

1
2
# 現在契約を取得する
SELECT * FROM contracts WHERE start_date >= 現在日付 AND end_date <= 現在日付

現在契約を取得するのは簡単ですが、その次の契約を取得するとなるとそう単純にはいきません。

そこで、相関サブクエリを使います。
相関サブクエリを使うことで次回契約を取得できます。

1
2
3
4
5
6
7
8
9
10
11
12
# 次回契約を取得する
SELECT *
  FROM contracts As cont
 WHERE start_date = (SELECT MIN(start_date)
                       FROM contracts as c1
                     WHERE c1.start_date > ( SELECT end_date
                                               FROM contracts as c2
                                             WHERE c2.start_date <= '20150528'
                                               AND c2.end_date >= '20150528'
                                               AND c1.account_id = c2.account_id)
                       AND cont.account_id = c1.account_id
                     GROUP BY c1.account_id);

結果

| id | account_id | start_date | end_date |
| — | — | — | — |
| 3 | 1 | 20160101 | 20161231 |
| 6 | 2 | 20160101 | 20161231 |

クエリの説明

クエリの説明をします。

クエリは内側からみていきます。
まずは、一番内側にある、 SELECT end_date ... AND c1.account_id = c2.account_idのクエリです。
このクエリでは現在の契約(ここでは2015/5/28とします)を取得します。

2つめのクエリで、次回以降の契約を取得します。
SELECT MIN(start_date) を使うことで、次回契約のなかで直近の契約を取得できます。
アカウントごとに直近の次回契約を取得したいので、GROUP BY account_id をしています。

c1.account_id = c2.account_id と cont.account_id = c1.account_id は行と行を比較するために必要です。

3つめのクエリ(SELECT * ... WHERE start_date =)で直近の次回契約を条件として、データを取得します。

手元で試したい方は以下のクエリでデータをつくれます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE `contracts` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `account_id` int(11) DEFAULT NULL,
  `start_date` int(11) DEFAULT NULL,
  `end_date` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `contracts` (`id`, `account_id`, `start_date`, `end_date`)
VALUES
    (1, 1, 20140101, 20141231),
    (2, 1, 20150101, 20151231),
    (3, 1, 20160101, 20161231),
    (4, 1, 20170101, 20171231),
    (5, 2, 20150101, 20151231),
    (6, 2, 20160101, 20161231);