{"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'},
--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\
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]);
/* 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)
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. */
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 (-x) 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)
--- /dev/null
+#!/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 || { cat swap_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 --swap a 2>/dev/null || fail=1
+returns_ 1 mv --swap a b c 2>/dev/null || fail=1
+
+# both files must exist
+returns_ 1 mv --swap a d 2>/dev/null || fail=1
+
+# swapping can't be used with -t or -u
+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