Search code examples
fileemacsdirectoryelispinteractive

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


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?


Solution

  • 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.