Common Lisp: relative path to absolute
Asked Answered
A

3

8

May be it is a really dumb question, but after playing around with all built-in pathname-family functions and cl-fad/pathname-utils packages I still can't figure out how to convert a relative path to absolute (with respect to $PWD):

; let PWD be "/very/long/way"
(abspath "../road/home"); -> "/very/long/road/home"

Where the hypothetical function abspath works just like os.path.abspath() in Python.

Aretina answered 21/6, 2017 at 17:4 Comment(0)
A
3

Here's the final solution (based on the previous two answers):

(defun abspath
       (path-string)
   (uiop:unix-namestring
    (uiop:merge-pathnames*
     (uiop:parse-unix-namestring path-string))))

uiop:parse-unix-namestring converts the string argument to a pathname, replacing . and .. references; uiop:merge-pathnames* translates a relative pathname to absolute; uiop:unix-namestring converts the pathname back to a string.

Also, if you know for sure what kind of file the path points to, you can use either:

(uiop:unix-namestring (uiop:file-exists-p path))

or

(uiop:unix-namestring (uiop:directory-exists-p path))

because both file-exists-p and directory-exists-p return absolute pathnames (or nil, if file does not exist).

UPDATE:

Apparently in some implementations (like ManKai Common Lisp) uiop:merge-pathnames* does not prepend the directory part if the given pathname lacks ./ prefix (for example if you feed it #P"main.c" rather than #P"./main.c"). So the safer solution is:

(defun abspath
       (path-string &optional (dir-name (uiop:getcwd)))
   (uiop:unix-namestring
    (uiop:ensure-absolute-pathname
     (uiop:merge-pathnames*
      (uiop:parse-unix-namestring path-string))
     dir-name)))
Aretina answered 21/6, 2017 at 19:3 Comment(0)
F
5

The variable *DEFAULT-PATHNAME-DEFAULTS* usually contains your initial working directory, you can merge the pathname with that;

(defun abspath (pathname)
  (merge-pathnames pathname *default-pathname-defaults*))

And since this is the default for the second argument to merge-pathnames, you can simply write:

(defun abspath (pathname)
  (merge-pathnames pathname))
Fawne answered 21/6, 2017 at 17:12 Comment(2)
Checked. Woks in ECL and SBCL. CLISP is broken (for some reason *default-pathname-defaults* is empty there). OK. But how to resolve .. references - cl-fad:canonical-pathname doesn't work?Aretina
Cool idea! I would recommend uiop:merge-pathnames* though.Cupped
C
4

UIOP

Here is what the documentation of UIOP says about cl-fad :-)

UIOP completely replaces it with better design and implementation

A good number of implementations ship with UIOP (used by ASDF3), so it's basically already available when you need it (see "Using UIOP" in the doc.). One of the many functions defined in the library is uiop:parse-unix-namestring, which understands the syntax of Unix filenames without checking if the path designates an existing file or directory. However the double-dot is parsed as :back or :up which is not necessarily supported by your implementation. With SBCL, it is the case and the path is simplified. Note that pathnames allows to use both :back and :up components; :back can be simplified easily by looking at the pathname only (it is a syntactic up directory), whereas :up is the semantic up directory, meaning that it depends on the actual file system. You have a better chance to obtain a canonical file name if the file name exists.

Truename

You can also call TRUENAME, which will probably get rid of the ".." components in your path. See also 20.1.3 Truenames which explains that you can point to the same file by using different pathnames, but that there is generally one "canonical" name.

Constitutionally answered 21/6, 2017 at 23:6 Comment(3)
Nope. (uiop:parse-unix-namestring "/very/long/way/../road/home") evals to #<Unprintable pathname> (just as any other input). As for truename - it does the job, but only when the file actually exists, otherwise it signals en error (which is very inconvenient).Aretina
@Aretina There seem to be a difference among implementations and/or versions. I have UIOP 3.1.5 here with SBCL which works fine with your example.Constitutionally
Seems like at least ECL doesn't support :BACK in the pathname directory component: (make-pathname :directory '(:absolute "very" "long" "way" :back "road") :name "home").Agrostology
A
3

Here's the final solution (based on the previous two answers):

(defun abspath
       (path-string)
   (uiop:unix-namestring
    (uiop:merge-pathnames*
     (uiop:parse-unix-namestring path-string))))

uiop:parse-unix-namestring converts the string argument to a pathname, replacing . and .. references; uiop:merge-pathnames* translates a relative pathname to absolute; uiop:unix-namestring converts the pathname back to a string.

Also, if you know for sure what kind of file the path points to, you can use either:

(uiop:unix-namestring (uiop:file-exists-p path))

or

(uiop:unix-namestring (uiop:directory-exists-p path))

because both file-exists-p and directory-exists-p return absolute pathnames (or nil, if file does not exist).

UPDATE:

Apparently in some implementations (like ManKai Common Lisp) uiop:merge-pathnames* does not prepend the directory part if the given pathname lacks ./ prefix (for example if you feed it #P"main.c" rather than #P"./main.c"). So the safer solution is:

(defun abspath
       (path-string &optional (dir-name (uiop:getcwd)))
   (uiop:unix-namestring
    (uiop:ensure-absolute-pathname
     (uiop:merge-pathnames*
      (uiop:parse-unix-namestring path-string))
     dir-name)))
Aretina answered 21/6, 2017 at 19:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.