sbclで外部プロセスを呼ぶ
gzip圧縮されたファイルを処理したくなったので*1、外部プログラムに解凍させてそれを解けた端から読んでいこうということにした。
sbclには sb-ext:run-program 関数があって、外部プロセスを読んでその出力を使える。
http://www.sbcl.org/manual/Running-external-programs.html
にマニュアルがある。
第1引数がプログラム名で、デフォルトだと絶対パスを渡すことになるが、:search に t をしてやれば $PATH から探してくれる。
第2引数がプログラムに渡す引数文字列のリスト。シェルでやるように複数の外部プログラムをパイプで繋いだ出力を使いたいという時はプログラム名に bash とかを指定したうえで (list "-c" (format nil "......" .....)) という感じにしてしまえばよい*2。
:output に :stream を指定すると出力をストリームとして扱える(戻り値のprocess構造体からprocess-outputで取れる)。t を指定するとプログラムの出力は標準出力に吐かれる。ファイル名を指定すればそこに吐かれる。nil では /dev/null を指定したことになる。
:error も:output と同様。
これを使って with-file-open よろしく gzipファイルを読むマクロを作ってみた。
(defparameter *zcat-program* "/usr/bin/gzcat") (defmacro with-open-gzip-file ((stream filespec) &body body) (let ((proc (gensym))) `(let* ((,proc (sb-ext:run-program "/bin/bash" (list "-c" (print (format nil "~A ~A" ,*zcat-program* ,filespec))) :wait nil :output :stream :error t)) (,stream (sb-ext:process-output ,proc))) (unwind-protect (progn ,@body) (sb-ext:process-kill ,proc sb-unix:sigint))))) #| (with-open-gzip-file (s "~/test.gz") (read-line s)) (with-open-gzip-file (s "~/data/*.gz") (dotimes (i 30) (print (read-line s)))) |#
入力の場合しか考えてないのとエラー処理(ファイルが見つからないなど)などはしていないので注意*3。あと多分windowsでは動かない。
リモートサーバー上のgzipファイルを読む
(defmacro with-open-gzip-file-ssh ((stream filespec host) &body body) (let ((proc (gensym))) `(let* ((,proc (sb-ext:run-program "/bin/bash" (list "-c" (format nil "ssh ~A cat \"~A\" | ~A" ,host ,filespec ,*zcat-program*)) :wait nil :output :stream :error t)) (,stream (sb-ext:process-output ,proc))) (unwind-protect (progn ,@body) (sb-ext:process-kill ,proc sb-unix:sigint))))) #| (with-open-gzip-file-ssh (s "~/file.gz" "hostname") (dotimes (i 30) (print (read-line s)))) |#
*1:最初は quicklisp からgzip-streamというのを入れてみたのだけど、あまり早くないのと、解析するプロセスと解凍するプロセスが同じなのはマルチコア環境ではちょっともったいないので
*2:ワイルドカードを使いたい時はシェルに展開を任せるのが楽
*3:本当にちゃんとやるなら定義 https://github.com/sbcl/sbcl/blob/master/src/code/macros.lisp#L428 https://github.com/sbcl/sbcl/blob/master/src/code/fd-stream.lisp#L2290 あたりを参考にすることになるだろう