commit 35aaa6b6aa9a2e7b42465603fb32355a009c510f (HEAD, refs/remotes/origin/master)
Author: Christopher Genovese <genovese@cmu.edu>
Date:   Tue Dec 20 17:41:56 2016 +0900

    ibuffer: New filters and commands
    
    Add several new filters and improve documentation.
    See discussion on:
    https://lists.gnu.org/archive/html/emacs-devel/2016-11/msg00399.html
    * lisp/ibuf-ext.el: Add paragraph to file commentary.
    (ibuffer-saved-filters, ibuffer-filtering-qualifiers)
    (ibuffer-filter-groups): Update doc string.
    (ibuffer-unary-operand): Add new function that transparently
    handles 'not' formats for compound filters.
    (ibuffer-included-in-filter-p): Handle 'not' fully; update doc string.
    (ibuffer-included-in-filter-p-1): Handle 'and' compound filters.
    (ibuffer-decompose-filter): Handle 'and' as well,
    and handle 'not' consistently with other uses.
    (ibuffer-and-filter): New defun analogous to 'ibuffer-or-filter'.
    (ibuffer--or-and-filter): New defun.
    (ibuffer-or-filter, ibuffer-and-filter): Use it.
    (ibuffer-format-qualifier): Handle 'and' filters as well.
    (ibuffer-filter-by-basename, ibuffer-filter-by-file-extension)
    (ibuffer-filter-by-directory, ibuffer-filter-by-starred-name)
    (ibuffer-filter-by-modified, ibuffer-filter-by-visiting-file):
    Add new pre-defined filters.
    (ibuffer-filter-chosen-by-completion): Add new interactive command
    for easily choosing a filter from the descriptions.
    * lisp/ibuffer.el (ibuffer-mode-map):
    Bind ibuffer-filter-by-basename, ibuffer-filter-by-file-extension,
    ibuffer-filter-by-starred-name, ibuffer-filter-by-modified,
    ibuffer-filter-by-visiting-file to '/b', '/.', '/*', '/i', '/v'
    respectively; bind 'ibuffer-or-filter', 'ibuffer-and-filter',
    'ibuffer-pop-filter' ,'ibuffer-pop-filter-group' and
    'ibuffer-filter-disable' to '/|', '/&', '/<up>', '/S-<up>'
    and '/ DEL' respectively.
    * test/lisp/ibuffer-tests.el (ibuffer-autoload): Add appropriate
    skip specification.
    Add menu entries for the new filters.
    (ibuffer-filter-inclusion-1, ibuffer-filter-inclusion-2
    ibuffer-filter-inclusion-3, ibuffer-filter-inclusion-4
    ibuffer-filter-inclusion-5, ibuffer-filter-inclusion-6
    ibuffer-filter-inclusion-7, ibuffer-filter-inclusion-8
    ibuffer-decompose-filter, ibuffer-and-filter
    ibuffer-or-filter): Add new tests; they are skipped unless
    ibuf-ext is loaded.
    ; * etc/NEWS: Add entries for new user-facing features.

diff --git a/etc/NEWS b/etc/NEWS
index e15ab79d50..ee74236a52 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -333,6 +333,27 @@ bound to 'Buffer-menu-unmark-all-buffers'.
 ** Ibuffer
 
 ---
+*** New filter commands `ibuffer-filter-by-basename',
+`ibuffer-filter-by-file-extension', `ibuffer-filter-by-directory',
+`ibuffer-filter-by-starred-name', `ibuffer-filter-by-modified'
+and `ibuffer-filter-by-visiting-file'; bound respectively
+to '/b', '/.', '//', '/*', '/i' and '/v'.
+
+---
+*** Two new commands 'ibuffer-filter-chosen-by-completion'
+and `ibuffer-and-filter', the second bound to '/&'.
+
+---
+*** The commands `ibuffer-pop-filter', `ibuffer-pop-filter-group',
+`ibuffer-or-filter' and `ibuffer-filter-disable' have the alternative
+bindings '/<up>', '/S-<up>', '/|' and '/DEL', respectively.
+
+---
+*** The data format specifying filters has been extended to allow
+explicit logical 'and', and a more flexible form for logical 'not'.
+See 'ibuffer-filtering-qualifiers' doc string for full details.
+
+---
 *** A new command 'ibuffer-copy-buffername-as-kill'; bound
 to 'B'.
 
diff --git a/lisp/ibuf-ext.el b/lisp/ibuf-ext.el
index 9ce7b5a484..7ebfecd374 100644
--- a/lisp/ibuf-ext.el
+++ b/lisp/ibuf-ext.el
@@ -28,6 +28,13 @@
 ;; These functions should be automatically loaded when called, but you
 ;; can explicitly (require 'ibuf-ext) in your ~/.emacs to have them
 ;; preloaded.
+;;
+;; For details on the structure of ibuffer filters and filter groups,
+;; see the documentation for variables `ibuffer-filtering-qualifiers',
+;; `ibuffer-filter-groups', and `ibuffer-saved-filters' in that order.
+;; The variable `ibuffer-filtering-alist' contains names and
+;; descriptions of the currently defined filters; also see the macro
+;; `define-ibuffer-filter'.
 
 ;;; Code:
 
@@ -139,19 +146,33 @@ Returns (OLD-FORMAT-DETECTED . UPDATED-SAVED-FILTERS-LIST)."
            (fixed (mapcar fix-filter filters)))
       (cons old-format-detected fixed))))
 
-(defcustom ibuffer-saved-filters '(("gnus"
+(defcustom ibuffer-saved-filters '(("programming"
+                                    (or (derived-mode . prog-mode)
+                                        (mode         . ess-mode)
+                                        (mode         . compilation-mode)))
+                                   ("text document"
+                                    (and (derived-mode . text-mode)
+                                         (not (starred-name))))
+                                   ("TeX"
+                                    (or (derived-mode . tex-mode)
+                                        (mode         . latex-mode)
+                                        (mode         . context-mode)
+                                        (mode         . ams-tex-mode)
+                                        (mode         . bibtex-mode)))
+                                   ("web"
+                                    (or (derived-mode . sgml-mode)
+                                        (derived-mode . css-mode)
+                                        (mode         . javascript-mode)
+                                        (mode         . js2-mode)
+                                        (mode         . scss-mode)
+                                        (derived-mode . haml-mode)
+                                        (mode         . sass-mode)))
+                                   ("gnus"
                                     (or (mode . message-mode)
                                         (mode . mail-mode)
                                         (mode . gnus-group-mode)
                                         (mode . gnus-summary-mode)
-                                        (mode . gnus-article-mode)))
-                                   ("programming"
-                                    (or (mode . emacs-lisp-mode)
-                                        (mode . cperl-mode)
-                                        (mode . c-mode)
-                                        (mode . java-mode)
-                                        (mode . idl-mode)
-                                        (mode . lisp-mode))))
+                                        (mode . gnus-article-mode))))
 
   "An alist mapping saved filter names to filter specifications.
 
@@ -214,8 +235,48 @@ Alternative ways to save the repaired value:
 "))
 
 (defvar ibuffer-filtering-qualifiers nil
-  "A list like (SYMBOL . QUALIFIER) which filters the current buffer list.
-See also `ibuffer-filtering-alist'.")
+  "A list specifying the filters currently acting on the buffer list.
+
+If this list is nil, then no filters are currently in
+effect. Otherwise, each element of this list specifies a single
+filter, and all of the specified filters in the list are applied
+successively to the buffer list.
+
+Each filter specification can be of two types: simple or compound.
+
+A simple filter specification has the form (SYMBOL . QUALIFIER),
+where SYMBOL is a key in the alist `ibuffer-filtering-alist' that
+determines the filter function to use and QUALIFIER is the data
+passed to that function (along with the buffer being considered).
+
+A compound filter specification can have one of four forms:
+
+-- (not FILTER-SPEC)
+
+   Represents the logical complement of FILTER-SPEC, which
+   is any single filter specification, simple or compound.
+   The form (not . FILTER-SPEC) is also accepted here.
+
+-- (and FILTER-SPECS...)
+
+   Represents the logical-and of the filters defined by one or
+   more filter specifications FILTER-SPECS..., where each
+   specification can be simple or compound.  Note that and is
+   implicitly applied to the filters in the top-level list.
+
+-- (or FILTER-SPECS...)
+
+   Represents the logical-or of the filters defined by one or
+   more filter specifications FILTER-SPECS..., where each
+   specification can be simple or compound.
+
+-- (saved . \"NAME\")
+
+   Represents the filter saved under the string NAME
+   in the alist `ibuffer-saved-filters'. It is an
+   error to name a filter that has not been saved.
+
+This variable is local to each ibuffer buffer.")
 
 ;; This is now frobbed by `define-ibuffer-filter'.
 (defvar ibuffer-filtering-alist nil
@@ -247,10 +308,18 @@ to this variable."
 (defvar ibuffer-compiled-filter-formats nil)
 
 (defvar ibuffer-filter-groups nil
-  "A list like ((\"NAME\" ((SYMBOL . QUALIFIER) ...) ...) which groups buffers.
-The SYMBOL should be one from `ibuffer-filtering-alist'.
-The QUALIFIER should be the same as QUALIFIER in
-`ibuffer-filtering-qualifiers'.")
+  "An alist giving this buffer's active filter groups, or nil if none.
+
+This alist maps filter group labels to filter specification
+lists.  Each element has the form (\"LABEL\" FILTER-SPECS...),
+where FILTER-SPECS... represents one or more filter
+specifications of the same form as allowed as elements of
+`ibuffer-filtering-qualifiers'.
+
+Each filter group is displayed as a separate section in the
+ibuffer list, headed by LABEL and displaying only the buffers
+that pass through all the filters associated with NAME in this
+list.")
 
 (defcustom ibuffer-show-empty-filter-groups t
   "If non-nil, then show the names of filter groups which are empty."
@@ -260,20 +329,21 @@ The QUALIFIER should be the same as QUALIFIER in
 (defcustom ibuffer-saved-filter-groups nil
   "An alist of filtering groups to switch between.
 
-This variable should look like ((\"STRING\" QUALIFIERS)
-                                (\"STRING\" QUALIFIERS) ...), where
-QUALIFIERS is a list of the same form as
-`ibuffer-filtering-qualifiers'.
+Each element is of the form (\"NAME\" . FILTER-GROUP-LIST),
+where NAME is a unique but arbitrary name and FILTER-GROUP-LIST
+is a list of filter groups with the same structure as
+allowed for `ibuffer-filter-groups'.
 
-See also the variables `ibuffer-filter-groups',
-`ibuffer-filtering-qualifiers', `ibuffer-filtering-alist', and the
-functions `ibuffer-switch-to-saved-filter-groups',
-`ibuffer-save-filter-groups'."
+See also the functions `ibuffer-save-filter-groups' and
+`ibuffer-switch-to-saved-filter-groups' for saving and switching
+between sets of filter groups, and the variable
+`ibuffer-save-with-custom' that affects how this information is
+saved."
   :type '(repeat sexp)
   :group 'ibuffer)
 
 (defvar ibuffer-hidden-filter-groups nil
-  "A list of filtering groups which are currently hidden.")
+  "The list of filter groups that are currently hidden.")
 
 (defvar ibuffer-filter-group-kill-ring nil)
 
@@ -602,18 +672,38 @@ To evaluate a form without viewing the buffer, see `ibuffer-do-eval'."
 
 ;;;###autoload
 (defun ibuffer-included-in-filters-p (buf filters)
+  "Return non-nil if BUF passes all FILTERS.
+
+BUF is a lisp buffer object, and FILTERS is a list of filter
+specifications with the same structure as
+`ibuffer-filtering-qualifiers'."
   (not
    (memq nil ;; a filter will return nil if it failed
-	 (mapcar
-	  ;; filter should be like (TYPE . QUALIFIER), or
-	  ;; (or (TYPE . QUALIFIER) (TYPE . QUALIFIER) ...)
-	  #'(lambda (qual)
-	      (ibuffer-included-in-filter-p buf qual))
-	  filters))))
+	 (mapcar #'(lambda (filter)
+                     (ibuffer-included-in-filter-p buf filter))
+                 filters))))
+
+(defun ibuffer-unary-operand (filter)
+  "Extracts operand from a unary compound FILTER specification.
+
+FILTER should be a cons cell of either form (f . d) or (f d),
+where operand d is itself a cons cell, or nil. Returns d."
+  (let* ((tail (cdr filter))
+         (maybe-q (car-safe tail)))
+    (if (consp maybe-q) maybe-q tail)))
 
 (defun ibuffer-included-in-filter-p (buf filter)
+  "Return non-nil if BUF pass FILTER.
+
+BUF is a lisp buffer object, and FILTER is a filter
+specification, with the same structure as an element of the list
+`ibuffer-filtering-qualifiers'."
   (if (eq (car filter) 'not)
-      (not (ibuffer-included-in-filter-p-1 buf (cdr filter)))
+      (let ((inner (ibuffer-unary-operand filter)))
+        ;; Allows (not (not ...)) etc, which may be overkill
+        (if (eq (car inner) 'not)
+            (ibuffer-included-in-filter-p buf (ibuffer-unary-operand inner))
+          (not (ibuffer-included-in-filter-p-1 buf inner))))
     (ibuffer-included-in-filter-p-1 buf filter)))
 
 (defun ibuffer-included-in-filter-p-1 (buf filter)
@@ -621,9 +711,19 @@ To evaluate a form without viewing the buffer, see `ibuffer-do-eval'."
    (not
     (pcase (car filter)
       (`or
+       ;;; ATTN: Short-circuiting alternative with parallel structure w/`and
+       ;;(catch 'has-match
+       ;;  (dolist (filter-spec (cdr filter) nil)
+       ;;    (when (ibuffer-included-in-filter-p buf filter-spec)
+       ;;      (throw 'has-match t))))
        (memq t (mapcar #'(lambda (x)
-			   (ibuffer-included-in-filter-p buf x))
-		       (cdr filter))))
+                           (ibuffer-included-in-filter-p buf x))
+                       (cdr filter))))
+      (`and
+       (catch 'no-match
+         (dolist (filter-spec (cdr filter) t)
+           (unless (ibuffer-included-in-filter-p buf filter-spec)
+             (throw 'no-match nil)))))
       (`saved
        (let ((data (assoc (cdr filter) ibuffer-saved-filters)))
 	 (unless data
@@ -916,17 +1016,17 @@ group definitions by setting `ibuffer-filter-groups' to nil."
     (when buf
       (ibuffer-jump-to-buffer (buffer-name buf)))))
 
-(defun ibuffer-push-filter (qualifier)
-  "Add QUALIFIER to `ibuffer-filtering-qualifiers'."
-  (push qualifier ibuffer-filtering-qualifiers))
+(defun ibuffer-push-filter (filter-specification)
+  "Add FILTER-SPECIFICATION to `ibuffer-filtering-qualifiers'."
+  (push filter-specification ibuffer-filtering-qualifiers))
 
 ;;;###autoload
 (defun ibuffer-decompose-filter ()
-  "Separate the top compound filter (OR, NOT, or SAVED) in this buffer.
+  "Separate this buffer's top compound filter (AND, OR, NOT, or SAVED).
 
 This means that the topmost filter on the filtering stack, which must
 be a complex filter like (OR [name: foo] [mode: bar-mode]), will be
-turned into two separate filters [name: foo] and [mode: bar-mode]."
+turned into separate filters, like [name: foo] and [mode: bar-mode]."
   (interactive)
   (unless ibuffer-filtering-qualifiers
     (error "No filters in effect"))
@@ -935,14 +1035,14 @@ turned into two separate filters [name: foo] and [mode: bar-mode]."
          (tail (cdr filters))
          (value
           (pcase (caar filters)
-            (`or (nconc head tail))
+            ((or `or 'and) (nconc head tail))
             (`saved
              (let ((data (assoc head ibuffer-saved-filters)))
                (unless data
                  (ibuffer-filter-disable)
                  (error "Unknown saved filter %s" head))
                (append (cdr data) tail)))
-            (`not (cons head tail))
+            (`not (cons (ibuffer-unary-operand (car filters)) tail))
             (_
              (error "Filter type %s is not compound" (caar filters))))))
     (setq ibuffer-filtering-qualifiers value))
@@ -971,31 +1071,36 @@ turned into two separate filters [name: foo] and [mode: bar-mode]."
 	  ibuffer-filtering-qualifiers))
   (ibuffer-update nil t))
 
+(defun ibuffer--or-and-filter (op decompose)
+  (if decompose
+      (if (eq op (caar ibuffer-filtering-qualifiers))
+          (ibuffer-decompose-filter)
+        (error "Top filter is not an %s" (upcase (symbol-name op))))
+    (when (< (length ibuffer-filtering-qualifiers) 2)
+      (error "Need two filters to %s" (upcase (symbol-name op))))
+    ;; If either filter is an op, eliminate unnecessary nesting.
+    (let ((first (pop ibuffer-filtering-qualifiers))
+          (second (pop ibuffer-filtering-qualifiers)))
+      (push (nconc (if (eq op (car first)) first (list op first))
+                   (if (eq op (car second)) (cdr second) (list second)))
+            ibuffer-filtering-qualifiers)))
+  (ibuffer-update nil t))
+
 ;;;###autoload
-(defun ibuffer-or-filter (&optional reverse)
+(defun ibuffer-or-filter (&optional decompose)
   "Replace the top two filters in this buffer with their logical OR.
-If optional argument REVERSE is non-nil, instead break the top OR
+If optional argument DECOMPOSE is non-nil, instead break the top OR
 filter into parts."
   (interactive "P")
-  (if reverse
-      (progn
-	(when (or (null ibuffer-filtering-qualifiers)
-		  (not (eq 'or (caar ibuffer-filtering-qualifiers))))
-	  (error "Top filter is not an OR"))
-	(let ((lim (pop ibuffer-filtering-qualifiers)))
-	  (setq ibuffer-filtering-qualifiers
-		(nconc (cdr lim) ibuffer-filtering-qualifiers))))
-    (when (< (length ibuffer-filtering-qualifiers) 2)
-      (error "Need two filters to OR"))
-    ;; If the second filter is an OR, just add to it.
-    (let ((first (pop ibuffer-filtering-qualifiers))
-	  (second (pop ibuffer-filtering-qualifiers)))
-      (if (eq 'or (car second))
-	  (push (nconc (list 'or first) (cdr second))
-		ibuffer-filtering-qualifiers)
-	(push (list 'or first second)
-	      ibuffer-filtering-qualifiers))))
-  (ibuffer-update nil t))
+  (ibuffer--or-and-filter 'or decompose))
+
+;;;###autoload
+(defun ibuffer-and-filter (&optional decompose)
+  "Replace the top two filters in this buffer with their logical AND.
+If optional argument DECOMPOSE is non-nil, instead break the top AND
+filter into parts."
+  (interactive "P")
+  (ibuffer--or-and-filter 'and decompose))
 
 (defun ibuffer-maybe-save-stuff ()
   (when ibuffer-save-with-custom
@@ -1069,7 +1174,9 @@ Interactively, prompt for NAME, and use the current filters."
 
 (defun ibuffer-format-qualifier (qualifier)
   (if (eq (car-safe qualifier) 'not)
-      (concat " [NOT" (ibuffer-format-qualifier-1 (cdr qualifier)) "]")
+      (concat " [NOT"
+              (ibuffer-format-qualifier-1 (ibuffer-unary-operand qualifier))
+              "]")
     (ibuffer-format-qualifier-1 qualifier)))
 
 (defun ibuffer-format-qualifier-1 (qualifier)
@@ -1078,14 +1185,16 @@ Interactively, prompt for NAME, and use the current filters."
      (concat " [filter: " (cdr qualifier) "]"))
     (`or
      (concat " [OR" (mapconcat #'ibuffer-format-qualifier
-			       (cdr qualifier) "") "]"))
+                               (cdr qualifier) "") "]"))
+    (`and
+     (concat " [AND" (mapconcat #'ibuffer-format-qualifier
+                                (cdr qualifier) "") "]"))
     (_
      (let ((type (assq (car qualifier) ibuffer-filtering-alist)))
        (unless qualifier
-	 (error "Ibuffer: bad qualifier %s" qualifier))
+         (error "Ibuffer: bad qualifier %s" qualifier))
        (concat " [" (cadr type) ": " (format "%s]" (cdr qualifier)))))))
 
-
 (defun ibuffer-list-buffer-modes (&optional include-parents)
   "Create a completion table of buffer modes currently in use.
 If INCLUDE-PARENTS is non-nil then include parent modes."
@@ -1103,7 +1212,7 @@ If INCLUDE-PARENTS is non-nil then include parent modes."
 
 ;;;###autoload (autoload 'ibuffer-filter-by-mode "ibuf-ext")
 (define-ibuffer-filter mode
-  "Toggle current view to buffers with major mode QUALIFIER."
+  "Limit current view to buffers with major mode QUALIFIER."
   (:description "major mode"
    :reader
    (let* ((buf (ibuffer-current-buffer))
@@ -1123,7 +1232,7 @@ If INCLUDE-PARENTS is non-nil then include parent modes."
 
 ;;;###autoload (autoload 'ibuffer-filter-by-used-mode "ibuf-ext")
 (define-ibuffer-filter used-mode
-  "Toggle current view to buffers with major mode QUALIFIER.
+  "Limit current view to buffers with major mode QUALIFIER.
 Called interactively, this function allows selection of modes
 currently used by buffers."
   (:description "major mode in use"
@@ -1142,7 +1251,7 @@ currently used by buffers."
 
 ;;;###autoload (autoload 'ibuffer-filter-by-derived-mode "ibuf-ext")
 (define-ibuffer-filter derived-mode
-    "Toggle current view to buffers whose major mode inherits from QUALIFIER."
+    "Limit current view to buffers whose major mode inherits from QUALIFIER."
   (:description "derived mode"
 		:reader
 		(intern
@@ -1153,22 +1262,73 @@ currently used by buffers."
 
 ;;;###autoload (autoload 'ibuffer-filter-by-name "ibuf-ext")
 (define-ibuffer-filter name
-  "Toggle current view to buffers with name matching QUALIFIER."
+  "Limit current view to buffers with name matching QUALIFIER."
   (:description "buffer name"
    :reader (read-from-minibuffer "Filter by name (regexp): "))
   (string-match qualifier (buffer-name buf)))
 
+;;;###autoload (autoload 'ibuffer-filter-by-starred-name "ibuf-ext")
+(define-ibuffer-filter starred-name
+    "Limit current view to buffers with name beginning and ending
+with *, along with an optional suffix of the form digits or
+<digits>."
+  (:description "starred buffer name"
+   :reader nil)
+  (string-match "\\`\\*[^*]+\\*\\(?:<[[:digit:]]+>\\)?\\'" (buffer-name buf)))
+
 ;;;###autoload (autoload 'ibuffer-filter-by-filename "ibuf-ext")
 (define-ibuffer-filter filename
-  "Toggle current view to buffers with filename matching QUALIFIER."
-  (:description "filename"
-   :reader (read-from-minibuffer "Filter by filename (regexp): "))
+    "Limit current view to buffers with full file name matching QUALIFIER.
+
+For example, for a buffer associated with file '/a/b/c.d', this
+matches against '/a/b/c.d'."
+  (:description "full file name"
+   :reader (read-from-minibuffer "Filter by full file name (regexp): "))
   (ibuffer-awhen (with-current-buffer buf (ibuffer-buffer-file-name))
     (string-match qualifier it)))
 
+;;;###autoload (autoload 'ibuffer-filter-by-basename "ibuf-ext")
+(define-ibuffer-filter basename
+    "Limit current view to buffers with file basename matching QUALIFIER.
+
+For example, for a buffer associated with file '/a/b/c.d', this
+matches against 'c.d'."
+  (:description "file basename"
+   :reader (read-from-minibuffer
+            "Filter by file name, without directory part (regex): "))
+  (ibuffer-awhen (with-current-buffer buf (ibuffer-buffer-file-name))
+    (string-match qualifier (file-name-nondirectory it))))
+
+;;;###autoload (autoload 'ibuffer-filter-by-file-extension "ibuf-ext")
+(define-ibuffer-filter file-extension
+    "Limit current view to buffers with filename extension matching QUALIFIER.
+
+The separator character (typically `.') is not part of the
+pattern. For example, for a buffer associated with file
+'/a/b/c.d', this matches against 'd'."
+  (:description "filename extension"
+   :reader (read-from-minibuffer
+            "Filter by filename extension without separator (regex): "))
+  (ibuffer-awhen (with-current-buffer buf (ibuffer-buffer-file-name))
+    (string-match qualifier (or (file-name-extension it) ""))))
+
+;;;###autoload (autoload 'ibuffer-filter-by-directory "ibuf-ext")
+(define-ibuffer-filter directory
+    "Limit current view to buffers with directory matching QUALIFIER.
+
+For a buffer associated with file '/a/b/c.d', this matches
+against '/a/b'. For a buffer not associated with a file, this
+matches against the value of `default-directory' in that buffer."
+  (:description "directory name"
+   :reader (read-from-minibuffer "Filter by directory name (regex): "))
+  (ibuffer-aif (with-current-buffer buf (ibuffer-buffer-file-name))
+      (let ((dirname (file-name-directory it)))
+        (when dirname (string-match qualifier dirname)))
+    (when default-directory (string-match qualifier default-directory))))
+
 ;;;###autoload (autoload 'ibuffer-filter-by-size-gt  "ibuf-ext")
 (define-ibuffer-filter size-gt
-  "Toggle current view to buffers with size greater than QUALIFIER."
+  "Limit current view to buffers with size greater than QUALIFIER."
   (:description "size greater than"
    :reader
    (string-to-number (read-from-minibuffer "Filter by size greater than: ")))
@@ -1177,16 +1337,30 @@ currently used by buffers."
 
 ;;;###autoload (autoload 'ibuffer-filter-by-size-lt  "ibuf-ext")
 (define-ibuffer-filter size-lt
-   "Toggle current view to buffers with size less than QUALIFIER."
+    "Limit current view to buffers with size less than QUALIFIER."
   (:description "size less than"
    :reader
    (string-to-number (read-from-minibuffer "Filter by size less than: ")))
   (< (with-current-buffer buf (buffer-size))
      qualifier))
 
+;;;###autoload (autoload 'ibuffer-filter-by-modified "ibuf-ext")
+(define-ibuffer-filter modified
+    "Limit current view to buffers that are marked as modified."
+  (:description "modified"
+   :reader nil)
+  (buffer-modified-p buf))
+
+;;;###autoload (autoload 'ibuffer-filter-by-visiting-file "ibuf-ext")
+(define-ibuffer-filter visiting-file
+    "Limit current view to buffers that are visiting a file."
+  (:description "visiting a file"
+   :reader nil)
+  (with-current-buffer buf (buffer-file-name)))
+
 ;;;###autoload (autoload 'ibuffer-filter-by-content "ibuf-ext")
 (define-ibuffer-filter content
-   "Toggle current view to buffers whose contents match QUALIFIER."
+   "Limit current view to buffers whose contents match QUALIFIER."
   (:description "content"
    :reader (read-from-minibuffer "Filter by content (regexp): "))
   (with-current-buffer buf
@@ -1196,12 +1370,33 @@ currently used by buffers."
 
 ;;;###autoload (autoload 'ibuffer-filter-by-predicate "ibuf-ext")
 (define-ibuffer-filter predicate
-   "Toggle current view to buffers for which QUALIFIER returns non-nil."
+   "Limit current view to buffers for which QUALIFIER returns non-nil."
   (:description "predicate"
    :reader (read-minibuffer "Filter by predicate (form): "))
   (with-current-buffer buf
     (eval qualifier)))
 
+;;;###autoload (autoload 'ibuffer-filter-chosen-by-completion "ibuf-ext")
+(defun ibuffer-filter-chosen-by-completion ()
+  "Select and apply filter chosen by completion against available filters.
+Indicates corresponding key sequences in echo area after filtering.
+
+The completion matches against the filter description text of
+each filter in `ibuffer-filtering-alist'."
+  (interactive)
+  (let* ((filters (mapcar (lambda (x) (cons (cadr x) (car x)))
+                          ibuffer-filtering-alist))
+         (match (completing-read "Filter by: " filters nil t))
+         (filter (cdr (assoc match filters)))
+         (command (intern (concat "ibuffer-filter-by-" (symbol-name filter)))))
+    (call-interactively command)
+    (message "%s can be run with key sequences: %s"
+             command
+             (mapconcat #'key-description
+                        (where-is-internal command ibuffer-mode-map nil t)
+                        "or "))))
+
+
 ;;; Sorting
 
 ;;;###autoload
diff --git a/lisp/ibuffer.el b/lisp/ibuffer.el
index 94cee329d5..5a740845bd 100644
--- a/lisp/ibuffer.el
+++ b/lisp/ibuffer.el
@@ -518,26 +518,37 @@ directory, like `default-directory'."
     (define-key map (kbd "s f") 'ibuffer-do-sort-by-filename/process)
     (define-key map (kbd "s m") 'ibuffer-do-sort-by-major-mode)
 
+    (define-key map (kbd "/ RET") 'ibuffer-filter-by-mode)
     (define-key map (kbd "/ m") 'ibuffer-filter-by-used-mode)
     (define-key map (kbd "/ M") 'ibuffer-filter-by-derived-mode)
     (define-key map (kbd "/ n") 'ibuffer-filter-by-name)
-    (define-key map (kbd "/ c") 'ibuffer-filter-by-content)
-    (define-key map (kbd "/ e") 'ibuffer-filter-by-predicate)
+    (define-key map (kbd "/ *") 'ibuffer-filter-by-starred-name)
     (define-key map (kbd "/ f") 'ibuffer-filter-by-filename)
-    (define-key map (kbd "/ >") 'ibuffer-filter-by-size-gt)
+    (define-key map (kbd "/ b") 'ibuffer-filter-by-basename)
+    (define-key map (kbd "/ .") 'ibuffer-filter-by-file-extension)
     (define-key map (kbd "/ <") 'ibuffer-filter-by-size-lt)
+    (define-key map (kbd "/ >") 'ibuffer-filter-by-size-gt)
+    (define-key map (kbd "/ i") 'ibuffer-filter-by-modified)
+    (define-key map (kbd "/ v") 'ibuffer-filter-by-visiting-file)
+    (define-key map (kbd "/ c") 'ibuffer-filter-by-content)
+    (define-key map (kbd "/ e") 'ibuffer-filter-by-predicate)
+
     (define-key map (kbd "/ r") 'ibuffer-switch-to-saved-filters)
     (define-key map (kbd "/ a") 'ibuffer-add-saved-filters)
     (define-key map (kbd "/ x") 'ibuffer-delete-saved-filters)
     (define-key map (kbd "/ d") 'ibuffer-decompose-filter)
     (define-key map (kbd "/ s") 'ibuffer-save-filters)
     (define-key map (kbd "/ p") 'ibuffer-pop-filter)
+    (define-key map (kbd "/ <up>") 'ibuffer-pop-filter)
     (define-key map (kbd "/ !") 'ibuffer-negate-filter)
     (define-key map (kbd "/ t") 'ibuffer-exchange-filters)
     (define-key map (kbd "/ TAB") 'ibuffer-exchange-filters)
     (define-key map (kbd "/ o") 'ibuffer-or-filter)
+    (define-key map (kbd "/ |") 'ibuffer-or-filter)
+    (define-key map (kbd "/ &") 'ibuffer-and-filter)
     (define-key map (kbd "/ g") 'ibuffer-filters-to-filter-group)
     (define-key map (kbd "/ P") 'ibuffer-pop-filter-group)
+    (define-key map (kbd "/ S-<up>") 'ibuffer-pop-filter-group)
     (define-key map (kbd "/ D") 'ibuffer-decompose-filter-group)
     (define-key map (kbd "/ /") 'ibuffer-filter-disable)
 
@@ -657,13 +668,43 @@ directory, like `default-directory'."
                   ibuffer-filter-by-derived-mode))
     (define-key-after map [menu-bar view filter filter-by-name]
       '(menu-item "Add filter by buffer name..." ibuffer-filter-by-name))
+    (define-key-after map [menu-bar view filter filter-by-starred-name]
+      '(menu-item "Add filter by starred buffer name..."
+                  ibuffer-filter-by-starred-name
+                  :help "List buffers whose names begin with a star"))
     (define-key-after map [menu-bar view filter filter-by-filename]
-      '(menu-item "Add filter by filename..." ibuffer-filter-by-filename))
+      '(menu-item "Add filter by full filename..." ibuffer-filter-by-filename
+                  :help
+                  (concat "For a buffer associated with file '/a/b/c.d', "
+                          "list buffer if a given pattern matches '/a/b/c.d'")))
+    (define-key-after map [menu-bar view filter filter-by-basename]
+      '(menu-item "Add filter by file basename..."
+                  ibuffer-filter-by-basename
+                  :help (concat "For a buffer associated with file '/a/b/c.d', "
+                                "list buffer if a given pattern matches 'c.d'")))
+    (define-key-after map [menu-bar view filter filter-by-file-extension]
+      '(menu-item "Add filter by file name extension..."
+                  ibuffer-filter-by-file-extension
+                  :help (concat "For a buffer associated with file '/a/b/c.d', "
+                                "list buffer if a given pattern matches 'd'")))
+    (define-key-after map [menu-bar view filter filter-by-directory]
+      '(menu-item "Add filter by filename's directory..."
+                  ibuffer-filter-by-directory
+                  :help
+                  (concat "For a buffer associated with file '/a/b/c.d', "
+                          "list buffer if a given pattern matches '/a/b'")))
     (define-key-after map [menu-bar view filter filter-by-size-lt]
       '(menu-item "Add filter by size less than..." ibuffer-filter-by-size-lt))
     (define-key-after map [menu-bar view filter filter-by-size-gt]
       '(menu-item "Add filter by size greater than..."
         ibuffer-filter-by-size-gt))
+    (define-key-after map [menu-bar view filter filter-by-modified]
+      '(menu-item "Add filter by modified buffer" ibuffer-filter-by-modified
+                  :help "List buffers that are marked as modified"))
+    (define-key-after map [menu-bar view filter filter-by-visiting-file]
+      '(menu-item "Add filter by buffer visiting a file"
+                  ibuffer-filter-by-visiting-file
+                  :help "List buffers that are visiting files"))
     (define-key-after map [menu-bar view filter filter-by-content]
       '(menu-item "Add filter by content (regexp)..."
         ibuffer-filter-by-content))
@@ -673,6 +714,12 @@ directory, like `default-directory'."
     (define-key-after map [menu-bar view filter pop-filter]
       '(menu-item "Remove top filter" ibuffer-pop-filter
         :enable (and (featurep 'ibuf-ext) ibuffer-filtering-qualifiers)))
+    (define-key-after map [menu-bar view filter and-filter]
+      '(menu-item "AND top two filters" ibuffer-and-filter
+        :enable (and (featurep 'ibuf-ext) ibuffer-filtering-qualifiers
+                     (cdr ibuffer-filtering-qualifiers))
+        :help
+        "Create a new filter which is the logical AND of the top two filters"))
     (define-key-after map [menu-bar view filter or-filter]
       '(menu-item "OR top two filters" ibuffer-or-filter
         :enable (and (featurep 'ibuf-ext) ibuffer-filtering-qualifiers
diff --git a/test/lisp/ibuffer-tests.el b/test/lisp/ibuffer-tests.el
index 92ed101e6b..40760abd96 100644
--- a/test/lisp/ibuffer-tests.el
+++ b/test/lisp/ibuffer-tests.el
@@ -24,7 +24,8 @@
   (require 'ibuf-macs))
 
 (ert-deftest ibuffer-autoload ()
-  "Tests to see whether reftex-auc has been autoloaded"
+  "Tests to see whether ibuffer has been autoloaded"
+  (skip-unless (not (featurep 'ibuf-ext)))
   (should
    (fboundp 'ibuffer-mark-unsaved-buffers))
   (should
@@ -138,5 +139,669 @@
           (should-not ibuffer-filtering-qualifiers))
       (setq ibuffer-filtering-qualifiers filters))))
 
+;; Test Filter Inclusion
+(let* (test-buffer-list  ; accumulated buffers to clean up
+       ;; Utility functions without polluting the environment
+       (set-buffer-mode
+        (lambda (buffer mode)
+          "Set BUFFER's major mode to MODE, a mode function, or fundamental."
+          (with-current-buffer buffer
+            (funcall (or mode #'fundamental-mode)))))
+       (set-buffer-contents
+        (lambda (buffer size include-content)
+          "Add exactly SIZE bytes to BUFFER, including INCLUDE-CONTENT."
+          (when (or size include-content)
+            (let* ((unit "\n")
+                   (chunk "ccccccccccccccccccccccccccccccc\n")
+                   (chunk-size (length chunk))
+                   (size (if (and size include-content (stringp include-content))
+                             (- size (length include-content))
+                           size)))
+              (unless (or (null size) (> size 0))
+                (error "size argument must be nil or positive"))
+              (with-current-buffer buffer
+                (when include-content
+                  (insert include-content))
+                (when size
+                  (dotimes (_ (floor size chunk-size))
+                    (insert chunk))
+                  (dotimes (_ (mod size chunk-size))
+                    (insert unit)))
+                ;; prevent query on cleanup
+                (set-buffer-modified-p nil))))))
+       (create-file-buffer
+        (lambda (prefix &rest args-plist)
+          "Create a file and buffer with designated properties.
+        PREFIX is a string giving the beginning of the name, and ARGS-PLIST
+        is a series of keyword-value pairs, with allowed keywords
+        :suffix STRING, :size NUMBER, :mode MODE-FUNC, :include-content STRING.
+        Returns the created buffer."
+          (let* ((suffix  (plist-get args-plist :suffix))
+                 (size    (plist-get args-plist :size))
+                 (include (plist-get args-plist :include-content))
+                 (mode    (plist-get args-plist :mode))
+                 (file    (make-temp-file prefix nil suffix))
+                 (buf     (find-file-noselect file t)))
+            (push buf test-buffer-list) ; record for cleanup
+            (funcall set-buffer-mode buf mode)
+            (funcall set-buffer-contents buf size include)
+            buf)))
+       (create-non-file-buffer
+        (lambda (prefix &rest args-plist)
+          "Create a non-file and buffer with designated properties.
+        PREFIX is a string giving the beginning of the name, and ARGS-PLIST
+        is a series of keyword-value pairs, with allowed keywords
+        :size NUMBER, :mode MODE-FUNC, :include-content STRING.
+        Returns the created buffer."
+          (let* ((size    (plist-get args-plist :size))
+                 (include (plist-get args-plist :include-content))
+                 (mode    (plist-get args-plist :mode))
+                 (buf     (generate-new-buffer prefix)))
+            (push buf test-buffer-list) ; record for cleanup
+            (funcall set-buffer-mode buf mode)
+            (funcall set-buffer-contents buf size include)
+            buf)))
+       (clean-up
+        (lambda ()
+          "Restore all emacs state modified during the tests"
+          (while test-buffer-list       ; created temporary buffers
+            (let ((buf (pop test-buffer-list)))
+              (with-current-buffer buf (bury-buffer)) ; ensure not selected
+              (kill-buffer buf))))))
+  ;; Tests
+  (ert-deftest ibuffer-filter-inclusion-1 ()
+    "Tests inclusion using basic filter combinators with a single buffer."
+    (skip-unless (featurep 'ibuf-ext))
+    (unwind-protect
+        (let ((buf
+               (funcall create-file-buffer "ibuf-test-1" :size 100
+                        :include-content "One ring to rule them all\n")))
+          (should (ibuffer-included-in-filters-p buf '((size-gt . 99))))
+          (should (ibuffer-included-in-filters-p buf '((size-lt . 101))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((mode . fundamental-mode))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((content . "ring to rule them all"))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((and (content . "ring to rule them all")))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((and (and (content . "ring to rule them all"))))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((and (and (and (content . "ring to rule them all")))))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((or (content . "ring to rule them all")))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((not (not (content . "ring to rule them all"))))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((and (size-gt . 99)
+                              (content . "ring to rule them all")
+                              (mode . fundamental-mode)
+                              (basename . "\\`ibuf-test-1")))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((not (or (not (size-gt . 99))
+                                  (not (content . "ring to rule them all"))
+                                  (not (mode . fundamental-mode))
+                                  (not (basename . "\\`ibuf-test-1")))))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((and (or (size-gt . 99) (size-lt . 10))
+                              (and (content . "ring.*all")
+                                   (content . "rule")
+                                   (content . "them all")
+                                   (content . "One"))
+                              (not (mode . text-mode))
+                              (basename . "\\`ibuf-test-1"))))))
+      (funcall clean-up)))
+
+  (ert-deftest ibuffer-filter-inclusion-2 ()
+    "Tests inclusion of basic filters in combination on a single buffer."
+    (skip-unless (featurep 'ibuf-ext))
+    (unwind-protect
+        (let ((buf
+               (funcall create-file-buffer "ibuf-test-2" :size 200
+                        :mode #'text-mode
+                        :include-content "and in the darkness find them\n")))
+          (should (ibuffer-included-in-filters-p buf '((size-gt . 199))))
+          (should (ibuffer-included-in-filters-p buf '((size-lt . 201))))
+          (should (ibuffer-included-in-filters-p buf '((not size-gt . 200))))
+          (should (ibuffer-included-in-filters-p buf '((not (size-gt . 200)))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((and (size-gt . 199) (size-lt . 201)))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((or (size-gt . 199) (size-gt . 201)))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((or (size-gt . 201) (size-gt . 199)))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((size-gt . 199) (mode . text-mode)
+                         (content . "darkness find them"))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((and (size-gt . 199) (mode . text-mode)
+                              (content . "darkness find them")))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((not (or (not (size-gt . 199)) (not (mode . text-mode))
+                                  (not (content . "darkness find them")))))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((or (size-gt . 200) (content . "darkness find them")
+                             (derived-mode . emacs-lisp-mode)))))
+          (should-not (ibuffer-included-in-filters-p
+                       buf '((or (size-gt . 200) (content . "rule them all")
+                                 (derived-mode . emacs-lisp-mode))))))
+      (funcall clean-up)))
+
+  (ert-deftest ibuffer-filter-inclusion-3 ()
+    "Tests inclusion with filename filters on specified buffers."
+    (skip-unless (featurep 'ibuf-ext))
+    (unwind-protect
+        (let* ((bufA
+                (funcall create-file-buffer "ibuf-test-3.a" :size 50
+                         :mode #'text-mode
+                         :include-content "...but a multitude of drops?\n"))
+               (bufB
+                (funcall create-non-file-buffer "ibuf-test-3.b" :size 50
+                         :mode #'text-mode
+                         :include-content "...but a multitude of drops?\n"))
+               (dirA (with-current-buffer bufA default-directory))
+               (dirB (with-current-buffer bufB default-directory)))
+          (should (ibuffer-included-in-filters-p
+                   bufA '((basename . "ibuf-test-3"))))
+          (should (ibuffer-included-in-filters-p
+                   bufA '((basename . "test-3\\.a"))))
+          (should (ibuffer-included-in-filters-p
+                   bufA '((file-extension . "a"))))
+          (should (ibuffer-included-in-filters-p
+                   bufA (list (cons 'directory dirA))))
+          (should-not (ibuffer-included-in-filters-p
+                       bufB '((basename . "ibuf-test-3"))))
+          (should-not (ibuffer-included-in-filters-p
+                       bufB '((file-extension . "b"))))
+          (should (ibuffer-included-in-filters-p
+                   bufB (list (cons 'directory dirB))))
+          (should (ibuffer-included-in-filters-p
+                   bufA '((name . "ibuf-test-3"))))
+          (should (ibuffer-included-in-filters-p
+                   bufB '((name . "ibuf-test-3")))))
+      (funcall clean-up)))
+
+  (ert-deftest ibuffer-filter-inclusion-4 ()
+    "Tests inclusion with various filters on a single buffer."
+    (skip-unless (featurep 'ibuf-ext))
+    (unwind-protect
+        (let ((buf
+               (funcall create-file-buffer "ibuf-test-4"
+                        :mode #'emacs-lisp-mode :suffix ".el"
+                        :include-content "(message \"--%s--\" 'emacs-rocks)\n")))
+          (should (ibuffer-included-in-filters-p
+                   buf '((file-extension . "el"))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((derived-mode . prog-mode))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((used-mode . emacs-lisp-mode))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((mode . emacs-lisp-mode))))
+          (with-current-buffer buf (set-buffer-modified-p t))
+          (should (ibuffer-included-in-filters-p buf '((modified))))
+          (with-current-buffer buf (set-buffer-modified-p nil))
+          (should (ibuffer-included-in-filters-p buf '((not modified))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((and (file-extension . "el")
+                              (derived-mode . prog-mode)
+                              (not modified)))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((or (file-extension . "tex")
+                             (derived-mode . prog-mode)
+                             (modified)))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((file-extension . "el")
+                         (derived-mode . prog-mode)
+                         (not modified)))))
+      (funcall clean-up)))
+
+  (ert-deftest ibuffer-filter-inclusion-5 ()
+    "Tests inclusion with various filters on a single buffer."
+    (skip-unless (featurep 'ibuf-ext))
+    (unwind-protect
+        (let ((buf
+               (funcall create-non-file-buffer "ibuf-test-5.el"
+                        :mode #'emacs-lisp-mode
+                        :include-content
+                        "(message \"--%s--\" \"It really does!\")\n")))
+          (should-not (ibuffer-included-in-filters-p
+                       buf '((file-extension . "el"))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((size-gt . 18))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((predicate . (lambda ()
+                                        (> (- (point-max) (point-min)) 18))))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((and (mode . emacs-lisp-mode)
+                              (or (starred-name)
+                                  (size-gt . 18))
+                              (and (not (size-gt . 100))
+                                   (content . "[Ii]t  *really does!")
+                                   (or (name . "test-5")
+                                       (not (filename . "test-5")))))))))
+      (funcall clean-up)))
+
+  (ert-deftest ibuffer-filter-inclusion-6 ()
+    "Tests inclusion using saved filters and DeMorgan's laws."
+    (skip-unless (featurep 'ibuf-ext))
+    (unwind-protect
+        (let ((buf
+               (funcall create-non-file-buffer "*ibuf-test-6*" :size 65
+                        :mode #'text-mode))
+              (buf2
+               (funcall create-file-buffer "ibuf-test-6a" :suffix ".html"
+                        :mode #'html-mode
+                        :include-content
+                        "<HTML><BODY><H1>Hello, World!</H1></BODY></HTML>")))
+          (should (ibuffer-included-in-filters-p buf '((starred-name))))
+          (should-not (ibuffer-included-in-filters-p
+                       buf '((saved . "text document"))))
+          (should (ibuffer-included-in-filters-p buf2 '((saved . "web"))))
+          (should (ibuffer-included-in-filters-p
+                   buf2 '((not (and (not (derived-mode . sgml-mode))
+                                    (not (derived-mode . css-mode))
+                                    (not (mode         . javascript-mode))
+                                    (not (mode         . js2-mode))
+                                    (not (mode         . scss-mode))
+                                    (not (derived-mode . haml-mode))
+                                    (not (mode         . sass-mode)))))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((and (starred-name)
+                              (or (size-gt . 50) (filename . "foo"))))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((not (or (not starred-name)
+                                  (and (size-lt . 51)
+                                       (not (filename . "foo")))))))))
+      (funcall clean-up)))
+
+  (ert-deftest ibuffer-filter-inclusion-7 ()
+    "Tests inclusion with various filters on a single buffer."
+    (skip-unless (featurep 'ibuf-ext))
+    (unwind-protect
+        (let ((buf
+               (funcall create-non-file-buffer "ibuf-test-7"
+                        :mode #'artist-mode)))
+          (should (ibuffer-included-in-filters-p
+                   buf '((not (starred-name)))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((not starred-name))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((not (not (not starred-name))))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((not (modified)))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((not modified))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((not (not (not modified)))))))
+      (funcall clean-up)))
+
+  (ert-deftest ibuffer-filter-inclusion-8 ()
+    "Tests inclusion with various filters."
+    (skip-unless (featurep 'ibuf-ext))
+    (unwind-protect
+        (let ((bufA
+               (funcall create-non-file-buffer "ibuf-test-8a"
+                        :mode #'artist-mode))
+              (bufB (funcall create-non-file-buffer "*ibuf-test-8b*" :size 32))
+              (bufC (funcall create-file-buffer "ibuf-test8c" :suffix "*"
+                             :size 64))
+              (bufD (funcall create-file-buffer "*ibuf-test8d" :size 128))
+              (bufE (funcall create-file-buffer "*ibuf-test8e" :suffix "*<2>"
+                             :size 16))
+              (bufF (and (funcall create-non-file-buffer "*ibuf-test8f*")
+                         (funcall create-non-file-buffer "*ibuf-test8f*"
+                                  :size 8))))
+          (with-current-buffer bufA (set-buffer-modified-p t))
+          (should (ibuffer-included-in-filters-p
+                   bufA '((and (not starred-name)
+                               (modified)
+                               (name . "test-8")
+                               (not (size-gt . 100))
+                               (mode . picture-mode)))))
+          (with-current-buffer bufA (set-buffer-modified-p nil))
+          (should-not (ibuffer-included-in-filters-p
+                       bufA '((or (starred-name) (visiting-file) (modified)))))
+          (should (ibuffer-included-in-filters-p
+                   bufB '((and (starred-name)
+                               (name . "test.*8b")
+                               (size-gt . 31)
+                               (not visiting-file)))))
+          (should (ibuffer-included-in-filters-p
+                   bufC '((and (not (starred-name))
+                               (visiting-file)
+                               (name . "8c[^*]*\\*")
+                               (size-lt . 65)))))
+          (should (ibuffer-included-in-filters-p
+                   bufD '((and (not (starred-name))
+                               (visiting-file)
+                               (name . "\\`\\*.*test8d")
+                               (size-lt . 129)
+                               (size-gt . 127)))))
+          (should (ibuffer-included-in-filters-p
+                   bufE '((and (starred-name)
+                               (visiting-file)
+                               (name . "8e.*?\\*<[[:digit:]]+>")
+                               (size-gt . 10)))))
+          (should (ibuffer-included-in-filters-p
+                   bufF '((and (starred-name)
+                               (not (visiting-file))
+                               (name . "8f\\*<[[:digit:]]>")
+                               (size-lt . 10))))))
+      (funcall clean-up))))
+
+;; Test Filter Combination and Decomposition
+(let* (ibuffer-to-kill       ; if non-nil, kill this buffer at cleanup
+       (ibuffer-already 'check) ; existing ibuffer buffer to use but not kill
+       ;; Utility functions without polluting the environment
+       (get-test-ibuffer
+        (lambda ()
+          "Returns a test ibuffer-mode buffer, creating one if necessary.
+        If a new buffer is created, it is named  \"*Test-Ibuffer*\" and is
+        saved to `ibuffer-to-kill' for later cleanup."
+          (when (eq ibuffer-already 'check)
+            (setq ibuffer-already
+                  (catch 'found-buf
+                    (dolist (buf (buffer-list) nil)
+                      (when (with-current-buffer buf
+                              (derived-mode-p 'ibuffer-mode))
+                        (throw 'found-buf buf))))))
+          (or ibuffer-already
+              ibuffer-to-kill
+              (let ((test-ibuf-name "*Test-Ibuffer*"))
+                (ibuffer nil test-ibuf-name nil t)
+                (setq ibuffer-to-kill (get-buffer test-ibuf-name))))))
+       (clean-up
+        (lambda ()
+          "Restore all emacs state modified during the tests"
+          (when ibuffer-to-kill         ; created ibuffer
+            (with-current-buffer ibuffer-to-kill
+              (set-buffer-modified-p nil)
+              (bury-buffer))
+            (kill-buffer ibuffer-to-kill)
+            (setq ibuffer-to-kill nil))
+          (when (and ibuffer-already (not (eq ibuffer-already 'check)))
+            ;; restore existing ibuffer state
+            (ibuffer-update nil t)))))
+  ;; Tests
+  (ert-deftest ibuffer-decompose-filter ()
+    "Tests `ibuffer-decompose-filter' for and, or, not, and saved."
+    (skip-unless (featurep 'ibuf-ext))
+    (unwind-protect
+        (let ((ibuf (funcall get-test-ibuffer)))
+          (with-current-buffer ibuf
+            (let ((ibuffer-filtering-qualifiers nil)
+                  (ibuffer-filter-groups nil)
+                  (filters '((size-gt . 100) (not (starred-name))
+                             (name . "foo"))))
+              (progn
+                (push (cons 'or filters) ibuffer-filtering-qualifiers)
+                (ibuffer-decompose-filter)
+                (should (equal filters ibuffer-filtering-qualifiers))
+                (setq ibuffer-filtering-qualifiers nil))
+              (progn
+                (push (cons 'and filters) ibuffer-filtering-qualifiers)
+                (ibuffer-decompose-filter)
+                (should (equal filters ibuffer-filtering-qualifiers))
+                (setq ibuffer-filtering-qualifiers nil))
+              (progn
+                (push (list 'not (car filters)) ibuffer-filtering-qualifiers)
+                (ibuffer-decompose-filter)
+                (should (equal (list (car filters))
+                               ibuffer-filtering-qualifiers))
+                (setq ibuffer-filtering-qualifiers nil))
+              (progn
+                (push (cons 'not (car filters)) ibuffer-filtering-qualifiers)
+                (ibuffer-decompose-filter)
+                (should (equal (list (car filters))
+                               ibuffer-filtering-qualifiers))
+                (setq ibuffer-filtering-qualifiers nil))
+              (let ((gnus (assoc "gnus" ibuffer-saved-filters)))
+                (push '(saved . "gnus") ibuffer-filtering-qualifiers)
+                (ibuffer-decompose-filter)
+                (should (equal (cdr gnus) ibuffer-filtering-qualifiers))
+                (ibuffer-decompose-filter)
+                (should (equal (cdr (cadr gnus)) ibuffer-filtering-qualifiers))
+                (setq ibuffer-filtering-qualifiers nil))
+              (when (not (assoc "__unknown__" ibuffer-saved-filters))
+                (push '(saved . "__uknown__") ibuffer-filtering-qualifiers)
+                (should-error (ibuffer-decompose-filter) :type 'error)
+                (setq ibuffer-filtering-qualifiers nil))
+              (progn
+                (push (car filters) ibuffer-filtering-qualifiers)
+                (should-error (ibuffer-decompose-filter) :type 'error)
+                (setq ibuffer-filtering-qualifiers nil)))))
+      (funcall clean-up)))
+
+  (ert-deftest ibuffer-and-filter ()
+    "Tests `ibuffer-and-filter' in an Ibuffer buffer."
+    (skip-unless (featurep 'ibuf-ext))
+    (unwind-protect
+        (let ((ibuf (funcall get-test-ibuffer)))
+          (with-current-buffer ibuf
+            (let ((ibuffer-filtering-qualifiers nil)
+                  (ibuffer-filter-groups nil)
+                  (filters [(size-gt . 100) (not (starred-name))
+                            (filename . "A") (mode . text-mode)]))
+              (should-error (ibuffer-and-filter) :type 'error)
+              (progn
+                (push (aref filters 1) ibuffer-filtering-qualifiers)
+                (should-error (ibuffer-and-filter) :type 'error))
+              (should (progn
+                        (push (aref filters 0) ibuffer-filtering-qualifiers)
+                        (ibuffer-and-filter)
+                        (and (equal (list 'and (aref filters 0) (aref filters 1))
+                                    (car ibuffer-filtering-qualifiers))
+                             (null (cdr ibuffer-filtering-qualifiers)))))
+              (should (progn
+                        (ibuffer-and-filter 'decompose)
+                        (and (equal (aref filters 0)
+                                    (pop ibuffer-filtering-qualifiers))
+                             (equal (aref filters 1)
+                                    (pop ibuffer-filtering-qualifiers))
+                             (null ibuffer-filtering-qualifiers))))
+              (should (progn
+                        (push (list 'and (aref filters 2) (aref filters 3))
+                              ibuffer-filtering-qualifiers)
+                        (push (list 'and (aref filters 0) (aref filters 1))
+                              ibuffer-filtering-qualifiers)
+                        (ibuffer-and-filter)
+                        (and (equal (list 'and (aref filters 0) (aref filters 1)
+                                          (aref filters 2) (aref filters 3))
+                                    (car ibuffer-filtering-qualifiers))
+                             (null (cdr ibuffer-filtering-qualifiers)))))
+              (pop ibuffer-filtering-qualifiers)
+              (should (progn
+                        (push (list 'or (aref filters 2) (aref filters 3))
+                              ibuffer-filtering-qualifiers)
+                        (push (list 'and (aref filters 0) (aref filters 1))
+                              ibuffer-filtering-qualifiers)
+                        (ibuffer-and-filter)
+                        (and (equal (list 'and (aref filters 0) (aref filters 1)
+                                          (list 'or (aref filters 2)
+                                                (aref filters 3)))
+                                    (car ibuffer-filtering-qualifiers))
+                             (null (cdr ibuffer-filtering-qualifiers)))))
+              (pop ibuffer-filtering-qualifiers)
+              (should (progn
+                        (push (list 'and (aref filters 2) (aref filters 3))
+                              ibuffer-filtering-qualifiers)
+                        (push (list 'or (aref filters 0) (aref filters 1))
+                              ibuffer-filtering-qualifiers)
+                        (ibuffer-and-filter)
+                        (and (equal (list 'and (list 'or (aref filters 0)
+                                                    (aref filters 1))
+                                          (aref filters 2) (aref filters 3))
+                                    (car ibuffer-filtering-qualifiers))
+                             (null (cdr ibuffer-filtering-qualifiers)))))
+              (pop ibuffer-filtering-qualifiers)
+              (should (progn
+                        (push (list 'or (aref filters 2) (aref filters 3))
+                              ibuffer-filtering-qualifiers)
+                        (push (list 'or (aref filters 0) (aref filters 1))
+                              ibuffer-filtering-qualifiers)
+                        (ibuffer-and-filter)
+                        (and (equal (list 'and
+                                          (list 'or (aref filters 0)
+                                                (aref filters 1))
+                                          (list 'or (aref filters 2)
+                                                (aref filters 3)))
+                                    (car ibuffer-filtering-qualifiers))
+                             (null (cdr ibuffer-filtering-qualifiers))))))))
+      (funcall clean-up)))
+
+  (ert-deftest ibuffer-or-filter ()
+    "Tests `ibuffer-or-filter' in an Ibuffer buffer."
+    (skip-unless (featurep 'ibuf-ext))
+    (unwind-protect
+        (let ((ibuf (funcall get-test-ibuffer)))
+          (with-current-buffer ibuf
+            (let ((ibuffer-filtering-qualifiers nil)
+                  (ibuffer-filter-groups nil)
+                  (filters [(size-gt . 100) (not (starred-name))
+                            (filename . "A") (mode . text-mode)]))
+              (should-error (ibuffer-or-filter) :type 'error)
+              (progn
+                (push (aref filters 1) ibuffer-filtering-qualifiers)
+                (should-error (ibuffer-or-filter) :type 'error))
+              (should (progn
+                        (push (aref filters 0) ibuffer-filtering-qualifiers)
+                        (ibuffer-or-filter)
+                        (and (equal (list 'or (aref filters 0) (aref filters 1))
+                                    (car ibuffer-filtering-qualifiers))
+                             (null (cdr ibuffer-filtering-qualifiers)))))
+              (should (progn
+                        (ibuffer-or-filter 'decompose)
+                        (and (equal (aref filters 0)
+                                    (pop ibuffer-filtering-qualifiers))
+                             (equal (aref filters 1)
+                                    (pop ibuffer-filtering-qualifiers))
+                             (null ibuffer-filtering-qualifiers))))
+              (should (progn
+                        (push (list 'or (aref filters 2) (aref filters 3))
+                              ibuffer-filtering-qualifiers)
+                        (push (list 'or (aref filters 0) (aref filters 1))
+                              ibuffer-filtering-qualifiers)
+                        (ibuffer-or-filter)
+                        (and (equal (list 'or (aref filters 0) (aref filters 1)
+                                          (aref filters 2) (aref filters 3))
+                                    (car ibuffer-filtering-qualifiers))
+                             (null (cdr ibuffer-filtering-qualifiers)))))
+              (pop ibuffer-filtering-qualifiers)
+              (should (progn
+                        (push (list 'and (aref filters 2) (aref filters 3))
+                              ibuffer-filtering-qualifiers)
+                        (push (list 'or (aref filters 0) (aref filters 1))
+                              ibuffer-filtering-qualifiers)
+                        (ibuffer-or-filter)
+                        (and (equal (list 'or (aref filters 0) (aref filters 1)
+                                          (list 'and (aref filters 2)
+                                                (aref filters 3)))
+                                    (car ibuffer-filtering-qualifiers))
+                             (null (cdr ibuffer-filtering-qualifiers)))))
+              (pop ibuffer-filtering-qualifiers)
+              (should (progn
+                        (push (list 'or (aref filters 2) (aref filters 3))
+                              ibuffer-filtering-qualifiers)
+                        (push (list 'and (aref filters 0) (aref filters 1))
+                              ibuffer-filtering-qualifiers)
+                        (ibuffer-or-filter)
+                        (and (equal (list 'or (list 'and (aref filters 0)
+                                                    (aref filters 1))
+                                          (aref filters 2) (aref filters 3))
+                                    (car ibuffer-filtering-qualifiers))
+                             (null (cdr ibuffer-filtering-qualifiers)))))
+              (pop ibuffer-filtering-qualifiers)
+              (should (progn
+                        (push (list 'and (aref filters 2) (aref filters 3))
+                              ibuffer-filtering-qualifiers)
+                        (push (list 'and (aref filters 0) (aref filters 1))
+                              ibuffer-filtering-qualifiers)
+                        (ibuffer-or-filter)
+                        (and (equal (list 'or
+                                          (list 'and (aref filters 0)
+                                                (aref filters 1))
+                                          (list 'and (aref filters 2)
+                                                (aref filters 3)))
+                                    (car ibuffer-filtering-qualifiers))
+                             (null (cdr ibuffer-filtering-qualifiers))))))))
+      (funcall clean-up))))
+
+(ert-deftest ibuffer-format-qualifier ()
+  "Tests string recommendation of filter from `ibuffer-format-qualifier'."
+  (skip-unless (featurep 'ibuf-ext))
+  (let ((test1 '(mode . org-mode))
+        (test2 '(size-lt . 100))
+        (test3 '(derived-mode . prog-mode))
+        (test4 '(or (size-gt . 10000)
+                    (and (not (starred-name))
+                         (directory . "\\<org\\>"))))
+        (test5 '(or (filename . "scratch")
+                    (filename . "bonz")
+                    (filename . "temp")))
+        (test6 '(or (mode . emacs-lisp-mode) (file-extension . "elc?")
+                    (and (starred-name) (name . "elisp"))
+                    (mode . lisp-interaction-mode)))
+        (description (lambda (q)
+                       (cadr (assq q ibuffer-filtering-alist))))
+        (tag (lambda (&rest args )
+               (concat " [" (apply #'concat args) "]"))))
+    (should (equal (ibuffer-format-qualifier test1)
+                   (funcall tag (funcall description 'mode)
+                            ": " "org-mode")))
+    (should (equal (ibuffer-format-qualifier test2)
+                   (funcall tag (funcall description 'size-lt)
+                            ": " "100")))
+    (should (equal (ibuffer-format-qualifier test3)
+                   (funcall tag (funcall description 'derived-mode)
+                            ": " "prog-mode")))
+    (should (equal (ibuffer-format-qualifier test4)
+                   (funcall tag "OR"
+                            (funcall tag (funcall description 'size-gt)
+                                     ": " (format "%s" 10000))
+                            (funcall tag "AND"
+                                     (funcall tag "NOT"
+                                              (funcall tag
+                                                       (funcall description
+                                                                'starred-name)
+                                                       ": " "nil"))
+                                     (funcall tag
+                                              (funcall description 'directory)
+                                              ": " "\\<org\\>")))))
+    (should (equal (ibuffer-format-qualifier test5)
+                   (funcall tag "OR"
+                            (funcall tag (funcall description 'filename)
+                                     ": "  "scratch")
+                            (funcall tag (funcall description 'filename)
+                                     ": " "bonz")
+                            (funcall tag (funcall description 'filename)
+                                     ": " "temp"))))
+    (should (equal (ibuffer-format-qualifier test6)
+                   (funcall tag "OR"
+                            (funcall tag (funcall description 'mode)
+                                     ": " "emacs-lisp-mode")
+                            (funcall tag (funcall description 'file-extension)
+                                     ": " "elc?")
+                            (funcall tag "AND"
+                                     (funcall tag
+                                              (funcall description 'starred-name)
+                                              ": " "nil")
+                                     (funcall tag
+                                              (funcall description 'name)
+                                              ": " "elisp"))
+                            (funcall tag (funcall description 'mode)
+                                     ": " "lisp-interaction-mode"))))))
+
+(ert-deftest ibuffer-unary-operand ()
+  "Tests `ibuffer-unary-operand': (not cell) or (not . cell) -> cell."
+  (skip-unless (featurep 'ibuf-ext))
+  (should (equal (ibuffer-unary-operand '(not . (mode "foo")))
+                 '(mode "foo")))
+  (should (equal (ibuffer-unary-operand '(not (mode "foo")))
+                 '(mode "foo")))
+  (should (equal (ibuffer-unary-operand '(not "cdr"))
+                 '("cdr")))
+  (should (equal (ibuffer-unary-operand '(not)) nil))
+  (should (equal (ibuffer-unary-operand '(not . a)) 'a)))
+
 (provide 'ibuffer-tests)
 ;; ibuffer-tests.el ends here

commit f8072cd5c16f855505f6a0ce6a6b30309735705d
Author: Mark Oteiza <mvoteiza@udel.edu>
Date:   Mon Dec 19 20:37:46 2016 -0500

    Update NEWS
    
    * etc/NEWS (Image-Dired): New section.

diff --git a/etc/NEWS b/etc/NEWS
index e69ba87484..e15ab79d50 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -462,6 +462,39 @@ for 'image-mode' that complement 'image-scroll-up' and
 'image-scroll-down': they have the same prefix arg behavior and stop
 at image boundaries.
 
+** Image-Dired
+
+*** Now provides a minor mode 'image-dired-minor-mode' which replaces
+the function 'image-dired-setup-dired-keybindings'.
+
+*** Thumbnail generation is now asynchronous
+The number of concurrent processes is limited by the variable
+'image-dired-thumb-job-limit'.
+
+*** 'image-dired-thumbnail-storage' has a new option 'standard-large'
+for generating 256x256 thumbnails according to the Thumbnail Managing
+Standard.
+
+*** Inherits movement keys from 'image-mode' for viewing full images.
+This includes the usual char, line, and page movement commands.
+
+*** All the -options types have been changed to argument lists
+instead of shell command strings.  This change affects
+'image-dired-cmd-create-thumbnail-options',
+'image-dired-cmd-create-temp-image-options',
+'image-dired-cmd-rotate-thumbnail-options',
+'image-dired-cmd-rotate-original-options',
+'image-dired-cmd-write-exif-data-options',
+'image-dired-cmd-read-exif-data-options', and introduces
+'image-dired-cmd-pngnq-options', 'image-dired-cmd-pngcrush-options',
+'image-dired-cmd-create-standard-thumbnail-options'
+
+*** Recognizes more tools by default, including pngnq-s9, optipng, and gm
+
+*** 'find-file' and related commands now work on thumbnails and
+displayed images, providing a default argument of the original file name
+via an addition to 'file-name-at-point-functions'.
+
 ---
 ** The default 'Info-default-directory-list' no longer checks some obsolete
 directory suffixes (gnu, gnu/lib, gnu/lib/emacs, emacs, lib, lib/emacs)

commit 5c266405f559823038dfa900aaad66605f0d5287
Author: Mark Oteiza <mvoteiza@udel.edu>
Date:   Mon Dec 19 20:35:02 2016 -0500

    Recognize graphicsmagick in image-dired
    
    * lisp/image-dired.el (image-dired-cmd-create-thumbnail-program):
    (image-dired-cmd-create-thumbnail-options):
    (image-dired-cmd-create-temp-image-program):
    (image-dired-cmd-create-temp-image-options):
    (image-dired-cmd-create-standard-thumbnail-options):
    (image-dired-cmd-rotate-thumbnail-program):
    (image-dired-cmd-rotate-thumbnail-options): Account for existence of
    gm(1) executable.

diff --git a/lisp/image-dired.el b/lisp/image-dired.el
index 560cadbe75..eed4280c03 100644
--- a/lisp/image-dired.el
+++ b/lisp/image-dired.el
@@ -221,14 +221,19 @@ expects to find pictures in this directory."
   :group 'image-dired)
 
 (defcustom image-dired-cmd-create-thumbnail-program
-  "convert"
+  (cond ((executable-find "gm") "gm")
+        ((executable-find "convert") "convert")
+        (t "convert"))
   "Executable used to create thumbnail.
 Used together with `image-dired-cmd-create-thumbnail-options'."
+  :version "26.1"
   :type 'file
   :group 'image-dired)
 
 (defcustom image-dired-cmd-create-thumbnail-options
-  '("-size" "%wx%h" "%f" "-resize" "%wx%h>" "-strip" "jpeg:%t")
+  `(,@(when (string-match "gm\\'" image-dired-cmd-create-thumbnail-program)
+        '("convert"))
+    "-size" "%wx%h" "%f" "-resize" "%wx%h>" "-strip" "jpeg:%t")
   "Options of command used to create thumbnail image.
 Used with `image-dired-cmd-create-thumbnail-program'.
 Available format specifiers are: %w which is replaced by
@@ -239,14 +244,20 @@ which is replaced by the file name of the thumbnail file."
   :type '(repeat (string :tag "Argument"))
   :group 'image-dired)
 
-(defcustom image-dired-cmd-create-temp-image-program "convert"
+(defcustom image-dired-cmd-create-temp-image-program
+  (cond ((executable-find "gm") "gm")
+        ((executable-find "convert") "convert")
+        (t "convert"))
   "Executable used to create temporary image.
 Used together with `image-dired-cmd-create-temp-image-options'."
+  :version "26.1"
   :type 'file
   :group 'image-dired)
 
 (defcustom image-dired-cmd-create-temp-image-options
-  '("-size" "%wx%h" "%f" "-resize" "%wx%h>" "-strip" "jpeg:%t")
+  `(,@(when (string-match "gm\\'" image-dired-cmd-create-temp-image-program)
+        '("convert"))
+    "-size" "%wx%h" "%f" "-resize" "%wx%h>" "-strip" "jpeg:%t")
   "Options of command used to create temporary image for display window.
 Used together with `image-dired-cmd-create-temp-image-program',
 Available format specifiers are: %w and %h which are replaced by
@@ -316,15 +327,17 @@ Available format specifiers are described in
   :group 'image-dired)
 
 (defcustom image-dired-cmd-create-standard-thumbnail-options
-  (append '("-size" "%wx%h" "%f")
-          (unless (or image-dired-cmd-pngcrush-program
-                      image-dired-cmd-pngnq-program)
-            (list
-             "-set" "Thumb::MTime" "%m"
-             "-set" "Thumb::URI" "file://%f"
-             "-set" "Description" "Thumbnail of file://%f"
-             "-set" "Software" (emacs-version)))
-          '("-thumbnail" "%wx%h>" "png:%t"))
+  `(,@(when (string-match "gm\\'" image-dired-cmd-create-thumbnail-program)
+        '("convert"))
+    "-size" "%wx%h" "%f"
+    ,@(unless (or image-dired-cmd-pngcrush-program
+                  image-dired-cmd-pngnq-program)
+        (list
+         "-set" "Thumb::MTime" "%m"
+         "-set" "Thumb::URI" "file://%f"
+         "-set" "Description" "Thumbnail of file://%f"
+         "-set" "Software" (emacs-version)))
+    "-thumbnail" "%wx%h>" "png:%t")
   "Options for creating thumbnails according to the Thumbnail Managing Standard.
 Available format specifiers are the same as in
 `image-dired-cmd-create-thumbnail-options', with %m for file modification time."
@@ -333,14 +346,19 @@ Available format specifiers are the same as in
   :group 'image-dired)
 
 (defcustom image-dired-cmd-rotate-thumbnail-program
-  "mogrify"
+  (cond ((executable-find "gm") "gm")
+        ((executable-find "mogrify") "mogrify")
+        (t "mogrify"))
   "Executable used to rotate thumbnail.
 Used together with `image-dired-cmd-rotate-thumbnail-options'."
+  :version "26.1"
   :type 'file
   :group 'image-dired)
 
 (defcustom image-dired-cmd-rotate-thumbnail-options
-  '("-rotate" "%d" "%t")
+  `(,@(when (string-match "gm\\'" image-dired-cmd-rotate-thumbnail-program)
+        '("mogrify"))
+    "-rotate" "%d" "%t")
   "Arguments of command used to rotate thumbnail image.
 Used with `image-dired-cmd-rotate-thumbnail-program'.
 Available format specifiers are: %d which is replaced by the

commit 36b9973dec65c8daf57d1cb73f0de5a3c59279fc
Author: Mark Oteiza <mvoteiza@udel.edu>
Date:   Mon Dec 19 19:47:06 2016 -0500

    Implement asynchronous thumbnail generation in image-dired
    
    Additionally, all FOO-options defcustoms that were in fact shell command
    strings have been converted to argument lists.  Another method for
    shrinking PNG thumbs with optipng(1) has been added.
    * lisp/image-dired.el: Remove TODO item in commentary.
    (image-dired-cmd-create-thumbnail-options):
    (image-dired-cmd-create-temp-image-options):
    (image-dired-cmd-rotate-thumbnail-options):
    (image-dired-cmd-rotate-original-options):
    (image-dired-cmd-write-exif-data-options):
    (image-dired-cmd-read-exif-data-options): Convert to argument lists.
    (image-dired-cmd-pngnq-program, image-dired-cmd-pngcrush-program):
    Change string type to file.
    (image-dired-cmd-create-standard-thumbnail-command): Remove.
    (image-dired-cmd-pngnq-options):
    (image-dired-cmd-create-standard-thumbnail-options):
    (image-dired-cmd-optipng-program, image-dired-cmd-optipng-options):
    New defcustoms.
    (image-dired-queue, image-dired-queue-active-jobs):
    (image-dired-queue-active-limit): New variables.
    (image-dired-pngnq-thumb, image-dired-pngcrush-thumb):
    (image-dired-optipng-thumb): New functions.
    (image-dired-create-thumb-1): Renamed from image-dired-create-thumb.
    Use start-process instead of call-process.  Set file modes.  Trigger
    PNG file optimization in process sentinel.
    (image-dired-thumb-queue-run, image-dired-create-thumb): New functions.
    (image-dired-display-thumbs):
    (image-dired-create-thumbs): Don't expect call-process return value.
    (image-dired-display-image, image-dired-rotate-thumbnail): Use
    start-process instead of call-process.
    (image-dired-rotate-original, image-dired-set-exif-data):
    (image-dired-get-exif-data): Adapt to arguments being an arg list.

diff --git a/lisp/image-dired.el b/lisp/image-dired.el
index 6902d742db..560cadbe75 100644
--- a/lisp/image-dired.el
+++ b/lisp/image-dired.el
@@ -118,8 +118,6 @@
 ;; * From thumbs.el: Add the "modify" commands (emboss, negate,
 ;;   monochrome etc).
 ;;
-;; * Asynchronous creation of thumbnails.
-;;
 ;; * Add `image-dired-display-thumbs-ring' and functions to cycle that.  Find
 ;; out which is best, saving old batch just before inserting new, or
 ;; saving the current batch in the ring when inserting it.  Adding it
@@ -230,14 +228,15 @@ Used together with `image-dired-cmd-create-thumbnail-options'."
   :group 'image-dired)
 
 (defcustom image-dired-cmd-create-thumbnail-options
-  "%p -size %wx%h \"%f\" -resize \"%wx%h>\" -strip jpeg:\"%t\""
-  "Format of command used to create thumbnail image.
-Available options are %p which is replaced by
-`image-dired-cmd-create-thumbnail-program', %w which is replaced by
+  '("-size" "%wx%h" "%f" "-resize" "%wx%h>" "-strip" "jpeg:%t")
+  "Options of command used to create thumbnail image.
+Used with `image-dired-cmd-create-thumbnail-program'.
+Available format specifiers are: %w which is replaced by
 `image-dired-thumb-width', %h which is replaced by `image-dired-thumb-height',
 %f which is replaced by the file name of the original image and %t
 which is replaced by the file name of the thumbnail file."
-  :type 'string
+  :version "26.1"
+  :type '(repeat (string :tag "Argument"))
   :group 'image-dired)
 
 (defcustom image-dired-cmd-create-temp-image-program "convert"
@@ -247,14 +246,15 @@ Used together with `image-dired-cmd-create-temp-image-options'."
   :group 'image-dired)
 
 (defcustom image-dired-cmd-create-temp-image-options
-  "%p -size %wx%h \"%f\" -resize \"%wx%h>\" -strip jpeg:\"%t\""
-  "Format of command used to create temporary image for display window.
-Available options are %p which is replaced by
-`image-dired-cmd-create-temp-image-program', %w and %h which is replaced by
+  '("-size" "%wx%h" "%f" "-resize" "%wx%h>" "-strip" "jpeg:%t")
+  "Options of command used to create temporary image for display window.
+Used together with `image-dired-cmd-create-temp-image-program',
+Available format specifiers are: %w and %h which are replaced by
 the calculated max size for width and height in the image display window,
 %f which is replaced by the file name of the original image and %t which
 is replaced by the file name of the temporary file."
-  :type 'string
+  :version "26.1"
+  :type '(repeat (string :tag "Argument"))
   :group 'image-dired)
 
 (defcustom image-dired-cmd-pngnq-program
@@ -264,49 +264,72 @@ is replaced by the file name of the temporary file."
 It quantizes colors of PNG images down to 256 colors or fewer
 using the Neuquant procedure."
   :version "26.1"
-  :type '(choice (const :tag "Not Set" nil) string)
+  :type '(choice (const :tag "Not Set" nil) file)
+  :group 'image-dired)
+
+(defcustom image-dired-cmd-pngnq-options
+  '("-f" "%t")
+  "Arguments to pass `image-dired-cmd-pngnq-program'.
+Available format specifiers are the same as in
+`image-dired-cmd-create-thumbnail-options'."
+  :version "26.1"
+  :type '(repeat (string :tag "Argument"))
   :group 'image-dired)
 
 (defcustom image-dired-cmd-pngcrush-program (executable-find "pngcrush")
   "The file name of the `pngcrush' program.
 It optimizes the compression of PNG images.  Also it adds PNG textual chunks
 with the information required by the Thumbnail Managing Standard."
-  :type '(choice (const :tag "Not Set" nil) string)
+  :type '(choice (const :tag "Not Set" nil) file)
   :group 'image-dired)
 
-(defcustom image-dired-cmd-create-standard-thumbnail-command
-  (concat
-   "%p -size %wx%h \"%f\" "
-   (unless (or image-dired-cmd-pngcrush-program image-dired-cmd-pngnq-program)
-     (concat
-      "-set \"Thumb::MTime\" \"%m\" "
-      "-set \"Thumb::URI\" \"file://%f\" "
-      "-set \"Description\" \"Thumbnail of file://%f\" "
-      "-set \"Software\" \"" (emacs-version) "\" "))
-   "-thumbnail \"%wx%h>\" png:\"%t\""
-   (if image-dired-cmd-pngnq-program
-       (concat
-        " ; " image-dired-cmd-pngnq-program " -f \"%t\""
-        (unless image-dired-cmd-pngcrush-program
-          " ; mv %q %t")))
-   (if image-dired-cmd-pngcrush-program
-       (concat
-        (unless image-dired-cmd-pngcrush-program
-          " ; cp %t %q")
-        " ; " image-dired-cmd-pngcrush-program " -q "
-        "-text b \"Description\" \"Thumbnail of file://%f\" "
-        "-text b \"Software\" \"" (emacs-version) "\" "
-        ;; "-text b \"Thumb::Image::Height\" \"%oh\" "
-        ;; "-text b \"Thumb::Image::Mimetype\" \"%mime\" "
-        ;; "-text b \"Thumb::Image::Width\" \"%ow\" "
-        "-text b \"Thumb::MTime\" \"%m\" "
-        ;; "-text b \"Thumb::Size\" \"%b\" "
-        "-text b \"Thumb::URI\" \"file://%f\" "
-        "%q %t"
-        " ; rm %q")))
-  "Command to create thumbnails according to the Thumbnail Managing Standard."
+(defcustom image-dired-cmd-pngcrush-options
+  `("-q"
+    "-text" "b" "Description" "Thumbnail of file://%f"
+    "-text" "b" "Software" ,(emacs-version)
+    ;; "-text b \"Thumb::Image::Height\" \"%oh\" "
+    ;; "-text b \"Thumb::Image::Mimetype\" \"%mime\" "
+    ;; "-text b \"Thumb::Image::Width\" \"%ow\" "
+    "-text" "b" "Thumb::MTime" "%m"
+    ;; "-text b \"Thumb::Size\" \"%b\" "
+    "-text" "b" "Thumb::URI" "file://%f"
+    "%q" "%t")
+  "Arguments for `image-dired-cmd-pngcrush-program'.
+Available format specifiers are the same as in
+`image-dired-cmd-create-thumbnail-options', with %q for a
+temporary file name (typically generated by pnqnq)"
   :version "26.1"
-  :type 'string
+  :type '(repeat (string :tag "Argument"))
+  :group 'image-dired)
+
+(defcustom image-dired-cmd-optipng-program (executable-find "optipng")
+  "The file name of the `optipng' program."
+  :type '(choice (const :tag "Not Set" nil) file)
+  :group 'image-dired)
+
+(defcustom image-dired-cmd-optipng-options '("-o5" "%t")
+  "Arguments passed to `image-dired-optipng-program'.
+Available format specifiers are described in
+`image-dired-cmd-create-thumbnail-options'."
+  :type '(repeat (string :tag "Argument"))
+  :link '(url-link "man:optipng(1)")
+  :group 'image-dired)
+
+(defcustom image-dired-cmd-create-standard-thumbnail-options
+  (append '("-size" "%wx%h" "%f")
+          (unless (or image-dired-cmd-pngcrush-program
+                      image-dired-cmd-pngnq-program)
+            (list
+             "-set" "Thumb::MTime" "%m"
+             "-set" "Thumb::URI" "file://%f"
+             "-set" "Description" "Thumbnail of file://%f"
+             "-set" "Software" (emacs-version)))
+          '("-thumbnail" "%wx%h>" "png:%t"))
+  "Options for creating thumbnails according to the Thumbnail Managing Standard.
+Available format specifiers are the same as in
+`image-dired-cmd-create-thumbnail-options', with %m for file modification time."
+  :version "26.1"
+  :type '(repeat (string :tag "Argument"))
   :group 'image-dired)
 
 (defcustom image-dired-cmd-rotate-thumbnail-program
@@ -317,14 +340,15 @@ Used together with `image-dired-cmd-rotate-thumbnail-options'."
   :group 'image-dired)
 
 (defcustom image-dired-cmd-rotate-thumbnail-options
-  "%p -rotate %d \"%t\""
-  "Format of command used to rotate thumbnail image.
-Available options are %p which is replaced by
-`image-dired-cmd-rotate-thumbnail-program', %d which is replaced by the
+  '("-rotate" "%d" "%t")
+  "Arguments of command used to rotate thumbnail image.
+Used with `image-dired-cmd-rotate-thumbnail-program'.
+Available format specifiers are: %d which is replaced by the
 number of (positive) degrees to rotate the image, normally 90 or 270
 \(for 90 degrees right and left), %t which is replaced by the file name
 of the thumbnail file."
-  :type 'string
+  :version "26.1"
+  :type '(repeat (string :tag "Argument"))
   :group 'image-dired)
 
 (defcustom image-dired-cmd-rotate-original-program
@@ -335,15 +359,16 @@ Used together with `image-dired-cmd-rotate-original-options'."
   :group 'image-dired)
 
 (defcustom image-dired-cmd-rotate-original-options
-  "%p -rotate %d -copy all -outfile %t \"%o\""
-  "Format of command used to rotate original image.
-Available options are %p which is replaced by
-`image-dired-cmd-rotate-original-program', %d which is replaced by the
+  '("-rotate" "%d" "-copy" "all" "-outfile" "%t" "%o")
+  "Arguments of command used to rotate original image.
+Used with `image-dired-cmd-rotate-original-program'.
+Available format specifiers are: %d which is replaced by the
 number of (positive) degrees to rotate the image, normally 90 or
 270 \(for 90 degrees right and left), %o which is replaced by the
 original image file name and %t which is replaced by
 `image-dired-temp-image-file'."
-  :type 'string
+  :version "26.1"
+  :type '(repeat (string :tag "Argument"))
   :group 'image-dired)
 
 (defcustom image-dired-temp-rotate-image-file
@@ -367,13 +392,14 @@ Used together with `image-dired-cmd-write-exif-data-options'."
   :group 'image-dired)
 
 (defcustom image-dired-cmd-write-exif-data-options
-  "%p -%t=\"%v\" \"%f\""
-  "Format of command used to write EXIF data.
-Available options are %p which is replaced by
-`image-dired-cmd-write-exif-data-program', %f which is replaced by
+  '("-%t=%v" "%f")
+  "Arguments of command used to write EXIF data.
+Used with `image-dired-cmd-write-exif-data-program'.
+Available format specifiers are: %f which is replaced by
 the image file name, %t which is replaced by the tag name and %v
 which is replaced by the tag value."
-  :type 'string
+  :version "26.1"
+  :type '(repeat (string :tag "Argument"))
   :group 'image-dired)
 
 (defcustom image-dired-cmd-read-exif-data-program
@@ -384,12 +410,13 @@ Used together with `image-dired-cmd-read-exif-data-options'."
   :group 'image-dired)
 
 (defcustom image-dired-cmd-read-exif-data-options
-  "%p -s -s -s -%t \"%f\""
-  "Format of command used to read EXIF data.
-Available options are %p which is replaced by
-`image-dired-cmd-write-exif-data-program', %f which is replaced
+  '("-s" "-s" "-s" "-%t" "%f")
+  "Arguments of command used to read EXIF data.
+Used with `image-dired-cmd-read-exif-data-program'.
+Available format specifiers are: %f which is replaced
 by the image file name and %t which is replaced by the tag name."
-  :type 'string
+  :version "26.1"
+  :type '(repeat (string :tag "Argument"))
   :group 'image-dired)
 
 (defcustom image-dired-gallery-hidden-tags
@@ -640,7 +667,81 @@ DIMENSION should be either the symbol 'width or 'height."
         (width image-dired-thumb-width)
         (height image-dired-thumb-height)))))
 
-(defun image-dired-create-thumb (original-file thumbnail-file)
+(defvar image-dired-queue nil
+  "List of items in the queue.
+Each item has the form (ORIGINAL-FILE TARGET-FILE).")
+
+(defvar image-dired-queue-active-jobs 0
+  "Number of active jobs in `image-dired-queue'.")
+
+(defvar image-dired-queue-active-limit 2
+  "Maximum number of concurrent jobs permitted for generating images.
+Increase at own risk.")
+
+(defun image-dired-pngnq-thumb (spec)
+  "Quantize thumbnail described by format SPEC with pngnq(1)."
+  (let ((process
+         (apply #'start-process "image-dired-pngnq" nil
+                image-dired-cmd-pngnq-program
+                (mapcar (lambda (arg) (format-spec arg spec))
+                        image-dired-cmd-pngnq-options))))
+    (setf (process-sentinel process)
+          (lambda (process status)
+            (if (and (eq (process-status process) 'exit)
+                     (zerop (process-exit-status process)))
+                ;; Pass off to pngcrush, or just rename the
+                ;; THUMB-nq8.png file back to THUMB.png
+                (if (and image-dired-cmd-pngcrush-program
+                         (executable-find image-dired-cmd-pngcrush-program))
+                    (image-dired-pngcrush-thumb spec)
+                  (let ((nq8 (cdr (assq ?q spec)))
+                        (thumb (cdr (assq ?t spec))))
+                    (rename-file nq8 thumb t)))
+              (message "command %S %s" (process-command process)
+                       (replace-regexp-in-string "\n" "" status)))))
+    process))
+
+(defun image-dired-pngcrush-thumb (spec)
+  "Optimize thumbnail decsribed by format SPEC with pngcrush(1)."
+  ;; If pngnq wasn't run, then the THUMB-nq8.png file does not exist.
+  ;; pngcrush needs an infile and outfile, so we just copy THUMB to
+  ;; THUMB-nq8.png and use the latter as a temp file.
+  (when (not image-dired-cmd-pngnq-program)
+    (let ((temp (cdr (assq ?q spec)))
+          (thumb (cdr (assq ?t spec))))
+      (copy-file thumb temp)))
+  (let ((process
+         (apply #'start-process "image-dired-pngcrush" nil
+                image-dired-cmd-pngcrush-program
+                (mapcar (lambda (arg) (format-spec arg spec))
+                        image-dired-cmd-pngcrush-options))))
+    (setf (process-sentinel process)
+          (lambda (process status)
+            (unless (and (eq (process-status process) 'exit)
+                         (zerop (process-exit-status process)))
+              (message "command %S %s" (process-command process)
+                       (replace-regexp-in-string "\n" "" status)))
+            (when (memq (process-status process) '(exit signal))
+              (let ((temp (cdr (assq ?q spec))))
+                (delete-file temp)))))
+    process))
+
+(defun image-dired-optipng-thumb (spec)
+  "Optimize thumbnail decsribed by format SPEC with optipng(1)."
+  (let ((process
+         (apply #'start-process "image-dired-optipng" nil
+                image-dired-cmd-optipng-program
+                (mapcar (lambda (arg) (format-spec arg spec))
+                        image-dired-cmd-optipng-options))))
+    (setf (process-sentinel process)
+          (lambda (process status)
+            (unless (and (eq (process-status process) 'exit)
+                         (zerop (process-exit-status process)))
+              (message "command %S %s" (process-command process)
+                       (replace-regexp-in-string "\n" "" status)))))
+    process))
+
+(defun image-dired-create-thumb-1 (original-file thumbnail-file)
   "For ORIGINAL-FILE, create thumbnail image named THUMBNAIL-FILE."
   (image-dired--check-executable-exists
    'image-dired-cmd-create-thumbnail-program)
@@ -649,25 +750,76 @@ DIMENSION should be either the symbol 'width or 'height."
          (modif-time (floor (float-time (nth 5 (file-attributes original-file)))))
          (thumbnail-nq8-file (replace-regexp-in-string ".png\\'" "-nq8.png"
                                                        thumbnail-file))
-         (command
-          (format-spec
-           (if (memq image-dired-thumbnail-storage '(standard standard-large))
-               image-dired-cmd-create-standard-thumbnail-command
-             image-dired-cmd-create-thumbnail-options)
-           (list
-            (cons ?p image-dired-cmd-create-thumbnail-program)
-            (cons ?w width)
-            (cons ?h height)
-            (cons ?m modif-time)
-            (cons ?f original-file)
-            (cons ?q thumbnail-nq8-file)
-            (cons ?t thumbnail-file))))
-         thumbnail-dir)
-    (when (not (file-exists-p
-                (setq thumbnail-dir (file-name-directory thumbnail-file))))
-      (message "Creating thumbnail directory.")
-      (make-directory thumbnail-dir t))
-    (call-process shell-file-name nil nil nil shell-command-switch command)))
+         (spec
+          (list
+           (cons ?w width)
+           (cons ?h height)
+           (cons ?m modif-time)
+           (cons ?f original-file)
+           (cons ?q thumbnail-nq8-file)
+           (cons ?t thumbnail-file)))
+         (thumbnail-dir (file-name-directory thumbnail-file))
+         process)
+    (when (not (file-exists-p thumbnail-dir))
+      (message "Creating thumbnail directory")
+      (make-directory thumbnail-dir t)
+      (set-file-modes thumbnail-dir #o700))
+
+    ;; Thumbnail file creation processes begin here and are marshalled
+    ;; in a queue by `image-dired-create-thumb'.
+    (setq process
+          (apply #'start-process "image-dired-create-thumbnail" nil
+                 image-dired-cmd-create-thumbnail-program
+                 (mapcar
+                  (lambda (arg) (format-spec arg spec))
+                  (if (memq image-dired-thumbnail-storage
+                            '(standard standard-large))
+                      image-dired-cmd-create-standard-thumbnail-options
+                    image-dired-cmd-create-thumbnail-options))))
+
+    (setf (process-sentinel process)
+          (lambda (process status)
+            ;; Trigger next in queue once a thumbnail has been created
+            (cl-decf image-dired-queue-active-jobs)
+            (image-dired-thumb-queue-run)
+            (if (not (and (eq (process-status process) 'exit)
+                          (zerop (process-exit-status process))))
+                (message "Thumb could not be created for %s: %s"
+                         (abbreviate-file-name original-file)
+                         (replace-regexp-in-string "\n" "" status))
+              (set-file-modes thumbnail-file #o600)
+              (clear-image-cache thumbnail-file)
+              ;; PNG thumbnail has been created since we are
+              ;; following the XDG thumbnail spec, so try to optimize
+              (when (memq image-dired-thumbnail-storage
+                          '(standard standard-large))
+                (cond
+                 ((and image-dired-cmd-pngnq-program
+                       (executable-find image-dired-cmd-pngnq-program))
+                  (image-dired-pngnq-thumb spec))
+                 ((and image-dired-cmd-pngcrush-program
+                       (executable-find image-dired-cmd-pngcrush-program))
+                  (image-dired-pngcrush-thumb spec))
+                 ((and image-dired-cmd-optipng-program
+                       (executable-find image-dired-cmd-optipng-program))
+                  (image-dired-optipng-thumb spec)))))))
+    process))
+
+(defun image-dired-thumb-queue-run ()
+  "Run a queued job if one exists and not too many jobs are running.
+Queued items live in `image-dired-queue'."
+  (while (and image-dired-queue
+              (< image-dired-queue-active-jobs
+                 image-dired-queue-active-limit))
+    (cl-incf image-dired-queue-active-jobs)
+    (apply #'image-dired-create-thumb-1 (pop image-dired-queue))))
+
+(defun image-dired-create-thumb (original-file thumbnail-file)
+  "Add a job for generating thumbnail to `image-dired-queue'."
+  (setq image-dired-queue
+        (nconc image-dired-queue
+               (list (list original-file thumbnail-file))))
+  (run-at-time 0 nil #'image-dired-thumb-queue-run))
 
 ;;;###autoload
 (defun image-dired-dired-toggle-marked-thumbs (&optional arg)
@@ -867,10 +1019,9 @@ thumbnail buffer to be selected."
           (goto-char (point-max)))
         (dolist (curr-file files)
           (setq thumb-name (image-dired-thumb-name curr-file))
-          (if (and (not (file-exists-p thumb-name))
-                   (not (= 0 (image-dired-create-thumb curr-file thumb-name))))
-              (message "Thumb could not be created for file %s" curr-file)
-            (image-dired-insert-thumbnail thumb-name curr-file dired-buf))))
+          (when (not (file-exists-p thumb-name))
+            (image-dired-create-thumb curr-file thumb-name))
+          (image-dired-insert-thumbnail thumb-name curr-file dired-buf)))
       (if do-not-pop
           (display-buffer buf)
         (pop-to-buffer buf))
@@ -1554,8 +1705,7 @@ With prefix argument ARG, create thumbnails even if they already exist
         (clear-image-cache (expand-file-name thumb-name)))
       (when (or (not (file-exists-p thumb-name))
                 arg)
-        (when (not (= 0 (image-dired-create-thumb curr-file thumb-name)))
-          (error "Thumb could not be created"))))))
+        (image-dired-create-thumb curr-file thumb-name)))))
 
 (defvar image-dired-slideshow-timer nil
   "Slideshow timer.")
@@ -1747,17 +1897,19 @@ original size."
         (image-type 'jpeg))
     (setq file (expand-file-name file))
     (if (not original-size)
-        (let* ((command
-                (format-spec
-                 image-dired-cmd-create-temp-image-options
-                 (list
-                  (cons ?p image-dired-cmd-create-temp-image-program)
-                  (cons ?w (image-dired-display-window-width window))
-                  (cons ?h (image-dired-display-window-height window))
-                  (cons ?f file)
-                  (cons ?t new-file))))
-               (ret (call-process shell-file-name nil nil nil
-				  shell-command-switch command)))
+        (let* ((spec
+                (list
+                 (cons ?p image-dired-cmd-create-temp-image-program)
+                 (cons ?w (image-dired-display-window-width window))
+                 (cons ?h (image-dired-display-window-height window))
+                 (cons ?f file)
+                 (cons ?t new-file)))
+               (ret
+                (apply #'call-process
+                       image-dired-cmd-create-temp-image-program nil nil nil
+                       (mapcar
+                        (lambda (arg) (format-spec arg spec))
+                        image-dired-cmd-create-temp-image-options))))
           (when (not (zerop ret))
             (error "Could not resize image")))
       (setq image-type (image-type-from-file-name file))
@@ -1811,14 +1963,10 @@ With prefix argument ARG, display image in its original size."
       (message "No thumbnail at point")
     (let* ((file (image-dired-thumb-name (image-dired-original-file-name)))
            (thumb (expand-file-name file))
-           command)
-      (setq command (format-spec
-                     image-dired-cmd-rotate-thumbnail-options
-                     (list
-                      (cons ?p image-dired-cmd-rotate-thumbnail-program)
-                      (cons ?d degrees)
-                      (cons ?t thumb))))
-      (call-process shell-file-name nil nil nil shell-command-switch command)
+           (spec (list (cons ?d degrees) (cons ?t thumb))))
+      (apply #'call-process image-dired-cmd-rotate-thumbnail-program nil nil nil
+             (mapcar (lambda (arg) (format-spec arg spec))
+                     image-dired-cmd-rotate-thumbnail-options))
       (clear-image-cache thumb))))
 
 (defun image-dired-rotate-thumbnail-left ()
@@ -1853,19 +2001,18 @@ overwritten.  This confirmation can be turned off using
    'image-dired-cmd-rotate-original-program)
   (if (not (image-dired-image-at-point-p))
       (message "No image at point")
-    (let ((file (image-dired-original-file-name))
-          command)
+    (let* ((file (image-dired-original-file-name))
+           (spec
+            (list
+             (cons ?d degrees)
+             (cons ?o (expand-file-name file))
+             (cons ?t image-dired-temp-rotate-image-file))))
       (unless (eq 'jpeg (image-type file))
         (error "Only JPEG images can be rotated!"))
-      (setq command (format-spec
-                     image-dired-cmd-rotate-original-options
-                     (list
-                      (cons ?p image-dired-cmd-rotate-original-program)
-                      (cons ?d degrees)
-                      (cons ?o (expand-file-name file))
-                      (cons ?t image-dired-temp-rotate-image-file))))
-      (if (not (= 0 (call-process shell-file-name nil nil nil
-				  shell-command-switch command)))
+      (if (not (= 0 (apply #'call-process image-dired-cmd-rotate-original-program
+                           nil nil nil
+                           (mapcar (lambda (arg) (format-spec arg spec))
+                                   image-dired-cmd-rotate-original-options))))
           (error "Could not rotate image")
         (image-dired-display-image image-dired-temp-rotate-image-file)
         (if (or (and image-dired-rotate-original-ask-before-overwrite
@@ -1931,32 +2078,30 @@ default value at the prompt."
   "In FILE, set EXIF tag TAG-NAME to value TAG-VALUE."
   (image-dired--check-executable-exists
    'image-dired-cmd-write-exif-data-program)
-  (let (command)
-    (setq command (format-spec
-                   image-dired-cmd-write-exif-data-options
-                   (list
-                    (cons ?p image-dired-cmd-write-exif-data-program)
-                    (cons ?f (expand-file-name file))
-                    (cons ?t tag-name)
-                    (cons ?v tag-value))))
-    (call-process shell-file-name nil nil nil shell-command-switch command)))
+  (let ((spec
+         (list
+          (cons ?f (expand-file-name file))
+          (cons ?t tag-name)
+          (cons ?v tag-value))))
+    (apply #'call-process image-dired-cmd-write-exif-data-program nil nil nil
+           (mapcar (lambda (arg) (format-spec arg spec))
+                   image-dired-cmd-write-exif-data-options))))
 
 (defun image-dired-get-exif-data (file tag-name)
   "From FILE, return EXIF tag TAG-NAME."
   (image-dired--check-executable-exists
    'image-dired-cmd-read-exif-data-program)
   (let ((buf (get-buffer-create "*image-dired-get-exif-data*"))
-        command tag-value)
-    (setq command (format-spec
-                   image-dired-cmd-read-exif-data-options
-                   (list
-                    (cons ?p image-dired-cmd-read-exif-data-program)
-                    (cons ?f file)
-                    (cons ?t tag-name))))
+        (spec (list (cons ?f file) (cons ?t tag-name)))
+        tag-value)
     (with-current-buffer buf
       (delete-region (point-min) (point-max))
-      (if (not (eq (call-process shell-file-name nil t nil
-				 shell-command-switch command) 0))
+      (if (not (eq (apply #'call-process image-dired-cmd-read-exif-data-program
+                          nil t nil
+                          (mapcar
+                           (lambda (arg) (format-spec arg spec))
+                           image-dired-cmd-read-exif-data-options))
+                   0))
           (error "Could not get EXIF tag")
         (goto-char (point-min))
         ;; Clean buffer from newlines and carriage returns before

commit 85aebc12de28667cdccde5b080972453544d015e
Author: Andreas Schwab <schwab@linux-m68k.org>
Date:   Mon Dec 19 23:07:42 2016 +0100

    Protect change of window's buffer in vertical-motion against unwinds (bug#25209)
    
    * indent.c (restore_window_buffer): New function.
    (Fvertical_motion): Use it to restore window's buffer.

diff --git a/src/indent.c b/src/indent.c
index b68b60297f..29c9ffd90c 100644
--- a/src/indent.c
+++ b/src/indent.c
@@ -1958,6 +1958,20 @@ window_column_x (struct window *w, Lisp_Object window,
   return x;
 }
 
+/* Restore window's buffer and point.  */
+
+static void
+restore_window_buffer (Lisp_Object list)
+{
+  struct window *w = decode_live_window (XCAR (list));
+  list = XCDR (list);
+  wset_buffer (w, XCAR (list));
+  list = XCDR (list);
+  set_marker_both (w->pointm, w->contents,
+		   XFASTINT (XCAR (list)),
+		   XFASTINT (XCAR (XCDR (list))));
+}
+
 DEFUN ("vertical-motion", Fvertical_motion, Svertical_motion, 1, 3, 0,
        doc: /* Move point to start of the screen line LINES lines down.
 If LINES is negative, this means moving up.
@@ -1997,10 +2011,9 @@ whether or not it is currently displayed in some window.  */)
   struct it it;
   struct text_pos pt;
   struct window *w;
-  Lisp_Object old_buffer;
-  EMACS_INT old_charpos UNINIT, old_bytepos UNINIT;
   Lisp_Object lcols;
   void *itdata = NULL;
+  ptrdiff_t count = SPECPDL_INDEX ();
 
   /* Allow LINES to be of the form (HPOS . VPOS) aka (COLUMNS . LINES).  */
   bool lcols_given = CONSP (lines);
@@ -2013,13 +2026,13 @@ whether or not it is currently displayed in some window.  */)
   CHECK_NUMBER (lines);
   w = decode_live_window (window);
 
-  old_buffer = Qnil;
   if (XBUFFER (w->contents) != current_buffer)
     {
       /* Set the window's buffer temporarily to the current buffer.  */
-      old_buffer = w->contents;
-      old_charpos = marker_position (w->pointm);
-      old_bytepos = marker_byte_position (w->pointm);
+      Lisp_Object old = list4 (window, w->contents,
+			       make_number (marker_position (w->pointm)),
+			       make_number (marker_byte_position (w->pointm)));
+      record_unwind_protect (restore_window_buffer, old);
       wset_buffer (w, Fcurrent_buffer ());
       set_marker_both (w->pointm, w->contents,
 		       BUF_PT (current_buffer), BUF_PT_BYTE (current_buffer));
@@ -2255,12 +2268,7 @@ whether or not it is currently displayed in some window.  */)
       bidi_unshelve_cache (itdata, 0);
     }
 
-  if (BUFFERP (old_buffer))
-    {
-      wset_buffer (w, old_buffer);
-      set_marker_both (w->pointm, w->contents,
-		       old_charpos, old_bytepos);
-    }
+  unbind_to (count, Qnil);
 
   return make_number (it.vpos);
 }

commit 504e3846041e4fcd1707a9ad6176ddaf3fec3d02
Author: Glenn Morris <rgm@gnu.org>
Date:   Mon Dec 19 13:32:55 2016 -0500

    Improve default load-path for uninstalled CANNOT_DUMP builds
    
    * src/lread.c (load_path_default) [CANNOT_DUMP]:
    Use build load-path if we seem to be running uninstalled.  (Bug#24974)
    I think this became an issue several years ago when we stopped
    using EMACSLOADPATH in the Makefiles; however this change should
    improve the CANNOT_DUMP uninstalled case in general.

diff --git a/src/lread.c b/src/lread.c
index 157a392a15..fdbf032911 100644
--- a/src/lread.c
+++ b/src/lread.c
@@ -4271,7 +4271,9 @@ load_path_check (Lisp_Object lpath)
    are running uninstalled.
 
    Uses the following logic:
-   If CANNOT_DUMP: Use PATH_LOADSEARCH.
+   If CANNOT_DUMP:
+     If Vinstallation_directory is not nil (ie, running uninstalled),
+     use PATH_DUMPLOADSEARCH (ie, build path).  Else use PATH_LOADSEARCH.
    The remainder is what happens when dumping works:
    If purify-flag (ie dumping) just use PATH_DUMPLOADSEARCH.
    Otherwise use PATH_LOADSEARCH.
@@ -4305,6 +4307,8 @@ load_path_default (void)
 #endif
 
   normal = PATH_LOADSEARCH;
+  if (!NILP (Vinstallation_directory)) normal = PATH_DUMPLOADSEARCH;
+
 #ifdef HAVE_NS
   lpath = decode_env_path (0, loadpath ? loadpath : normal, 0);
 #else

commit fe3188b1cecc7ac5534616c8edf14a84b1b3bbb0
Author: Eli Zaretskii <eliz@gnu.org>
Date:   Mon Dec 19 19:11:16 2016 +0200

    Fix crashes upon C-g on Posix TTY frames
    
    * src/thread.h (struct thread_state): New member not_holding_lock.
    (maybe_reacquire_global_lock): Add prototype.
    * src/thread.c: Include syssignal.h.
    (maybe_reacquire_global_lock): New function.
    (really_call_select): Set the not_holding_lock member of the
    thread state before releasing the lock, and rest it after
    re-acquiring the lock when the select function returns.  Block
    SIGINT while doing this to make sure we are not interrupted on TTY
    frames.
    * src/sysdep.c (block_interrupt_signal, restore_signal_mask): New
    functions.
    * src/syssignal.h (block_interrupt_signal, restore_signal_mask):
    Add prototypes.
    * src/keyboard.c (read_char) [THREADS_ENABLED]: Call
    maybe_reacquire_global_lock.  (Bug#25178)

diff --git a/src/keyboard.c b/src/keyboard.c
index 1fb1d492ce..f2ee313b8c 100644
--- a/src/keyboard.c
+++ b/src/keyboard.c
@@ -2571,6 +2571,9 @@ read_char (int commandflag, Lisp_Object map,
 	 so restore it now.  */
       restore_getcjmp (save_jump);
       pthread_sigmask (SIG_SETMASK, &empty_mask, 0);
+#if THREADS_ENABLED
+      maybe_reacquire_global_lock ();
+#endif
       unbind_to (jmpcount, Qnil);
       XSETINT (c, quit_char);
       internal_last_event_frame = selected_frame;
diff --git a/src/sysdep.c b/src/sysdep.c
index 3d2b9bdeee..96c9e53840 100644
--- a/src/sysdep.c
+++ b/src/sysdep.c
@@ -765,6 +765,23 @@ unblock_child_signal (sigset_t const *oldset)
   pthread_sigmask (SIG_SETMASK, oldset, 0);
 }
 
+/* Block SIGINT.  */
+void
+block_interrupt_signal (sigset_t *oldset)
+{
+  sigset_t blocked;
+  sigemptyset (&blocked);
+  sigaddset (&blocked, SIGINT);
+  pthread_sigmask (SIG_BLOCK, &blocked, oldset);
+}
+
+/* Restore previously saved signal mask.  */
+void
+restore_signal_mask (sigset_t const *oldset)
+{
+  pthread_sigmask (SIG_SETMASK, oldset, 0);
+}
+
 #endif	/* !MSDOS */
 
 /* Saving and restoring the process group of Emacs's terminal.  */
diff --git a/src/syssignal.h b/src/syssignal.h
index 3de83c7175..62704fc351 100644
--- a/src/syssignal.h
+++ b/src/syssignal.h
@@ -25,6 +25,8 @@ along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.  */
 extern void init_signals (bool);
 extern void block_child_signal (sigset_t *);
 extern void unblock_child_signal (sigset_t const *);
+extern void block_interrupt_signal (sigset_t *);
+extern void restore_signal_mask (sigset_t const *);
 extern void block_tty_out_signal (sigset_t *);
 extern void unblock_tty_out_signal (sigset_t const *);
 
diff --git a/src/thread.c b/src/thread.c
index e8cb430119..bf2cf1b06c 100644
--- a/src/thread.c
+++ b/src/thread.c
@@ -24,6 +24,7 @@ along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.  */
 #include "buffer.h"
 #include "process.h"
 #include "coding.h"
+#include "syssignal.h"
 
 static struct thread_state primary_thread;
 
@@ -100,6 +101,23 @@ acquire_global_lock (struct thread_state *self)
   post_acquire_global_lock (self);
 }
 
+/* This is called from keyboard.c when it detects that SIGINT
+   interrupted thread_select before the current thread could acquire
+   the lock.  We must acquire the lock to prevent a thread from
+   running without holding the global lock, and to avoid repeated
+   calls to sys_mutex_unlock, which invokes undefined behavior.  */
+void
+maybe_reacquire_global_lock (void)
+{
+  if (current_thread->not_holding_lock)
+    {
+      struct thread_state *self = current_thread;
+
+      acquire_global_lock (self);
+      current_thread->not_holding_lock = 0;
+    }
+}
+
 
 
 static void
@@ -493,11 +511,20 @@ really_call_select (void *arg)
 {
   struct select_args *sa = arg;
   struct thread_state *self = current_thread;
+  sigset_t oldset;
 
+  block_interrupt_signal (&oldset);
+  self->not_holding_lock = 1;
   release_global_lock ();
+  restore_signal_mask (&oldset);
+
   sa->result = (sa->func) (sa->max_fds, sa->rfds, sa->wfds, sa->efds,
 			   sa->timeout, sa->sigmask);
+
+  block_interrupt_signal (&oldset);
   acquire_global_lock (self);
+  self->not_holding_lock = 0;
+  restore_signal_mask (&oldset);
 }
 
 int
diff --git a/src/thread.h b/src/thread.h
index e6084b13c2..7dee67d659 100644
--- a/src/thread.h
+++ b/src/thread.h
@@ -171,6 +171,13 @@ struct thread_state
      interrupter should broadcast to this condition.  */
   sys_cond_t *wait_condvar;
 
+  /* This thread might have released the global lock.  If so, this is
+     non-zero.  When a thread runs outside thread_select with this
+     flag non-zero, it means it has been interrupted by SIGINT while
+     in thread_select, and didn't have a chance of acquiring the lock.
+     It must do so ASAP.  */
+  int not_holding_lock;
+
   /* Threads are kept on a linked list.  */
   struct thread_state *next_thread;
 };
@@ -224,6 +231,7 @@ extern void unmark_threads (void);
 extern void finalize_one_thread (struct thread_state *state);
 extern void finalize_one_mutex (struct Lisp_Mutex *);
 extern void finalize_one_condvar (struct Lisp_CondVar *);
+extern void maybe_reacquire_global_lock (void);
 
 extern void init_threads_once (void);
 extern void init_threads (void);

commit 657bcaf5ac30449915e070c3fa80a2eaaf1ee7e1
Author: Sam Steingold <sds@gnu.org>
Date:   Mon Dec 19 11:44:18 2016 -0500

    avoid Eager macro-expansion failure: (void-function string-to-list)
    
    * loadup.el [ns]: "ucs-normalize" uses `string-to-list' which is defined
      in "mule-util", so we have to load "mule-util" before "ucs-normalize",
      otherwise I get "Eager macro-expansion failure" on "make bootstrap"

diff --git a/lisp/loadup.el b/lisp/loadup.el
index e9dd683b28..5350024031 100644
--- a/lisp/loadup.el
+++ b/lisp/loadup.el
@@ -301,6 +301,7 @@
       ;; already produced, because it needs uni-*.el files that might
       ;; not be built early enough during bootstrap.
       (when (load-history-filename-element "charprop\\.el")
+        (load "international/mule-util")
         (load "international/ucs-normalize")
         (load "term/ns-win"))))
 (if (fboundp 'x-create-frame)
