Collin Funk <[email protected]> writes:

> Thanks for the detailed report!
>
> I can reproduce it on coreutils-9.12 as well. I'll take a look at it
> later today.

This patch should do the trick. The issue also occurs with target
directories, i.e., the following commands:

    $ touch a; mkdir -p b
    $ mv --exchange a b
    mv: cannot exchange 'a' and 'b/a': Unknown error -1
    $ mv --exchange --target-directory=b a
    mv: cannot exchange 'a' and 'b/a': Unknown error -1

So, I've added test cases for those and the more common invocation that
you shared.

I'll push it tomorrow to allow others a chance to review.

Thanks again for the report,
Collin

>From 0841f4875ff1442f217f9352271d7ed64c646a12 Mon Sep 17 00:00:00 2001
Message-ID: <0841f4875ff1442f217f9352271d7ed64c646a12.1793847375.git.collin.fu...@gmail.com>
From: Collin Funk <[email protected]>
Date: Wed, 4 Nov 2026 18:42:54 -0800
Subject: [PATCH] mv: don't lose EEXIST errors when --exchange is used

* THANKS.in: Add Igor Giamoniano, who reported the bug.
* src/copy.c (copy_internal): Save the EEXIST errno when stat fails,
since renameat would fail for the same reason.
* tests/mv/mv-exchange.sh: Add a few test cases where the bug could be
seen.
* NEWS: Mention the bug fix.
Fixes https://bugs.gnu.org/82186
---
 NEWS                    |  5 +++++
 THANKS.in               |  1 +
 src/copy.c              |  9 ++++++++-
 tests/mv/mv-exchange.sh | 21 +++++++++++++++++++--
 4 files changed, 33 insertions(+), 3 deletions(-)

diff --git a/NEWS b/NEWS
index a856efff8..ae5c8e32a 100644
--- a/NEWS
+++ b/NEWS
@@ -110,6 +110,11 @@ GNU coreutils NEWS                                    -*- outline -*-
   just-moved FIFO.
   [bug introduced with SELinux support in coreutils-8.22]
 
+  'mv --exchange A B' now emits a more meaningful error message when file B does
+  not exist.  Previously, it would print the error message as if errno were set
+  to -1, e.g., "Unknown error -1" on glibc.
+  [bug introduced with the --exchange option in coreutils-9.5]
+
   'nl -d ""' no longer mistakenly treats empty lines as headers.
   [bugs introduced in coreutils-9.11]
 
diff --git a/THANKS.in b/THANKS.in
index 94669125b..3704a3bf8 100644
--- a/THANKS.in
+++ b/THANKS.in
@@ -264,6 +264,7 @@ Ian Jackson                         [email protected]
 Ian Kent                            [email protected]
 Ian Lance Taylor                    [email protected]
 Ian Turner                          [email protected]
+Igor Giamoniano                     [email protected]
 Iida Yosiaki                        [email protected]
 Illia Bobyr                         [email protected]
 Ilya N. Golubev                     [email protected]
diff --git a/src/copy.c b/src/copy.c
index cf9be7389..cfd9deaad 100644
--- a/src/copy.c
+++ b/src/copy.c
@@ -1748,7 +1748,14 @@ copy_internal (char const *src_name, char const *dst_name,
               rename_errno = EEXIST;
             }
           else if (errno == ENOENT)
-            new_dst = true;
+            {
+              /* Using renameatu would fail with ENOENT as well.
+                 Prefer the error message describing the failed rename
+                 instead of the failed stat.  */
+              if (x->exchange)
+                rename_errno = ENOENT;
+              new_dst = true;
+            }
           else if (errno == ELOOP && !use_lstat
                    && x->unlink_dest_after_failed_open)
             {
diff --git a/tests/mv/mv-exchange.sh b/tests/mv/mv-exchange.sh
index 63348a1ec..cd237dcb0 100755
--- a/tests/mv/mv-exchange.sh
+++ b/tests/mv/mv-exchange.sh
@@ -36,7 +36,24 @@ touch c || framework_failure_
 returns_ 1 mv --exchange a 2>/dev/null || fail=1
 returns_ 1 mv --exchange a b c 2>/dev/null || fail=1
 
-# Both files must exist.
-returns_ 1 mv --exchange a d 2>/dev/null || fail=1
+# Both files must exist.  Check the behavior with a missing destination.
+echo "mv: cannot exchange 'b' and 'd': $ENOENT" >exp-err || framework_failure_
+returns_ 1 mv --exchange b d >out 2>err || fail=1
+compare /dev/null out || fail=1
+compare exp-err err || fail=1
+
+# Check the behavior with a missing source.
+echo "mv: cannot stat 'd': $ENOENT" >exp-err || framework_failure_
+returns_ 1 mv --exchange d b >out 2>err || fail=1
+compare /dev/null out || fail=1
+compare exp-err err || fail=1
+
+# Test the behavior with a missing destination in a target directory.
+echo "mv: cannot exchange 'b' and 'a/b': $ENOENT" >exp-err || framework_failure_
+for opt in '' --target-directory=; do
+  returns_ 1 mv --exchange b ${opt}a >out 2>err || fail=1
+  compare /dev/null out || fail=1
+  compare exp-err err || fail=1
+done
 
 Exit $fail
-- 
2.56.0

Reply via email to