windrg00
windrg00

Reputation: 457

How to remove double dots in a file name (path) with Elisp

I'm looking for a way to remove .. and get a path without .. . For example, assume there is a given path

a/b/c/../../1

then I'd like to get from the above

a/1

I've tried to use file-truename but it prepends default-directory at the beginning of the given path.

Is there any convenient way? Please let me know.

PS : I'd like to avoid using file-truename if possible.

Upvotes: 0

Views: 183

Answers (2)

Lindydancer
Lindydancer

Reputation: 26134

You can do this using plain string manipulation. Hence, you don't need to resort to using function that only work when the file actually exists in the file system.

The function below splits the string on /:s and walk through the resulting list. It ignores any . and whenever it sees a .. it drops on of the already seen path components.

For example:

(defun my-simplify-path (path)
  "Simplify PATH by removing redundant parts.

The operation is only performed using string operations, so PATH
does not have to exist in the file system."
  (let ((old-list (split-string path "/"))
        (new-list-reversed '()))
    (dolist (element old-list)
      (cond ((equal element "."))
            ((and (equal element "..")
                  (not (null new-list-reversed)))
             (pop new-list-reversed))
            (t
             (push element new-list-reversed))))
    (if (null new-list-reversed)
        "."
      (string-join (nreverse new-list-reversed) "/"))))

The following ERT test case demonstrates how this function works:

(ert-deftest my-simplify-path-test ()
  (should (equal (my-simplify-path "alpha") "alpha"))
  (should (equal (my-simplify-path "./alpha") "alpha"))
  (should (equal (my-simplify-path "alpha/./beta") "alpha/beta"))
  (should (equal (my-simplify-path "alpha/beta") "alpha/beta"))
  (should (equal (my-simplify-path "alpha/../beta") "beta"))
  (should (equal (my-simplify-path "alpha/beta/gamma/delta/../../..") "alpha"))
  (should (equal (my-simplify-path "../alpha") "../alpha"))
  (should (equal (my-simplify-path "alpha/../..") ".."))

  (should (equal (my-simplify-path "a/b/c/../../1") "a/1")))

Upvotes: 1

Drew
Drew

Reputation: 30708

a/b/c/../../1 is not really a path (absolute file name, in Emacs parlance). A path starts with a directory.

At most, that pattern is a relative file name. Relative to what? In Emacs, default-directory, by default.

Perhaps what you really meant was /a/b/c/../../1, which is an absolute file name ("path")? If you use that then, as you say, file-truename gives you what you want. And so does expand-file-name.

If you really want to massage a/b/c/../../1 to produce a/1 then you can do so by using expand-file-name with "/" as the default-directory argument. Or file-truename like this:

(let ((default-directory  "/"))
  (file-truename "a/b/c/../../1"))

You can of course use that in a named function:

(defun foo (relname)
  (let ((default-directory  "/"))
    (file-truename relname)))

Of course, this gives you /a/1, not a/1. If you really want the latter then just use substring to remove the first /.

(Maybe you could tell us the use case for what you need. It might change the suggestions you get, and thus be more helpful.)

Upvotes: 3

Related Questions