Crystal 言語で CLI ツールを書いてみる
Crystal はつくりかけの言語です.この記事は 0.8.0 を元に書きましたが,今後かなり変更されることが予想されます
Crystal は Ruby に強くインスパイアされたコンパイル言語です.結構前から気になっていて,Crystal のコンパイラが Ruby で書かれていた頃(今はセルフホストしています)から見ていて最近はぼちぼちコントリビュートしたりしてます.インスパイアされているというだけで,色々 Ruby と違うところもあります(例えばいま話題の文字列は Crystal では全て immutable です)
僕は Ruby が好きなので,ちょっとしたツールや書き捨てのスクリプトは特に理由が無い場合は Ruby で書いています.それもあって Crystal でちょっとしたツールを書くのは次のような利点があります.
- Ruby と同じように楽に(そして雑に)書ける
- Ruby とは違い実行速度が速い(まだあまり実感してないですが)
- Ruby とは違いコンパイル時に型チェックが入る(各式が取り得る型の union type で型付けします)
- Ruby とは違いバイナリファイル1つになる(共有ライブラリなどを使わなければ)
コマンドラインツールは Crisp (Crystal で書かれた Lisp 処理系)や crdoc (コマンドラインから Crystal のドキュメントを検索&開くツール)をつくってみたので,今回は自分なりのやり方の手順をまとめてみます.
といっても大体 Ruby と同じですが…
準備
Go 言語における go
コマンドのように,Crystal にも crystal
コマンドがあります. 公式のインストールページ に従ってインストールします.残念ながらまだ Windows では利用できません(対応はぼちぼち進んでいるようです).
次に開発環境を入れます.お好みに合わせて導入してください.(もし需要があれば開発ツール周りもブログに書きたい.)
- Atom: language-crystal-actual
- Sublime Text: sublime-crystal
- Vim: vim-crystal
- Emacs: emacs-crystal-mode
リポジトリ生成
試しにファイルの中身を表示するだけのコマンド mycat
をつくってみます.
まずは crystal init
コマンドで初期のリポジトリを生成します.
$ crystal init app mycat
次のようなディレクトリ構成が自動でつくられます.
--- mycat |--- .gitignore |--- LICENSE |--- README.md |--- .travis.yml |--- shard.yml |--- src | |--- mycat.cr | |--- mycat/version.cr | |--- spec | |--- spec_helper.cr | |--- mycat_spec.cr | |--- .gitignore
僕はコマンドラインツールを書く時はなるべくライブラリとしても使えるようにつくるので,src/mycat.cr
は API だけ書いて,bin/mycat.cr
にコマンド実行処理を書きます.そのほうがテストもしやすいです.
最終的にはこんな感じです.
--- mycat |--- .gitignore |--- LICENSE |--- README.md |--- .travis.yml |--- shard.yml |--- bin | |--- mycat.cr | |--- src | |--- mycat.cr | |--- mycat/version.cr | |--- spec | |--- spec_helper.cr | |--- mycat_spec.cr | |--- .gitignore
ドキュメントを書く
最初の時点で脳内で決まっている範囲で簡単なドキュメント(intro,コマンドの引数など)を README.md
に書きます.生成された README.md
はスニペットになっているので,GitHub のユーザ名などを自分で埋めます.
また,(なぜか)LICENSE が MIT で決め打ちになっているので,必要があれば修正します.
shard.yml
を書く
npm の package.json
と同じように,パッケージ情報を shard.yml
に書きます.これは半標準(いずれ本体に取り込まれる?)のパッケージマネージャ shards を使う前提で生成されています.
name: mycat version: 0.0.1 authors: - 名前 <mail@address> license: MIT
依存するパッケージが出てきた時はここに書きます.
スペックを書く
まずは mycat
ディレクトリでおもむろに crystal spec
と実行してみます.
次のように失敗すると思います.
F Failures: 1) Mycat works Failure/Error: false.should eq(true) expected: true got: false # ./spec/mycat_spec.cr:7 Finished in 0.55 milliseconds 1 examples, 1 failures, 0 errors, 0 pending Failed examples: crystal spec ./spec/mycat_spec.cr:6 # Mycat works
それも当然です.spec/mycat_spec.cr
は次のようになっています.
require "./spec_helper" describe Mycat do # TODO: Write tests it "works" do false.should eq(true) end end
ここで実装を考えながら必要なメソッドのテストを書きます.ファイル名を受け取って IO に出力に出すメソッド meow
を持ったクラス Cat
をつくることにするとこんな感じでしょうか.test.txt
は spec ファイルと同じディレクトリに適当につくります.__DIR__
はそのファイルの親ディレクトリを表す大域定数(みたいなもの)です.
require "./spec_helper" describe Mycat do describe Mycat::Cat do describe "meow" do it "writes the content of file to specified IO" do io = StringIO.new c = Mycat::Cat.new io c.meow "#{__DIR__}/test.txt" io.to_s.should eq("foo\nbar\nbaz\n") end end end end
まだ実装が無いのでもちろんこれも失敗します.
実装を書く
require "./mycat/*" module Mycat # TODO Put your code here end
最初はこうなっていると思います.
require "./mycat/*" module Mycat class Cat def initialize(@mouse) end def meow(file_name) File.open file_name do |f| @mouse << f.read end end end end
できました.受け取った IO にファイルの中身を吐くだけなので簡単ですね.コンストラクタの引数のように,直接インスタンス変数名を指定してその変数に代入することができます.(コンストラクタ以外のメソッドでも使えます)
試しにビルドしてみましょう.crystal build
コマンドを使います.
$ crystal build src/mycat.cr
問題が無ければ,先ほどと同じ方法で spec を実行します.今度はテストが通ると思います.
コマンド実行処理をつくる
bin/mycat.cr
を実装します.
require "../src/mycat" require "option_parser" begin OptionParser.parse! do |parser| parser.banner = "Usage: mycat file_name" parser.on("-v", "--version", "Show version") { puts Mycat::VERSION; exit 0 } parser.on("-h", "--help", "Show this help") { puts parser; exit 0 } end cat = Mycat::Cat.new STDOUT ARGV.first?.try do |arg| cat.meow arg end rescue e STDERR.puts e exit 1 end
オプションの実装は Ruby でもおなじみの OptionParser
が使えます.今回はバージョンとヘルプだけをパースしています.
Enumerable#first?
はリストの最初の要素があればそれを返し,無ければ nil
を返します.Object#try
は Ruby ではお馴染みの,nil
で無い値で実行すればブロック部分が実行されるメソッドです.例えばここで間違えて cat.meow ARGV.first?
など書くとコンパイラエラーになってくれます.(コンパイラは Cat#meow(f: String)
に推論したのに String?
な値を渡しているため)
試しに実行してみましょう.crystal
コマンドはデフォルトでファイルを直接実行する(実際はコンパイルして一時ファイルを生成しそれを実行する)ので,下記のようにします.crystal
コマンドで実行されるプログラム側に引数を与えるには --
オプションの後に置く必要があります.
$ crystal ./bin/mycat.cr -- README.md $ crystal ./bin/mycat.cr -- -v $ crystal ./bin/mycat.cr -- --help
正しく動いていることが確認できたらリリースビルドで最適化されたバイナリを作成します.
$ crystal build --release ./bin/mycat.cr
カレントディレクトに mycat
というバイナリファイルができていると思うので,それをパスの通ったところに置くなりします.
Travis での CI
デフォルトで .travis.yml
が生成されますが,中身はほぼ空っぽです.
次のようにテストスクリプトを書きます.
Crystal の CI 環境は Crystal コミュニティによって提供されているので,こちらで頑張って用意する必要はありません.
language: crystal script: - crystal spec - crystal build ./bin/mycat.cr
まとめ
プログラミング言語 Crystal でコマンドラインツールを書く自分なりの手順をまとめました.まだ開発途中の言語なのでこれを使ってでかいツールを…とはなかなかいかないかもしれませんが,僕みたいに Ruby 好きな人は書いてみるとハードルも低く新鮮で楽しいかもしれません.
GitHub のトレンドリポジトリを見逃さない,Trendy をつくりました
僕が1日に1回ぐらいの頻度で見ているページの中の1つに GitHub の Trending repositories のページがあります.このページには言語ごとに日毎・週毎・月毎の単位で GitHub 上で人気のリポジトリがランキング形式で表示されます.
話題になっているライブラリやソフトウェアの一次ソースとして便利なのですが,微妙にアクセスが悪い位置にあり,言語ごとにしか見られません.また,ランキングには常に人気な「常連」リポジトリが多々いるので,新しく話題になっているリポジトリはその中に埋もれがちになってしまいます.
そこで,今回はこれらの問題を解決すべく,GitHub のトレンドクライアント Trendy を Electron ベースでつくりました.
Trendy - Menubar App to Keep You in the Trend
Trendy は GitHub のトレンドページを監視し,トレンドに新しいリポジトリが現れた時に通知したり,過去にトレンドに挙がったリポジトリを管理して後から検索したりできます.メニューバーの中に常駐するので,いつでもメニューバーからアクセスできます.
README を書いたり,今回は勉強もかねて Landing Page 書いたりしましたが,せっかくなので簡単に紹介します.
インストール
GitHub のリリースページ からお使いの OS に合うファイルをダウンロードできます.OS X は Trendy.app
が入っているので,それをそのまま使ってください.Windows の場合は trendy.exe
を,Linux の場合は trendy
という executable があるのでそれらをそれぞれ使ってください.インストールは不要なので好きな場所に置いて OK です.
ファイルを実行すると,Trendy のメニューバーアイコンが出ると同時に初回は監視したいトレンドを選択するウィンドウが立ち上がります.
上にあるフォームでインクリメンタル検索ができるので,気になる言語をいくつか選んだら Go! ボタンを押します.
Trendy が各トレンドページをスクレイピングして GitHub API を使ってリポジトリの情報を収集した後,メニューバーの中にあるアイコンが赤くなると思います. それ以降は1時間に1回のペースでトレンドページを見に行って新着リポジトリを探します.
使い方
'New' タブ
新着のリポジトリが表示されます.チェック済みのリポジトリはマウスオーバーすると左の方に表示されるでかいチェックマークボタンを押すと 'New' タブから削除されます.
'Current' タブ
現在のトレンドページのランキングです.
'All' タブ
Trendy が今まで出てきた収集したすべてのリポジトリ情報一覧です.
通知機能
Trendy は1時間に1度スクレイピングを行い,新しいリポジトリを見つけた時はメニューバーのアイコンの色を変えて通知します.緊急性のある通知ではないので,この程度の目立たなさの通知が一番気に入っています.
通知時 | 通常時 |
---|---|
組み込みブラウザ
リポジトリやリポジトリ作者のページなどへのリンクをクリックすると,外部のブラウザではなく,モバイルアプリのようにその場でブラウザを開きます.開くページは,ウィンドウが小さい場合はモバイルページ,大きい場合は PC ページになります.
サイドメニュー
右上の3本線ボタンを押すとサイドメニューが開きます.メニュー内では次の3つの機能が使えます.
- 検索フォームからリポジトリを検索できます.検索ワードを入れて Enter を押すと,現在のタブ内のリポジトリが絞り込み検索されます.
- 現在のタブをトレンドでフィルタリングすることができます.普段はすべてのトレンドページの結果がタブ内に表示されています.
- 最下段では設定ファイルを開いたり,手動で更新したり,アプリを終了したりできます.
メニューウィンドウとノーマルウィンドウ
ここまで Trendy はメニューバーに統合されたアプリだと紹介しましたが,普通のウィンドウで使うことも出来ます.
Windows ではタスクバーが下に無いとメニューウィンドウの位置がおかしくなってしまうので,デフォルトでこの設定になっています.
後述する config.json
を書き換えるとメニューウィンドウとノーマルウィンドウを切り替えられます.
ノーマルウィンドウ時は横幅が充分あることを想定して,サイドメニューは常に表示するようになっています.
GitHub API リミット
スクレイピングは対象ページの構成に依存して追従しないといけないため,極力スクレイピングに頼らないような設計になっています.Trendy では最低限の情報(リポジトリ名と作者名)だけスクレイピングで収集し,リポジトリのその他の情報は GitHub API を使っています.
ログインしていない場合は1時間に60回しか API を呼べないため,スクレイピング対象が少し多いとこの制限にひっかかってしまいます.その場合はウィンドウ内に赤いアラートダイアログが表示され,ログインを促されます.
'login' リンクをクリックするとログインウィンドウが開くので,ログイン情報を入力してログインしてください.2段階認証にも対応しています.これで API リミットが 60 から 5000 まで増えるので,上記エラーは出なくなるはずです.
カスタマイズ
上記の2タイプのウィンドウを含め,いくつかの項目がカスタマイズ可能です.詳しくは README に書きました.
Electron + TypeScript + React
TypeScript 1.6 が beta になった頃につくりはじめたので,今回は renderer プロセス側も main プロセス側も両方 TypeScript でつくってみました.使ってみたうえでの感想は Qiita のほうにも簡単に書きましたが,
Qiita - TypeScript1.6 + React 書いてみてハマったポイントとか
メソッド名やキー名の間違いなど,うっかりミスを大体拾ってくれるので概ね良い感じです.
まとめ
GitHub のトレンドページクライアント Trendy をつくりました.
元は 最強のTwitterクライアント戦争 というのでぼちぼちアプリをつくっていて,ちょっと息抜きに前々から困っていたトレンドページを改善するか程度の気持ちだったのですが,なんだかんだで時間がかかってしまいました…
GitHub のアレコレを補完する github-complete.vim をつくりました
GitHub のユーザ名やリポジトリ名,絵文字,リンクURLを GitHub API を使って補完する github-complete.vim をつくりました.
https://github.com/rhysd/github-complete.vim
Vim では Markdown 編集中のオムニ補完は HTML のものになっていて使わないので,どうせなら GitHub のアレコレが補完できれば便利だなと思ってつくってみました. 5カ月前ぐらいに8割方できていたんですが,最後のリンク URL 補完を完成させずに放置してしまっていたので,yokohama.vim #6 で完成させました.使えるのは下記の5種類の補完です.
- 絵文字補完
- ユーザ名補完
- リポジトリ名補完
- issue 番号補完
- リンクURL補完
markdown
および gitcommit
ファイルタイプでのオムニ補完として実装されていますので,Markdown なファイルや git のコミットメッセージの編集中に <C-x><C-o>
(Ctrl + x → Ctrl + o)で補完を発動できます.
また,一応 neocomplete.vim のソースも同梱されており,g:github_complete_enable_neocomplete
に 1
をセットすることで使えますが,自動補完で大量の API 呼び出しが発生すると思うので呼び出し制限にひっかかる恐れがあります.
せっかくなので各補完を簡単に紹介します.
絵文字補完
":" の後にカーソルがある時に補完できます.GitHub の絵文字が補完できます.スクリーンショットでは絵文字が表示できていますが,これは Terminal.app が対応しているからで,それ以外の環境では表示できません.(現状 Vim の制限.gVim はフォント設定で何とかなる?)
ただ,これだけだと OS X + ターミナルでないと補完候補が分かりにくかったので,日本語での説明を頑張って追加しました.g:github_complete_emoji_japanese_workaround
を 1 にセットすると,このように候補に日本語の説明が付きます.
ユーザ名補完
"@何か" か "github.com/何か" の後ろにカーソルがある時に "何か" をユーザ名として検索して補完できます.僕のようにツイッターと GitHub でユーザ名が違う場合など,ユーザ名思い出せない時に便利です.
リポジトリ名補完
"ユーザ名/何か" の後ろにカーソルがある時に "何か" をリポジトリ名として補完します.上のユーザ名と合わせて,GitHub の URL をユーザ名,リポジトリ名と順に補完することでリポジトリ名とか覚えていなくてもブラウザを開いて確認する必要が無くなります.
Issue 番号補完
"#番号" で参照できる issue/pull request 番号ですが,活発なリポジトリだとよく忘れます.そこで,"#" の後ろにカーソルがある時に補完できるようにしてみました.番号と一緒に issue のタイトルも表示されます. コミットから issue を閉じるためにタイトルに Close #1 とか書くことも多いかと思いますが,issue 番号を忘れてもその場で補完できるようになります.
リンクURL補完
[何か](
の後ろにカーソルがあるときに "何か" で GitHub を検索した結果の URL を候補として補完できます.
記事内やコミットメッセージで使ったライブラリや紹介したいツールなどのリポジトリへのリンクを書く時に,リンクタイトルにそのライブラリやツールの名前を書いておけば URL が分からなくても補完できます(特に作者名はよく忘れますね…).
Vim 関連の記事を書く時に [プラグイン名](プラグイン URL)
とかよく使うので,そこで重宝しています.
まとめ
GitHub のアレコレを補完できる github-complete.vim をつくってみました.
今この記事を書いている時もリンク補完などを使っていますが,補完速度も1〜2秒程度で手動補完で使う分にはかなり良い感じです.もしよろしければお試しください.