开发者

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

Suppose I have variables dir and file containing strings representing a directory and开发者_StackOverflow 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.


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.


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"


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.


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.


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"


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!

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜