On 2024-03-05 06:16, Pádraig Brady wrote:
I think I'll remove the as yet unreleased mv --swap from coreutils,
given that
util-linux is as widely available as coreutils on GNU/Linux platforms.
Although removing that "mv --swap" implementation was a win, I don't
think we can simply delegate this to util-linux's exch command.
Exchanging files via a renameat-like call is not limited to the Linux
kernel; it's also possible on macOS via renameatx_np with RENAME_SWAP,
and there have been noises about adding similar things to other
operating systems.
I just now added support for macOS renameatx_np to Gnulib, so coreutils
does not need to worry about the macOS details; it can simply use
renameatu with the Linux flags. See:
https://git.savannah.gnu.org/cgit/gnulib.git/commit/?id=af32ee824ee18255839f9812b8ed61aa5257a82b
Even with Linux it's dicey. People may have older util-linux installed
and so lack the 'exch' utility; this is true for both Fedora 39 and
Ubuntu 23.10, the current releases. Ubuntu is also odd in that it
doesn't install all the util-linux utilities as part of the util-linux
package, so it's not clear what they will do with 'exch'.
So I propose that we implement the idea in coreutils in a better way,
that interacts more nicely with -t, -T, etc. Also, I suggest using the
Linuxish name "--exchange" instead of the macOSish name "--swap", and
(for now at least) not giving the option a single-letter equivalent as I
expect it to be useful from scripts, not interactively.
After looking at various ways to do it I came up with the attached
proposed patch. This should work on both GNU/Linux and macOS, if your OS
is recent enough and the file system supports atomic exchange.From d522aba06107d3532ad6103470727bf9057f8d2c Mon Sep 17 00:00:00 2001
From: Paul Eggert <egg...@cs.ucla.edu>
Date: Sat, 16 Mar 2024 22:50:17 -0700
Subject: [PATCH] mv: new option --exchange
* 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 | 7 ++++++
doc/coreutils.texi | 18 ++++++++++++++
src/copy.c | 54 +++++++++++++++++++++++------------------
src/copy.h | 4 +++
src/mv.c | 16 +++++++++---
tests/local.mk | 1 +
tests/mv/mv-exchange.sh | 41 +++++++++++++++++++++++++++++++
7 files changed, 114 insertions(+), 27 deletions(-)
create mode 100755 tests/mv/mv-exchange.sh
diff --git a/NEWS b/NEWS
index f21efc7c0..67bb27ebb 100644
--- a/NEWS
+++ b/NEWS
@@ -81,6 +81,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.
diff --git a/doc/coreutils.texi b/doc/coreutils.texi
index d07ed7e76..c456a03d9 100644
--- a/doc/coreutils.texi
+++ b/doc/coreutils.texi
@@ -10269,6 +10269,24 @@ 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.
+The exchange is atomic if the source and destination are both in a
+single file system that supports atomic exchange;
+exchanges are not yet supported in other situations.
+
+This option can be used to replace one directory with another, atomically.
+When used this way, it should be combined with
+@code{--no-target-directory} (@option{-T})
+to avoid confusion about the destination location.
+Also, if the two directories 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.
+For example, you might use @samp{mv -T --exchange --no-copy
+@var{d1} @var{d2}} to exchange the directories @var{d1} and @var{d2}.
+
@item -u
@itemx --update
@opindex -u
diff --git a/src/copy.c b/src/copy.c
index 8d99f8562..e7bf6022f 100644
--- a/src/copy.c
+++ b/src/copy.c
@@ -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;
}
diff --git a/src/copy.h b/src/copy.h
index dfa9435b3..ab89c75fd 100644
--- a/src/copy.h
+++ b/src/copy.h
@@ -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;
diff --git a/src/mv.c b/src/mv.c
index 9dc40fe3e..692943a70 100644
--- 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);
}
diff --git a/tests/local.mk b/tests/local.mk
index 7cd1ef7b5..f0ac0386f 100644
--- a/tests/local.mk
+++ b/tests/local.mk
@@ -698,6 +698,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
index 000000000..485403a1d
--- /dev/null
+++ b/tests/mv/mv-exchange.sh
@@ -0,0 +1,41 @@
+#!/bin/sh
+# Test mv --exchange.
+
+# Copyright (C) 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
--
2.40.1