1
0
mirror of git://git.sv.gnu.org/coreutils.git synced 2026-04-19 10:15:48 +02:00

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.
This commit is contained in:
Paul Eggert
2024-03-20 14:00:25 -07:00
parent 425b8a2f53
commit 5d3fd24f42
7 changed files with 116 additions and 27 deletions

7
NEWS
View File

@@ -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.

View 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

View 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;
}

View 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;

View File

@@ -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);
}

View 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 \

41
tests/mv/mv-exchange.sh Executable file
View File

@@ -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