スポンサーリンク

2016年1月30日土曜日

Windows NTEmacs で ag (The Silver Searcher) を使う

IDE全盛の昨今、Emacs で(elisp 以外の)プログラミングコードを書くことはめっきりなくなりました。しかし文章を書く作業は今でもEmacsが中心です。

Windows 上で Emacs を使っていると鬼門なのが外部コマンドとの連携です。例えば grep を呼び出して文章を検索するなどということはよくやりますが、英語は検索できても日本語が通らないといったケースがよくあります。

また ag (The Silver Searcher) の検索の早さには目を見張るものがあります。一度使ってみるとどうしても Emacs からも使えるようにしておきたくなります。

単に使うだけであればインストールさえすれば、最低限英語の検索はできます。自分も長い間その状態で放って置いたのですが、ちゃんと日本語も検索できるようにしておこうと思い調べてみました。

調べてみると結構根深い問題があったりして、自分でも勉強になったので、ここにメモを残しておこうと思います。

まず簡単に自分の環境を説明しておきます。

  • Windows10 Pro x64
  • NTEmacs 24.5.1 (IMEパッチなし)
  • Cygwin64 + ag

ag をインストール

ag は Cygwin64 上で自分でビルドしたものを使います。既にお持ちの方はここはスキップしてください。

ag は Cygwin のパッケージになっていないので、自力でビルドする必要があります。ソースコードはここからダウンロードしてください。


ビルドに必要なパッケージはだいたい以下の通りです。

 gcc
 make
 autoconf
 automake
 pkg-config
 zlib-devel
 libpcre1
 libpcre-devel
 liblzma-devel
 liblzma5
 pcre

これらがインストールされていれば、後は展開したソースの ./build.sh を Cygwin のコマンドラインから実行するだけです。いくつか警告が出ますが無視して構いません。実行したディレクトリに ag.exe が生成されている筈です。

あとは make install すれば Cygwin の /usr/local/bin にコピーされます。このディレクトリをWindowsの PATH に追加しておいて下さい。

ag.el を動かす

まずは ag.el パッケージを動くようにしてみます。経験から言うと、外部コマンド絡みの問題はほんとんどが文字コードに起因するものです。逆に言うと文字コードさえ適切に設定してやれば、ほんとんどはちゃんと動くとも言えます。

Mac や Linux であれば何も考えずとにかく utf-8 にしておけばまず問題は起きません。しかしWindows の場合、SJIS(cp932)が嫌がらせのようにあちこちに顔を出します。

更にやっかいなのが、Emacs には文字コードを設定するにも似たようなコマンドや変数がいくつもあり、いったい何をどう使えばいいのかよく分からないというEmacsの闇です。

では Cygwin のコマンドを呼び出すには文字コードとして何を使えばいいのでしょうか?意外なことに、こうすべきという情報が見つかりませんでした。

そこで試行錯誤的に試してみました。結論から言うと、コマンドへの入力は cp932、コマンドからの出力は utf-8-dosにすればとりあえず動くということが分かりました。つまり入力と出力が非対称なのです。

外部プロセスに対する文字コード指定はいくつか方法がありますが、今回は変数 default-process-coding-system を使いました。この変数は Emacs のすべての外部プロセス呼び出しに影響します。というか特に指定が無かった場合のフォールバック先として機能します。

以下を初期化ファイルに追加してください。

  (setq default-process-coding-system '(utf-8-dos . cp932))


これだけで日本語が検索できるようになりました。M-x ag で検索文字列とディレクトリを入力すれば物凄い勢いで検索してくれます。

ただし、(prefer-coding-system ...) や (set-default-coding-systems ...) があると上書きされてしまうので、これらを使っている人はそれ以降に記述してください。Emacs の闇です。

helm-ag を動かす

helm-ag も普通に実行してみると日本語が検索できません。こちらは結構やっかいでした。

まずは以下のコードを見てください。helm-ag が ag コマンドを呼び出している部分です。


(defun helm-ag--init ()
  (let ((buf-coding buffer-file-coding-system))      ; (1)
    (helm-attrset 'recenter t)
    (with-current-buffer (helm-candidate-buffer 'global)
      (let* ((default-directory (or helm-ag--default-directory
                                    default-directory))
             (cmds (helm-ag--construct-command (helm-attr 'search-this-file)))
             (coding-system-for-read buf-coding)     ; (2)
             (coding-system-for-write buf-coding))   ; (3)
        (setq helm-ag--ignore-case (helm-ag--ignore-case-p cmds helm-ag--last-query))
        (let ((ret (apply 'process-file (car cmds) nil t nil (cdr cmds))))   ; (4)
          (if (zerop (length (buffer-string)))
              (error "No output: '%s'" helm-ag--last-query)
            (unless (zerop ret)
              (unless (executable-find (car cmds))
                (error "'ag' is not installed."))
              (error "Failed: '%s'" helm-ag--last-query))))
        (helm-ag--save-current-context)))))


(1)でバッファのファイル・コーディングシステムを取り出し、それを (2)(3) で coding-system-for-read, coding-system-for-write に設定しています。これが外部プロセスに対して read/write する文字コード(コーディングシステム)になります。そして(4)で外部コマンド ag を呼び出しています。

つまりバッファのファイル・コーディングシステムを強制的に ag コマンドの入・出力に使っているのです。これじゃWindowsでまともに動くわけありません。この部分意図がさっぱり理解できませんが、とにかく原因は分かりました。

原因が分かれば後は対策です。変数の設定でどうにかできるレベルではないので、今回は外部プロセスを呼び出している関数(process-file)を advice という仕組みで書き換え、コーディングシステムを強制的に設定することにしました。advice にもいくつか方法がありますが、Emacs24.4 から導入された nadvice.el を使いました。古い Emacs を使っている方は注意してください。一応古い advice を使った方法もコメントとして残しておきましたので、必要であればそちらを使って下さい。

以下が最終的なコードです。これで helm-ag で日本語の検索、絞り込みができるようになりました。


(when (executable-find "ag")
  (require 'helm-ag)

  (defvar helm-ag-base-command)
  (defvar helm-ag-insert-at-point)
  (defvar helm-ag-ignore-patterns)

  ;; ag のデフォルトのコマンドオプションを指定
  ;; -n を消すとサブディレクトリも再帰的に検索
  ;; (setq helm-ag-base-command "ag --nocolor --nogroup -n")
  (setq helm-ag-base-command "ag --nocolor --nogroup")

  ;;; ポイント位置のシンボルをデフォルトのクエリにする
  (setq helm-ag-insert-at-point 'symbol)

  ;; 検索で無視するファイルパターン (ag --ignore xxxx に渡す文字列を設定)
  ;; (setq helm-ag-ignore-patterns '("*~" "#.*#" "TAGS"))
  ;; 無視パターンに grep.el の変数を使う
  (setq helm-ag-use-grep-ignore-list t)

  ;; process-file を呼び出す前に R/W の coding system を強制的に設定
  ;; (defadvice process-file (before configure-process-coding activate)
  ;;   "Configure process coding for Cyrgin application."
  ;;   (setq coding-system-for-read  'utf-8-dos)     ;; 行末の ^M を避けるため -dos が必要
  ;;   (setq coding-system-for-write 'cp932-dos)
  ;;   )

  ;; :around による advice
  (defun set-rw-coding-system:around (orig-func &rest args)
    (let ((coding-system-for-read  'utf-8-dos) ; 行末の ^M を避けるため -dos が必要
          (coding-system-for-write 'cp932-dos))
      (apply orig-func args)          ; オリジナル関数を呼び出し
      ))
  (advice-add 'process-file :around #'set-rw-coding-system:around)


  ;; === キーバインド ===
  (global-set-key (kbd "C-M-g") 'helm-ag)
  (global-set-key (kbd "C-M-k") 'backward-kill-sexp) ;推奨
  )



0 件のコメント :

コメントを投稿