最近Atomでコードを書く時間を増やしている

最近思うところあってAtomでコードを書く時間を増やしている。

といっても普段書くPerlはこれまで通りVimで、趣味でTypeScriptを書く時にAtomを使うようにした。

Vimの使い勝手に慣れ切ったつもりだけど、昨年一年間はIntelliJScalaを書く時間が多かった。それが思いの外に馴染んだので、もしかしたらVim以外でもなんとかなるのでは? と思い、まずは趣味の時間からちょくちょく使いはじめることにした。

Vim設定をGitで管理してけっこう設定に凝っている。道具を自分の手に馴染むように変えるスタイル。

対してAtomで書く時は、あまりカスタマイズせず、キーボードショートカットも覚えずにGUIでぽちぽちすることにしている。

いままで設定を練ったVimよりできることは減ったり手間がかかるようになったはずだけれども、耐え難い感じはしない。

むしろGUIがリッチなので便利に思う時が多い。

その後、ENSIMEを試してみたところVimより安定して使えたのでScalaを書く時もAtomにしようかな。

grpcを見たメモ

クライアント (ブラウザ) とサーバーがやりとりするためにgrpcを使えないかなと思ってサンプルコードを見たりちょっと触ったメモ。

結論からいうと今回の用途には合わないので見送ったけど、後のために。

ブラウザでは使えない 😭

Can I use it in the browser?

Not yet. This is an area that's being actively explored and we welcome feedback and contributions. We are collecting names of people interested in early access program here. There is node.js support for server side JavaScript.

grpc / FAQ

メモ

  • protoc(1) でサービス定義からクライアント実装と、サーバー実装の雛形を生成する
  • サーバーは定義されたRPCインターフェースを実装するのと、listenさせるところまでは最低限書く必要がある
  • G SuiteのAPIはgrpc (Protobuf?) で提供されている

Scalaで書いたWebアプリケーションのレスポンスにデプロイされているGitコミットのSHA1を含める

WebアプリケーションのレスポンスヘッダにGitのsha1を含めておくと、デプロイに失敗していて古いバージョンだった (そのせいでおかしかった) ということに気がつきやすくなって多少便利。

Perl (PSGI) だと、 `git rev-parse HEAD > VERSION` などしておいて Plack::Middleware でファイルの内容を読んで `Plack::Util::header_set` と `Plack::Util::response_cb` で用が済むけれど、Scalaコンパイル時にいろいろできそうな気がしたので調べた。

sbt-buildinfoでビルド情報をcase objectに出力する

sbt-buildinfoというsbtプラグインを使うとプロジェクト名やバージョンなどの情報を含むcase objectのコードを生成してくれる。

Usageより:

lazy val root = (project in file(".")).
  enablePlugins(BuildInfoPlugin).
  settings(
    buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion),
    buildInfoPackage := "hello"
  )
GitHub - sbt/sbt-buildinfo: I know this because build.sbt knows this.

`build.sbt` にこう書いておくと `hello.BuildInfo` に `name` や `version` が埋められたcase objectを定義するコードが出力される。

sbt-gitでGitのコミット情報をプロジェクトのバージョン文字列にする

sbt-gitというsbtプラグインを使うとプロジェクトのバージョン文字列をGitのコミット情報から生成することができる。

`git describe` を使うようにするとか、自分でsha1を加工する (e.g. 先頭8文字だけを使う) ことができるので使い勝手がよい。

タグは使っていないのでsha1の先頭8文字を埋めるようにしている。

lazy val root = (project in file(".")).
  enablePlugins(GitVersioning).
  settings(
    git.formattedShaVersion := git.gitHeadCommit.value map { sha =>
      sha.substring(0, 8)
    }
  )

Playのレスポンスを加工してバージョンを含める

Action.async { implicit request =>
  val result = ???
  result.withHeaders("x-version" -> app.BuildInfo.version)
}

こういうかんじ。

筋トレティータイム

僕はコーヒーが苦手で、飲むと必ずお腹を壊すので避けるようにしている。

その代わりではないけれど、気分転換したいときには紅茶を淹れて飲むことが多い。

コーヒーと違って蒸らし時間があるので待ち時間がどうしても発生する。ティーバッグを淹れてそれを処分する関係上、自席にも戻りづらい。

そこで腹筋ローラーの出番となる。

もともと学生時代と比べて筋肉の衰えが気になりはじめたので買ってオフィスに送ったのだけれども、持って帰るのが面倒で置きっぱなしにしていた。

あるとき、ふと思い立って紅茶を蒸らしているあいだに腹筋ローラーをしてみたらちょうどいい時間だったので、これ幸いとしばらく続けている。

紅茶の蒸らし時間はだいたい2分くらいで、慣れていないうちは10回腹筋ローラーするだけでもそれくらいの時間がかかる。

数回続けると慣れて素早くできるようになってくるので、成長と共に蒸らし時間あたりの回数が増えていって、ティータイムと共に成長の実感が得られて思わぬ喜びがあった。

腹筋ローラー、1オフィスに1台オススメです。

デフォルトでHTTPレスポンスがキャッシュされるのはどんな場合か

HTTP APIのキャッシュ戦略を考えながら、ふとcache-controlなどを指定しなかったとき、デフォルトのキャッシュ戦略はどのように定義されているのか気になったので調べた。

Storing Responses in Caches

RFC7234のStoring Responses in Cachesにレスポンスをキャッシュしてもよい条件が述べられている。

A cache MUST NOT store a response to any request, unless:

  • The request method is understood by the cache and defined as being cacheable, and the response status code is understood by the cache, and
  • the "no-store" cache directive (see Section 5.2) does not appear in request or response header fields, and
  • the "private" response directive (see Section 5.2.2.6) does not appear in the response, if the cache is shared, and
  • the Authorization header field (see Section 4.2 of RFC7235) does not appear in the request, if the cache is shared, unless the response explicitly allows it (see Section 3.2), and
  • the response either:
    • contains an Expires header field (see Section 5.3), or
    • contains a max-age response directive (see Section 5.2.2.8), or
    • contains a s-maxage response directive (see Section 5.2.2.9) and the cache is shared, or
    • contains a Cache Control Extension (see Section 5.2.3) that allows it to be cached, or
    • has a status code that is defined as cacheable by default (see Section 4.2.2), or
    • contains a public response directive (see Section 5.2.2.5).
RFC 7234 - Hypertext Transfer Protocol (HTTP/1.1): Caching

(マークアップは筆者による)

Cacheable Methods

まず1つ目でHTTPメソッドがcacheableであるかとある。特に言及はないけどRFC7231で定義されている。

this specification defines GET, HEAD, and POST as cacheable, although the overwhelming majority of cache implementations only support GET and HEAD.

RFC 7231 - Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content

注意したいのはPOSTもcacheableである点。「ほとんどの実装は対応していないけどね」と但し書きされているが、明示しておくに越したことはなさそう。

a status code that is defined as cacheable by default

巡り巡ってRFC7231のSection 6.1で定義されている。

Responses with status codes that are defined as cacheable by default (e.g., 200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501 in this specification

RFC 7231 - Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content

501 Not Implementedはキャッシュ可能と定義されているので、5xxがキャッシュされないという認識でいると注意が必要かもしれない。

その他

range付きリクエストのレスポンスのキャッシュ戦略については別途述べられているが、必要としなかったので読んでいない。

結論

  • HTTPメソッドがGET, HEADもしくはPOSTの場合で、かつ特定のステータスコードの場合はキャッシュ可能と定義されている

まずどんなcache-controlが付けられそうか考えるだけでも、HTTP APIの特徴を掴めそうなのでこれからもやっていきたい。

nginx: 同じ configuration level で auth_basic off は指定できない

nginx の ngx_http_auth_basic_module を使って BASIC 認証をかけることができる。

ドキュメントを読むと明示的に `auth_basic off` を指定することで BASIC 認証を切ることができそうだったので試したけど、うまくいかないケースがある。

結論からいうと `auth_basic off` は上位のコンテキストから継承した設定を打ち消すためのもので、同じコンテキストで指定された `auth_basic` は打ち消せない。

実際のコード例:

# app.nginx.conf
server {
    server_name app.example.com;

    include "/path/to/config/auth.nginx.conf"; # 同じコンテキストで auth_basic が指定されている
    auth_basic off; # 祖先コンテキストではなく同じコンテキストなのでダメ
}

auth.nginx.conf:

auth_basic "Authentication Required";
auth_basic_user_file "/etc/nginx/conf/auth.htpasswd";

実際に config test を行うとエラーになる:

nginx: [emerg] "auth_basic" directive duplicate in app.nginx.conf:5
nginx: configuration file /etc/nginx/nginx.conf test failed

これはドキュメント中の以下の文章で示されている:

The special value off allows cancelling the effect of the auth_basic directive inherited from the previous configuration level.

http://nginx.org/en/docs/http/ngx_http_auth_basic_module.html#auth_basic

生活における夢を集めるTrelloのボードを作った

f:id:aereal:20170414214141p:plain

プライベートで締切のあるタスクや、忘れてはいけないタスクは To Do 管理ツールでリマインドしている。

それらのようなタスクは、誰かに依頼されるだとかして具体的なタスクとして直接インプットされる一方、日々の生活における夢はたとえば「石油を掘り当てたい」といった曖昧でふわっとしているし、なんなら実現可能性なんてものを考えていない。

ふとした瞬間に省みると、自分が細々としたルーチンを繰り返す生活の奴隷に思えることがあってげんなりする。

しかし自分はまだまだ生活に夢や希望を抱いていて、壮大とはいえぬものの少なくないそれらを支えにして生きているわけだから、目に見えるようにしておくと楽しいのではないかと思って夢を集める場所を作った。

それがタイトルにあるとおり Trello のボードでその名も「生活の夢」。

Trello にした深い理由はなく、たまたま楽しい気分で Trello を見ていたら思いついただけ。

ルールや運用なんてものは存在せず、ただ思いついたときに思いついたことを書いていくだけ。達成したら Done に移す。

達成したタスクを捨てたりせずただ Done と名付けられたリストへ移動するやり方は、カンバンに由来するもので、叶った・叶えた夢が集まってくるというところがとても楽しい。

最近は「テレビを買う」という夢を叶えた。「テレビを買う」ことがなぜ夢たりえたかというと、あると楽しいだろうけれど必要というわけではなく、そのわりに高いのでどうかと思っていたから。贅沢ともいう。

世の中においては理性的な振る舞いが求められるので、この Trello の前では自分の欲望に素直になろうと思います。

YAPC::Kansai 2017 OSAKAに参加した

YAPC::Kansai 2017 OSAKA

オーディエンスとして参加したので聞いたトークのメモなど。

Webアプリケーションのキャッシュ戦略とそのパターン (40min)

speakerdeck.com

主に Web アプリケーションの世界におけるキャッシュの話。目新しい話題ではないけれど、キャッシュは本質的にむずかしい話題だし、よくまとまった内容なのでこれからキャッシュの戦略を考える際の議論に役立ちそう。

「パターンに名前を与えると、名前を使って認知し議論できます」という話がよかった。確かにどれもよくあるパターンだけど、普段会話するときは「get_or_set 的なやつで……」みたいな表現をしがちだったので、名前をつけることだけでもとても意義のある内容だと思う。

後日 id:moznion さんと個人的に会話して Varnish の話をするなど、発表をもとに会話が生まれる技術カンファレンスの原体験みたいなことが起きて、そういう意味でもいいトークだったなーと思う。

いい話聞けたし、最近自分も Varnish を触っているので YAPC::Fukuoka 2017トークに応募しようかな、と思った。

Perl でわかる!サーバレス (20min)

www.slideshare.net

別のところ (社内だったかも?) でも「ServerlessってつまりCGIじゃん」みたいな話を聞いたので、静かに普及しつつある見解っぽい。

Microsoft Azure Functions のランタイムが公開されているところは後発ならではでよいなと思った。

利用しているBaaSが終了するときにすべきこと または Parse.com の終了と私たちの取り組み (20min)

speakerdeck.com

parse.com 終了は対岸の火事だったけど、このトークを聞きながら何度か自分のことのように肝が冷えることがあって、臨場感のあるトークだった。

撤退の作戦を考える際に DNS のレイヤからアプリケーションのレイヤまで検討できるだけのエンジニアがいるチームだからこそ切り抜けられたのかもなあ、と思った。小さいチームっぽいけど、いいエンジニアが集まっているチームという印象を受けた。

あとこういう内容だとともすればヘイトばかり撒き散らしがちだけど、ケーススタディとして具体性があって役立ちそうだし、parse.com へのリスペクトを欠かしていなくて建設的で聞いていて安心感があった。

カヤックのゲーム開発・運用の「今」 力技と効率化の先に我々が目にしたものとは (40min)

speakerdeck.com

Google スプレッドシートプログラマブルであるという意味でエンジニアにも受け入れられていて、良くも悪くも「エクセル」はもはや表計算ソフトではなくて「エクセルなんだなあ……とか思った。

イベントでものすごいスパイクがやってくるとか、カラムにサイズの大きなデータを入れていて転送量が逼迫するとか、心当たりのあるできごとが多くて他山の石感があった。

マスターデータとコードでは、コンフリクト解決の方法や頻度が違うので別リポジトリにしているのはなるほどってかんじ。

はてなシステムの考古学 (40min)

speakerdeck.com

15年の歴史をご紹介という内容。

こうして俯瞰してみるとコンウェイの法則のお手本みたいだなーと思った。人が増えてきて効率化のために Ridge のようなフレームワークが生まれ、さらに増えて人の多様性が増すと実装としてのフレームワークが薄まる揺り戻しが起きる、というような。

感想

気付いたときには懇親会のチケットが売り切れていて、それだけが個人的に残念であったものの、イベントとしてかなり濃い Perl トークが多くて楽しかった。

次回は福岡で、温故知新にかけて未来をテーマとのこと。福岡観光したいしトーク応募考えます。

Git であるディレクトリ以下に最近追加されたファイルを見る

git log \
  --grep 'Revert' --invert-grep \ # revert コミットを除く
  --diff-filter=A \ # 新規追加のみ
  --format='' --name-only \
  -- lib/Plack/Middleware

lib/Plack/Middleware/ 以下に最近ファイルを新規追加したコミットの一覧:

lib/Plack/Middleware/IIS7KeepAliveFix.pm
lib/Plack/Middleware/IIS6ScriptNameFix.pm
lib/Plack/Middleware/LighttpdScriptNameFix.pm
lib/Plack/Middleware/Head.pm
lib/Plack/Middleware/Recursive.pm
lib/Plack/Middleware/HTTPExceptions.pm
lib/Plack/Middleware/BufferedStreaming.pm
lib/Plack/Middleware/Dechunk.pm
lib/Plack/Middleware/Log4perl.pm
lib/Plack/Middleware/LogDispatch.pm

Vue.js で分割払いの手数料をわかりやすく表示するページを作った

Card Payment

リポジトリ: playground/card-payment at gh-pages · aereal/playground · GitHub

実質年率って意味わからないし計算がめんどうなので、金額と支払い回数を入れると総支払い額と月々の支払い額を表示するページを作った。

普段自分が使うブラウザで動けばいいので arrow function を生で使ってままで、トランスパイラを使って ES5 に変換していない。

Vue.js はライブラリを読み込んでさっと使いはじめられてよい。

UIKit という CSS ライブラリを試してみた。Bootstrap と同じくらいの範囲をカバーしつつも、その名の通りフォームとか操作するためのモジュールがたくさんある。

クラス属性を使ったり `uk-grid` のようなカスタム属性を使ったりと節操がない印象。

分割払いしまくりたいわけではなく、むしろカジュアルに手数料の高さなどを確認して戒めようというもの。

……X-T2 ほしい。

Go の text/scanner で改行をトークナイズしたいときは

Go の text/scanner はレキサを書く面倒な仕事の大半をいいかんじにしてくれて本質的なコードに取り組みやすくとても便利。

しかしはてな記法Markdown のように、行全体が単なるテキストではなくいくつかのパターンからなる文法のレキサを書くときは、パターン中に明示的に改行を含めたい。

どういうことかというと:

line: inlines CR

みたいに書きたい。

しかし scanner.Scan が改行を含む空白文字をスキップしてしまうので改行をトークナイズする、ということができない。

結論からいうと Scanner.ScanScanner.Whitespace というフラグを見て空白文字かどうか判断するので、このフラグを変えて改行は空白文字ではないと教えてあげるとよい。

(よく読めば Scanner.Whitespace にコメントで説明が書いてあった)

実際のコード:

Support inline HTTP annotations by aereal · Pull Request #2 · aereal/go-text-hatena · GitHubn

WANDRD Prvke Pack を買った - カメラバッグ考2017

PRVKE Pack というバックパックを買ったのでそれの紹介と、バックパックを選ぶにあたっていろいろ比較検討したメモについて書いておく。

新たなバッグに求めること

  • 素早くカメラにアクセスできること (サイドアクセス)
  • フルサイズ DSLR とレンズが入ること
  • 見た目

昨年、フルサイズの PENTAX K-1 に買い替えたうえ、さらに DFA☆ 70-200mm F2.8 という望遠ズームレンズも買い足した。

望遠ズームは大きいし重いので、これに加えていくつかのレンズを入れて持ち歩けるだけの容量のあるバッグを欲しくなったというのが、そもそものきっかけ。

機材が増えて多少重くなっても体に負担がかからないようバックパックを求める一方、レンズ交換が手間にならないようカメラやレンズの気室が分かれていることも条件とした。

他に検討したバックパック

Google Spreadsheet に諸元などをまとめて比較し、PRVKE Pack に加えて以下を最後まで検討した:

  • Chrome Niko Pack
  • ZKIN Raw Yeti
  • Thule Covert DSLR Rolltop Backpack

Chrome Niko Pack は要件をほとんど満たしていたものの容量が少なかった。 24L くらいで、実物を見るとかなり小さい印象だった。

ZKIN Raw Yeti は多少重いこと以外ほとんどよかったけれどいかんせん高すぎる。 ヨドバシで7万くらいして、レンズ買えるじゃん……と思って候補から落ちた。

Thule のバッグはだいたいよかったけれど見た目がやぼったくて実物を見るとあまり欲しくなくなった。

WANDRD PRVKE Pack

カメラと交換レンズを入れて撮りに出かける時のためのバックパックとして PRVKE Pack を選んで買った。

1ヶ月半くらい比較検討してみて求める要素をすべて持っていたので購入に至った。

Kickstarter で資金を集めながらバッグを作っているアメリカの小さなメーカーで個人輸入するしかなく、実物を手に取る機会がなかったのは若干不安だった。 その分、レビュー記事や YouTube の動画を通してサイズ感を念入りにチェックした。

ただほとんどのレビューは自分と比べると大柄な男性であることが多くて背負った印象はあまり当てにならなかった。 その点では SLRLounge のレビューに載っている写真は、レビュアーが女性だったので身長は近しく、レビュー本文も充実していてだいぶ有益だった。

価格は270米ドルで、PayPal 経由で決済した。当時のレートでは33,996円になった。 他に検討していた候補より少し高かったけれど、他はどれも求めるところにあと一歩足りない印象だったのでほとんど選択肢はなくなっていた。

気に入っているところ

  • サイドからカメラ用気室にアクセスできる
  • 手持ちのフルサイズと交換レンズを入れられるだけの容量がある
  • 加えて着替えなども入れられる容量がある
  • 荷室に背中側からアクセスでき荷造りしやすい
  • 見た目

公式では目安としてフルサイズの DSLR とズームレンズが2〜3本入るよと言っており、実際にそれくらいのサイズ感だった。 今の手持ちレンズで大きいものは DFA☆ 70-200mm F2.8 と DFA 15-30mm F2.8 で問題なく収まる。

一般にバックパックは中身を取り出すのに手間がかかるけれどもレンズ交換に手間はかけたくないのでサイドアクセスができると便利。

さらに飛行機に乗るときはなるべく受託手荷物を無くしたいのでバッグひとつで搭乗したい。

そうするとカメラやレンズに加えて着替えなども収められるだけの容量がほしい。 とはいえ数泊程度で、かつ下着や肌着くらいで済ませるので大した量ではないけれど。

clamshell-opening

あと荷室に背中側からアクセスできるためカメラやレンズを荷造りするときに楽。

いまいちなところ

だいたい満足しているけれど、強いて挙げると大きさと重さはちょっと気になる。

とはいえ普段使い用にはこれまで使っていたバッグも使うし、もっぱら旅行やレンズを持ち出したい時用のつもりなので、現実的には許容できる。

あと価格がちょっと高かった。2万円代だったらなあ、と思うけれどもう買ってしまったのでどうでもよくなった。

セットアップスクリプトでは必ず rbenv exec/rbenv which したほうがいい理由

簡単に言うと、rbenv の shims はシェルの `PATH` 探索とは独立して実行ファイルを探索しているため。

以下、単に rbenv としているけれどクローンである plenv なども同様と考えてよい。

shims とは

rbenv の README が詳しい。

Shims are lightweight executables that simply pass your command along to rbenv. So with rbenv installed, when you run, say, rake, your operating system will do the following:

  • Search your PATH for an executable file named rake
  • Find the rbenv shim named rake at the beginning of your PATH
  • Run the shim named rake, which in turn passes the command along to rbenv
GitHub - rbenv/rbenv: Groom your app’s Ruby environment

シェルは shims だけを探索する。

shims を実行すると rbenv によって Ruby のバージョンごとの実行ファイルが探索され、見つかった実行ファイルを `exec` する。

セットアップスクリプトの例

アプリケーションのセットアップを自動化するために以下のようなスクリプトが添えられていることが多いと思う:

#!/bin/bash

set -e

which bundle || gem install bundler
bundle install

Bundler がなかったら入れて、 `bundle install` で依存ライブラリを入れて終わり。

`which(1)` が成功するけど `rbenv which` が失敗する例

そんなことがあるのかと思うけれど、shims のライフサイクルを考えるとありうる。

rbenv で管理している Ruby のいずれかで Bundler をインストールした時点で `bundle(1)` の shim が作られる。

一方で、各バージョンごとの実行ファイルの実体は、各バージョンにおいてインストール (gem install) しないと存在しない。

つまり以下のようになる:

rbenv shell 2.0.0
rbenv exec gem install bundler
which bundle # => ~/.rbenv/shims/bundle: この時点で shim が作られる
rbenv which bundle # => ~/.rbenv/versions/2.0.0/bin/bundle: 実体も作られる

rbenv shell 2.4.0
which bundle # => ~/.rbenv/shims/bundle: 既に 2.0.0 でインストールし shim は作られている
rbenv which bundle # => (fail): 2.4.0 ではまだインストールしていないので存在しない!

rbenv exec gem install bundler
which bundle # => ~/.rbenv/shims/bundle: 既に 2.0.0 でインストールし shim は作られている
rbenv which bundle # => ~/.rbenv/versions/2.4.0/bin/bundle: めでたく 2.4.0 に対応する実体もインストールされた

先に紹介したセットアップスクリプトは `which bundle` で shim が探索されて見つかってしまうので `gem install bundler` が実行されないが、そのアプリケーションで使うバージョンに対応する実行ファイルの実体は存在しないので `bundle install` が失敗する、ということが起きてしまう。

そこで rbenv exec/rbenv which

どうすればいいかというと、タイトルにあるとおり `rbenv exec` / `rbenv which` を使うとよい。

これらは基本的に `exec(1)` `which(1)` の shims を考慮するバージョンと考えてよいので単純にコマンドに前置するだけで対応はほぼ完了する。

まとめ

  • rbenv ないし rbenv のクローンを使っているときは `rbenv exec` `rbenv which` を使う

Perl で memcached を使うときは Cache::Memcached::Fast::Safe がよさそう

memcached はコマンドインジェクションの機会が主に2つあって、1つはプロトコルで定められたキー長250文字を越えた場合と、キーに改行文字を含む場合。

詳しくは: 第2回 memcachedのセキュリティと脆弱性:memcachedの活用と運用 実践編|gihyo.jp … 技術評論社

今やっているプロジェクトでは Cache::Memcached::Fast を使っているけど、安全なキーの構築は Cache::Memcached::Fast を呼び出すレイヤでそれぞれ行うようになっている。

キー長の制約を知らなくて250文字以上のキーを使おうとしてうまく動かなくてハマったし、そもそもよく知られたセキュリティリスクがあるならライブラリレベルで対策されているとよさそう。

ということで調べたら Cache::Memcached::Fast::Safe というのがあったのでこれがよさそう。

  • Cache::Memcached::Fast のサブクラスなので同じように使える
  • キーはデフォルトでサニタイズされる
  • ついでに get_or_set という便利なやつが生えている

markdown-toc: Go で Markdown の見出しをパースして目次を作る

github.com

Go でテキスト処理をする練習のために書いてみた。

mdtoc - The markdown ToC generator - ウェブログ - Hail2u.net が元ネタで再実装というかんじ。

小難しいことをやるとどうしても泥臭くなるけど、普段書いている高水準なコードはこれくらい泥臭くてともすれば効率が悪いんだろうな〜、ということを Go を書くたびに思っている気がする。