Skip to content

Commit

Permalink
mv: add --swap (-x) option to atomically swap 2 paths
Browse files Browse the repository at this point in the history
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-swap.sh: Add test for mv -x.
* tests/local.mk: Reference new test.
* NEWS: Mention the new option.
  • Loading branch information
petris authored and pixelb committed Mar 3, 2024
1 parent fcfba90 commit 6cd2d5e
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 1 deletion.
3 changes: 3 additions & 0 deletions NEWS
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions doc/coreutils.texi
Expand Up @@ -10302,6 +10302,16 @@ 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 exist and reside on the same file system.
If the atomic swap is not supported, exit with failure status.

@optTargetDirectory

@optNoTargetDirectory
Expand Down
39 changes: 38 additions & 1 deletion src/mv.c
Expand Up @@ -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'},
Expand Down Expand Up @@ -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\
Expand Down Expand Up @@ -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]);
Expand All @@ -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)
Expand Down Expand Up @@ -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. */
Expand All @@ -434,6 +443,34 @@ 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 (-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)
Expand Down
1 change: 1 addition & 0 deletions tests/local.mk
Expand Up @@ -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 \
Expand Down
47 changes: 47 additions & 0 deletions tests/mv/mv-swap.sh
@@ -0,0 +1,47 @@
#!/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

0 comments on commit 6cd2d5e

Please sign in to comment.