What is the correct way to join multiple path components into a single complete path in emacs lisp?

StackOverflow https://stackoverflow.com/questions/3964715

Question

Suppose I have variables dir and file containing strings representing a directory and a filename, respectively . What is the proper way in emacs lisp to join them into a full path to the file?

For example, if dir is "/usr/bin" and file is "ls", then I want "/usr/bin/ls". But if instead dir is "/usr/bin/", I still want the same thing, with no repeated slash.

Was it helpful?

Solution

Reading through the manual for Directory Names, you'll find the answer:

Given a directory name, you can combine it with a relative file name using concat:

 (concat dirname relfile)

Be sure to verify that the file name is relative before doing that. If you use an absolute file name, the results could be syntactically invalid or refer to the wrong file.

If you want to use a directory file name in making such a combination, you must first convert it to a directory name using file-name-as-directory:

 (concat (file-name-as-directory dirfile) relfile) 

Don't try concatenating a slash by hand, as in

 ;;; Wrong!
 (concat dirfile "/" relfile) 

because this is not portable. Always use file-name-as-directory.

Other commands that are useful are: file-name-directory, file-name-nondirectory, and others in the File Name Components section.

OTHER TIPS

You can use expand-file-name for this:

(expand-file-name "ls" "/usr/bin")
"/usr/bin/ls"
(expand-file-name "ls" "/usr/bin/")
"/usr/bin/ls"

Edit: this only works with absolute directory names. I think Trey's answer is the preferable solution.

I wanted to join multiple nested directories onto a path. Originally I used multiple expand-file-name calls, like so:

(expand-file-name "b" (expand-file-name "a" "/tmp"))
"/tmp/a/b"

However this is rather verbose, and reads backwards.

Instead I wrote a function which acts like Python's os.path.join:

(defun joindirs (root &rest dirs)
  "Joins a series of directories together, like Python's os.path.join,
  (dotemacs-joindirs \"/tmp\" \"a\" \"b\" \"c\") => /tmp/a/b/c"

  (if (not dirs)
      root
    (apply 'joindirs
           (expand-file-name (car dirs) root)
           (cdr dirs))))

It works like so:

(joindirs "/tmp" "a" "b")
"/tmp/a/b"
(joindirs "~" ".emacs.d" "src")
"/Users/dbr/.emacs.d/src"
(joindirs "~" ".emacs.d" "~tmp")
"/Users/dbr/.emacs.d/~tmp"

Here's what I use:

(defun catdir (root &rest dirs)
  (apply 'concat (mapcar
          (lambda (name) (file-name-as-directory name))
          (push root dirs))))

Differences from @dbr's:

  1. Returns an "emacs directory name", i.e. a value with a trailing slash
  2. It does not expand the path if root is relative (see notes)
  3. Treats root as the root, whereas joindirs will use the first component starting with "/" as the root.

Notes

Many file handling functions (all, most, ???) will normalize redundant slashes and call expand-file-name (or similar) on relative paths, so #2 and #3 may not really matter.

If you use a convenient file and directory manipulation library f.el, you only need f-join. The below code is for those, who for some reason refuse to use this library.

(defun os-path-join (a &rest ps)
  (let ((path a))
    (while ps
      (let ((p (pop ps)))
        (cond ((string-prefix-p "/" p)
               (setq path p))
              ((or (not path) (string-suffix-p "/" p))
               (setq path (concat path p)))
              (t (setq path (concat path "/" p))))))
    path))

This behaves exactly as Python's os.path.join.

ELISP> (os-path-join "~" "a" "b" "")
"~/a/b/"
ELISP> (os-path-join "~" "a" "/b" "c")
"/b/c"

string-suffix-p doesn't exist before Emacs 24.4, so i wrote my own at Check if a string ends with a suffix in Emacs Lisp.

This question was asked in 2010, but at the time of writing it's the top hit for searches like "join file paths in elisp", so I thought I'd update the answer.

Since 2010, things have moved on a lot in the world of Emacs. This is somewhat of a duplicate answer since it was mentioned briefly in an answer below, but I'll flesh it out a little. There's now a dedicated library for file interactions, f.el:

Much inspired by @magnars's excellent s.el and dash.el, f.el is a modern API for working with files and directories in Emacs.

Don't try to reinvent the wheel. You should use this library for file path manipulations. The function you want is f-join:

(f-join "path")                   ;; => "path"
(f-join "path" "to")              ;; => "path/to"
(f-join "/" "path" "to" "heaven") ;; => "/path/to/heaven"

You may need to install the package first. It should be available on MELPA.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top