1
Fork 0
mirror of git://git.sv.gnu.org/emacs.git synced 2025-12-05 22:20:24 -08:00

Improve Dired handling of file names with newlines (bug#79528)

* doc/emacs/dired.texi (Dired Enter): Update documentation of
Dired's display and handling of file names that contain newlines.
Document new user option 'dired-auto-toggle-b-switch'.

* etc/NEWS: Announce new warning and new user option.

* lisp/dired.el (dired-auto-toggle-b-switch): New user option.
(dired-internal-noselect): Pop up a warning if the Dired listing
displays a literal newline.
(dired-switches-escape-p): Take the 'ls' switch
'--quoting-style=escape' into account.
(dired-mode): Add 'dired--toggle-b-switch' to 'post-command-hook'.
(dired-move-to-end-of-filename): When the Dired listing includes a
file name containing a newline, this can result in no change in
the 'dired-filename' text property on the last file name, so in
this case take the position just before the final newline as the
end of the last file name to prevent a wrong-type-argument error.
(dired--filename-with-newline-p, dired--remove-b-switch)
(dired--toggle-b-switch, dired--set-auto-toggle-b-switch)
(dired--display-filename-with-newline-warning): New functions.
This commit is contained in:
Stephen Berman 2025-10-22 00:06:03 +02:00
parent 38c32ed3ea
commit 9a56cf9194
3 changed files with 191 additions and 10 deletions

View file

@ -136,15 +136,57 @@ options (that is, single characters) requiring no arguments, and long
options (starting with @samp{--}) whose arguments are specified with options (starting with @samp{--}) whose arguments are specified with
@samp{=}. @samp{=}.
Dired does not handle files that have names with embedded newline You can declare @code{dired-listing-switches} as a connection-local
characters well. If you have many such files, you may consider adding variable in order to adjust its value to match what a remote system
@samp{-b} to @code{dired-listing-switches}. This will quote all expects (@pxref{Connection Variables}).
special characters and allow Dired to handle them better. (You can
also use the @kbd{C-u C-x d} command to add @samp{-b} temporarily.)
@code{dired-listing-switches} can be declared as connection-local @cindex file names with newline character in Dired
variable to adjust it to match what a remote system expects @cindex newline character in file names in Dired
(@pxref{Connection Variables}). @anchor{File names with newline}
When a file name contains a newline character, Dired displays it by
default as a literal newline, so the display of this file name occupies
more than one line in the Dired buffer. If you invoke a Dired operation
on such a file listing, in many cases it will fail and signal an error.
For this reason, when Dired displays a file name containing a literal
newline, Emacs recognizes this and automatically pops up a buffer with
an informative warning. For such file names, Dired offers an
alternative display, using the @command{ls} switch @samp{-b}, in which
newline characters are represented by @samp{\n} and the Dired listing of
the file occupies one line as usual, so you can execute all applicable
Dired operations on it.@footnote{Note that with the @samp{-b} switch
Dired displays tab characters in file names as @samp{\t} and escapes
other control characters and also spaces in file names with @samp{\}.}
Emacs provides two different ways to make Dired use the @samp{-b}
switch:
@itemize @bullet
@item
You can add @samp{-b} to @code{dired-listing-switches} before invoking
@code{dired}. Since this variable is a user option, you can alter its
value persistently either by using the Customization interface
(@pxref{Saving Customizations}) or by using the @code{setopt} macro in
your initialization file (@pxref{Examining}).@footnote{If
@code{dired-listing-switches} contains @samp{-b} when you invoke dired
on a directory containing a file name with a newline, the newline is
displayed as @samp{\n}, so Emacs will not pop up a warning.} You can
also add @samp{-b} just for the next @code{dired} invocation by typing
@kbd{C-u C-x d}.
@item
@vindex dired-auto-toggle-b-switch
If you enable the user option @code{dired-auto-toggle-b-switch}, then
when you visit a directory containing a file whose name has a newline,
Emacs will automatically add the @samp{-b} switch and redisplay the
directory in Dired to show @samp{\n} in the file name. If you edit the
file name and remove the @samp{\n} character, then on completing the
edit Emacs automatically removes the @samp{-b} switch and redisplays the
Dired buffer, so that file names with tab or space characters now show
literal spaces without a backslash. If you enable or disable
@code{dired-auto-toggle-b-switch} after visiting a directory containing
a file name with a newline, Emacs will add or remove the @samp{-b}
switch as appropriate and automatically redisplay the Dired buffer.
@end itemize
@vindex dired-switches-in-mode-line @vindex dired-switches-in-mode-line
Dired displays in the mode line an indication of what were the Dired displays in the mode line an indication of what were the

View file

@ -2042,6 +2042,24 @@ name of the directory now reverts the Dired buffer.
With a new value of the prefix argument (1), this command copies file With a new value of the prefix argument (1), this command copies file
names relative to the root directory of the current project. names relative to the root directory of the current project.
+++
*** Warning when Dired displays a file name with a literal newline.
On visiting a directory that contains a file whose name has a newline,
and Dired displays that character as a literal newline, Emacs now
automatically pops up a buffer warning that such a display can be
problematic for Dired and showing a way to change the display to use the
unproblematic character '\n'.
+++
*** New user option 'dired-auto-toggle-b-switch'.
When this user option is non-nil and 'dired-listing-switches' does not
include the '-b' switch, then on visiting a directory containing a file
whose name has a newline, Emacs automatically adds the '-b' switch and
redisplays the directory in Dired to show '\n' in the file name instead
of a literal newline. This prevents executing many Dired operations on
such a file from failing and signaling an error. The default value of
this user option is nil.
** Grep ** Grep
+++ +++

View file

@ -550,6 +550,16 @@ displayed instead."
:group 'dired :group 'dired
:version "30.1") :version "30.1")
(defcustom dired-auto-toggle-b-switch nil
"Whether to automatically add or remove the `b' switch.
If non-nil, the function `dired--toggle-b-switch' (which see) is added
to `post-command-hook' in Dired mode."
:type 'boolean
:group 'dired
:initialize #'custom-initialize-default
:set #'dired--set-auto-toggle-b-switch
:version "31.1")
;;; Internal variables ;;; Internal variables
@ -1437,6 +1447,16 @@ The return value is the target column for the file names."
(dired-initial-position dirname)) (dired-initial-position dirname))
(when (consp dired-directory) (when (consp dired-directory)
(dired--align-all-files)) (dired--align-all-files))
;; Pop up a warning if the Dired listing displays a literal newline.
;; We do this here in order to get the warning not only when
;; interactively invoking `dired' on a directory, but also e.g. when
;; passing the directory name as a command line argument when
;; starting Emacs from the shell.
(unless (or dired-auto-toggle-b-switch
(dired-switches-escape-p dired-listing-switches)
(dired-switches-escape-p dired-actual-switches))
(when (dired--filename-with-newline-p)
(dired--display-filename-with-newline-warning buffer)))
(set-buffer old-buf) (set-buffer old-buf)
buffer)) buffer))
@ -1699,7 +1719,7 @@ BEG..END is the line where the file info is located."
(defun dired-switches-escape-p (switches) (defun dired-switches-escape-p (switches)
"Return non-nil if the string SWITCHES contains -b or --escape." "Return non-nil if the string SWITCHES contains -b or --escape."
;; Do not match things like "--block-size" that happen to contain "b". ;; Do not match things like "--block-size" that happen to contain "b".
(dired-check-switches switches "b" "escape")) (dired-check-switches switches "b" "\\(quoting-style=\\)?escape"))
(defun dired-switches-recursive-p (switches) (defun dired-switches-recursive-p (switches)
"Return non-nil if the string SWITCHES contains -R or --recursive." "Return non-nil if the string SWITCHES contains -R or --recursive."
@ -2855,6 +2875,8 @@ Keybindings:
(add-hook 'file-name-at-point-functions #'dired-file-name-at-point nil t) (add-hook 'file-name-at-point-functions #'dired-file-name-at-point nil t)
(add-hook 'isearch-mode-hook #'dired-isearch-filenames-setup nil t) (add-hook 'isearch-mode-hook #'dired-isearch-filenames-setup nil t)
(add-hook 'context-menu-functions 'dired-context-menu 5 t) (add-hook 'context-menu-functions 'dired-context-menu 5 t)
(when dired-auto-toggle-b-switch
(add-hook 'post-command-hook #'dired--toggle-b-switch nil t))
(run-mode-hooks 'dired-mode-hook)) (run-mode-hooks 'dired-mode-hook))
@ -3439,7 +3461,14 @@ If EOL, it should be an position to use instead of
;; On failure, signals an error (with non-nil NO-ERROR just returns nil). ;; On failure, signals an error (with non-nil NO-ERROR just returns nil).
;; This is the UNIX version. ;; This is the UNIX version.
(if (get-text-property (point) 'dired-filename) (if (get-text-property (point) 'dired-filename)
(goto-char (next-single-property-change (point) 'dired-filename)) (goto-char (or (next-single-property-change (point) 'dired-filename)
;; No property change can happen on or before the
;; last file name in the Dired listing when there
;; is at least one prior file name containing a
;; newline. To prevent an error in this case we
;; take the position just before the final newline
;; as the end of the last file name (bug#79528).
(1- (point-max))))
(let ((opoint (point)) (let ((opoint (point))
(used-F (dired-check-switches dired-actual-switches "F" "classify")) (used-F (dired-check-switches dired-actual-switches "F" "classify"))
(eol (line-end-position)) (eol (line-end-position))
@ -3973,6 +4002,98 @@ Considers buffers closer to the car of `buffer-list' to be more recent."
(memq buffer1 (buffer-list)) (memq buffer1 (buffer-list))
(not (memq buffer1 (memq buffer2 (buffer-list)))))) (not (memq buffer1 (memq buffer2 (buffer-list))))))
(defun dired--filename-with-newline-p ()
"Check if a file name in this directory has a newline.
Return non-nil if at least one file name in this directory contains
either a literal newline or the string \"\\n\")."
(save-excursion
(goto-char (point-min))
(catch 'found
(while (not (eobp))
(when (dired-move-to-filename)
(let ((fn (buffer-substring-no-properties
(point) (dired-move-to-end-of-filename))))
(when (or (memq 10 (seq-into fn 'list))
(string-search "\\n" fn))
(throw 'found t))))
(forward-line)))))
(defun dired--remove-b-switch ()
"Remove all variants of the `b' switch from `dired-actual-switches'.
This removes not only all occurrences of the short form `-b' but also
the long forms `--escape' and `--quoting-style=escape'."
(let (switches)
(dolist (s (string-split dired-actual-switches))
(when (string-match "\\`-[^-]" s)
(setq s (remove ?b s)))
(unless (or (string= s "-")
(string-match "escape" s))
(cl-pushnew s switches :test 'equal)))
(mapconcat #'identity (nreverse switches) " ")))
(defun dired--toggle-b-switch ()
"Add or remove `b' switch and redisplay Dired buffer.
When the current Dired buffer has a file name containing a newline, add
the `b' switch to the actual switches if it isn't already among them;
otherwise remove the `b' switch unless it is in `dired-listing-switches'.
Then redisplay the Dired buffer. This function is called from
`post-command-hook' in Dired mode buffers."
(when (eq major-mode 'dired-mode)
(if (and (dired--filename-with-newline-p) dired-auto-toggle-b-switch)
(unless (dired-switches-escape-p dired-actual-switches)
(setq dired-actual-switches (concat dired-actual-switches " -b"))
(dired-revert))
(unless (dired-switches-escape-p dired-listing-switches)
(when (dired-switches-escape-p dired-actual-switches)
(setq dired-actual-switches (dired--remove-b-switch))
(dired-revert))))))
(defun dired--set-auto-toggle-b-switch (symbol value)
"The :set function for user option `dired-auto-toggle-b-switch'."
(custom-set-default symbol value)
(if value
(add-hook 'post-command-hook #'dired--toggle-b-switch nil t)
(remove-hook 'post-command-hook #'dired--toggle-b-switch t))
(dolist (b (buffer-list))
(with-current-buffer b
(dired--toggle-b-switch))))
(defun dired--display-filename-with-newline-warning (dir)
"Display a warning if buffer DIR has a file name with a newline."
(let ((msg "Literal newline in file name.
This Dired buffer displays a file name containing a literal newline character.
Executing Dired operations on files displayed this way may fail and signal an
error. To avoid this you can temporarily change the display for all Dired
buffers, so that newlines in file names appear as \"\\n\", by typing `M-:' and
entering `(setopt dired-auto-toggle-b-switch t)' in the minibuffer. To change
the display only for this Dired buffer click or press RETURN `%s'.
See `%s' for other alternatives and more information."))
(display-warning
'dired
(format-message
msg
(buttonize "here"
(lambda (_)
(pop-to-buffer dir)
(when (dired--filename-with-newline-p)
(unless (dired-switches-escape-p dired-actual-switches)
(setq dired-actual-switches
(concat dired-actual-switches " -b"))
(dired-revert))))
nil "mouse-2: Change newline display")
(buttonize "(emacs) Dired Enter"
(lambda (_)
(info "(emacs) Dired Enter")
(declare-function Info-goto-node "info")
(with-current-buffer "*info*"
(Info-goto-node "File names with newline")))
nil "mouse-2: Jump to Info node")))
;; Display *Warnings* buffer with point at start of message instead
;; of at the end.
(with-current-buffer "*Warnings*"
(set-window-point (get-buffer-window)
(search-backward "Warning (dired)")))))
;;; Deleting files ;;; Deleting files