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:
7
NEWS
7
NEWS
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
54
src/copy.c
54
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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
16
src/mv.c
16
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
41
tests/mv/mv-exchange.sh
Executable 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
|
||||
Reference in New Issue
Block a user