Ugrep作成日記

前置き

howm-helperというのがあって、これを使うとhowmの検索が非常に早くなる。
unix由来のGrepをつかっているらしい。
これを、howmだけでなく普通のGrepでも行いたいな、と。
howm-helperの該当箇所を読んでもさっぱりだったので、勉強がてら作ってみようと考えました。
で、ある程度までできました。

ugrep.l
;;; -*- Mode: Lisp; Package: EDITOR -*-
;;; ugrep.l

(provide "ugrep")

(in-package "editor")

(export '(ugrep
	  ugrep-dialog
	  ugrep-split-window
	  exec-ugrep
	  scan-ugrepfile
	  scan-ugrepfileR
	  *ugrep-mode-hook*
	  *search-ugrepdialog-template* *ugrep-search-word* *ugrep-mode-map*))

(defvar *ugrep-search-word* nil)
(defvar *ugrep-search-dir* nil)
(defvar *ugrep-mode-hook* nil)
(defvar *ugrep-list* nil)




(defvar *search-ugrepdialog-template*
  '(dialog 0 0 271 65
    (:caption "UGrep")
    (:font 9 "MS Pゴシック")
    (:control
     (:static nil "パターン(&P):" #x50020000 7 10 42 8)
     (:combobox pat nil #x50210042 51 8 157 96)
		 (:static nil "フォルダ(&D):" #x50020000 7 27 42 8)
		 (:combobox dir nil #x50210042 51 25 157 96)
     (:button IDOK "検索(&S)" #x50010001 214 7 50 14)
     (:button IDCANCEL "キャンセル" #x50010000 214 24 50 14)
     (:button ref "参照(&R)..." #x50010000 214 41 50 14))))

(defun ugrep-dialog ()
  (interactive)
  (setq *ugrep-search-dir* (pop si:*command-line-args*))
  (multiple-value-bind (result data)
      (dialog-box *search-ugrepdialog-template*
		  (list (cons 'dir *ugrep-search-dir*))
		  `(list (ref :related dir :directory-name-dialog (:title "参照"))))
    (setq *ugrep-search-word* (cdr (assoc 'pat data)))
    (setq *ugrep-search-dir* (cdr (assoc 'dir data)))
    )
  )




(defun ugrep-split-window (file list-buffer)
  (interactive)
  (delete-other-windows)
  (setq *ugrep-list* (switch-to-buffer list-buffer))
  (insert-file-contents file)
  (split-window -25 nil)
  (set-buffer-fold-width nil)
  (setq need-not-save t))






(defun exec-ugrep (command key dir temp-file)
  (call-process (concat command " " key " " dir "/*") :output temp-file :show :hide :wait t))


(defun ugrep-open-file-next ()
  (interactive)
  (unless (equal (window-buffer (selected-window)) *ugrep-list*)
    (other-window))
  (next-line)
  (ugrep-open-file))

(defun ugrep-open-file-this ()
  (interactive)
  (unless (equal (window-buffer (selected-window)) *ugrep-list*)
    (other-window))
  (ugrep-open-file))


(defvar *ugrep-old-pathname* nil)
(defun ugrep-open-file ()
  (interactive)
  (let ((splits (split-string (buffer-substring (progn (goto-eol) (point)) (progn (goto-bol)(point))) #\:)))
    (setq path (concat (car splits) ":" (cadr splits)))
    (cond ((equal path *ugrep-old-pathname*)
	   (progn (other-window)
	     (set-buffer "viewfile")
	     (scan-buffer *ugrep-search-word* :no-dup t :tail t)
	     (other-window)
	     (message "~A は同じ" *ugrep-old-pathname*)))
	  ((file-exist-p path) 
	   (progn (other-window)
	     (switch-to-buffer "viewfile")
	     (erase-buffer(selected-buffer))
	     (insert-file-contents path)
	     (scan-buffer *ugrep-search-word* :no-dup t)
	     (setq need-not-save t)
	     (setq *ugrep-old-pathname* path)
	     (message "~A " *ugrep-old-pathname*)
	     (other-window)))
	  (t (message "~A はファイルなのか?" path)))))
	  
(defun scan-ugrepfile ()
  (interactive)
  (unless (equal (buffer-name (selected-buffer)) "viewfile")
    (other-window)
    (switch-to-buffer "viewfile"))
  (scan-buffer *ugrep-search-word* :no-dup t)
  )
(defun scan-ugrepfileR ()
  (interactive)
  (unless (equal (buffer-name (selected-buffer)) "viewfile")
    (other-window)
    (switch-to-buffer "viewfile"))
  (scan-buffer *ugrep-search-word* :reverse t :no-dup t)
  )


(defun ugrep ()
  (interactive)
  (ugrep-dialog)
  (let ((command "grep.exe")
	(key *ugrep-search-word*)
	(dir *ugrep-search-dir*)
	(tmp-file (make-temp-file-name))
	(buffer "view-list"))
    (exec-ugrep command key dir tmp-file)
    (ugrep-split-window tmp-file buffer)
    (switch-to-buffer buffer)
    (ugrep-mode)
    (switch-to-buffer "viewfile")
    (ugrep-mode)
    (delete-file tmp-file))
  (ugrep-open-file-this))

(defun ugrep-mode ()
  (interactive)
  (kill-all-local-variables)
  (setq buffer-mode 'ugrep-mode)
  (setq mode-name "Ugrep")
  (use-keymap *ugrep-mode-map*)
  (make-local-variable 'need-not-save)
  (setq need-not-save t)
  (make-local-variable 'auto-save)
  (setq auto-save nil)
  (run-hooks '*ugrep-mode-hook*))

(defvar *ugrep-mode-map* nil)
  (unless *ugrep-mode-map*
    (setq *ugrep-mode-map* (make-sparse-keymap))
    (define-key *ugrep-mode-map* #\F10 'ugrep-open-file-this)
    (define-key *ugrep-mode-map* #\F11 'ugrep-open-file-next)
    (define-key *ugrep-mode-map* #\F3 'scan-ugrepfile)
    (define-key *ugrep-mode-map* #\S-F3 'scan-ugrepfileR))

はてなってテキストファイルとかソースファイルだけ、別個にリンク貼るとかできないのかな。たぶんすごい見にくいよね、これ。

早くなったのか、というと早くなった気がする、ということだけは言える。
場合によって、Grep.exeをロードするときに時間がかかったりするんだよね。その場合にはデフォのGrepと速度的には大差ない。けど、ロードが一瞬の場合は、検索も一瞬で終わる。何でなんだろう。

構造とかの説明とか

適当な説明


外部プログラムのgrep.exeを使って検索をしたい
そして、その結果をxyzzyで表示して、デフォルトのGrepっぽい動作をさせたい

作り始める前に考えたプラン


まずは、grep.exeをxyzzy上から起動する必要があるでしょう。
そのためには外部プログラムを動かすコマンドについて調べないと行けない
そして、その結果をどっかのファイルに出力させればいい。

このファイルをどうやって読み込むか、ということを次にする必要。このためのコマンドはいろいろとそろっているっぽい。
そして、ウィンドウを分割する方法とか知ったらいいんでしょう。
こうしたら、とりあえず表示をデフォルトっぽくするところまでは行くだろう。

あとは、検索したファイルの一覧表示からパスを取得して、別のウィンドウに表示するコマンドを作ったらいい

検索対象フォルダは、fenrirから投げるという仕方にしたいけど、これは何とかなるだろう。

結果


call-processというのが何かを調べるところから初めて、だいたい10日くらいで完成(とりあえず)。
つまづいたとことか、勉強の仕方として学んだこととか、後になって自分の役に立ちそうなのを書いておこうと思う。

学んだこととか

外部プログラム


外部プログラムを起動するコマンドはcall-processとexecute-shell-commandの二つ。call-processだけで十分。
call-processでは引数に出力ファイルも設定できる。

(call-process コマンド :output 出力ファイル)

ここから、色々やって完成したのがこれ。

(defun exec-ugrep (command key dir temp-file)
  (call-process (concat command " " key " " dir "/*") :output temp-file :show :hide :wait t))

comand(とうぜんgrep.exeが入ることになる)に、検索したいキーワードと検索対象フォルダを引数としてつけている。あと、出力ファイルを設定して、コマンドプロンプトを表示しないようにしてコマンドが実行し終わるまで待つようにしている。
grep.exeに引数をつけたい場合はどうしたらいいんだろう。(call-process "grep.exe key dir")みたいにしたらkeyとかdirとか変数ととってくれないよな、とか思ってたらconcatを使えばいいだけだった。

表示の仕方

call-processでできた出力ファイルを読みこみ、表示する。
grep.exeでできたファイルをみると、

D:\data1\TTD/075-2.txt:日記−200x1027(wed)

みたいに出力するようだ。「:」で区切っているのだから、これをセパレーターにして、ファイルパスだけを読み込み、それを別ウィンドウに表示すればいい。
でもその場合、ドライブ名「D」のあとの「:」も区切ることになってしまう
フルパスだけ取り出す方法とかないかなと調べてみたけど途中であきらめ、「:」をセパレーターにしてsplit-stringで得たものの一番目と二番目を「:」でつなげるようにした。

  (let ((splits (split-string (buffer-substring (progn (goto-eol) (point)) (progn (goto-bol)(point))) #\:)))
    (setq path (concat (car splits) ":" (cadr splits)))

何となくイマイチ

コマンドの意味とか組み合わせとか

作り出してから、最初にxyzzylispの説明を読んだだけでは分からなかったことが少しずつ分かってきた。

  • letは初期化に使う
  • ダイアログの情報はmultiple-value-bindで取得する。
  • ドットリストは、assocと組み合わせて必要な情報を検索するのに使える
  • defvarとdefunの組み合わせで、プログラムは成り立っている。
  • defvarでグローバル変数を設定。
  • xyzzyへの引数を得るときは(pop si:*command-line-args*)
  • prognのような複数のコマンドを順に実行するものは、条件分岐とかの、括弧の順番で意味合いが変わってくるときに使う
  • コンパイルするときのことを考えて、変数とかはあまり散らばっていてはいけない。
  • 役割に応じて、作るコマンドは分割しておいたほうがいい。

ugrepを使う準備

ちなみにugrepをつかうことによって起こるいかなることにも責任は持てないです、はい。

  1. grep.exeをどこかから*1拾ってきて、xyzzyディレクトリに置く。
  2. site-lispにugrep.lを置く
  3. .xyzzyに(require "ugrep")と記述

以上がxyzzy側の準備。
単純に使ってみたいだけならこれでもいけるのかもしれない。
M-x ugrep
としたら使える。

次にfenrir側の準備。
2g.txtという空のテキストファイルを作り、そのなかに次をコピー。適当に読み替えてください

[.***;
xyzzy-ugrep|D:\bin\xyzzy\xyzzy.exe -f ugrep "%P"
]

使い方

fenrirで、検索したいパスを絞り込む。それを選択し、Ctrl+G
するとugrepが起動、ダイアログが表示されるはず。そこでパターン欄に検索したい語を打ち込む。Enter。
これで、検索結果が表示されると思う。
使えるキーは、F10 F11 F3 Shift+F3なので、適当にやったら分かると思う

既知の不具合

サブフォルダを検索できない
ファイルの編集ができない
キャンセル時の動作が変

これから先の改善案みたいなの

サブフォルダの検索について。-rオプションで、再帰的にサブディレクトリを検索というのができるらしい。この場合、ファイル数が多すぎて時間がかかりすぎるという事態が起こりうる。途中でスキャンをキャンセルする方法とか、サブディレクトリを検索するかしないかを選択するとかを実装する必要。

ファイルの編集は、たとえばhowmみたいにEnterなりおしたらそれを別バッファで開いて編集可能にするとかかなぁ

勉強の仕方

M.Hiroi's Home Pageのxyzzy Lisp Programmingが非常にためになる。ここで、外部プログラムの実行の仕方とかも学んだ。
また、適当に調べたいコマンドを「xyzzy」という語と一緒にググってみたり。けどほしい情報はあまり手に入らなかった。
リファレンスも役に立つ。これを見ながら、*scratch*でいろいろなコマンドを試した
先人が作成した拡張lispファイルや、lispフォルダのなかのも見てみたりしたけれど今の僕の知識じゃあまり理解できない
あとは、なぜか持っている入門xyzzyを読んだりした。これも結構役に立つ

*1:僕はGNUから拾った