3

How to write a function for finding all files in a directory which not ending with pyc and pyo? I've tried something like (find-lisp-find-files root "^(?!.*pyc$).*$") but it does not return any possible result.

  • Note that for interactive use there is also the customizable variable completion-ignored-extensions. If you add .pyc and .pyo there files with those extensions are not offered for completion. Sometimes that is all one needs. – Tobias Jun 13 '17 at 5:44
  • Please accept one of the answers to mark this question answered. Have you tried list-files from my answer? It should cover pretty everything what you could possibly need interactively as well as for coding. I test it occasionally since I wrote it and it seems to work well. Best regards, – Tobias Jun 15 '17 at 6:13
5

Note that (?!.*pyc$) is regular expression syntax of perl (zero negative lookahead) that does not work in emacs.

You can use

(require 'cl-lib)

(cl-remove-if
 (lambda (file) (or
                 (file-directory-p file)
                 (string-match "\\.py[co]$" file)))
 (directory-files default-directory))

It maybe that you have already the (require 'cl-lib) in your initialization. In that case you do not need it here.

There are many ways to modify the above code segment. For an example you can add a regular expression filter to directory-files. See the help for directory-files. You can also wrap this segment with a defun for easy use. You can also replace directory-files by directory-files-recursively if you want to recurse into sub-directories.

An example for a (sparsely tested) command list-files with optional regular expressions for filtering files in and out:

(require 'cl-lib)
(require 'dired)

(defun dired-files-noselect (dir files)
  "Print files from list FILES in directory DIR using `dired-mode'."
  (with-current-buffer (get-buffer-create (directory-file-name dir))
    (let ((inhibit-read-only t))
      (cd dir)
      (delete-region (point-min) (point-max))
      (insert "  " default-directory (format ":\n  dired-files %d\n" (length files)))
      (dolist (file files)
        (let ((a (file-attributes file 'string)))
          (insert (format "  %s %d %s %s %6d %s %s\n"
                          (nth 8 a) ; file modes like ls -l
                          (nth 1 a) ; number of links to file
                          (nth 2 a) ; uid as string
                          (nth 3 a) ; gid as string
                          (or (nth 7 a) 0) ; size in bytes
                          (format-time-string "%d. %b %Y" (nth 5 a)) ; modification time
                          file))))
      (dired-mode)
      (dired-build-subdir-alist)
      (buffer-name))))

(defun dired-files (dir files)
  "Print files from list FILES in directory DIR using `dired-mode'."
  (display-buffer (dired-files-noselect dir files)))

(defconst list-files-options
  '((:directory default-directory "files of this directory are listed" read-directory-name "Directory: " nil nil t)
    (:recursively nil "recurse into sub-directories" y-or-n-p "Recurse into sub-directories (y/n)?: ")
    (:include ".*" "files to list must match this regular expression or fulfill this predicate" read-regexp "Include files matching: ")
    (:exclude nil "files that match this regular expression or fulfill this predicate are filtered out" read-regexp "Exclude files matching: ")
    (:include-directories nil "directories are included in the listing if this option is non-nil" y-or-n-p "Include sub-directories (y/n)?: "))
  "Alist of options for `list-files'.
Each option has the structure (key . (default-value doc-string)).")

(eval `(defun list-files (&rest options)
         ,(concat
           "List files.
Options (with default value in parentheses):
"
           (mapconcat (lambda (opt)
                        (concat (symbol-name (car opt))
                                " ("
                                (prin1-to-string (cadr opt))
                                ") "
                                (caddr opt)))
                      list-files-options
                      "\n")
           "

Example:
(list-files :exclude \"\\\\.py[co]$\")")
  (interactive
   (let (opt options)
     (while (null (string-equal
                   ""
                   (setq opt (completing-read "Option (try tab-completion): " (mapcar (lambda (opt) (substring (symbol-name (car opt)) 1)) list-files-options) nil t))))
       (setq opt (assoc-string (concat ":" opt) list-files-options))
       (setq options (append (list (car opt) (apply #'funcall (nthcdr 3 opt))) options)))
     options))
  (let* ((dir (plist-get options :directory))
         (dir (if dir dir default-directory))
         (include (plist-get options :include))
         (exclude (plist-get options :exclude))
         (full (plist-get options :full))
         (file-list (if (plist-get options :recursively)
                        (directory-files-recursively dir
                                                     (or (and (stringp include) include)  ".*")
                                                     (plist-get options :include-directories))
                      (directory-files dir t (and (stringp include) include))))
         (file-list (if (functionp include)
                        (cl-remove-if-not include file-list)
                      file-list))
         (file-list (if exclude
                        (cl-remove-if (if (functionp exclude)
                                          exclude
                                        `(lambda (file)
                                           (string-match ,exclude file)))
                                      file-list)
                      file-list))
         (file-list (if full 
                        file-list
                      (let ((default-directory dir))
                        (mapcar #'file-relative-name file-list)))))
    (when (called-interactively-p 'any)
      (dired-files dir file-list))
    file-list)))
5

The way you intended this to be done:

(find-lisp-find-files "." "\\(^..?\\|py[^co]\\|[^p]..\\|.[^y].\\)$")

which, probably requires some explanation.

Why your attempt didn't work

Emacs doesn't have zero-width assertions in regular expressions. Also, you need to escape the parenthesis in Emacs regexp.

What does the regular expression match

It matches three characters at the end of the file name. There are three cases:

  1. py followed by something that is neither c nor o.
  2. p followed by something that is not y, followed by any character.
  3. any character but p followed by y and then any other character.

thus ensuring anything that ends in either pyc or pyo will not match, but everything else will.


PS. You might want to check M-xregexp-builder.

  • 2
    Cool regexp! It assumes filename has at least 3 characters, it is almost right except for something like /f, though it's very very rare. – xuchunyang Jun 12 '17 at 10:57
  • 1
    @xuchunyang oops, right, well, that can be fixed too :) – wvxvw Jun 12 '17 at 11:01
  • 1
    I though \\(?:...\\) was a noncapturing group. – npostavs Jun 12 '17 at 13:56
  • @npostavs yes, it is. But in a different sense. The lookaround kind of groups don't capture in the sense they don't consume any input, and this one is noncapturing in the sense it doesn't create backreferences. I think the names for these things don't really reflect what they do... – wvxvw Jun 12 '17 at 14:09
  • \(?:...\) is very definitely a non-capturing (or "shy") group. What Emacs doesn't have is (certain kinds of) zero-width look-ahead/behind assertions. – phils Jun 13 '17 at 14:06
4

Elisp regular expressions are hard to read, hard to get right and even harder to maintain. Here are two alternative approaches:

foreign-regexp.el -- https://github.com/k-talo/foreign-regexp.el

Allows you to use regexps just like in perl, python, javascript or ruby. This is not a widely used or tested package but great if you really do not want to deal with elisp regexps.

rx -- built-in macro, see its documentation string and examples at http://francismurillo.github.io/2017-03-30-Exploring-Emacs-rx-Macro/

This macro takes a readable description of the matches needed and generates the regexp. Anyone can go and modify the description without getting lost in the forest of backlashes.

The following is a reworking of previous examples. Changing unless to if lists matching files.

(delq nil
  (mapcar
   (lambda (i)
     (unless (string-match-p
               (rx ".py" (any "c" "o") eos )
               i)
       i))
   (directory-files default-directory)))
  • 2
    Seems a bit pointless to add (require 's) just so you can write s-matches-p instead of string-match-p. – npostavs Jun 13 '17 at 13:16
  • Correct and corrected. Blaming copy and paste and laziness. – Heikki Jun 13 '17 at 14:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.