Emacs Lisp: Prompting for new file creation, part 2

So as discussed in part I, the project at hand was the "perlnow.el" package, last time I talked about writing user prompts for the new script creation function, this time I'm going to talk about the prompts for a function that handles creating a new module. Why is this so different? Because a perl module has a name space of it's own embedded within/overlapping the file system name space:

   /usr/lib/perl/Modular/Stuff.pm
   /usr/lib/perl/ Modular::Stuff

You can't just enter the usual full path to a file, there's an additional piece of information that needs to be input somehow: the point where you've entered perl's module namespace.

The simplest thing to do is probably just to ask the user two questions. Using "interactive" it turns out to be trivially easy to prompt for two pieces of info (though you need to hunt through the examples in The GNU Emacs Lisp Reference Manual to get it):

  (interactive "DSome directory: \nsSome string: ")

That prompts for a directory, then prompts for a string. The "\n" in the middle is the trick you need to know for pasting together multiple interactive codes.

So that's a possible approach, first we'll ask for the location of the module, then we'll ask for it's name in double-colon separated form, and input that just as a string.

First on the subject of reading in directory names.

A quick check of "Code Characters for `interactive'" shows that there's only one code character for reading directories, the "D":

(defun gimme-a-dir (directory)
  "Asking for it."
  (interactive "DWhere to? ")
  (message "You can go here: %s" directory))

But this uses the current "default-directory" as the default. I preferred to be able to tell it to use a different standard location (most likely, for a given project there's going to be one place where you're going to want to put the modules you're creating). So should I just set the default-directory to the module location before doing this? Wait a minute, where can I set the default-directory? I don't want to have to do it every time before this code is called, and when you're using one of these interactive special codes, there's no place to stick in a line of code before it goes into the prompt. (Or if there is, I just don't know where it is... emacs has more hooks than the old Cadillac Ranch, so you want to be careful about saying things like this). Any way, I'd rather not have a function that changes the default-directory as a side effect (I might have to change it, then remember to change it back).

This is the trick I came up with:

(defun perlnow-module-two-questions-stub (inc-spot package-name)
  "Quickly jump into development of a new perl module.  Asks
the user two questions to get the INC-SPOT and the PACKAGE-NAME."
  (interactive
   (let ((default-directory perlnow-module-location))
     (call-interactively 'perlnow-prompt-for-module-to-create)))
   ; ... real code ellided for this stub ...
   (message "%s %s" inc-spot package-name))

(defun perlnow-prompt-for-module-to-create (where what)
  "Internally used by \\[perlnow-module-two-questions\] to ask the two questions.
Asks for the WHERE, i.e. the \"module root\" location, and the WHAT, the name
of the perl module to create there.  Checks to see if one exists already,
and if so, asks for another name.  The location defaults to the current
`default-directory'.  Returns a two element list, location and package-name."

  (interactive "DLocation for new module?  \nsName of new module \(e.g. New::Module\)? ")
  (let* ((filename (perlnow-full-path-to-module where what))
         (dirname (convert-standard-filename (file-name-directory filename))))
  (while (file-exists-p filename)
    (setq what
          (read-from-minibuffer "That module name is already in use. Please choose another: " what))
    (setq filename (perlnow-full-path-to-module where what)))
  (list where what)))

Note that the perlnow-full-path-to-module function is a bit "beyond the scope", though there's nothing particularly involved about it. See the Appendix below.

The gimmick here is that I set the default-directory variable in the first function, then call the second using "call-interactively", as a way making use of the existing single letter code forms of interactive.

This makes use of the fact that the emacs "let" does "dynamic scoping" rather than "lexical scoping" (and if you don't know the difference, all I can say is you're fortunate to have never had to think about it. Note to perl programmers: "let" is like perl's "local", elisp has no native equivalent of perl's "my", though I think there's a "lexical-let" in the cl.el library). What "dynamic scope" means is that any other functions that are called by a function see the values assigned using "let" in that first function. Got that? The idea of "lexical scope" is for each function to be it's own little world, but with "dynamic scope", the calling function can mess with the values used in the called functions.

An added bonus is that when the first function completes, the original value of default-directory is restored, without any additional work.

There's a little more information about working with interactive functions below in appendix 2: Living with Two Faces.

But okay, so this approach works, more-or-less. But it has an unfortunate limitation: Auto completion only works for the directory read, not for the module name, which is just being read in as a string. The leading portion of module names map to the file system tree, and there's no reason in principle that you shouldn't have auto-completion for the "Text::Munger::" portion of the name when creating a new "Text::Munger::Fancy".

An even worse problem though, is that I personally found the double prompt to be very annoying. In switching back and forth between this and the single prompt perlnow-script (see Part I), I kept expecting perlnow-module to have only a single prompt also. I would make mistakes like entering this to the directory prompt:

   /home/doom/lib/Text::Munger::Fancy

It didn't help that directory autocompletion made it easy to insert the first level of the module name ("Text/" with a slash of course) by accident.

So... I began looking for a way to implement a function that would know how to understand this single-prompt form of input that I kept entering by mistake.

And on to Part III: NEXT


Appendix 1:

Some excerpts from the perlnow.el package that might (or might not) make the above code examples a little easier to follow:

(defconst perlnow-slash (convert-standard-filename "/")
  "A \(possibly\) more portable form of the file system name separator.")
; Using this instead of "/", as a stab at portability (e.g. for windows).
; But even if this helps, there are still other places
; dependencies have crept in, e.g. patterns that use [^/].
(defun perlnow-full-path-to-module (inc-spot package-name)
  "Piece together a INC-SPOT and a PACKAGE-NAME into a full file name.
Given \"/home/doom/lib\" and the perl-style \"Text::Gibberish\" would
yield /home/doom/lib/Text/Gibberish.pm or in other words, the
file-system path."
  (let ((filename
         (concat
          (mapconcat 'identity (split-string package-name "::") perlnow-slash)
          ".pm")))
  (setq inc-spot (file-name-as-directory inc-spot))
  (concat  inc-spot filename)))

That's clear, I hope. The split-string subdivides the perl-style package name into the different levels of the package name space, then the mapconcat glues it together using a "/" in place of each "::". This probably could've been done just as easily with

  (replace-regexp-in-string "::" "/" package-name)

Why didn't I? I don't know. Some days one method comes to mind, some days another.

Anyway, then the rest of the path passed in as the inc-spot is prepended to get the full path to the module file. That "file-name-as-directory" function (mostly) just makes sure there's a trailing slash on the end, so I can concat the two together.

Appendix 2: Living with Two-faces

As I was saying in chapter one of our saga: "you do realize that Emacs "interactive" functions are two-faced in that they don't *have* to be called interactively"...

Arguably a well-written function should keep all of the interactive stuff hidden inside the interactive at the beginning of the function. That way if it's called non-interactively all of that interactive stuff will be skipped. But that means you're supposed to avoid doing anything inside the "interactive" that's necessary in the non-interactive case. (Though you can check the context the function is called in with an interactive-p, and have a second block of conditional code that carefully replicates all of the side-effects of the interactive stuff... is your brain hurting yet?).

My recommendation is: don't be afraid to punt. It's okay, if not ideal, to stick a note in the docstring saying "Don't use this non-interactively." If you want to be classy about it, provide some suggestions as to what a programmer might want to do instead. The confusion you save may be your own.


On to Part III: NEXT

Further discussion of Emacs Lisp: Devnotes


Joseph Brenner, 28 Feb 2004