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 FILE a 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