15. Practical: A Portable Pathname Library

As I discussed in the previous chapter, Common Lisp provides an abstraction, the pathname, that's supposed to insulate you from the details of how different operating systems and file systems name files. Pathnames provide a useful API for manipulating names as names, but when it comes to the functions that actually interact with the file system, things get a bit hairy.

The root of the problem, as I mentioned, is that the pathname abstraction was designed to represent filenames on a much wider variety of file systems than are commonly used now. Unfortunately, by making pathnames abstract enough to account for a wide variety of file systems, Common Lisp's designers left implementers with a fair number of choices to make about how exactly to map the pathname abstraction onto any particular file system. Consequently, different implementers, each implementing the pathname abstraction for the same file system, just by making different choices at a few key junctions, could end up with conforming implementations that nonetheless provide different behavior for several of the main pathname-related functions.

However, one way or another, all implementations provide the same basic functionality, so it's not too hard to write a library that provides a consistent interface for the most common operations across different implementations. That's your task for this chapter. In addition to giving you several useful functions that you'll use in future chapters, writing this library will give you a chance to learn how to write code that deals with differences between implementations.

The API

The basic operations the library will support will be getting a list of files in a directory and determining whether a file or directory with a given name exists. You'll also write a function for recursively walking a directory hierarchy, calling a given function for each pathname in the tree.

In theory, these directory listing and file existence operations are already provided by the standard functions DIRECTORY and PROBE-FILE. However, as you'll see, there are enough different ways to implement these functions--all within the bounds of valid interpretations of the language standard--that you'll want to write new functions that provide a consistent behavior across implementations.

*FEATURES* and Read-Time Conditionalization

Before you can implement this API in a library that will run correctly on multiple Common Lisp implementations, I need to show you the mechanism for writing implementation-specific code.

While most of the code you write can be "portable" in the sense that it will run the same on any conforming Common Lisp implementation, you may occasionally need to rely on implementation-specific functionality or to write slightly different bits of code for different implementations. To allow you to do so without totally destroying the portability of your code, Common Lisp provides a mechanism, called read-time conditionalization, that allows you to conditionally include code based on various features such as what implementation it's being run in.

The mechanism consists of a variable *FEATURES* and two extra bits of syntax understood by the Lisp reader. *FEATURES* is a list of symbols; each symbol represents a "feature" that's present in the implementation or on the underlying platform. These symbols are then used in feature expressions that evaluate to true or false depending on whether the symbols in the expression are present in *FEATURES*. The simplest feature expression is a single symbol; the expression is true if the symbol is in *FEATURES* and false if it isn't. Other feature expressions are boolean expressions built out of NOT, AND, and OR operators. For instance, if you wanted to conditionalize some code to be included only if the features foo and bar were present, you could write the feature expression (and foo bar).

The reader uses feature expressions in conjunction with two bits of syntax, #+ and #-. When the reader sees either of these bits of syntax, it first reads a feature expression and then evaluates it as I just described. When a feature expression following a #+ is true, the reader reads the next expression normally. Otherwise it skips the next expression, treating it as whitespace. #- works the same way except it reads the form if the feature expression is false and skips it if it's true.

The initial value of *FEATURES* is implementation dependent, and what functionality is implied by the presence of any given symbol is likewise defined by the implementation. However, all implementations include at least one symbol that indicates what implementation it is. For instance, Allegro Common Lisp includes the symbol :allegro, CLISP includes :clisp, SBCL includes :sbcl, and CMUCL includes :cmu. To avoid dependencies on packages that may or may not exist in different implementations, the symbols in *FEATURES* are usually keywords, and the reader binds *PACKAGE* to the KEYWORD package while reading feature expressions. Thus, a name with no package qualification will be read as a keyword symbol. So, you could write a function that behaves slightly differently in each of the implementations just mentioned like this:

(defun foo ()
  #+allegro (do-one-thing)
  #+sbcl (do-another-thing)
  #+clisp (something-else)
  #+cmu (yet-another-version)
  #-(or allegro sbcl clisp cmu) (error "Not implemented"))

In Allegro that code will be read as if it had been written like this:

(defun foo ()
  (do-one-thing))

while in SBCL the reader will read this:

(defun foo ()
  (do-another-thing))

while in an implementation other than one of the ones specifically conditionalized, it will read this:

(defun foo ()
  (error "Not implemented"))

Because the conditionalization happens in the reader, the compiler doesn't even see expressions that are skipped.1 This means you pay no runtime cost for having different versions for different implementations. Also, when the reader skips conditionalized expressions, it doesn't bother interning symbols, so the skipped expressions can safely contain symbols from packages that may not exist in other implementations.

Packaging the Library

Listing a Directory

You can implement the function for listing a single directory, list-directory, as a thin wrapper around the standard function DIRECTORY. DIRECTORY takes a special kind of pathname, called a wild pathname, that has one or more components containing the special value :wild and returns a list of pathnames representing files in the file system that match the wild pathname.2 The matching algorithm--like most things having to do with the interaction between Lisp and a particular file system--isn't defined by the language standard, but most implementations on Unix and Windows follow the same basic scheme.

The DIRECTORY function has two problems that you need to address with list-directory. The main one is that certain aspects of its behavior differ fairly significantly between different Common Lisp implementations, even on the same operating system. The other is that while DIRECTORY provides a powerful interface for listing files, to use it properly requires understanding some rather subtle points about the pathname abstraction. Between these subtleties and the idiosyncrasies of different implementations, actually writing portable code that uses DIRECTORY to do something as simple as listing all the files and subdirectories in a single directory can be a frustrating experience. You can deal with those subtleties and idiosyncrasies once and for all, by writing list-directory, and forget them thereafter.

One subtlety I discussed in Chapter 14 is the two ways to represent the name of a directory as a pathname: directory form and file form.

To get DIRECTORY to return a list of files in /home/peter/, you need to pass it a wild pathname whose directory component is the directory you want to list and whose name and type components are :wild. Thus, to get a listing of the files in /home/peter/, it might seem you could write this:

(directory (make-pathname :name :wild :type :wild :defaults home-dir))

where home-dir is a pathname representing /home/peter/. This would work if home-dir were in directory form. But if it were in file form--for example, if it had been created by parsing the namestring "/home/peter"--then that same expression would list all the files in /home since the name component "peter" would be replaced with :wild.

To avoid having to worry about explicitly converting between representations, you can define list-directory to accept a nonwild pathname in either form, which it will then convert to the appropriate wild pathname.

To help with this, you should define a few helper functions. One, component-present-p, will test whether a given component of a pathname is "present," meaning neither NIL nor the special value :unspecific.3 Another, directory-pathname-p, tests whether a pathname is already in directory form, and the third, pathname-as-directory, converts any pathname to a directory form pathname.

(defun component-present-p (value)
  (and value (not (eql value :unspecific))))

(defun directory-pathname-p  (p)
  (and
   (not (component-present-p (pathname-name p)))
   (not (component-present-p (pathname-type p)))
   p))

(defun pathname-as-directory (name)
  (let ((pathname (pathname name)))
    (when (wild-pathname-p pathname)
      (error "Can't reliably convert wild pathnames."))
    (if (not (directory-pathname-p name))
      (make-pathname
       :directory (append (or (pathname-directory pathname) (list :relative))
                          (list (file-namestring pathname)))
       :name      nil
       :type      nil
       :defaults pathname)
      pathname)))

Now it seems you could generate a wild pathname to pass to DIRECTORY by calling MAKE-PATHNAME with a directory form name returned by pathname-as-directory. Unfortunately, it's not quite that simple, thanks to a quirk in CLISP's implementation of DIRECTORY. In CLISP, DIRECTORY won't return files with no extension unless the type component of the wildcard is NIL rather than :wild. So you can define a function, directory-wildcard, that takes a pathname in either directory or file form and returns a proper wildcard for the given implementation using read-time conditionalization to make a pathname with a :wild type component in all implementations except for CLISP and NIL in CLISP.

(defun directory-wildcard (dirname)
  (make-pathname
   :name :wild
   :type #-clisp :wild #+clisp nil
   :defaults (pathname-as-directory dirname)))

Note how each read-time conditional operates at the level of a single expression After #-clisp, the expression :wild is either read or skipped; likewise, after #+clisp, the NIL is read or skipped.

Now you can take a first crack at the list-directory function.

(defun list-directory (dirname)
  (when (wild-pathname-p dirname)
    (error "Can only list concrete directory names."))
  (directory (directory-wildcard dirname)))

As it stands, this function would work in SBCL, CMUCL, and LispWorks. Unfortunately, a couple more implementation differences remain to be smoothed over. One is that not all implementations will return subdirectories of the given directory. Allegro, SBCL, CMUCL, and LispWorks do. OpenMCL doesn't by default but will if you pass DIRECTORY a true value via the implementation-specific keyword argument :directories. CLISP's DIRECTORY returns subdirectories only when it's passed a wildcard pathname with :wild as the last element of the directory component and NIL name and type components. In this case, it returns only subdirectories, so you'll need to call DIRECTORY twice with different wildcards and combine the results.

Once you get all the implementations returning directories, you'll discover they can also differ in whether they return the names of directories in directory or file form. You want list-directory to always return directory names in directory form so you can differentiate subdirectories from regular files based on just the name. Except for Allegro, all the implementations this library will support do that. Allegro, on the other hand, requires you to pass DIRECTORY the implementation-specific keyword argument :directories-are-files NIL to get it to return directories in file form.

Once you know how to make each implementation do what you want, actually writing list-directory is simply a matter of combining the different versions using read-time conditionals.

(defun list-directory (dirname)
  (when (wild-pathname-p dirname)
    (error "Can only list concrete directory names."))
  (let ((wildcard (directory-wildcard dirname)))

    #+(or sbcl cmu lispworks)
    (directory wildcard)

    #+openmcl
    (directory wildcard :directories t)

    #+allegro
    (directory wildcard :directories-are-files nil)

    #+clisp
    (nconc
     (directory wildcard)
     (directory (clisp-subdirectories-wildcard wildcard)))

    #-(or sbcl cmu lispworks openmcl allegro clisp)
    (error "list-directory not implemented")))

The function clisp-subdirectories-wildcard isn't actually specific to CLISP, but since it isn't needed by any other implementation, you can guard its definition with a read-time conditional. In this case, since the expression following the #+ is the whole DEFUN, the whole function definition will be included or not, depending on whether clisp is present in *FEATURES*.

#+clisp
(defun clisp-subdirectories-wildcard (wildcard)
  (make-pathname
   :directory (append (pathname-directory wildcard) (list :wild))
   :name nil
   :type nil
   :defaults wildcard))

Testing a File's Existence

To replace PROBE-FILE, you can define a function called file-exists-p. It should accept a pathname and return an equivalent pathname if the file exists and NIL if it doesn't. It should be able to accept the name of a directory in either directory or file form but should always return a directory form pathname if the file exists and is a directory. This will allow you to use file-exists-p, along with directory-pathname-p, to test whether an arbitrary name is the name of a file or directory.

In theory, file-exists-p is quite similar to the standard function PROBE-FILE; indeed, in several implementations--SBCL, LispWorks, and OpenMCL--PROBE-FILE already gives you the behavior you want for file-exists-p. But not all implementations of PROBE-FILE behave quite the same.

Allegro and CMUCL's PROBE-FILE functions are close to what you need--they will accept the name of a directory in either form but, instead of returning a directory form name, simply return the name in the same form as the argument it was passed. Luckily, if passed the name of a nondirectory in directory form, they return NIL. So with those implementations you can get the behavior you want by first passing the name to PROBE-FILE in directory form--if the file exists and is a directory, it will return the directory form name. If that call returns NIL, then you try again with a file form name.

CLISP, on the other hand, once again has its own way of doing things. Its PROBE-FILE immediately signals an error if passed a name in directory form, regardless of whether a file or directory exists with that name. It also signals an error if passed a name in file form that's actually the name of a directory. For testing whether a directory exists, CLISP provides its own function: probe-directory (in the ext package). This is almost the mirror image of PROBE-FILE: it signals an error if passed a name in file form or if passed a name in directory form that happens to name a file. The only difference is it returns T rather than a pathname when the named directory exists.

But even in CLISP you can implement the desired semantics by wrapping the calls to PROBE-FILE and probe-directory in IGNORE-ERRORS.4

(defun file-exists-p (pathname)
  #+(or sbcl lispworks openmcl)
  (probe-file pathname)

  #+(or allegro cmu)
  (or (probe-file (pathname-as-directory pathname))
      (probe-file pathname))

  #+clisp
  (or (ignore-errors
        (probe-file (pathname-as-file pathname)))
      (ignore-errors
        (let ((directory-form (pathname-as-directory pathname)))
          (when (ext:probe-directory directory-form)
            directory-form))))

  #-(or sbcl cmu lispworks openmcl allegro clisp)
  (error "file-exists-p not implemented"))

The function pathname-as-file that you need for the CLISP implementation of file-exists-p is the inverse of the previously defined pathname-as-directory, returning a pathname that's the file form equivalent of its argument. This function, despite being needed here only by CLISP, is generally useful, so define it for all implementations and make it part of the library.

(defun pathname-as-file (name)
  (let ((pathname (pathname name)))
    (when (wild-pathname-p pathname)
      (error "Can't reliably convert wild pathnames."))
    (if (directory-pathname-p name)
      (let* ((directory (pathname-directory pathname))
             (name-and-type (pathname (first (last directory)))))
        (make-pathname
         :directory (butlast directory)
         :name (pathname-name name-and-type)
         :type (pathname-type name-and-type)
         :defaults pathname))
      pathname)))

Walking a Directory Tree

Finally, to round out this library, you can implement a function called walk-directory. Unlike the functions defined previously, this function doesn't need to do much of anything to smooth over implementation differences; it just needs to use the functions you've already defined. However, it's quite handy, and you'll use it several times in subsequent chapters. It will take the name of a directory and a function and call the function on the pathnames of all the files under the directory, recursively. It will also take two keyword arguments: :directories and :test. When :directories is true, it will call the function on the pathnames of directories as well as regular files. The :test argument, if provided, specifies another function that's invoked on each pathname before the main function is; the main function will be called only if the test function returns true.

(defun walk-directory (dirname fn &key directories (test (constantly t)))
  (labels
      ((walk (name)
         (cond
           ((directory-pathname-p name)
            (when (and directories (funcall test name))
              (funcall fn name))
            (dolist (x (list-directory name)) (walk x)))
           ((funcall test name) (funcall fn name)))))
    (walk (pathname-as-directory dirname))))

Now you have a useful library of functions for dealing with pathnames. As I mentioned, these functions will come in handy in later chapters, particularly Chapters 23 and 27, where you'll use walk-directory to crawl through directory trees containing spam messages and MP3 files. But before we get to that, though, I need to talk about object orientation, the topic of the next two chapters.


1One slightly annoying consequence of the way read-time conditionalization works is that there's no easy way to write a fall-through case. For example, if you add support for another implementation to foo by adding another expression guarded with #+, you need to remember to also add the same feature to the or feature expression after the #- or the ERROR form will be evaluated after your new code runs.

2Another special value, :wild-inferiors, can appear as part of the directory component of a wild pathname, but you won't need it in this chapter.

3Implementations are allowed to return :unspecific instead of NIL as the value of pathname components in certain situations such as when the component isn't used by that implementation.

4This is slightly broken in the sense that if PROBE-FILE signals an error for some other reason, this code will interpret it incorrectly. Unfortunately, the CLISP documentation doesn't specify what errors might be signaled by PROBE-FILE and probe-directory, and experimentation seems to show that they signal simple-file-errors in most erroneous situations.