mixi engineer blog

*** 引っ越しました。最新の情報はこちら → https://medium.com/mixi-developers *** ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

manを書こう

チャリンコ通勤もそろそろ寒くなってきたと感じる今日この頃のmikioです。今回は、manの書き方について述べてみます。

manとは

UNIX系のフリーソフトウェア/オープンソースソフトウェアを世に出す場合、その使い方を示した「man」形式のマニュアルを付属させるのが一般的です。端末上で「man hoge」とやると「hoge」のマニュアルを見ることができるので大変便利で、UNIXを使っている方は日々お世話になっている機構だと思います。ちなみに「man -t hoge」とやるとPostScript形式のデータが出力されるので印刷して見ることもできるんです。

そういうわけでUNIXのソフトウェアはmanをつけて配布するのがあたりまえ的になっていて、つけてないと「なんでやねん」とお叱りをうけることもあります。Debian/GNU Linuxでは、パッケージに含まれる全てのコマンドには各々に対応するmanファイルをつけるべきであると決められているくらいです。

データ形式

manで閲覧するからman形式と便宜的に呼んでいるわけですが、正確にはroff(実際にはnroff/troff/groff)という文書整形コマンドのマクロパッケージ「an」ないし「andoc」がサポートするデータ形式です。roffファミリがマクロパッケージを読み込ませる時の引数が「-m」なので、実際には「-man」とか「-mandoc」とかいう指定になって、マニュアルなんだなとわかりますね。man形式の詳しい書式についてLinux上で「man 7 man」を実行すると説明を見ることができます(Web版)。

さて、このman形式、実際に書いてみるとわかりますが、すげー面倒です。原始的なマークアップ言語のひとつだそうですが、SGML/HTML/XMLに比べるとものすごく書きにくく読みにくい形式なんです。最も簡単な例を見てみましょう。

.TH "HADOUKEN" 1 "2007-10-24" "Man Page" "Utility Commands"

.SH NAME
hadouken \\- launch wave propagation of fighting spilit

.SH SYNOPSIS
.B hadouken
[\\fB\\-f\\fR|\\fB-d\\fR] \\fItarget\\fR [\\fIlevel\\fR]

.SH DESCRIPTION
.PP
Launch wave propagation of fighting spilit toward an enemy and beat him.
\\fIenemy\\fR specifies the intended target.
\\fIlevel\\fR specifies the strength level from 1 (soft) to 3 (hadd).
\\fBGouki\\fR can do this blow in the air.

.SH OPTIONS
.TP
.B \\-f
switch to burning mode and daunt the ememy
.TP
.B \\-d
switch to electricity mode and fall the enemy unconscious

.SH SEE ALSO
.BR syouryuuken (1),
.BR tatsumakisenpuukyaku (1)

これを「hadouken.1」とかいう名前のファイルとして保存して、「less hadouken.1」とすると整形済みのページを見ることができます。

hadouken.png

「.TH」でメタデータを指定して、「.SH」で見出しを記述して、「.PP」でパラグラフの開始を指示するとかはまだいいでしょう。面倒なのは、ボールドにするために行を分けて「.B」するのとか、行中でボールドやイタリックにするためにエスケープシーケンス「\fB」「\fI」を使って、さらに元に戻すのに「\fR」とかやらねばならないことです。例えば、コマンドラインの書式として「tcucodec mime [-d] [-en name] [-q] [file]」を表そうとすると、「\fBtcucodec mime \fR[\fB\-d\fR]\fB \fR[\fB\-en \fIname\fB\fR]\fB \fR[\fB\-q\fR]\fB \fR[\fB\fIfile\fB\fR]\fB\fR」とか書かなくちゃならないんです。こんなのいちいち手で入力してたら人生終わってしまいますよね。

HTMLからの変換

Tokyo CabinetのマニュアルはHTMLで記述しているのですが、上記の理由から同じ内容のman形式のデータもパッケージに入れないといけないので、気が進まないながらも「.TH」とか「.B」とか書いていた私です。しかし、いけどもいけどもルーチンワーク。こんなの自動化しなきゃ嘘ですよね。そもそもHTMLと同じ内容なんだからイチイチ手で書き直すなんて愚の骨頂じゃないですか。

ということで、コンバーターを書きました。上場企業なのにPHPを使わないで恐縮ですが、今回は小粒で便利なテキスト処理言語であるAWKを用います。

#! /bin/awk -f

function strip(text){
  gsub("^ *<[a-zA-Z0-9]*[^>]*>", "", text)
  gsub("</[a-zA-Z0-9]*> *$", "", text)
  return text
}

function unescape(text){
  gsub("&lt;", "<", text)
  gsub("&gt;", ">", text)
  gsub("&quot;", "\\"", text)
  gsub("&amp;", "\\\\&", text)
  gsub("-", "\\\\-", text)
  return text
}

BEGIN {
  date = strftime("%Y-%m-%d")
  printf(".TH \\"%s\\" %d \\"%s\\" \\"%s\\" \\"%s\\"\\n\\n", "INTRO", 3, date, "Man Page", "Tokyo Cabinet")
}

/ *<h[1-3] *[^>]*>.*<\\/h[1-3]> *$/ {
  text = $0
  text = strip(text)
  text = unescape(text)
  text = toupper(text)
  printf("\\n")
  printf(".SH %s\\n", text)
}

/ *<p *[^>]*>.*<\\/p> *$/ {
  text = $0
  text = strip(text)
  text = gensub("<code *[^>]*>([^<]*)</code>", "\\\\\\\\fB\\\\1\\\\\\\\fR", "g", text)
  text = gensub("<var *[^>]*>([^<]*)</var>", "\\\\\\\\fI\\\\1\\\\\\\\fR", "g", text)
  gsub("<[^>]*>", "", text)
  text = unescape(text)
  printf(".PP\\n")
  printf("%s\\n", text)
}

/ *<dl *[^>]*> *$/ {
  printf(".PP\\n")
  printf(".RS\\n")
}
/ *<\\/dl> *$/ {
  printf(".RE\\n")
}
/ *<dt *[^>]*>.*<\\/dt> *$/ {
  text = $0
  text = strip(text)
  text = gensub("<var *[^>]*>([^<]*)</var>", "\\\\\\\\fI\\\\1\\\\\\\\fB", "g", text)
  gsub("<[^>]*>", "", text)
  gsub("[\\\\||\\\\[|\\\\]]", "\\\\fR&\\\\fB", text)
  text = unescape(text)
  printf(".br\\n")
  printf("\\\\fB%s\\\\fR\\n", text)
}
/ *<dd *[^>]*>.*<\\/dd> *$/ {
  text = $0
  text = strip(text)
  text = gensub("<code *[^>]*>([^<]*)</code>", "\\\\\\\\fB\\\\1\\\\\\\\fR", "g", text)
  text = gensub("<var *[^>]*>([^<]*)</var>", "\\\\\\\\fI\\\\1\\\\\\\\fR", "g", text)
  gsub("<[^>]*>", "", text)
  text = unescape(text)
  printf(".RS\\n")
  printf("%s\\n", text)
  printf(".RE\\n")
}

/ *<ul *[^>]*> *$/ {
  printf(".PP\\n")
  printf(".RS\\n")
}
/ *<\\/ul> *$/ {
  printf(".RE\\n")
}
/ *<li *[^>]*>.*<\\/li> *$/ {
  text = $0
  text = strip(text)
  text = gensub("<code *[^>]*>([^<]*)</code>", "((\\\\\\\\fB\\\\1\\\\\\\\fR))", "g", text)
  text = gensub("<var *[^>]*>([^<]*)</var>", "[[\\\\\\\\fI\\\\1\\\\\\\\fR]]", "g", text)
  gsub("<[^>]*>", "", text)
  text = unescape(text)
  printf("%s\\n", text)
  printf(".br\\n")
}

END {
  printf("\\n")
  printf(".SH SEE ALSO\\n")
  printf(".PP\\n")
  printf(".BR tcutil (3),\\n")
  printf(".BR tchdb (3),\\n")
  printf(".BR tcbdb (3)\\n")
  printf(".PP\\n")
  printf("Please see\\n")
  printf(".I http://tokyocabinet.sourceforge.net/spex-en.html\\n")
  printf("for detail.\\n")
}

もちろん、こんな小さいプログラムには一般的なHTMLを完全にman形式に直す能力はありません。しかし、手間を減らすのが目的なのですから、自分のユースケースのサブセットを処理できればよいのです(時に「完璧」なものを作りたくなって湯水の如く時間を使ってしまうこともありますが...)。今回の場合は、Tokyo Cabinetのマニュアルに表れる、h1, h2, h3, p, dl, dt, dl, ul, li, code, var だけ処理できればOKです。

まとめ

そんなこんなで、man形式のマニュアルが無事に完成しました。UNIX系のソフトウェアの公開を継続して行っていくならば、manを書くスキルがおいおい必要になってくると思います。ただ、手で書くのはめちゃくちゃ面倒なので、HTMLやその他の形式で書いてから、プログラムで自動変換することになるでしょう。その際に楽するためには、論理的な構造だけはmanで期待されるものに近いようにしておくとよいでしょう。なお、変換にはdoxygenやpod2manなどの既存のツールを使うというのもよい考えです。

そうそう、Tokyo Cabinet 1.0.0がついにリリースされました。B+木やトランザクションも実装されて、QDBMに相当する機能を全てサポートして、性能はQDBMを完全に凌駕するようになりました。DBMに興味のある方にはぜひ使っていただきたいと思います。