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

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.


How about just using remove-if?

(remove-if (lambda (x) (member x '("." "..")))
           (directory-files path))

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)))