読者です 読者をやめる 読者になる 読者になる

Gaucheでシェルスクリプト

最近、Let Over Lambda (LOL) という本を読んでます。

Let Over Lambda

Let Over Lambda

LOLは、On Lispの発展(?)としてLispのマクロに関して書かれた本です。まだ半分くらいしか読んでないですが、マクロってこんな使い方もできるのか!と発見も多く、非常に面白いです。

LOLの3章では defmacro/g! というマクロが登場するんですが、これがなかなか便利で楽しいマクロです。defmacro/g!は、普通のdefmacroと同じようにマクロを定義するマクロなんですが、defmacroとの違いはボディに現れる"g!"から始まるシンボルの扱い方にあります。
defmacro/g!は"g!"から始まるシンボルを自動的に、インターンされていないシンボルに束縛します。つまり、

(defmacro/g! or (&rest exprs)
  (if exprs
    `(let ((,g!x (car exprs)))
       (if ,g!x
         ,g!x
         (or ,@(cdr exprs))))))

みたいなのが、

(defmacro or (&rest exprs)
  (let ((g!x (gensym)))
    (if exprs
      `(let ((,g!x (car exprs)))
         (if ,g!x
           ,g!x
           (or ,@(cdr exprs)))))))

というふうに展開されるわけです(たぶん)。これは、defmacro/g!のボディに与えられた式をトラバースして、"g!"から始まるシンボルだけを集めてletの束縛を作ることで実現されています。


この、マクロに与えた式をトラバースして特定のシンボルに対してだけ決まった処理をするというアイディアは他でもいろいろ使えそうです。で、何に使えるかと考えていたときに、Gaucheからコマンドを手軽に呼べたら便利かも、という案が思い浮かびました。

Gaucheからコマンドをっていう話は以前に、Gaucheでシェルスクリプトとかいうアレは - 日記を書く [・w・] はやみずさんで見ていたのですが、この方法だとあらかじめ定義しておいたコマンドしか呼べないのが難点でした(さらにこんな話題にまで)。
ところが、どれがコマンドの呼び出しかというのはコードの書き手には分かっているのだから、それを示す"マーク"をあらかじめ付けておけば、あとはdefmacro/g!と同じように「マクロで自動的に処理」ということができそうです。

そこで、

(with-command
  (コマンド名 引数1 引数2 ...))

を、

(let ((コマンド名 コマンドを呼び出す関数))
  (コマンド名 引数1 引数2 ...))

に展開してくれるwith-commandというマクロを作ります。with-commandは頭に"!"がついたシンボルをコマンド名として扱うことにします。ちょうどviの外部コマンド実行と同じ見た目になっていいかなぁと。

(use gauche.process)
(use srfi-1)
(use srfi-13)

(define (tree-filter f tree)
  (define (rec t s)
    (cond [(null? t) s]
          [(pair? t) (rec (car t) (rec (cdr t) s))]
          [(f t) (cons t s)]
          [else s]))
  (rec tree '()))

(define (command? sym)
  (and (symbol? sym)
       (string=? (string-take (symbol->string sym) 1) "!")))

(define (command-name com)
  (string->symbol (string-drop (symbol->string com) 1)))

(define (exec-command command . args)
  (process-output->string-list (cons command args)))

(define-macro (with-command . body)
  (let1 commands (delete-duplicates (tree-filter command? body) eq?)
    `(let ,(map (lambda (command)
                  `(,command
                    (cut exec-command ',(command-name command) <...>)))
                commands)
       ,@body)))

さらに、REPLにも手を加えて入力されたすべての式をwith-commandで囲ってしまうことにします。

(define (main args)
  (define (reader)
    (let1 exp (read)
      (if (eof-object? exp)
        exp
        `(with-command ,exp))))
  (read-eval-print-loop reader)
  0)


これを実行してみると、下のように使うことができます。

gosh> (!mkdir "test")
()
gosh> (sys-chdir "test")
#t
gosh> (!touch 'a 'b 'c)
()
gosh> (!ls)
("a" "b" "c")
gosh> (dolist (file (!ls)) (!mv file #`",|file|.txt"))
()
gosh> (for-each print (!ls '-l))
total 0
-rw-r--r--  1 sohta  staff  0  5 30 22:40 a.txt
-rw-r--r--  1 sohta  staff  0  5 30 22:40 b.txt
-rw-r--r--  1 sohta  staff  0  5 30 22:40 c.txt
#<undef>
gosh> 

なかなかに便利です。
ただこれ、with-commandが最終的にletに展開されるので、その中で定義された関数はグローバルに参照することができなくなってしまいます。つまり、この修正を加えたREPLからは実質的に関数定義ができなくなっちゃうわけです。
「関数定義をまったくする必要がないけど、コマンドだけは便利に使いたい!」っていう用途であれば問題ないのかもしれないですが、やっぱりREPLに手を入れるんではなくて、明示的にwith-commandを使った方がベターなのかなぁと思います。