PComplete:根据上下文环境进行补全
我在 /What’s New In Emacs 24/系列文章中(part one, part two) 简单地提到过pcomplete(Eshell中使用的可编程补全库)现在也能直接在 M-x shell
中使用了.
这对喜欢shell mode的人来说可真是个好消息, 因为原来的shell mode并没补全功能(而补全的能力是底层shell本身就提供的).
令人吃惊的是,补全机制在Emacs已经存在很久了,但是它在Emacs中很少用到,这使得大多数人对它知之甚少. 事实上,我估计只有Eshell,ERC,Org Mode和现在Shell Mode中采用到了这个包吧.
Programmable, Context-Sensitive Completion
从Emacs 24开始,你什么都不用做就能使用 pcomplete
, 当你打开一个新的shell session时,Emacs会自动做好相应的准备.
Emacs自带了很多 pcomplete
函数能够加强Emacs之前那种单纯的文件名补全的能力,使之能提供类似bash/zsh的那种基于上下文的补全.
尤其为 scp
, ssh
, mount
, umount
和 make
命令提供了很好的补全支持.
下面列出的是那些shell mode支持补全的命令(事实上该列表适用于任何支持pcomplete的mode,包括Eshell.)
bzip2
补全参数,并且只列出经过bzip2压缩后的文件.
cd
补全目录.
chgrp
补全系统中已有的祖名.
chown
补全用户和组,但只有使用
user.group
时才有用.cvs
补全命令和参数选项,还有cvs entry 以及 module.
gdb
补全目录以及具有执行权限的文件
gzip
补全参数并只列出经过gzip压缩过后的文件.
kill
在 - 后面会列出signal,否则的话会补全所有的进程号.
make
补全参数以及目录中的所有makefile; 在
make -f FILE
后则会补全文件中的rule名称.mount
补全参数,以及
mount -t TYPE
中的文件系统类型.pushd
与cd的补全方式一样
rm
补全参数,文件名以及目录.
rmdir
补全目录.
rpm
rpm的补全机制实现的非常精妙. 能够根据上下文环境补全几乎所有的子命令,包括补全package名称.
scp
能够补全命令参数, SSH认证过的主机,并且在命令格式为
scp host:/
时还能补全远程服务器上的文件路径(借助TRAMP来实现).ssh
补全命令参数,以及SSH认证过的主机.
tar
补全命令参数,尤其能根据上下文智能补全符合POSIX标准的参数,还能补全文件名.
time
补全目录,以及带有执行权限的命令.
umount
补全命令参数,挂载的目录以及文件系统类型(与mount类似)
which
本应能提供对所有二进制文件的补全,但貌似不能正常工作.
xargs
补全目录以及带有可执行权限的文件.
Custom Completion
一个补全库,既然号称 programmable completion 那它自然就是可编程的了.
要用它来实现简单的参数补全是很简单的,但是除此之外的补全功能实现起来就有点棘手了,因为这个库并没有什么文档说明在里面(虽然乐观主义者会说代码就是最好的文档...).
我接下来会演示一下,如何为 git
添加初步的补全功能
首先我们需要确认一下命令参数的顺序; 对于 git
命令来说,这基本上是固定的: git [options] <command> [<args>]
这里我只关注于常用的那几个命令. 把这些命令放在一个list中:
(defconst pcmpl-git-commands '("add" "bisect" "branch" "checkout" "clone" "commit" "diff" "fetch" "grep" "init" "log" "merge" "mv" "pull" "push" "rebase" "reset" "rm" "show" "status" "tag" ) "List of `git' commands")
pcomplete
的语法规则很灵活(clever): 它会根据一定的命名规则来动态地选择调用哪个elisp函数来进行补全(it will use dynamic dispatch to resolve the elisp function provided it is named a certain way).
一个command的补全函数按 pcomplete/COMMAND
或 pcomplete/MAJOR-MODE/COMMAND
这两种规则进行命名.
只要遵照这个命名规则进行命名,就能够实现补全了.
下一步,我们需要提供一个包含有效子命令的列表 – 在这个例子中,这个列表就是 pcmpl-git-commands
的值, 不过实际上,提供给命令 pcomplete-here
的可以是任意的form(译者注:pcomplete-here内部会运行该form,然后将结果作为补全的依据).
(defun pcomplete/git () "Completion for `git'" (pcomplete-here* pcmpl-git-commands))
现在,当你按下tab来补全git的第一个参数时,就会列出所有的子命令了.真不错.
现在让我们扩展一下这个函数,让它也为 add
和 rm
子命令添加补全支持. 我希望当子命令是 add
或 rm
时能补全文件名/文件路径.
借助 pcomplete-match
函数,要实现这个功能出奇地容易. pcomplete-match
函数能够检查特定位置的参数是否匹配某个正则表达式.
你应该留意, 这里是在一个 while
循环中调用 pcomplete-here
的; 这样你就可以一个接一个地补全任意多个文件.
使用 pcomplete-here
的好处在于,它能自动忽略那些之前已经补全过的文件 - 这在为 add
子命令提供补全时非常有用.
(defun pcomplete/git () "Completion for `git'" ;; Completion for the command argument. (pcomplete-here* pcmpl-git-commands) ;; complete files/dirs forever if the command is `add' or `rm'. (if (pcomplete-match (regexp-opt '("add" "rm")) 1) (while (pcomplete-here (pcomplete-entries)))))
Ok, 到目前为止都还比较容易实现. 现在让我们把它变得更加动态一些,我们来扩展这段代码使之支持 git checkout
命令, 补全出本地可用的分支出来.
要实现这个,我们需要一个辅助函数,这个辅助函数将 shell-command
的输出转换成一个elisp列表. 这个应该很容易实现.
变量 pcmpl-git-ref-list-cmd
中保存的是我们希望Emacs运行的shell命令. 这个命令会返回repo中的所有ref,然后我们再根据ref的子类型进行过滤 (heads, tags, etc.).
函数 pcmpl-git-get-refs
接受一个参数,type,用于作为过滤ref的类型参数.
(defvar pcmpl-git-ref-list-cmd "git for-each-ref refs/ --format='%(refname)'" "The `git' command to run to get a list of refs") (defun pcmpl-git-get-refs (type) "Return a list of `git' refs filtered by TYPE" (with-temp-buffer (insert (shell-command-to-string pcmpl-git-ref-list-cmd)) (goto-char (point-min)) (let ((ref-list)) (while (re-search-forward (concat "^refs/" type "/\\(.+\\)$") nil t) (add-to-list 'ref-list (match-string 1))) ref-list)))
最后,我们把这些代码全部整合起来. 为了让代码更清晰,我改成用cond语句来保证可读性.
(defconst pcmpl-git-commands '("add" "bisect" "branch" "checkout" "clone" "commit" "diff" "fetch" "grep" "init" "log" "merge" "mv" "pull" "push" "rebase" "reset" "rm" "show" "status" "tag" ) "List of `git' commands") (defvar pcmpl-git-ref-list-cmd "git for-each-ref refs/ --format='%(refname)'" "The `git' command to run to get a list of refs") (defun pcmpl-git-get-refs (type) "Return a list of `git' refs filtered by TYPE" (with-temp-buffer (insert (shell-command-to-string pcmpl-git-ref-list-cmd)) (goto-char (point-min)) (let ((ref-list)) (while (re-search-forward (concat "^refs/" type "/\\(.+\\)$") nil t) (add-to-list 'ref-list (match-string 1))) ref-list))) (defun pcomplete/git () "Completion for `git'" ;; Completion for the command argument. (pcomplete-here* pcmpl-git-commands) ;; complete files/dirs forever if the command is `add' or `rm' (cond ((pcomplete-match (regexp-opt '("add" "rm")) 1) (while (pcomplete-here (pcomplete-entries)))) ;; provide branch completion for the command `checkout'. ((pcomplete-match "checkout" 1) (pcomplete-here* (pcmpl-git-get-refs "heads")))))
这就完成了. 为git命令而创建的简单补全机制. 把这些代码放在你的 .emacs
或其他初始化文件中就能用了.