4

I have a directory name (as string) with a tilde: ~/projects.

I want to get its fullpath: /home/user/projects. How do I do that ?

The goal is to pass it to uiop:run-program, that doesn't seem to do the right thing©.


With this answer: How to translate (make-pathname :directory '(:absolute :home "directoryiwant") into absolute path

(merge-pathnames 
          (make-pathname
           :directory '(:relative "~/projects"))
          (user-homedir-pathname))
#P"/home/me/~/projects/"

=> WRONG

Thank you.


edit I'll share more context.

I wanted to run a program through uiop:launch-program. I had a user-defined list of directories such as ~/projects. Using it as is created the ./~/projects directory instead of /home/user/projects.

truename doesn't work if the directory doesn't exist.

On SBCL, (namestring "~/doesntexist") returns also its tilde.

merge-pathnames didn't work, still the tilde problem.

Feeding ensure-directories-exist with this result created a directory named ~.

Given the answers, I had no choice but to adapt the logic to expand the directory name of a directory we actually want to exist.

;; Create a directory
;; Ensure its name (string) ends with a slash.
(setf mydir
        (str:concat (string-right-trim (list #\/) mydir)
                    "/"))
(ensure-directories-exist base)

Then I could use its truename.

Ehvince
  • 17,274
  • 7
  • 58
  • 79
  • 4
    Note that this isn't CL-specific at all; tildes are *generally* expected to be expanded before a path leaves the user's interactive, POSIX-compliant shell and is handled by applications. No standard UNIX tool does tilde expansion -- if you run `rm '~/foo.txt'`, it'll look for a file in a directory named `~` -- so it's generally expected that applications just won't *try* to put this in-scope. – Charles Duffy Sep 18 '19 at 15:37
  • A good reminder! – Ehvince Sep 18 '19 at 16:17

3 Answers3

6

General remarks about ~

Your Lisp implementation may or may not support tilde syntax.

If it does (e.g. CCL, ABCL, CLISP, ECL, LispWorks), then truename would consistently expand to a filename:

(truename "~/projects")
 => /home/user/projects

If your implementation doesn't, or if you want to code portably, you have to merge relatively to (user-homedir-pathname):

(truename (merge-pathnames #p"projects" (user-homedir-pathname)))
 => /home/user/projects

Note that the tilde, if it is supported, seems to only be supported for strings used as pathnames, and not in directory components; (:relative "~") does not work as you would expect, and refers to a directory literaly named "~".

Instead, at least for SBCL, the appropriate directory is (:absolute :home), or, if you want to refer to another user, you can wrap the component in a list:

(make-pathname :directory '(:absolute (:home "root")))
=> #P"~root/"

Notice how it only works if the :home form is just after :absolute, it doesn't work otherwise (see Home Directory Specifiers).

Expanding to non-existent pathnames

truename would require that the thing exists?

Yes, if you want to build the absolute path to a file that does not exist (yet), then you need to call truename on the part that exists, and merge with that. In your case, that would be (truename "~/"), which is the same as (user-homedir-pathname).

As pointed out by Rainer Joswig, calling namestring on implementations other than SBCL returns an expanded pathname, translating ~ as /home/user. In SBCL you have to call sb-ext:native-namestring to obtain the same effect.

In other words, in order to expand to a filename that does not necessarily exist, you could write the following portability layer:

(defun expand-file-name (pathname)
  (check-type pathname pathname)
  (block nil
    #+(or lispworks clozure cmu clisp ccl armedbear ecl)
    (return (namestring pathname))
    #+sbcl
    (return (native-namestring pathname))
    #+(not (or sbcl lispworks clozure cmu clisp ccl armedbear ecl))
    (let ((expanded (namestring pathname)))
      (prog1 expanded
        (assert (not find #\~ expanded) () 
                 "Tilde not supported")))))

See also https://github.com/xach/tilde/blob/master/tilde.lisp for inspiration if your Lisp doesn't support the syntax.

coredump
  • 37,664
  • 5
  • 43
  • 77
2

There is a native-namestring function in uiop, which should be available in all implementations:

(uiop:native-namestring "~/projects")
=> /home/user/projects
0

Anselm Farber's solution, involving uiop:native-namestring breaks on some pathnames that don't have native-namestrings, like the following:

(uiop:native-namestring "~/Music/[Video] performance.mp4")
==>
The pathname #P"~/Music/[Video] performance.mp4"
does not have a native namestring because
of the :NAME component #<SB-IMPL::PATTERN (:CHARACTER-SET
                                           . "Video")
                                          " performance">.
   [Condition of type SB-KERNEL:NO-NATIVE-NAMESTRING-ERROR]

Here is a direct solution that only uses pathname- functions:

(defun expand-user-homedir (f)
  (let ((d (pathname-directory f)))
    (if (and (eql (car d) :absolute)
             (eql (cadr d) :home))
        (make-pathname :directory (append (pathname-directory (user-homedir-pathname))
                                          (cddr d)) 
                       :name (pathname-name f) 
                       :type (pathname-type f))
        f)))