# A utility to swap two files

While I have never had to swap two files I have always wondered if there was command for swapping two files and recently I decided to see if there was one. Eventually I found out that there wasn't one and as a result I decided to create one.

Here is the code: main.cc

#include <iostream>
#include <filesystem>
#include <cstring>
#include <cassert>

static auto print_help() -> void {
std::cout << "Usage: swap [file1] [file2]\n";
std::cout << "swaps the contents of file1 and file2\n";
std::cout << "use swap --help to print this message\n";
}

static auto validate_files(const std::filesystem::path& file1_path, const std::filesystem::path& file2_path) -> void {
{
/* check if file exists */
const auto file1_exists = std::filesystem::exists(file1_path);
const auto file2_exists = std::filesystem::exists(file2_path);
const auto exists = file1_exists && file2_exists;

if (!exists) {
if (!file1_exists) std::cerr << "cannot find file " << file1_path << '\n';
if (!file2_exists) std::cerr << "cannot find file " << file2_path << '\n';
exit(EXIT_FAILURE);
}
}

{
if (file1_path == file2_path) {
std::cerr << "swaping the same two files does nothing\n";
exit(EXIT_SUCCESS);
}
}
}

static auto get_temp_filename(char* template_name) -> void {
/* tmpnam_s does not work on linux */
#if defined(WIN32) || defined(_WIN32)
errno_t err = tmpnam_s(template_name, L_tmpnam);
assert(!err);
#else
int err = mkstemp(template_name);
assert(err != -1);
#endif
}

int main(int argc, char** argv) {
std::ios::sync_with_stdio(false);
switch (argc) {
case 2: {
/* convert the second arg to upper case */
std::transform(argv[1],
argv[1] + strlen(argv[1]),
argv[1],
::toupper);
if (!strcmp(argv[1], "--HELP")) {
print_help();
return EXIT_SUCCESS;
}
else {
std::cerr << "Invalid args see --help for usage\n";
return EXIT_FAILURE;
}
}
case 3:
break;
default: {
std::cerr << "Invalid args see --help for usage\n";
return EXIT_FAILURE;
}
}
const auto file1_path = std::filesystem::path{ argv[1] };
const auto file2_path = std::filesystem::path{ argv[2] };

validate_files(file1_path, file2_path);
char temp_filename[L_tmpnam] = "XXXXXX";
get_temp_filename(temp_filename);
const auto temp_filepath = std::filesystem::path{ temp_filename };
/* move-swap the files instead of copy-swaping */
/* renaming a file is the same as moving it */
std::filesystem::rename(file1_path, temp_filepath);
std::filesystem::rename(file2_path, file1_path);
std::filesystem::rename(temp_filepath, file2_path);
}


Here is an example of usage:

swap doc1.txt doc2.txt

• What would the expected behavior be if there is a path included in one or both filenames? How would you handle the two files being on different drives? Or if both files are on one file (that is not the current working directory), as the temp file would then be on a different drive? – 1201ProgramAlarm Aug 17 '20 at 3:17
• I don't know what the behavior would be but what do you do supposed I should do about it? – nullptr Aug 17 '20 at 15:59
• Expected #ifdef RENAME_EXCHANGE renameat2(...) #else to use the actual system call if possible. – Joshua Aug 17 '20 at 18:16
• See man7.org/linux/man-pages/man2/rename.2.html for RENAME_EXCHANGE to "atomically exchange oldpath and newpath". Linux is common enough to be worth taking advantage of that feature that's existed since Linux 3.15. – Peter Cordes Aug 17 '20 at 21:43
• the Windows version of std::filesystem::rename uses docs.microsoft.com/en-us/windows/win32/api/winbase/… with the option MOVEFILE_COPY_ALLOWED which allows for moving files across different drives. However, the Linux version of std::filesystem::rename just uses std::rename which does not allow moving files across different drives. – nullptr Aug 17 '20 at 22:25

• file1_path == file2_path surely tells that paths refer to the same file. However, even if file1_path != file2_path they still may refer to the same file.

• file1_exists = std::filesystem::exists(file1_path); introduces a TOC-TOU race condition. The file may exist at the time of test, yet disappear by the time of use. See the next bullet.

• std::filesystem::rename may fail. You call it tree times. If the second, or third call fails (with an exception!), the filesystem ends up not exactly in a state one would expect. Use a noexcept overload, test the error_code after each call, and roll back all actions prior to failure. That would also automagically take care of the nonexistent paths.

• Do not assert. It is only good to catch bugs, not the runtime problems. In the production code (compiled with -DNDEBUG) it does nothing, and your program wouldn't detect an mkstemp failure.

• The program silently does nothing if called with, say, 4 arguments. It also takes so much effort if called with 2 arguments. Calling print_help() any time argc != 3 is much more straightforward.

I'm guessing the 'question' is asking for code critiques. If I'm wrong about that, please be gentle.. LOL

My first impression is the readability of main. The large argument parsing switch is hiding the more important logic..

Maybe make it easier to read (string objects and case insensitive compares?).. Even better: relegate to a housekeeping helper method.

(Also: Sometimes simplistic is OK. If you don't receive two filename arguments you could immediately spit out the help, rather than telling user to explicitly ask for help.)