2009/04/23

LaTeXでSubversionのキーワード展開を使うパッケージ

LaTeX で Subversion のメタ情報(つまり、$Id$ とかのキーワード展開)を使いたい。

Subversion におけるキーワード展開について基本をまとめると、たぶんこんな感じ。
  • 各ファイルのsvn:keywords属性を展開したいキーワードに設定する。
  • 原稿ファイル中にある $Id: ... $ などが展開されるのは、svn checkout や svn update でリポジトリの変更を取ってきた時点。
困っちゃうのは、ファイルごとにしかメタ情報を使えないところ。LaTeX で原稿を複数のファイルに分けて執筆している場合に、1ページめに原稿全体としての最新リビジョンを出力するようなことができない。1ページめをコンパイルする時点では、そのページの元になっている *.tex ファイルの $Id: ... $ しか展開されなからだ。後ろのほうのファイルだけが更新されたら、その新しいリビジョン番号には追随できなくなってしまう。
この悩みを解決する LaTeX のパッケージがいくつかある。svn-multi が新しいめで高機能そうなんだけど、今回は svninfo を使うことにした。以下は備忘録。

book.tex から zenhan.tex と kouhan.tex を include していて、Id キーワードの情報が使いたいとする。
% プリアンブルいろいろ
\begin{document}
\include{zenhan}
\include{kouhan}
\end{document}
まずは svninfo パッケージを利用できるようにする。\usepackage{svninfo} は \begin{document} の直前に置かないといけないようだ。
% プリアンブルいろいろ
\usepackage{svninfo}
\begin{document}
\include{zenhan}
\include{kouhan}
\end{document}
つぎに以下のような行を zenhan.tex と kouhan.tex に追記する。最後の $ のあとにスペース必須。 "file rev YYYY-MM-DD hh:mm:ss owner" の部分は、現在のファイルの情報(svn log -rHEAD zenhan.tex とかで得られる情報)か何かを手書きしておく。
\svnInfo $Id: file rev YYYY-MM-DD hh:mm:ss owner $ ← スペースあり
忘れずに svn:keywords を設定してコミット。
> svn propset svn:keywords "Id" *.tex
> svn commit -m"added svnInfo Id"
準備はこれでOK。この情報を LaTeX で出力するには、\svnId とか \svnInfoRevision といった svninfo のコマンドを使う。
複数のファイルにある $Id: ... $ を全部チェックして最新のリビジョン番号をとってきてくれる \svnInfoMaxRevision というコマンドもある。この \svnInfoMaxRevision をzenhan.tex に書いておけば、最新のリビジョンで更新されたのが kouhan.tex だけでも、その最新のリビジョン番号に展開される。たとえば、zenhan.tex に対する最後の変更がリビジョン 1000 で、kouhan.tex だけが 1001 で変更されたとすると、どんなに svn up しても zenhan.tex にある $Id: ... $ の部分はリビジョン 1000 に相当する情報にしか展開されない。ところが \svnMaxRevision のほうは 1001 に展開されてくれる。
svninfo に用意されているほかのコマンドは、マニュアルのPDFを参照。

The svninfo package(PDMマニュアル)
http://ftp.yz.yamagata-u.ac.jp/pub/CTAN/macros/latex/contrib/svninfo/svninfo.pdf

2009/04/21

Gauche の string-split が便利だ。便利なんだけど、セパレータとしてプロシージャを渡したとき、文字列にある各文字に対してプロシージャが順番に呼ばれないっぽいのが困る。
まずはふつうの例。こんな文字列があったとき、
(define text-with-block
"words, |entering a block|, got out of the block.")
スペースかどうか判断するプロシージャを is-space? 渡して、この文字列をぶったぎりたい。
(define is-space? (pa$ char=? #\space))
(string-split text-with-block is-space?)
=> ("words," "|entering" "a" "block|," "got" "out" "of" "the" "block.")
これは問題ない。
今度は、文字列のうちで縦棒 "|" でくくられている部分を塊とみなし、その内部にあるスペースは無視したいとする。つまりこんな結果がほしい。
(string-split text-with-block is-isolated-space?)
=> ("words," "|entering a block|," "got" "out" "of" "the" "block.")
クロージャーの出番です。is-isolated-space? はこんな定義でいいだろう。
(define is-isolated-space?
(let ((inblock? #f))
(lambda (c)
(cond ((char=? c #\|)
(set! inblock? (if inblock? #f #t))
#f)
(inblock?
#f)
((char=? #\space c)
#t)
(else
#f)))))
実際、 text-with-block に対して is-isolated-space? を繰り返し呼べば、縦棒 "|" でくくられた内部がセパレータとみなされないことが確かめられる。
(let R ((ls (string->list text-with-block)))
(cond ((null? ls)
'())
((is-isolated-space? (car ls))
(cons 1 (R (cdr ls))))
(else
(cons 0 (R (cdr ls))))))
=> (0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 1 0 0 1 0 0 0 1 0 0 0 0 0 0)
見づらいけど、 "|entering a block|," の位置に相当する部分がぜんぶゼロになっているのがポイント。
だから、string-split に渡したプロシージャが文字列にある各文字に対して順番に呼ばれるなら、目的の結果が得られるはず。でも得られない。
(string-split text-with-block is-isolated-space?)
=> ("words," "|entering" "a" "block|, got out of the block.")
しかも、もういっかい呼び出すと結果が変わる。
=> ("words, |entering" "a" "block|, got out of the block.")
ということは、各文字ごとに is-isolated-space? の抱えているクロージャがクリアされているわけではないんだよな。2回目の呼び出しでは、inblock? の初期値が #t になっているようだ。
ちなみに、マニュアルにはこうある。

splitter に手続きが与えられた場合、string にある各文字に対してその手続きが呼ばれ、
splitter が真の値を返すような連続した文字群がデリミタとして使われます。
また、ソースの stringutil.scm にある string-split の定義を見ると、ふつうに文字列の先頭から各文字をプロシージャーで処理してるっぽい。なにがおかしいのかなあー。
仕方がないので代わりの関数をでっちあげる。
(define (string-split-by str proc)
(let ((n (string-index str proc)))
(if n
(receive (h r)
(values
(string-take str n)
(string-drop str n))
(if (= (string-length r) 0)
'()
(if (> n 0)
(cons h (string-split-by (string-drop r 1) proc))
(string-split-by (string-drop r 1) proc))))
`(,str))))

(string-split-by text-with-block is-isolated-space?)
=> ("words," "|entering a block|," "got" "out" "of" "the" "block.")