* 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.
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.
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
{
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
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")
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
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))
{
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);
_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);
}
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
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;
}
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;
enum
{
DEBUG_OPTION = CHAR_MAX + 1,
+ EXCHANGE_OPTION,
NO_COPY_OPTION,
STRIP_TRAILING_SLASHES_OPTION
};
{"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. */
"), 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\
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;
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);
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);
}
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 \
--- /dev/null
+#!/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