68

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.

Ryan C. Thompson
  • 40,856
  • 28
  • 97
  • 159
  • 1
    The wording of the title ("join multiple path components") is really a bit more general than the actual problem, but if anyone is looking for a solution which handles values of "multiple" > 2, see http://stackoverflow.com/questions/9694661 – phils Nov 21 '12 at 02:02
  • Well, if you can join two components, then you can use recursion to join an arbitrary number. I suppose I consider the two problems equivalent by induction. – Ryan C. Thompson Nov 24 '12 at 03:55

8 Answers8

81

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.

Trey Jackson
  • 73,529
  • 11
  • 197
  • 229
  • 7
    Duh, no wonder I see those `concat` all over the place, even our manual encourages this use. But it's generally much better to use `expand-file-name`, except in those cases where you want the end result to be a relative file. – Stefan Nov 20 '12 at 13:47
  • 1
    Why is `expand-file-name` preferred over `concat`? Other than the style issue, I don't see the practical benefit? – THIS USER NEEDS HELP May 07 '16 at 11:10
  • 1
    Might you also want to call `convert-standard-filename` on the result if interested in portability? – Sam Brightman Oct 19 '16 at 13:07
  • @SamBrightman: No, `convert-standard-filename` should basically only be used with *constants*, e.g. to define the actual name to use for `~/.emacs`. Most other uses are simply confused. – Stefan Jun 05 '19 at 20:58
  • Okay, but the manual suggests it makes names e.g. DOS compatible; that doesn’t seem specific to constants. Why would I not want to ensure such compatibility? E.g. I construct a full path from a prefix and some data - I rather get the platform safe version than an error. – Sam Brightman Jun 07 '19 at 06:02
34

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.

Hugh
  • 8,872
  • 2
  • 37
  • 42
15

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"
dbr
  • 165,801
  • 69
  • 278
  • 343
  • @dbr, I use a similar function -see my answer be below. Care to comment on what I outlined as the differences and their pros/cons? – sshaw Sep 27 '15 at 19:39
11

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.

JCC
  • 492
  • 3
  • 17
  • This should be the top answer, hoping this is merged into Emacs mainline so we finally have a standard file interface. Can add f.el using use-package as follows: ```(use-package f :demand)``` – holocronweaver Jul 03 '22 at 05:17
  • 1
    Your comment doesn't work in default Emacs. Never *EVER* assume your environment (I suspect it's some "distribution" like Doom or Space) is the standard. – Jürgen A. Erhard Jul 05 '22 at 07:27
6

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"
Tsingyi
  • 679
  • 6
  • 5
4

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.

sshaw
  • 994
  • 6
  • 10
3

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.

Community
  • 1
  • 1
Mirzhan Irkegulov
  • 17,660
  • 12
  • 105
  • 166
0

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!

PRouleau
  • 150
  • 6