PComplete: Context-Sensitive Completion in Emacs
In my What’s New In Emacs 24 series (part one, part two) I briefly mentioned that pcomplete, the programmable completion library featured prominently in Eshell, now supports M-x shell out of the box. That’s great news for shell mode fans as the completion mechanism adds a lot of nifty functionality to a mode that lacks the native completion provided by underlying the shell itself.
The most amazing thing about the completion mechanism is that it has been in Emacs for ages but never made much of a public appearance and has gone virtually unnoticed due to its limited use in Emacs. In fact, I think it’s only used in EShell, ERC, Org Mode and now, finally, Shell Mode.
Programmable, Context-Sensitive Completion
To use pcomplete you won’t have to do anything, because as of Emacs 24 it is now supported automatically when you launch a new shell session. Emacs ships with a handful of pcomplete functions that enhance the otherwise drab filename completion with context-sensitive completion similar to what you can do with bash/zsh completion. Of particular note is the scp, ssh, mount, umount and make support.
The following table lists the commands supported by shell mode (or indeed any mode that supports pcomplete, including Eshell.)
- bzip2
Completes arguments and lists only bzipped files.
- cd
Completes directories.
- chgrp
Completes list of known groups on the system.
- chown
Completes user and group perms, but only if you use
user.group.- cvs
Completes commands and parameter options and cvs entries and modules.
- gdb
Completes only directories or files with eXecute permission.
- gzip
Completes arguments and lists only gzipped files.
- kill
Lists signals if completed with just a
-, otherwise it completes all system PIDs.- make
Completes arguments and valid makefiles in the directory; if a valid makefile is completed with
make -f FILEa list of rule names from the file itself are completed.- mount
Completes arguments and valid filesystem types if completed with
mount -t TYPE.- pushd
Identical to
cd.- rm
Completes arguments and filenames and directories.
- rmdir
Completes directories.
- rpm
Very sophisticated completion mechanism for most of rpm, the Redhat Package Manager. Context-sensitive completion for almost all commands, including package lookup.
- scp
Completes arguments, SSH known hosts and remote file lookup (using TRAMP) if the format is
scp host:/.- ssh
Completes arguments and SSH known hosts.
- tar
Completes arguments, including context-sensitive completion for POSIX arguments, and file name completion.
- time
Completes directories and files with eXecutable permission.
- umount
Completes arguments, mounted directories and filesystem types (like
mount)- which
Supposed to provide simple filename completion of all known binaries (wouldn’t be useful otherwise!) but appears to not work right.
- xargs
Completes directories and files with eXecutable permission.
Custom Completion
It goes without saying that a completion library called programmable completion is, well, programmable.
Adding simple parameter completion is an easy job but anything more than that and it gets hairy as, not surprisingly, this library is virtually undocumented (though an optimist would say the source is all the documentation you need…)
I’ll demonstrate how to add rudimentary support for git.
The first thing we need to do is establish the order in which parameters must be given; for git, it’s somewhat consistent: git [options] <command> [<args>]
For now I’ll stick to the commands as that’s what people use the most anyway. The commands, in list form, are:
(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")The syntax for pcomplete is rather clever: it will use dynamic dispatch to resolve the elisp function provided it is named a certain way. All commands are named pcomplete/COMMAND or pcomplete/MAJOR-MODE/COMMAND. Provided you follow that naming scheme your command will automagically work.
Next, we need to present a list of valid commands – in this case the ones in pcmpl-git-commands, but it could be any form – to the command pcomplete-here.
(defun pcomplete/git ()
"Completion for `git'"
(pcomplete-here* pcmpl-git-commands))Now when you try to tab-complete the first argument to git it will list our commands. Sweet.
Let’s extend it further by adding support for the add and rm commands. I want the aforementioned commands to provide the standard filename/filepath completion if, and only if, the command is add or rm.
This is surprisingly easy to do using pcomplete-match, a function that asserts a certain regexp matches a particular function argument index. Note that the call to pcomplete-here is in a while loop; this is so you can complete as many files as you like, one after another. One advantage of pcomplete-here is that it won’t display files you have already completed earlier in the argument trail – that’s very useful for a command like 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, that was easy. Now let’s make it a bit more dynamic by extending our code to support the git checkout command so it will complete the list of branches available to us locally.
To do this we need a helper function that takes the output of a call to shell-command and maps it to an internal elisp list. This is easily done with some quick hackery.
The variable pcmpl-git-ref-list-cmd holds the shell command we want Emacs to run for us. It gets every ref there is and we then filter by sub-type (heads, tags, etc.) later. The function pcmpl-git-get-refs takes one argument, type, which is the ref type to filter by.
(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)))And finally, we put it all together. To keep the code clean I’ve switched to using a cond form for readability.
(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")))))And that’s that. A simple completion mechanism for git. Put this in your .emacs or init file and you’re done.
/code
