Emacs Lisp: Prompting for new file creation, part 1

This is the beginning of a discussion on methods of prompting the user for new file creation, with the emphasis on the "new": this is for when you want to make sure the file doesn't exist yet.

So here's the situation: I was working on an emacs extension to provide some shortcuts for perl programming: perlnow.el. It has one command for creating a new perl script, and another for creating a new perl module. Both of them need to prompt the user for some information about what to create.

This kind of thing is supposed to be easy with emacs. You can do things like this:

(defun gimme-something (filename)
  "Prompt the user for a filename, just to be annoying."
  (interactive "fGimme: ")
  (message "So what do I do with this: %s ?" filename))

And if you're reading this inside of emacs, you can position your cursor after the last parenthesis and do a C-x C-e to evaluate this. Then you can play with it: "M-x gimme-something".

That special interactive code "f" prompts you for a filename, (with prompt "Gimme: "), and it gives you all the helpful interactive features we've come to expect -- since emacs set the standard: The current default-directory shows up in the prompt, ready to be edited into some other form, there are history commands (M-p and M-n), you can do TAB or SPACE to automatically complete the names of files or directories after first typing a fragment, and if the fragment isn't unique, you get a *Help* buffer popping up, showing you a list of possible completions.

There are oodles of these special codes, practically one for every occasion. You can look them up in the Emacs Lisp Reference Manual (available in info format, of course, see the node: Code Characters for `interactive').

This particular special code, "f" wasn't quite right for me, because it's for when you're looking for an existing file. My application was for creating a new file, and in fact, it would probably be a bad idea for the user to be allowed to run it on an existing file. That would almost certainly be a mistake, since one of the things the command is supposed to do is insert some boilerplate (the hashbang line, "use strict", a pod framework, and so on).

So I could use "F" instead of "f", since that doesn't require that the file exist already. But for my application it is required that the file doesn't exist, which is a somewhat different case. And further, I'd like to have an alternate prompt that tells the user something like: "Sorry that exists, try again."

So I can't use this simple form of interactive read. But luckilly emacs has lots of lower level primitives, in case you're and oddball like myself (though you know, this doesn't seem like that odd a thing to want to do...).

After some scrounging around, I came to the conclusion I needed to use "read-file-name" to do the job, and that it needed to be stuck in the following framework, which despite being brief has a few confusing things in, (which makes it perfect column fodder):

(defun perlnow-do-script-stub (filename)
  "Quickly jump into development of a new perl script.
Prompts the user for the FILENAME."
  (interactive
   (perlnow-prompt-user-for-file-to-create
    "Name for the new perl script? " perlnow-script-location))
   ;;; ... skipping the real code from "perlnow-do-script" ...
  (message "So this will be the new file: %s ?" filename))
(defun perlnow-prompt-user-for-file-to-create (ask-mess default-location)
  "Ask for the name of the file to create.
Check to see if one exists already, and if so, ask for another name.
Asks the question ASK-MESS, and defaults to the using the location
DEFAULT-LOCATION.  Returns a list of a single string, full file name
with path."
  (let ( filename )
  (setq default-location (file-name-as-directory default-location))
  (while (progn
           (setq filename
                 (expand-file-name
                  (read-file-name ask-mess default-location)))
           (setq ask-mess
                 "That name is already in use, please use another name: " )
           (file-exists-p filename)))
  (list filename)))

Where to begin? Well, first of all, you might wonder why it's two functions rather than one: this way the second function can be re-used in other contexts (and I certainly have... you might note that the second question, while hardcoded, is written pretty generally so you can use this for any kind of new file creation job).

By the way, you do realize that Emacs "interactive" functions are two-faced in that they don't have to be called interactively. You can use this in a program like so:

   (perlnow-do-script-sub "/tmp/somefile.txt")

What happens to the prompt business in that case? It just gets skipped. None of the code inside the "(interactive ... )" construct gets run if the code is called in a non-interactive context... though you can force an interactive context from within a program like so:

   (call-interactively
      (perlnow-do-script-sub))

More about that trick later.

Notice the way this flows: there's an argument list with a single variable called "filename" and the value of that variable can come from two places: If it's called non-interactively, it's fed in from wherever it was called; but if it's called interactively you look down into the code, and whatever's going on inside the (interactive ... ) construct eventually has to return something that becomes the list of arguments.

And that's definitely a *list* of arguments. Even in this case where there's only one item, You've got to get it into list form to get it to work: (list filename).

Here's something simple I've been bitten on with more complicated code: Look inside perlnow-do-script-stub. There's no "let" statement to declare a local value of "filename"... does that mean it's a global? Nah, the names in the argument list have an implicit "let" done on them. (Yet another sign that we're not perl programming any more...)

Anyway the last subject of some interest here is this mess, which believe-it-or-not, appears to be the Right Way of implementing a repeat/until loop in elisp:

  (while (progn
           (setq filename
                 (expand-file-name
                  (read-file-name ask-mess default-location)))
           (setq ask-mess
                 "That name is already in use, please use another name: " )
           (file-exists-p filename)))

What this is, is an empty while loop, the actual code is all inside the loop condition. Then the lisp oddity "progn" is used to glue the multiple statements inside the condition together, and further progn handles passing through the results from the final statement into the "while". So here the logical results of "file-exists-p" becomes the loop exit condition. The first time through the loop we have the given value of "ask-mess", the second time through (if any) it has been changed to the generic "already in use" message (it might seem inelegant that we always change the value of ask-mess, even if we don't need to, but what the hell).

Oh, and what's with the "expand-file-name"? I threw that in to make sure that "~/" gets expanded into the absolute path to your home directory.

Want more? NEXT.


Further discussion of Emacs Lisp: Devnotes


Joseph Brenner, 28 Feb 2004