File Paths in Lisp

Here’s the translation of the Go code example to Lisp, formatted in Markdown suitable for Hugo:

The cl-fad package provides functions to parse and construct file paths in a way that is portable between operating systems; dir/file on Linux vs. dir\file on Windows, for example.

(ql:quickload :cl-fad)
(ql:quickload :cl-ppcre)

(defun main ()
  ;; Join should be used to construct paths in a portable way.
  ;; It takes any number of arguments and constructs a hierarchical path from them.
  (let ((p (cl-fad:merge-pathnames-as-file "dir1" "dir2" "filename")))
    (format t "p: ~A~%" p)

    ;; You should always use merge-pathnames-as-file instead of
    ;; concatenating /s or \s manually. In addition to providing portability,
    ;; merge-pathnames-as-file will also normalize paths by removing
    ;; superfluous separators and directory changes.
    (format t "~A~%" (cl-fad:merge-pathnames-as-file "dir1//" "filename"))
    (format t "~A~%" (cl-fad:merge-pathnames-as-file "dir1/../dir1" "filename"))

    ;; pathname-directory and pathname-name can be used to split a path to the
    ;; directory and the file. There's no direct equivalent to Split in Common Lisp.
    (format t "Dir(p): ~A~%" (pathname-directory p))
    (format t "Base(p): ~A~%" (pathname-name p))

    ;; We can check whether a path is absolute.
    (format t "~A~%" (cl-fad:pathname-absolute-p "dir/file"))
    (format t "~A~%" (cl-fad:pathname-absolute-p "/dir/file"))

    (let ((filename "config.json"))
      ;; Some file names have extensions following a dot. We can split the extension
      ;; out of such names with pathname-type.
      (let ((ext (pathname-type filename)))
        (format t "~A~%" ext)

        ;; To find the file's name with the extension removed, use pathname-name.
        (format t "~A~%" (pathname-name filename)))

      ;; There's no direct equivalent to Rel in Common Lisp, but we can implement
      ;; a similar function using cl-fad:pathname-as-directory and cl-ppcre:regex-replace.
      (labels ((rel (base target)
                 (let* ((base-path (cl-fad:pathname-as-directory base))
                        (target-path (cl-fad:pathname-as-directory target))
                        (base-str (namestring base-path))
                        (target-str (namestring target-path)))
                   (cl-ppcre:regex-replace (format nil "^~A" base-str) target-str ""))))
        (format t "~A~%" (rel "a/b" "a/b/t/file"))
        (format t "~A~%" (rel "a/b" "a/c/t/file"))))))

(main)

To run the program, save it as file-paths.lisp and use your Lisp interpreter. For example, with SBCL:

$ sbcl --script file-paths.lisp
p: #P"dir1/dir2/filename"
#P"dir1/filename"
#P"dir1/filename"
Dir(p): (:RELATIVE "dir1" "dir2")
Base(p): "filename"
NIL
T
"json"
"config"
t/file
../c/t/file

Note that the exact output may vary depending on your operating system and Lisp implementation. Common Lisp’s pathname system is more complex and powerful than Go’s filepath package, but it can also be more challenging to use in a completely portable way. The cl-fad library helps to abstract away some of these differences.