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

argius note

プログラミング関連

開発しています



grepコマンド実装(1) - without REGEXP -

Lisp

Lispをスクリプトとして使うのが前提なので、まずはコマンドの真似から始めてみます。
今回は、"grep"です。以下のような実行結果が得られたら成功とします。
fgrepについての説明を追記しました。

$ grep -FHn Me *.dat
file1.dat:1:Love Me Do
file2.dat:2:Please Please Me
file2.dat:3:Can't Buy Me Love
$ grep -FHn er *.dat
file1.dat:2:I Saw Her Standing There
file2.dat:1:And I Love Her
$ grep -FHn er < file1.dat
2:I Saw Her Standing There
$

grepコマンドは正規表現が使えますが、今回は正規表現無しのものを作ってみます。つまり、"fgrep"です。また、オプションは"-FHn"固定(F:非正規表現、H:ファイル名表示、n:行番号表示)とします。(今やってみたら、標準入力からの場合はファイル名に"(標準入力)"と表示されることに気が付いたが、上記の仕様とする。)
今回も、Rubyの例を挙げておきます。

Ruby

def grep_each_line(ptn, f, path=nil)
  head = if path.nil? then "" else path + ":" end
  while line = f.gets
    if line.include?(ptn)
      printf "%s%d:%s", head, f.lineno, line
    end
  end
end

if ARGV.empty?
  puts "usage : grep.rb pattern [files]"
else
  ptn = ARGV.shift
  grep = Proc.new do |f, path|
    grep_each_line(ptn, f, path)
  end
  if ARGV.empty?
    grep[STDIN]
  else
    ARGV.each do |path|
      open(path, "r") do |f|
        grep[f, path]
      end
    end
  end
end

標準入力とファイルのどちらかを判断する前に、パターンだけ決定している状態をクロージャにしています。クロージャの2番目の引数は、任意*1になっています。後は、文字列にパターンが含まれているかのチェックです。

Common Lisp

(defun grep-each-line (ptn &optional f path
                           &aux (head "") (line-number 0))
  (unless (null path)
    (setf head (concatenate 'string path ":")))
  (loop for line = (read-line f nil nil) while line
        do
        (incf line-number)
        (unless (null (search ptn line))
          (format t "~A~D:~A~%" head line-number line))))

(if (null *args*)
    (format t "usage : grep.lisp pattern [files]")
  (let ((grep 
         (lambda (&optional f path)
           (grep-each-line (car *args*) f path))))
    (if (null (cdr *args*))
        (funcall grep)
      (mapc (lambda (path)
              (with-open-file (f path)
                              (funcall grep f path)))
            (cdr *args*)))))

CLの場合、read-lineに渡す最初のパラメータがnilだと、標準入力とみなしてくれるようです(明示するには"*standard-input*"を使う)。任意パラメータのところはすっきり書けましたが、補助変数(&aux)のところはネストを深くしたくないために捻じ曲げた感じがします。あと、funcallは見栄えが悪いですね。
パターン一致の判定は、searchを使っています。これは、RubyではString#indexに似ていて、見つかった場合はその位置、見つからなかった場合はnilを返します。
行番号は、RubyのIO#linenoに相当するものが分からなかったので、自分でカウントしています。

Emacs Lisp

コマンドとして使う方法が分からない(無理?)のでパス。
ちなみに、Emacsからgrepを実行すると、Emacsが代わりにshellでgrepコマンドを実行してくれます。

Scheme

(use srfi-13)

(define (grep-each-line ptn port path)
  (let ((head ""))
    (unless (null? path)
            (set! head (string-append path ":")))
    (with-input-from-port port
      (lambda ()
        (port-for-each
         (lambda (line)
           (when (string-contains line ptn)
                 (format #t "~A~D:~A~%"
                         head
                         (- (port-current-line port) 1)
                         line)))
         read-line)))))

(if (null? *argv*)
    (print "usage : grep.scm pattern [files]")
    (let ((grep
           (lambda (port . rest)
             (grep-each-line (car *argv*) port
                             (if (null? rest) () (car rest))))
           ))
      (if (null? (cdr *argv*))
          (grep (current-input-port))
          (for-each
           (lambda (path)
             (grep (open-input-file path) path))
           (cdr *argv*)))))

これは、Gaucheサイトのリファレンスに載っているものを参考にしました。
Schemeでは、funcallが要らないのはうれしいのですが、任意パラメータのところが上手く書けませんでした。色々試行錯誤してみましたが、ギブアップ。
行番号は、port-current-lineを使っています。-1しているのは、使う時には次の行を指してしまっているからだと思います。
パターン一致判定は、string-containsを使っています。これは、Rubyので使っているString#include?に似ています。これは、文字列モジュール"srfi-13"を有効にしないと使えません。

まとめ

今のレベルにしては上手く書けたと思いました。まだほとんどLispを知らなかった頃は、こういう用途で使えるなどと想像もしていなかったので、「Lispって何でも出来るんですね。伊達に歳を取っていませんねー!」と感服(?)いたしております。
次回からは、正規表現バージョンや、オプション対応、それにもっと簡潔な書き方にするとかをやっていきたいと考えております。

*1:Proc.newの代わりにlambdaを使うと、ArgumentErrorになってしまいます。