What is the correct way to join multiple path components into a single complete path in emacs lisp?
ElispFilenamesPath ManipulationElisp 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:
- Returns an "emacs directory name", i.e. a value with a trailing slash
- It does not expand the path if
root
is relative (see notes) - Treats
root
as the root, whereasjoindirs
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"