On 02/03/2024 01:41, Rob Landley wrote:
On 3/1/24 05:05, Petr Malat wrote:
Hi!
On Fri, Mar 01, 2024 at 09:18:31AM +0000, Padraig Brady wrote:
On 29/02/2024 22:02, Petr Malat wrote:
renameat2() syscall allows atomically swapping 2 paths on one
file system. Expose this ability to the user using -s option.
* NEWS: Mention the new option
Thanks for the patch.
That was suggested previously:
https://lists.gnu.org/archive/html/coreutils/2021-05/msg00030.html
I agree this is useful functionality,
but the thinking is we should add this to a "replace" utility
rather than to mv.
I have modified mv, because for me it would be a logical place where
to look for such a feature, and to make something usable, it must be
easy to find.
I used "mv -x" for it, but if you have a strong preference for -s it hasn't been
in a release version yet.
I can submit a patch to busybox to add it there if we agree on a UI. (I will not
be adding a "replace" command to toybox: mv already replaces files and this is a
flag to the rename() system call that "mv" is already built around. This is an
atomic exchange, the new flag is RENAME_EXCHANGE, hence "-x" meaning exchange.)
Ok so there seems to be a bit of consensus for adding this to mv.
I've a slight preference for --swap, and that's short enough
that there is little benefit to a short option.
I realize toybox nor bsd support long options, so since -x is already used by
toybox
we can use that for compat. Also install(1) already has -s taken, and this
functionality
might also be a consideration for use there.
So how about -x,--swap as the short and long options?
Should we add this to install(1) ?
Petr, I've adjusted your patch a bit to better diagnose the "not supported"
case,
which is also used to make the test more robust, when run on (file) systems
that don't support the option.
I'll apply this soon, after waiting a while for comment.
thanks,
Pádraig
From 3d4c87718978814834da17d833f3b8e895c37aea Mon Sep 17 00:00:00 2001
From: Petr Malat <o...@malat.biz>
Date: Thu, 29 Feb 2024 23:02:03 +0100
Subject: [PATCH] mv: add --swap (-x) option to atomically swap 2 paths
renameat2() syscall allows atomically swapping 2 paths on one
file system. Expose this ability to the user with --swap.
* doc/coreutils.texi: Describe mv --swap option.
* src/mv.c (main): Support --swap.
* tests/mv/mv-s.sh: Add test for mv --swap.
* tests/local.mk: Reference new test.
* NEWS: Mention the new option.
---
NEWS | 3 +++
doc/coreutils.texi | 9 +++++++++
src/mv.c | 40 +++++++++++++++++++++++++++++++++++++++-
tests/local.mk | 1 +
tests/mv/mv-swap.sh | 45 +++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 97 insertions(+), 1 deletion(-)
create mode 100755 tests/mv/mv-swap.sh
diff --git a/NEWS b/NEWS
index 7a5fbfd28..baeded0cb 100644
--- a/NEWS
+++ b/NEWS
@@ -76,6 +76,9 @@ GNU coreutils NEWS -*- outline -*-
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.
+ mv now accepts the --swap (-x) option, which atomically swaps two files on
+ one file system (where RENAME_EXCHANGE is supported).
+
tail now supports following multiple processes, with repeated --pid options.
** Improvements
diff --git a/doc/coreutils.texi b/doc/coreutils.texi
index 9e3aa6c1c..f0e6911c9 100644
--- a/doc/coreutils.texi
+++ b/doc/coreutils.texi
@@ -10302,6 +10302,15 @@ to cause @command{cp} write to arbitrary target directories.
@optBackupSuffix
+@item -x
+@itemx --swap
+@opindex -x
+@opindex --swap
+@cindex swapping files
+Atomically exchange all data and metadata for two specified files.
+They can be of a different type, but must reside on the same file system.
+If the atomic swap is not supported, exit with failure status.
+
@optTargetDirectory
@optNoTargetDirectory
diff --git a/src/mv.c b/src/mv.c
index 9dc40fe3e..5e53e9ddd 100644
--- a/src/mv.c
+++ b/src/mv.c
@@ -75,6 +75,7 @@ static struct option const long_options[] =
{"strip-trailing-slashes", no_argument, nullptr,
STRIP_TRAILING_SLASHES_OPTION},
{"suffix", required_argument, nullptr, 'S'},
+ {"swap", no_argument, nullptr, 'x'},
{"target-directory", required_argument, nullptr, 't'},
{"update", optional_argument, nullptr, 'u'},
{"verbose", no_argument, nullptr, 'v'},
@@ -283,6 +284,10 @@ If you specify more than one of -i, -f, -n, only the final one takes effect.\n\
--strip-trailing-slashes remove any trailing slashes from each SOURCE\n\
argument\n\
-S, --suffix=SUFFIX override the usual backup suffix\n\
+"), stdout);
+ fputs (_("\
+ -x, --swap atomically swap SOURCE and DEST, they may be\n\
+ different types, but on the same file system\n\
"), stdout);
fputs (_("\
-t, --target-directory=DIRECTORY move all SOURCE arguments into DIRECTORY\n\
@@ -323,6 +328,7 @@ main (int argc, char **argv)
char **file;
bool selinux_enabled = (0 < is_selinux_enabled ());
bool no_clobber = false;
+ bool swap = false;
initialize_main (&argc, &argv);
set_program_name (argv[0]);
@@ -337,7 +343,7 @@ main (int argc, char **argv)
/* Try to disable the ability to unlink a directory. */
priv_set_remove_linkdir ();
- while ((c = getopt_long (argc, argv, "bfint:uvS:TZ", long_options, nullptr))
+ while ((c = getopt_long (argc, argv, "bfint:uvS:TxZ", long_options, nullptr))
!= -1)
{
switch (c)
@@ -412,6 +418,9 @@ main (int argc, char **argv)
make_backups = true;
backup_suffix = optarg;
break;
+ case 'x':
+ swap = true;
+ break;
case 'Z':
/* As a performance enhancement, don't even bother trying
to "restorecon" when not on an selinux-enabled kernel. */
@@ -434,6 +443,35 @@ main (int argc, char **argv)
n_files = argc - optind;
file = argv + optind;
+ if (swap)
+ {
+ if (target_directory || x.update)
+ {
+ error (0, 0, _("cannot combine --swap with "
+ "--target-directory (-t) or --update (-u)"));
+ usage (EXIT_FAILURE);
+ }
+ if (n_files != 2)
+ {
+ error (0, 0, _("option --swap takes 2 file operands, "
+ "but %d were given"), n_files);
+ usage (EXIT_FAILURE);
+ }
+ if (renameatu (AT_FDCWD, file[0], AT_FDCWD, file[1],
+ RENAME_EXCHANGE))
+ {
+ if (errno == EINVAL || is_ENOTSUP (errno))
+ error (EXIT_FAILURE, 0,
+ _("atomic swap of %s and %s is not supported"),
+ quoteaf_n (0, file[0]), quoteaf_n (1, file[1]));
+ else
+ error (EXIT_FAILURE, errno, _("swap of %s and %s failed"),
+ quoteaf_n (0, file[0]), quoteaf_n (1, file[1]));
+ }
+
+ main_exit (EXIT_SUCCESS);
+ }
+
if (n_files <= !target_directory)
{
if (n_files <= 0)
diff --git a/tests/local.mk b/tests/local.mk
index 7cd1ef7b5..3a8a13361 100644
--- a/tests/local.mk
+++ b/tests/local.mk
@@ -699,6 +699,7 @@ all_tests = \
tests/mv/into-self-4.sh \
tests/mv/leak-fd.sh \
tests/mv/mv-n.sh \
+ tests/mv/mv-swap.sh \
tests/mv/mv-special-1.sh \
tests/mv/no-copy.sh \
tests/mv/no-target-dir.sh \
diff --git a/tests/mv/mv-swap.sh b/tests/mv/mv-swap.sh
new file mode 100755
index 000000000..1ca5621a6
--- /dev/null
+++ b/tests/mv/mv-swap.sh
@@ -0,0 +1,45 @@
+#!/bin/sh
+# Test whether mv -x,--swap swaps targets
+
+# 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 swapping files
+touch a || framework_failure_
+mkdir b || framework_failure_
+if ! mv -x a b 2>swap_err; then
+ grep 'not supported' swap_err || fail=1
+else
+ test -d a || fail=1
+ test -f b || fail=1
+fi
+
+# test wrong number of arguments
+touch a b c || framework_failure_
+returns_ 1 mv --swap a 2>/dev/null || fail=1
+returns_ 1 mv --swap a b c 2>/dev/null || fail=1
+
+# swapping can't be used with -t or -u
+touch a b || framework_failure_
+mkdir d
+returns_ 1 mv --swap -t d a b 2>/dev/null || fail=1
+returns_ 1 mv --swap -t d a 2>/dev/null || fail=1
+returns_ 1 mv --swap -u a b 2>/dev/null || fail=1
+
+Exit $fail
--
2.43.0