Logical pathnames are a powerful feature of Common Lisp. Abstracting system-specific filenames and locations behind a special hostname. Making UNIX and Windows filenames easily representable with a common format. Making build-specific directories discoverable and reusable, regardless of packaging. Allowing weird shortcut names saving typing. Logical pathnames (LP (not Linkin Park) from now) are cool. Except...
So I (as any Lisper) decided to experiment with these obscure things. And figure out how they actually work. Including the gory details of how implementations do LPs and how to use them “portably”.
This post was prompted by Nicolas Martyanoff’s post on pathnames and the mention of logical pathnames there.
Logical pathnames are host-prefixed semicolon-separated directory+file paths:
;; Implementation-reserved SYS: host
#p"sys:encodings;tools"
;; CCL-specific CCL and HOME
#p"ccl:"
#p"home:ccl-init"
;; Imaginary logical pathnames, might be defined later
#p"junk:test.lisp"
#p"l:"
#p"git:cl-minifloats;cl-minifloats.asd"
#p"site:lisp-design-patterns"
The only mandatory part is the host.
The rest should be directories (separated by semicolons) and files (regular
Which is a significant restriction.
But, given that we—people defining logical pathnames—are a programming minority
and have a luxury of control over our file naming...
We’re fine.
But Logical Pathname path syntax is not the only type of syntax one encounters with these.
There are also match patterns that define what kind of LPs are valid for a host:
Notice the abundance of asterisks there.
These stars are
There are only three entry points defining how logical pathnames are processed:
And that’s it.
The rest is handled by Common Lisp internals, resolving logical pathnames when actually using them.
(Maybe calling
Note that I’m using a UNIX-like notation of forward slash separated dirs/files.
Which makes my logical pathnames less portable than they might be.
If I aimed for portability across Linux, Mac, and Windows, I’d rather do something like
Now, all this syntax looks off, so I’m going to define a macro:
So what happens is that:
This LP I defined is supposed to be a shortcut to an individual file.
So it will be optimal to use it as
Most of these are overkill for a single-file LP.
But the range and the possibilities are clear: you can pattern-match paths of varying complexity with these.
The fun stuff starts when you substitute path parts into the pattern:
Notice the symmetry: we take
The overall heuristic seems to be:
Oh, right!
We can also provide multiple patterns to match paths against,
to be matched in the order of appearance:
What if I tried doing something smarter, like repeating the same wildcard twice in replacement?
So we’re finally hitting the limitations of logical pathnames.
They are powerful, but not in the Turing-complete way like
other DSLs in CL.
Use caution.
Logical pathnames are a nice idea with a lot of potential.
But they are under-specified and implementations are inconsistent at times.
To summarize the behavior:
So yeah, you can use logical pathnames for their many benefits, as long as you stick to portable patterns.
I’m going to use some of these in my setup:
Match Patterns
"" ;; Works everywhere except ECL
"*.*"
"**;*.*"
"L:**;*.*.*"
"MAIL;**;*.MAIL"
"CODE;DOCUMENTATION.*.*"
Translation APIs
logical-pathname-translations
(setf (logical-pathname-translations "l")
`(("L:**;*.*" "/home/aartaka/junkyard/test.lisp")))
(defmacro define-logical-pathname (host (pattern expansion) &rest patterns)
`(setf (logical-pathname-translations ,host)
(list (list ,pattern ,expansion)
,@(loop for (pattern expansion) in patterns
collect `(list ,pattern ,expansion)))))
;; Better now
(define-logical-pathname "l" ("L:**;*.*.*" "/home/aartaka/junkyard/test.lisp"))
We take a host to add translation to;
Simple Translation
;; All OK
(define-logical-pathname "l" ("L:**;*.*.*" "/home/aartaka/junkyard/test.lisp")) ;; Version element
(define-logical-pathname "l" ("L:**;*.*" "/home/aartaka/junkyard/test.lisp")) ;; No version
(define-logical-pathname "l" ("**;*.*" "/home/aartaka/junkyard/test.lisp")) ;; Omitted obvious host
;; OK, but only for empty or file-only paths (l:file.type)
(define-logical-pathname "l" ("*.*" "/home/aartaka/junkyard/test.lisp"))
;; OK, but only for empty or extension-less file paths (l:file)
(define-logical-pathname "l" ("*" "/home/aartaka/junkyard/test.lisp"))
;; OK, but only for empty paths (l:) and not everywhere
(define-logical-pathname "l" ("" "/home/aartaka/junkyard/test.lisp"))
Substituting Translation
(define-logical-pathname "a" ("**;*.*" "/home/aartaka/**/*.*"))
;; Same, a standard shortcut for file omission?
(define-logical-pathname "a" ("**;*.*" "/home/aartaka/**/"))
(translate-logical-pathname #p"a:web;logical-pathnames.htm")
;; => #P"/home/aartaka/web/logical-pathnames.htm"
(define-logical-pathname "X" ("X:a;*;b;*;*.*" "/hello/*/hi/*/what/*.*"))
(translate-logical-pathname #p"x:a;bonjour;b;barev;greetings.me")
;; => #P"/hello/bonjour/hi/barev/what/greetings.me"
(define-logical-pathname "a"
("W;**;*.*" "/home/aartaka/web/**/*.*")
("G;**;*.*" "/home/aartaka/git/**/*.*")
("**;*.*" "/home/aartaka/**/"))
(translate-logical-pathname #p"a:w;logical-pathnames.htm")
;; => #P"/home/aartaka/web/logical-pathnames.htm"
(define-logical-pathname "a"
("G;*.asd" "/home/aartaka/git/*/*.asd"))
(translate-logical-pathname #p"a:g;cl-minifloats.asd")
;; SBCL:
;; :WILD-INFERIORS is not paired in from and to patterns:
;; (:ABSOLUTE "G") (:ABSOLUTE "home" "aartaka" "git" :WILD)
;; ECL:
;; #P"A:G;CL-MINIFLOATS.ASD" is not a specialization of path #P"A:G;*.ASD"
;; CCL:
;; Error: No translation for #P"a:g;cl-minifloats.asd"
;; ABCL:
;; Unsupported case in TRANSLATE-DIRECTORY-COMPONENTS.
Logical pathnames are a DSL for file path resolution.
Gotcha: some implementations allow other chars, but that’s not portable.
Gotcha: rules are resolved in the order they are provided in.
Gotcha: Left and right hand side should (not must—that’s implementation-specific) have matching wild components.
One by one and strictly once per wildcard.
#-ccl
(define-logical-pathname "home"
("HOME:**;*.*.*" "/home/aartaka/**/*.*"))
(define-logical-pathname "cfg"
("CFG:**;*.*.*" "/home/aartaka/.config/common-lisp/**/*.*"))
(define-logical-pathname "git"
("GIT:**;*.*.*" "/home/aartaka/git/**/*.*"))