]> git.ipfire.org Git - thirdparty/coreutils.git/commitdiff
mv: new option --exchange
authorPaul Eggert <eggert@cs.ucla.edu>
Wed, 20 Mar 2024 21:00:25 +0000 (14:00 -0700)
committerPaul Eggert <eggert@cs.ucla.edu>
Wed, 20 Mar 2024 21:40:29 +0000 (14:40 -0700)
* src/copy.h (struct cp_options): New member 'exchange'.
* src/copy.c (copy_internal): Support the new member.
* src/mv.c (EXCHANGE_OPTION): New constant.
(long_options): Add --exchange.
(usage): Document --exchange.
(main): Support --exchange.
* tests/mv/mv-exchange.sh: New test case.
* tests/local.mk (all_tests): Add it.

NEWS
doc/coreutils.texi
src/copy.c
src/copy.h
src/mv.c
tests/local.mk
tests/mv/mv-exchange.sh [new file with mode: 0755]

diff --git a/NEWS b/NEWS
index b3004273b6aaae8b3021e7bdfbfae17f8f77c281..cb47621880657c03170d276af445950581b2eba6 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -92,6 +92,13 @@ GNU coreutils NEWS                                    -*- outline -*-
   and the command exits with failure status if existing files.
   The -n,--no-clobber option is best avoided due to platform differences.
 
+  mv now accepts an --exchange option, which causes the source and
+  destination to be exchanged.  It should be combined with
+  --no-target-directory (-T) if the destination is a directory.
+  The exchange is atomic if source and destination are on a single
+  file system that supports atomic exchange; --exchange is not yet
+  supported in other situations.
+
   od now supports printing IEEE half precision floating point with -t fH,
   or brain 16 bit floating point with -t fB, where supported by the compiler.
 
index ec15a467ff869ce3b3732f3421f9f41677cb7729..ffe47aad542254e3b7c8c7396a3b449a2dac09d8 100644 (file)
@@ -10273,6 +10273,26 @@ skip existing files but not fail.
 If a file cannot be renamed because the destination file system differs,
 fail with a diagnostic instead of copying and then removing the file.
 
+@item --exchange
+@opindex --exchange
+Exchange source and destination instead of renaming source to destination.
+Both files must exist; they need not be the same type.
+
+This option can be used to replace one directory with another.
+When used this way, it should be combined with
+@code{--no-target-directory} (@option{-T})
+to avoid confusion about the destination location.
+For example, you might use @samp{mv -T --exchange @var{d1} @var{d2}}
+to exchange two directories @var{d1} and @var{d2}.
+
+Exchanges are atomic if the source and destination are both in a
+single file system that supports atomic exchange.
+Non-atomic exchanges are not yet supported.
+
+If the source and destination might not be on the same file system,
+using @code{--no-copy} will prevent future versions of @command{mv}
+from implementing the exchange by copying.
+
 @item -u
 @itemx --update
 @opindex -u
index 8d99f8562258342f003e59a4ee2854fc8a49d0a8..e7bf6022f5705eb09be06692940831807aa63294 100644 (file)
@@ -2223,9 +2223,11 @@ copy_internal (char const *src_name, char const *dst_name,
     {
       if (rename_errno < 0)
         rename_errno = (renameatu (AT_FDCWD, src_name, dst_dirfd, drelname,
-                                   RENAME_NOREPLACE)
+                                   (x->exchange
+                                    ? RENAME_EXCHANGE : RENAME_NOREPLACE))
                         ? errno : 0);
-      nonexistent_dst = *rename_succeeded = rename_errno == 0;
+      *rename_succeeded = rename_errno == 0;
+      nonexistent_dst = *rename_succeeded && !x->exchange;
     }
 
   if (rename_errno == 0
@@ -2246,7 +2248,7 @@ copy_internal (char const *src_name, char const *dst_name,
 
       src_mode = src_sb.st_mode;
 
-      if (S_ISDIR (src_mode) && !x->recursive)
+      if (S_ISDIR (src_mode) && !x->recursive && !x->exchange)
         {
           error (0, 0, ! x->install_mode /* cp */
                  ? _("-r not specified; omitting directory %s")
@@ -2289,7 +2291,7 @@ copy_internal (char const *src_name, char const *dst_name,
      treated the same as nonexistent files.  */
   bool new_dst = 0 < nonexistent_dst;
 
-  if (! new_dst)
+  if (! new_dst && ! x->exchange)
     {
       /* Normally, fill in DST_SB or set NEW_DST so that later code
          can use DST_SB if NEW_DST is false.  However, don't bother
@@ -2657,7 +2659,7 @@ skip:
      Also, with --recursive, record dev/ino of each command-line directory.
      We'll use that info to detect this problem: cp -R dir dir.  */
 
-  if (rename_errno == 0)
+  if (rename_errno == 0 || x->exchange)
     earlier_file = nullptr;
   else if (x->recursive && S_ISDIR (src_mode))
     {
@@ -2752,7 +2754,7 @@ skip:
 
   if (x->move_mode)
     {
-      if (rename_errno == EEXIST)
+      if (rename_errno == EEXIST && !x->exchange)
         rename_errno = (renameat (AT_FDCWD, src_name, dst_dirfd, drelname) == 0
                         ? 0 : errno);
 
@@ -2781,7 +2783,7 @@ skip:
                  _destination_ dev/ino, since the rename above can't have
                  changed those, and 'mv' always uses lstat.
                  We could limit it further by operating
-                 only on non-directories.  */
+                 only on non-directories when !x->exchange.  */
               record_file (x->dest_info, dst_relname, &src_sb);
             }
 
@@ -2828,7 +2830,7 @@ skip:
          where you'd replace '18' with the integer in parentheses that
          was output from the perl one-liner above.
          If necessary, of course, change '/tmp' to some other directory.  */
-      if (rename_errno != EXDEV || x->no_copy)
+      if (rename_errno != EXDEV || x->no_copy || x->exchange)
         {
           /* There are many ways this can happen due to a race condition.
              When something happens between the initial follow_fstatat and the
@@ -2841,25 +2843,29 @@ skip:
              destination file are made too restrictive, the rename will
              fail.  Etc.  */
           char const *quoted_dst_name = quoteaf_n (1, dst_name);
-          switch (rename_errno)
-            {
-            case EDQUOT: case EEXIST: case EISDIR: case EMLINK:
-            case ENOSPC: case ETXTBSY:
+          if (x->exchange)
+            error (0, rename_errno, _("cannot exchange %s and %s"),
+                   quoteaf_n (0, src_name), quoted_dst_name);
+          else
+            switch (rename_errno)
+              {
+              case EDQUOT: case EEXIST: case EISDIR: case EMLINK:
+              case ENOSPC: case ETXTBSY:
 #if ENOTEMPTY != EEXIST
-            case ENOTEMPTY:
+              case ENOTEMPTY:
 #endif
-              /* The destination must be the problem.  Don't mention
-                 the source as that is more likely to confuse the user
-                 than be helpful.  */
-              error (0, rename_errno, _("cannot overwrite %s"),
-                     quoted_dst_name);
-              break;
+                /* The destination must be the problem.  Don't mention
+                   the source as that is more likely to confuse the user
+                   than be helpful.  */
+                error (0, rename_errno, _("cannot overwrite %s"),
+                       quoted_dst_name);
+                break;
 
-            default:
-              error (0, rename_errno, _("cannot move %s to %s"),
-                     quoteaf_n (0, src_name), quoted_dst_name);
-              break;
-            }
+              default:
+                error (0, rename_errno, _("cannot move %s to %s"),
+                       quoteaf_n (0, src_name), quoted_dst_name);
+                break;
+              }
           forget_created (src_sb.st_ino, src_sb.st_dev);
           return false;
         }
index dfa9435b38d0dbee708c3faf4e22712e944cf114..ab89c75fd3456f418f778e58c397d2b630f746dc 100644 (file)
@@ -155,6 +155,10 @@ struct cp_options
      If that fails and NO_COPY, fail instead of copying.  */
   bool move_mode, no_copy;
 
+  /* Exchange instead of renaming.  Valid only if MOVE_MODE and if
+     BACKUP_TYPE == no_backups.  */
+  bool exchange;
+
   /* If true, install(1) is the caller.  */
   bool install_mode;
 
index 9dc40fe3e82f8ffea68b726e10f909516525ac3f..692943a70da8ae5ef8867f47bf4c85eba9df89f7 100644 (file)
--- a/src/mv.c
+++ b/src/mv.c
@@ -48,6 +48,7 @@
 enum
 {
   DEBUG_OPTION = CHAR_MAX + 1,
+  EXCHANGE_OPTION,
   NO_COPY_OPTION,
   STRIP_TRAILING_SLASHES_OPTION
 };
@@ -67,6 +68,7 @@ static struct option const long_options[] =
   {"backup", optional_argument, nullptr, 'b'},
   {"context", no_argument, nullptr, 'Z'},
   {"debug", no_argument, nullptr, DEBUG_OPTION},
+  {"exchange", no_argument, nullptr, EXCHANGE_OPTION},
   {"force", no_argument, nullptr, 'f'},
   {"interactive", no_argument, nullptr, 'i'},
   {"no-clobber", no_argument, nullptr, 'n'},   /* Deprecated.  */
@@ -271,6 +273,9 @@ Rename SOURCE to DEST, or move SOURCE(s) to DIRECTORY.\n\
 "), stdout);
       fputs (_("\
       --debug                  explain how a file is copied.  Implies -v\n\
+"), stdout);
+      fputs (_("\
+      --exchange               exchange source and destination\n\
 "), stdout);
       fputs (_("\
   -f, --force                  do not prompt before overwriting\n\
@@ -361,6 +366,9 @@ main (int argc, char **argv)
         case DEBUG_OPTION:
           x.debug = x.verbose = true;
           break;
+        case EXCHANGE_OPTION:
+          x.exchange = true;
+          break;
         case NO_COPY_OPTION:
           x.no_copy = true;
           break;
@@ -469,7 +477,7 @@ main (int argc, char **argv)
   else
     {
       char const *lastfile = file[n_files - 1];
-      if (n_files == 2)
+      if (n_files == 2 && !x.exchange)
         x.rename_errno = (renameatu (AT_FDCWD, file[0], AT_FDCWD, lastfile,
                                      RENAME_NOREPLACE)
                           ? errno : 0);
@@ -514,11 +522,13 @@ main (int argc, char **argv)
       strip_trailing_slashes (file[i]);
 
   if (make_backups
-      && (x.interactive == I_ALWAYS_SKIP
+      && (x.exchange
+          || x.interactive == I_ALWAYS_SKIP
           || x.interactive == I_ALWAYS_NO))
     {
       error (0, 0,
-             _("--backup is mutually exclusive with -n or --update=none-fail"));
+             _("cannot combine --backup with "
+               "--exchange, -n, or --update=none-fail"));
       usage (EXIT_FAILURE);
     }
 
index 4075525baa1ea01832e6740ac41314a8d14ca776..fdbf369464aa07190ec1953af018900b99eaca99 100644 (file)
@@ -699,6 +699,7 @@ all_tests =                                 \
   tests/mv/into-self-3.sh                      \
   tests/mv/into-self-4.sh                      \
   tests/mv/leak-fd.sh                          \
+  tests/mv/mv-exchange.sh                      \
   tests/mv/mv-n.sh                             \
   tests/mv/mv-special-1.sh                     \
   tests/mv/no-copy.sh                          \
diff --git a/tests/mv/mv-exchange.sh b/tests/mv/mv-exchange.sh
new file mode 100755 (executable)
index 0000000..35e3392
--- /dev/null
@@ -0,0 +1,41 @@
+#!/bin/sh
+# Test mv --exchange.
+
+# Copyright 2024 Free Software Foundation, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
+print_ver_ mv
+
+
+# Test exchanging files.
+touch a || framework_failure_
+mkdir b || framework_failure_
+if ! mv -T --exchange a b 2>exchange_err; then
+  grep 'not supported' exchange_err || { cat exchange_err; fail=1; }
+else
+  test -d a || fail=1
+  test -f b || fail=1
+fi
+
+# Test wrong number of arguments.
+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
+
+Exit $fail