Mike McClurg

Emacs!

with 2 comments

I love Emacs. I’ve been using it ever since I took my first programming language theory class in college. I had played around with it before then, but it took my professor’s recommendation and half a class worth of a tutorial and I was hooked.

We studied Scheme in that class, which is why my prof recommended that we all use Emacs. Scheme is a variant of Lisp, which is the language that Emacs is built around. And Elisp, as it’s called, is a great embedded language for customizing Emacs. You could think of it as Visual Basic for Applications, which is Microsoft’s language for customizing Office products, but much more simple and powerful to use (as long as parentheses don’t scare you).

I’ve only got one or two friends that might consider themselves to be Emacs gurus, and one of them wrote me the other day with a challenge. “I believe this problem is trivial,” he said, which is never a good sign, “but I’ve only had 20 minutes to think about and I’ve got to run.” He had been working on an Emacs library to help him configure a massive Ant build script. Part of his build process involved him logging into numerous test and dev machines, and he wanted to automate that in Emacs. He had written a list of interactive functions that would allow him to rlogin into each machine by typing M-x machine (M stands for Alt, or Meta, in Emacs, and is used to specify commands from the keyboard). These functions were each defined with syntax like the following:

(defun machine1 ()
  (interactive)
  (rlogin "machine1" "*machine1*"))

 

This list of functions had grown quite a bit in the months since he implemented this feature, and he wanted to find a way to automatically define a function that would log him into a specified machine by simply typing M-x machine. Easy, right?

A first attempt

My first approach to this problem was to create an association list of functions and their names. We would create a single interactive function that would select and run the appropriate generated function.

(defun my-login (fn)
  (interactive "sWhich function? ")
  (funcall
    (cdr (assoc (read fn) my-fnassoc)))))

There are a few things going on in this function. The (interactive "sWhich function? ") statement tells Emacs that this function can be called interactively using the M-x my-login syntax. The strange string “sWhich function? ” is just a format string telling Emacs to prompt the user for a string argument (the “s”) with the prompt “Which function? “. If this were a multivariate function, we would simply break each format string with a “\n” character in order to prompt for each of our arguments. The read function just turns our argument string into a symbol, and the call (cdr (assoc (read fn) fnassoc)) looks the symbol up in the association list my-fnassoc. funcall executes the returned function. Now we just need to build my-fnassoc.

(defun mkfunls (name)
  (let* ((host (prin1-to-string name))
         (buff (format "*%s*" host)))
    `(,name . (lambda () (rlogin ,host ,buff)))))

This function creates a single association pair of the form (host . login-function). Since our name parameter is a symbol, we need to convert it to a string using prin1-to-string. The Emacs rlogin command allows us to specify the name of the buffer in which we spawn the remote shell. We can use the format function to create a standard buffer name for each rlogin session we create.

The final line of mkfunls takes advantage of Lisp’s backquoting feature. The syntax looks a little strange at first, but if you can put up with Lisp’s other oddities then there’s no sense not learning about backquoting too. Quoting in Lisp, accomplished with either the quote special form or the abbreviation ' (a single apostrophe). Quoting prevents evaluation of the form being quoted, so evaluating (+ 1 2) returns the value 3, while evaluating (quote (+ 1 2)), or the equivalent '(+ 1 2), returns the value (+ 1 2) instead.

Backquoting accomplishes the same thing, but allows the programmer to selectively specify which parts of a list are to be evaluated and which parts aren’t. It’s called backquoting because we use a backwards apostrophe (` — found under the tilde) instead of quote or the regular apostrophe. We select the forms we want to evaluate using a comma. So the line `(,name . (lambda () (rlogin ,host ,buff))) evaluates the name, host, and buff variables but quotes all the rest of the symbols. So the call (mkfunls 'machine1) evaluates to (machine1 lambda nil (rlogin "machine1" "*machine1*")). Now all that’s left is to create our host/rlogin association list and we’ll be able to log in to remote hosts with a single command.

(setq my-fnassoc (mapcar 'mkfunls '(m1 m2 m3)))

This creates an association list with the host names m1, m2, m3 and sets the variable my-fnassoc. The function mapcar applies it’s first argument, in the case the function mkfunls, to it’s second argument, which must be a list.

Now when we type M-x my-login we receive the prompt “Which host?”, to which we can reply with any of the hosts we specified in the my-fnassoc list above.

But this doesn’t quite solve my buddy’s problem, does it? His list of login functions, while a little cumbersome to maintain, defined interactive functions at the top level, which allowed him to run M-x machine1 instead of the more indirect call to M-x my-login above.

A better solution

What we want is to automatically generate named, interactive functions. My initial thought was that this was a perfect task for macros, but it turns out that we don’t even have to bother with macros to make this work. Instead, we’ll use the backquoting syntax from above to build up defuns and then evaluate them.

(defun mkdefun (name)
  (let* ((host (prin1-to-string name))
         (buff (format "*%s*" host)))
    (eval
     `(defun ,name () (interactive) (rlogin ,host ,buff)))))

This is very similar to our mkfunls function above. Instead of returning an association list, we’re evaluating a defun. We use the backquote syntax to selectively evaluate the name, host, and buff variables. eval then evaluates the defun for us and creates a new top level definition. We can then map over a list of function names like we did before:

(mapc 'mkdefun '(m1 m2 m3))

mapc is like mapcar, except that it doesn’t return the a list of the results of the function evaluations. This is useful when we only care about a function’s side effect and not its results.

So now we have a simple method to automatically generate repetitive Emacs commands. All we have to do is append new function names to the list in the mapc command and we have a new function!

Written by mcclurmc

January 19, 2010 at 1:43 am

Posted in Programming

Tagged with ,

2 Responses

Subscribe to comments with RSS.

  1. Is there some subtly of elisp that I don’t know about that makes spitting code which you then eval better than using macros, which spit code that is then eval’d?

    Matt E.

    April 15, 2010 at 2:58 pm

    • No, I don’t think so. It’s probably just the way Lisp works in my mind, but macros are usually not my first choice for implementing something, if I know there’s a more “functional” way to go about doing it. I had written out a macro do to the job and thought about comparing the two methods, but the post seemed long enough. Maybe I’ll have to dig it up and post a follow-up…

      mcclurmc

      April 18, 2010 at 9:42 pm


Leave a comment