Una utilidad para intercambiar dos archivos

Aug 17 2020

Si bien nunca tuve que intercambiar dos archivos, siempre me pregunté si había un comando para intercambiar dos archivos y recientemente decidí ver si había uno. Eventualmente descubrí que no había uno y como resultado decidí crear uno.

Aquí está el código: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);
}

Aquí hay un ejemplo de uso:

swap doc1.txt doc2.txt

Respuestas

11 vnp Aug 17 2020 at 06:22
  • file1_path == file2_pathseguramente dice que las rutas se refieren al mismo archivo. Sin embargo, incluso si file1_path != file2_pathtodavía pueden hacer referencia al mismo archivo.

  • file1_exists = std::filesystem::exists(file1_path);introduce una condición de carrera TOC-TOU. El archivo puede existir en el momento de la prueba, pero desaparecer en el momento del uso. Vea la siguiente viñeta.

  • std::filesystem::renamePuede fallar. Lo llamas tiempos de árbol. Si la segunda o tercera llamada falla (¡con una excepción!), el sistema de archivos no termina exactamente en el estado que uno esperaría. Utilice una noexceptsobrecarga, pruebe error_codedespués de cada llamada y revierta todas las acciones antes de que se produzca un error. Eso también se ocuparía automáticamente de los caminos inexistentes.

  • no assert_ Solo es bueno para detectar errores, no los problemas de tiempo de ejecución. En el código de producción (compilado con -DNDEBUG) no hace nada y su programa no detectaría una mkstempfalla.

  • El programa silenciosamente no hace nada si se llama con, digamos, 4 argumentos. También requiere mucho esfuerzo si se llama con 2 argumentos. Llamar en print_help()cualquier momento argc != 3es mucho más sencillo.

1 Razzle Aug 28 2020 at 20:26

Supongo que la 'pregunta' está pidiendo críticas de código. Si me equivoco en eso, por favor sea gentil... LOL

Mi primera impresión es la legibilidad de main. El gran interruptor de análisis de argumentos oculta la lógica más importante.

Tal vez hacer que sea más fácil de leer (¿objetos de cadena y comparaciones que no distinguen entre mayúsculas y minúsculas?). Aún mejor: relegarlo a un método auxiliar de limpieza.

(Además: a veces , lo simplista está bien. Si no recibe dos argumentos de nombre de archivo, puede escupir inmediatamente la ayuda, en lugar de decirle al usuario que pida ayuda explícitamente).