アクトインディ開発者ブログ

子供とお出かけ情報「いこーよ」を運営する、アクトインディ株式会社の開発者ブログです

隙があればlispを詰め込んで行きたい (2)

こんにちは、chibaです。
今回もネタがないのでLISPネタです。
日常の作業では、テキストファイルを加工することが割とあったりすると思いますが、そういう時には、sedや、awkって便利ですよね。
ちょっとした一行野郎が大活躍、ということは結構あると思います。
自分もそういう一行野郎が好きではあるのですが、いやしかし、隙があればlispを詰め込んで行きたい。
最近もsedを使いたくなるようなHTMLの編集作業に遭遇しました。
「指定したディレクトリ以下に含まれるすべてのHTMLファイルの<div class="foo"から、</div>の間を、用意したテキストファイルの内容と置き換える。」
という仕事です。
sedで一発だろうと思いましたが、とりあえず、Common Lispで書いてみました。

(LOOP :FOR FILE :IN
   (DIRECTORY "/tmp/foo/**/*.html")
   :DO
   (WITH-OPEN-FILE (IN FILE)
     (WITH-OPEN-FILE (OUT (FORMAT NIL "~A.new.html" (PATHNAME FILE))
                          :DIRECTION :OUTPUT
                          :IF-EXISTS :SUPERSEDE
                          :IF-DOES-NOT-EXIST :CREATE)
       (LOOP :WITH OPEN
          :FOR LINE := (READ-LINE IN NIL NIL) :WHILE LINE
          :DO (PROGN
                (WHEN (SEARCH "<div class=\"foo\">" LINE)
                  (SETQ OPEN 'T))
                (COND ((AND OPEN (SEARCH "</div>" LINE))
                       (SETQ OPEN NIL)
                       (WITH-OPEN-FILE (IN2 "g000001/foo.html")
                         (LOOP :FOR LINE := (READ-LINE IN2 NIL NIL) :WHILE LINE
                            :DO (WRITE-LINE LINE OUT))))
                      ((NOT OPEN)
                       (WRITE-LINE LINE OUT)))
                )))))

気分は一行野郎なのでコードもいきあたりばったりで汚ないです。
これで仕事は片付いたのですが、率直な感想として、sedに比べると書くのがかなりめんどくさいです。(一切ライブラリを使ってないということもありますが…)
こんなにめんどうでは、やっぱりCommon Lispよりsedを使ってしまいます。
いやしかし、こういう道具を沢山書き溜めておいて、必要なときにさっと出せるようになれば、そのうちsedawkではなく、自然にCommon Lispを使うようになるかもしれません。
ということで、若干無理はあるものの、コードを纒めて次の機会に備えておくことにしました。

(DEFUN SED (START-PAT END-PAT NEW
            &KEY (IN *STANDARD-INPUT*) (OUT *STANDARD-OUTPUT*))
  (LOOP :WITH OPEN
        :FOR LINE := (READ-LINE IN NIL NIL) :WHILE LINE
        :DO (PROGN
              (WHEN (SEARCH START-PAT LINE)
                (SETQ OPEN 'T))
              (COND ((AND OPEN (SEARCH END-PAT LINE))
                     (SETQ OPEN NIL)
                     (WRITE-LINE NEW OUT))
                    ((NOT OPEN)
                     (WRITE-LINE LINE OUT))))))

(DEFUN MAP-FILE-INTO (FILES FUNCTION)
  (LET ((TEMPNAME-SUFFIX (GENSYM "TEMP-FILE-")))
    (DOLIST (FILE FILES)
      (LET ((TEMPFILE-NAME (FORMAT NIL "~A_~A" FILE TEMPNAME-SUFFIX)))
        (WITH-OPEN-FILE (IN FILE)
          (WITH-OPEN-FILE (OUT TEMPFILE-NAME
                           :DIRECTION :OUTPUT
                           :IF-EXISTS :SUPERSEDE
                           :IF-DOES-NOT-EXIST :CREATE)
            (FUNCALL FUNCTION IN OUT)))
        (RENAME-FILE TEMPFILE-NAME FILE)))))

この2つを使えば、今回の仕事は、

(MAP-FILE-INTO (DIRECTORY "**/*.html")
               (LAMBDA (IN OUT)
                 (SED "<div class='foo'>"
                      "</div>"
                      (KL:READ-FILE-TO-STRING "foo.txt")
                      :IN IN
                      :OUT OUT)))

のように書けます。
いまのところぱっとしませんが、そのうちCommon Lispだけでテキスト仕事は片付けられるようになることを目指します!