Un programma di utilità per scambiare due file

Aug 17 2020

Anche se non ho mai dovuto scambiare due file, mi sono sempre chiesto se esistesse un comando per scambiare due file e recentemente ho deciso di vedere se ce n'era uno. Alla fine ho scoperto che non ce n'era uno e di conseguenza ho deciso di crearne uno.

Ecco il codice: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);
}

Ecco un esempio di utilizzo:

swap doc1.txt doc2.txt

Risposte

11 vnp Aug 17 2020 at 06:22
  • file1_path == file2_pathsicuramente dice che i percorsi si riferiscono allo stesso file. Tuttavia, anche se file1_path != file2_pathpossono ancora fare riferimento allo stesso file.

  • file1_exists = std::filesystem::exists(file1_path);introduce una race condition TOC-TOU. Il file potrebbe esistere al momento del test, ma scomparire al momento dell'utilizzo. Vedi il prossimo punto.

  • std::filesystem::renamepotrebbe fallire. Lo chiami tre volte. Se la seconda o la terza chiamata fallisce (con un'eccezione!), il filesystem finisce non esattamente in uno stato che ci si aspetterebbe. Utilizzare un noexceptsovraccarico, testare error_codedopo ogni chiamata ed eseguire il rollback di tutte le azioni prima dell'errore. Ciò si occuperebbe anche automaticamente dei percorsi inesistenti.

  • Non farlo assert. È utile solo rilevare i bug, non i problemi di runtime. Nel codice di produzione (compilato con -DNDEBUG) non fa nulla e il tuo programma non rileverebbe un mkstemperrore.

  • Il programma silenziosamente non fa nulla se chiamato con, diciamo, 4 argomenti. Ci vuole anche così tanto impegno se chiamato con 2 argomenti. Chiamare in print_help()qualsiasi momento argc != 3è molto più semplice.

1 Razzle Aug 28 2020 at 20:26

Immagino che la "domanda" richieda critiche al codice. Se mi sbaglio, per favore sii gentile.. LOL

La mia prima impressione è la leggibilità di main. Il grande interruttore di analisi degli argomenti nasconde la logica più importante.

Forse rendere più facile la lettura (oggetti stringa e confronti senza distinzione tra maiuscole e minuscole?) .. Ancora meglio: relegare a un metodo di aiuto per le pulizie.

(Inoltre: a volte semplicistico va bene. Se non ricevi due argomenti di nome file potresti sputare immediatamente l'aiuto, piuttosto che dire all'utente di chiedere esplicitamente aiuto.)