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

ElispFilenamesPath Manipulation

Elisp Problem Overview


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.

Elisp Solutions


Solution 1 - Elisp

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.

Solution 2 - Elisp

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.

Solution 3 - Elisp

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"

Solution 4 - Elisp

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.

Solution 5 - Elisp

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 https://stackoverflow.com/q/22403751/596361.

Solution 6 - Elisp

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.

Solution 7 - Elisp

Just to complete what was said before with a link to the Emacs manual:

As others have said before, the answer to the OP question is to use the expand-file-name. That is a built-in function, implemented in C and therefore does not require the use of any external library.

This is described in the Emacs Lisp Manual section titled Functions that Expand Filenames.

And according to Emacs on-line help this function was introduced in version ... 1.6 of Emacs! So... it should be available!

Solution 8 - Elisp

For those who come to the question after 2021. elisp builtin function file-name-concat would do the job. It's much simpler now.

Document can be found in emacs with following keystroke:

C-h f file-name-concat <enter>

> Append COMPONENTS to DIRECTORY and return the resulting string. > > Elements in COMPONENTS must be a string or nil. DIRECTORY or the non-final elements in COMPONENTS may or may not end with a slash -- if they don't end with a slash, a slash will be inserted before contatenating. > > Other relevant functions are documented in the file-name group.
> Probably introduced at or before Emacs version 28.1.
> This function does not change global state, including the match data.

(file-name-concat "/usr/bin/" "ls")
;; ==> "/usr/bin/ls"

(file-name-concat "/usr" "bin" "ls")
;; ==> "/usr/bin/ls"

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRyan C. ThompsonView Question on Stackoverflow
Solution 1 - ElispTrey JacksonView Answer on Stackoverflow
Solution 2 - ElispHughView Answer on Stackoverflow
Solution 3 - ElispdbrView Answer on Stackoverflow
Solution 4 - ElispJCCView Answer on Stackoverflow
Solution 5 - ElispMirzhan IrkegulovView Answer on Stackoverflow
Solution 6 - ElispsshawView Answer on Stackoverflow
Solution 7 - ElispPRouleauView Answer on Stackoverflow
Solution 8 - ElispTsingyiView Answer on Stackoverflow