Emacs lisp: Concise way to get `directory-files` without "." and ".."?
Asked Answered
T

3

21

The function directory-files returns the . and .. entries as well. While in a sense it is true, that only this way the function returns all existing entries, I have yet to see a use for including these. On the other hand, every time a use directory-files I also write something like

(unless (string-match-p "^\\.\\.?$" ... 

or for better efficiency

(unless (or (string= "." entry)
            (string= ".." entry))
   ..)

Particularly in interactive use (M-:) the extra code is undesirable.

Is there some predefined function that returns only actual subentries of a directory efficiently?

Trifurcate answered 18/6, 2013 at 9:3 Comment(2)
(member entry '("." "..")) is a better way to test if a string is equal to one of a fixed set of elements.Repugn
Good point. I just benchmarked it out and aside from being easier to read, the member version is about 20% faster than the (or (string= ...)) version. In this case using a regexp-comparison is slower. And even when using the MATCH argument of directory-files, it isn't faster -- so the more general approach of filtering the output afterwards with (delete nil (mapcar .. using member, that avoids messing with the MATCH argument, is better overall. At least if one decides to using a custom function rather than using directory-files directly (which I now do).Trifurcate
C
9

If you use f.el, a convenient file and directory manipulation library, you only need function f-entries.

However, if you don't want to use this library for some reason and you are ok for a non-portable *nix solution, you can use ls command.

(defun my-directory-files (d)
  (let* ((path (file-name-as-directory (expand-file-name d)))
         (command (concat "ls -A1d " path "*")))
    (split-string (shell-command-to-string command) "\n" t)))

The code above suffice, but for explanation read further.

Get rid of dots

According to man ls:

   -A, --almost-all
          do not list implied . and ..

With split-string that splits a string by whitespace, we can parse ls output:

(split-string (shell-command-to-string "ls -A"))

Spaces in filenames

The problem is that some filenames may contain spaces. split-string by default splits by regex in variable split-string-default-separators, which is "[ \f\t\n\r\v]+".

   -1     list one file per line

-1 allows to delimit files by newline, to pass "\n" as a sole separator. You can wrap this in a function and use it with arbitrary directory.

(split-string (shell-command-to-string "ls -A1") "\n")

Recursion

But what if you want to recursively dive into subdirectories, returning files for future use? If you just change directory and issue ls, you'll get filenames without paths, so Emacs wouldn't know where this files are located. One solution is to make ls always return absolute paths. According to man ls:

   -d, --directory
          list directory entries instead of contents, and do not dereference symbolic links

If you pass absolute path to directory with a wildcard and -d option, then you'll get a list of absolute paths of immediate files and subdirectories, according to How can I list files with their absolute path in linux?. For explanation on path construction see In Elisp, how to get path string with slash properly inserted?.

(let ((path (file-name-as-directory (expand-file-name d))))
  (split-srting (shell-command-to-string (concat "ls -A1d " path "*")) "\n"))

Omit null string

Unix commands have to add a trailing whitespace to output, so that prompt is on the new line. Otherwise instead of:

user@host$ ls
somefile.txt
user@host$

there would be:

user@host$ ls
somefile.txtuser@host$

When you pass custom separators to split-string, it treats this newline as a line on its own. In general, this allows to correctly parse CSV files, where an empty line may be valid data. But with ls we end up with a null-string, that should be omitted by passing t as a third parameter to split-string.

Cablet answered 13/3, 2014 at 9:41 Comment(1)
That is not the best answer, the best answer is below with (directory-files "/tmp" t directory-files-no-dot-files-regexp)Radack
U
23

You can do this as part of the original function call.

(directory-files DIRECTORY &optional FULL MATCH NOSORT)

If MATCH is non-nil, mention only file names that match the regexp MATCH.

so:

(directory-files (expand-file-name "~/") nil "^\\([^.]\\|\\.[^.]\\|\\.\\..\\)")

or:

(defun my-directory-files (directory &optional full nosort)
  "Like `directory-files' with MATCH hard-coded to exclude \".\" and \"..\"."
  (directory-files directory full "^\\([^.]\\|\\.[^.]\\|\\.\\..\\)" nosort))

although something more akin to your own approach might make for a more efficient wrapper, really.

(defun my-directory-files (directory &optional full match nosort)
  "Like `directory-files', but excluding \".\" and \"..\"."
  (delete "." (delete ".." (directory-files directory full match nosort))))

although that's processing the list twice, and we know there's only one instance of each of the names we wish to exclude (and there's a fair chance they'll appear first), so something more like this might be a good solution if you're expecting to deal with large directories on a frequent basis:

(defun my-directory-files (directory &optional full match nosort)
  "Like `directory-files', but excluding \".\" and \"..\"."
  (let* ((files (cons nil (directory-files directory full match nosort)))
         (parent files)
         (current (cdr files))
         (exclude (list "." ".."))
         (file nil))
    (while (and current exclude)
      (setq file (car current))
      (if (not (member file exclude))
          (setq parent current)
        (setcdr parent (cdr current))
        (setq exclude (delete file exclude)))
      (setq current (cdr current)))
    (cdr files)))
Uhl answered 18/6, 2013 at 9:19 Comment(3)
So, no simple builtin available?Trifurcate
You can get same result with (directory-files "~/" t directory-files-no-dot-files-regexp)Marquetry
@Marquetry your answer is best, but nobody sees it.Radack
C
9

If you use f.el, a convenient file and directory manipulation library, you only need function f-entries.

However, if you don't want to use this library for some reason and you are ok for a non-portable *nix solution, you can use ls command.

(defun my-directory-files (d)
  (let* ((path (file-name-as-directory (expand-file-name d)))
         (command (concat "ls -A1d " path "*")))
    (split-string (shell-command-to-string command) "\n" t)))

The code above suffice, but for explanation read further.

Get rid of dots

According to man ls:

   -A, --almost-all
          do not list implied . and ..

With split-string that splits a string by whitespace, we can parse ls output:

(split-string (shell-command-to-string "ls -A"))

Spaces in filenames

The problem is that some filenames may contain spaces. split-string by default splits by regex in variable split-string-default-separators, which is "[ \f\t\n\r\v]+".

   -1     list one file per line

-1 allows to delimit files by newline, to pass "\n" as a sole separator. You can wrap this in a function and use it with arbitrary directory.

(split-string (shell-command-to-string "ls -A1") "\n")

Recursion

But what if you want to recursively dive into subdirectories, returning files for future use? If you just change directory and issue ls, you'll get filenames without paths, so Emacs wouldn't know where this files are located. One solution is to make ls always return absolute paths. According to man ls:

   -d, --directory
          list directory entries instead of contents, and do not dereference symbolic links

If you pass absolute path to directory with a wildcard and -d option, then you'll get a list of absolute paths of immediate files and subdirectories, according to How can I list files with their absolute path in linux?. For explanation on path construction see In Elisp, how to get path string with slash properly inserted?.

(let ((path (file-name-as-directory (expand-file-name d))))
  (split-srting (shell-command-to-string (concat "ls -A1d " path "*")) "\n"))

Omit null string

Unix commands have to add a trailing whitespace to output, so that prompt is on the new line. Otherwise instead of:

user@host$ ls
somefile.txt
user@host$

there would be:

user@host$ ls
somefile.txtuser@host$

When you pass custom separators to split-string, it treats this newline as a line on its own. In general, this allows to correctly parse CSV files, where an empty line may be valid data. But with ls we end up with a null-string, that should be omitted by passing t as a third parameter to split-string.

Cablet answered 13/3, 2014 at 9:41 Comment(1)
That is not the best answer, the best answer is below with (directory-files "/tmp" t directory-files-no-dot-files-regexp)Radack
T
1

How about just using remove-if?

(remove-if (lambda (x) (member x '("." "..")))
           (directory-files path))
Tarantella answered 14/12, 2021 at 4:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.