From: Dominique Martinet <dominique.marti...@atmark-techno.com> gnulib supports RENAME_EXCHANGE in renameatu, but there is currently no way of using it. Add a new switch to allow users to swap files atomically. ---
Note: I'm not suggesting this as a final version of the patch; it's just something I hacked in a few minutes just now and appears to work. A real patch would need at least adding to help string, proper documentation, possibly some test cases... The code probably could be made prettier too. Anyway, some rationale: Linux and BSDs ave had ways of swapping two files atomically for a while (linux has renameat2 with RENAME_EXCHANGE flag since 3.15 (2014), with most filesystems implementing it since around 2015, some BSDs have had renamex_np and renameatx_np with RENAME_SWAP for at least 5 years as well (didn't check as much)) Yet I do not see any tool making use of it; if you try to search online the best hits seem to be people suggesting to use tcc in a shell function to JIT code that calls renameat2[1] or to use a gist on github[2]... Not exactly a good UX. [1] https://unix.stackexchange.com/a/625900 [2] https://gist.github.com/eatnumber1/f97ac7dad7b1f5a9721f The gnulib's renameatu helper that mv uses supports the flag, so there is no technical reason not to expose it through a new e.g. --swap command line switch if we so wish to do -- it doesn't have fallback for the case the flag is not supported, but I personally think it's a good thing: this will warn user the swap is not atomic, so they need to handle some sort of recovery in case of hard crash between the renames. If mv would just emulate the operation there would be no way of detecting that. Looking at the list archives, one person offered to implement such a flag and never got any reply in 2018[3], but the subject never came up otherwise that I can see. [3] https://lists.gnu.org/archive/html/coreutils/2018-12/msg00004.html Since there were no reply I took it a step further with a simple proof of concept, but my request is basically the same: if I were to polish this up a bit, clear up documentation etc would such a patch be accepted? I'm really surprised the topic didn't come up at least once, so perhaps I have missed something; please just tell me if you're not interested or if there is a reason not to do this. src/mv.c | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/mv.c b/src/mv.c index e82cc097218b..451bc4bc6823 100644 --- a/src/mv.c +++ b/src/mv.c @@ -47,7 +47,8 @@ non-character as a pseudo short option, starting with CHAR_MAX + 1. */ enum { - STRIP_TRAILING_SLASHES_OPTION = CHAR_MAX + 1 + STRIP_TRAILING_SLASHES_OPTION = CHAR_MAX + 1, + SWAP_OPTION = CHAR_MAX + 2, }; /* Remove any trailing slashes from each SOURCE argument. */ @@ -63,6 +64,7 @@ static struct option const long_options[] = {"no-target-directory", no_argument, NULL, 'T'}, {"strip-trailing-slashes", no_argument, NULL, STRIP_TRAILING_SLASHES_OPTION}, {"suffix", required_argument, NULL, 'S'}, + {"swap", no_argument, NULL, SWAP_OPTION}, {"target-directory", required_argument, NULL, 't'}, {"update", no_argument, NULL, 'u'}, {"verbose", no_argument, NULL, 'v'}, @@ -344,6 +346,7 @@ main (int argc, char **argv) struct cp_options x; char *target_directory = NULL; bool no_target_directory = false; + bool swap_targets = false; int n_files; char **file; bool selinux_enabled = (0 < is_selinux_enabled ()); @@ -383,6 +386,10 @@ main (int argc, char **argv) case STRIP_TRAILING_SLASHES_OPTION: remove_trailing_slashes = true; break; + case SWAP_OPTION: + swap_targets = true; + no_target_directory = true; + break; case 't': if (target_directory) die (EXIT_FAILURE, 0, _("multiple target directories specified")); @@ -442,6 +449,9 @@ main (int argc, char **argv) usage (EXIT_FAILURE); } + if (swap_targets && target_directory) + die (EXIT_FAILURE, 0, _("cannot combine --swap and --target-directory")); + if (no_target_directory) { if (target_directory) @@ -471,6 +481,14 @@ main (int argc, char **argv) quoteaf (file[n_files - 1])); } + if (swap_targets) { + x.rename_errno = renameatu (AT_FDCWD, file[0], AT_FDCWD, file[1], + RENAME_EXCHANGE) ? errno : 0; + /* die early to avoid fallbacks such as copy on EXDEV */ + if (x.rename_errno) + die (EXIT_FAILURE, x.rename_errno, _("swapping files failed")); + } + if (x.interactive == I_ALWAYS_NO) x.update = false; -- 2.30.2