Best way to swap filenames

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?

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

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.

Method 2

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

Method 3

A bit late to the party but you can atomicly name-swap files in newer and older versions of Linux by using tcc — 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 - "[email protected]" <<"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 PATH2n", 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.

Method 4

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.

Method 5

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"


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x