18

I need to swap filenames of two files (file and file_1). I'm using the following code for it.

mv file .phfile
mv file_1 file
mv .phfile file

This works but is very buggy, It sometimes even results in loss of data. Is there a better way to do this?

4
  • 7
    Something you should be aware of and fear - any program that has an open file handle to either one of those files will retain the open file handle after the rename because it's pointing to the same inode that it had when it originally opened the file. Until all programs that have the files open close them, this will be the case and cause data corruption if you expect that renaming them 'breaks' any existing writer handles. Commented Feb 28, 2012 at 1:08
  • (1) “This works”?  No, this always destroys file_1.  (2) Most people who have two files would call them file1 and file2.  10% of people would call them file0 and file1.  1% of people would call them andy and bob, or fred and barney, or fred and wilma.  Using weird filenames makes it easy to make typos, like what the above obviously is, and for them to go unnoticed for ten years.  (3) Have you debugged the data loss that you have experienced?  Can you describe how it happens?  (3a) What sort of processing are the files subject to?  What’s the big picture?  … (Cont’d) Commented Jul 31, 2022 at 6:48
  • (Cont’d) …  (4) You should specify whether answers should assume that the two files are in the same directory, in different directories on the same filesystem, or on different filesystems. … … … … … … … … … … … … … … … … … … … … … … … … … … … … … … Please do not respond in comments; edit your question to make it clearer and more complete. Commented Jul 31, 2022 at 6:48
  • @G-ManSays'ReinstateMonica' probably wants to say that you can't, uh, "reinstate" .phfile (or whatever you use in the initial move, if for some reason you don't use mktemp) -- not file_1
    – usretc
    Commented May 12 at 17:57

7 Answers 7

17

The renameat2 syscall on Linux systems, with the RENAME_EXCHANGE flag, should do exactly that, this is a cli tool that claims to use it.

2
7

There's no low-level way to swap files in traditional Unix systems, so you need to use an intermediate temporary name. For robustness, make sure that the temporary name won't be used by any other program (so use mktemp) and that it's on the same filesystem as one of the files (otherwise the files would be needlessly copied instead of being just renamed).

swap_files () {
  tmp_name=$(TMPDIR=$(dirname -- "$1") mktemp) &&
  mv -f -- "$1" "$tmp_name" &&
  mv -f -- "$2" "$1" &&
  mv -f -- "$tmp_name" "$2"
}
swap_files file file_1

Beware that if an error occurs, the first file could still be under its temporary name, and the second file may or may not have been moved yet. If you need robustness in case of interruptions and crashes, a variant with two temporary names may be easier to recover from.

swap_files2 () {
  tmp_dir1=$(TMPDIR=$(dirname -- "$1") mktemp -d .swap_files.XXXXXXXXXXXX) &&
  tmp_dir2=$(TMPDIR=$(dirname -- "$2") mktemp -d .swap_files.XXXXXXXXXXXX) &&
  mv -f -- "$1" "$tmp_dir1/" &&
  mv -f -- "$2" "$tmp_dir2/" &&
  mv -f -- "$tmp_dir1/"* "$1" &&
  mv -f -- "$tmp_dir2/"* "$2" &&
  rmdir -- "$tmp_dir1" "$tmp_dir2"
}

If the temporary directories .swap_files.???????????? are present on a reboot, it means that a file swap was interrupted by a power failure. Beware that it's possible that one of the files has already been moved into place and the other one hasn't, so the code here doesn't take care of all cases, it depends what kind of recovery you want.

Modern Linux kernels (since 3.15, relased in June 2014) have a system call to swap files: renameat2(…, RENAME_EXCHANGE). However there doesn't seem to be a commonly available command line utility for it. Even glibc support was only added recently (2.28, released in August 2018).

4
  • 1
    "There's no low-level way to swap files" that doesn't seem to be true, see unix.stackexchange.com/a/561609/102517
    – dreua
    Commented Apr 11, 2020 at 15:14
  • 1
    "However there doesn't seem to be a commonly available command line utility for it" — If you count tcc - and you should - then there is. See my answer: unix.stackexchange.com/a/625900/353063 Commented Jun 10, 2022 at 12:18
  • 1
    @TendersMcChiken Tcc is commonly available in the sense of being available in many distributions, but not in the sense of being installed on a typical system. Even gcc is far more common. If you want to make a system call with commonly available tools, you can do it in python, which is part of most default non-embedded unix installations these days. Commented Jun 10, 2022 at 14:36
  • The question didn't limit solutions to preinstalled utilities only so I went with the less constrictive interpretation. You most certainly can implement my solution in python if you so choose. Cheers. Commented Jun 11, 2022 at 22:36
6

A bit late to the party but you can atomicly name-swap files in newer and older versions of Linux by using tcc or gcc — both available in all major linux distributions — to JIT yourself a low-level tool that uses the correct kernel syscall and use that. No third-party tools needed:

swapname() {
    tcc -run - "$@" <<"CODE"
    #include <unistd.h>
    #include <fcntl.h> 
    #include <stdio.h>
    #include <sys/syscall.h>
    
    // Ubuntu 18.04 does not define RENAME_EXCHANGE
    // Value obtained manually from '/usr/include/linux/fs.h'
    // You should switch to RENAME_EXCHANGE on modern systems
    // Just remove the following line, then remove the `local_`
    // prefix where it appears later in this function.
    int local_RENAME_EXCHANGE = (1 << 1);
    
    int main(int argc, char **argv) {
        if (argc != 3) { 
            fprintf(stderr, "Error: Could not swap names. Usage: %s PATH1 PATH2\n", argv[0]);
            return 2; 
        }
        int r = syscall(
            SYS_renameat2,
            AT_FDCWD, argv[1],
            AT_FDCWD, argv[2], 
            local_RENAME_EXCHANGE
        );
        if (r < 0) {
            perror("Error: Could not swap names");
            return 1;
        }
        else return 0;
    }
CODE
} 

Running this bash function in the following manner will cleanly and atomically swaps filenames:

swapname "/path/to/file-1" "/path/to/file-2"

Note that renameat2 with RENAME_EXCHANGE may require that both files be under the same filesystem mountpoint. See the error section in the man page that covers renameat2 (i.e. rename(2)) for more information.

Using GCC instead of TCC

If you prefer to use gcc instead of tcc, simply remove the line starting with tcc -run ... and add the following one in its place:

( EXEC="$(mktemp)" && gcc -x c - -o "$EXEC" && "$EXEC" "$@"; rm "$EXEC" ) <<"CODE" 
4
  • This works really well on some file systems but not all. Probably unsurprising. On Window Subsystem for Linux (WSL2) for example, it works fine on the home directory but does not work on the automounted Windows files under /mnt/c etc. I imagine it won't work on quite a few network mounts either. Fortunately it is error checked. You get an 'INVALID ARGUMENT' error. Commented Jun 10, 2022 at 8:08
  • @BillSellers IIRC I don't believe RENAME_EXCHANGE can swap files if they live in different filesystems. Commented Jun 10, 2022 at 11:42
  • "No third-party tools needed" liar. You need tcc.
    – b0fh
    Commented Nov 21, 2022 at 20:56
  • 1
    @b0fh Didn't lie. "No third-party tools" means that your distribution includes it in their official repositories; i.e. you don't need to fetch it from github, sourceforge or any other third party. With that said, you can certainly use gcc instead of tcc which is usually installed by default. See my edit above. Cheers! Commented Nov 23, 2022 at 4:44
2

The GNU coreutils mv command now (since version 9.5) has an option --exchange to cover this! see mv: new option --exchange

There is now also a exch utility in util-linux since version 2.40.

2
  • Do you know which systems this is supported on?
    – muru
    Commented May 22 at 5:10
  • I tested it and it worked on Arch Linux (6.6.30-4-rpi) and macOS (Sonoma 14.5)
    – muru
    Commented May 22 at 5:25
1

This is more robust:

TMPFILE=tmp.$$
mv -- "$file1" $TMPFILE && mv -- "$file2" "$file1" && mv -- $TMPFILE "$file2"

quoting is for preventing problems with spaces in filenames, it uses a tmp file and && make the following command run only if the preceding ended successfully.

13
  • How's this? file1=1stfile && file2=2ndfile && temp="$(mktemp -dp /mnt/sdcard)" && mv "$file1" $temp && mv "$file2" "$file1" && mv $temp/"$file1" "$file2". It works. Thanks for your input.
    – Binoy Babu
    Commented Feb 28, 2012 at 1:47
  • see this stackoverflow.com/q/9475497/1068546
    – Binoy Babu
    Commented Feb 28, 2012 at 3:04
  • using mktemp is good; nice find
    – guido
    Commented Feb 28, 2012 at 3:10
  • but running a command with && in java with runtime.exec() don't work. :(
    – Binoy Babu
    Commented Feb 28, 2012 at 3:15
  • you have to quote your yourShellInput variable: String yourShellInput = " \"echo hi && echo ho\" "; or if ouy have double quotes in your command String yourShellInput = " 'echo hi && echo ho' ";
    – guido
    Commented Feb 28, 2012 at 3:33
1

Here's what I ended up using :

file1=1stfile
file2=2ndfile

tempdir="$(mktemp -d)"

mv "$file1" "$tempdir/tmpfile" &&
mv "$file2" "$file1" &&
mv "$tempdir/tmpfile" "$file2" &&
rm -rf "$tempdir"
1
  • 1
    This copies+deletes the first file to /tmp and back (very slow for large files), unless the files were already in the same filesystem as /tmp (which is unlikely on many modern Linux systems where /tmp is on tmpfs, not disk.) Commented Oct 18, 2021 at 1:56
0
ln file1 file1.orig
mv file2 file1
mv file1.orig file2
2
  • 2
    (1) You should link related commands like this with &&, so if one command fails, the list terminates (and does not go on to do things that might be destructive.  For example, if your first command fails, and you go on to execute the second command, it will destroy file1.)  … (Cont’d) Commented Jul 31, 2022 at 3:42
  • (Cont’d) …  (2) Your answer is pretty much the same as three previous answers, except for choice of filenames (which is inconsequential) and the use of ln instead of mv in the first command.  Why do you believe that this makes your answer better than the others? … … … … … … … … … … … … Please do not respond in comments; edit your answer to make it clearer and more complete. Commented Jul 31, 2022 at 3:42

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .