Sistema de Ficheros en C++17

Comenzar a utilizar característica experimental filesystem C++17 (g++)

Simplemente debemos "decir" al compilador que estamos escribiendo código C++17 (-c++1z) y que añada la librería estándar con la librería filesystem (-lstdc++fs).

g++ -std=c++1z main.cpp -lstdc++fs && ./a.out

Veamos un ejemplo muy simple utilizando la clase std::filesystem::path.

#include <experimental/filesystem>
#include <iostream>

namespace fs = std::experimental::filesystem;
using namespace std;

int main()
{
    fs::path aPath {"./path/to/file.txt"};

    cout << "Parent path: " << aPath.parent_path() << endl;
    cout << "Filename: " << aPath.filename() << endl;
    cout << "Extension: " << aPath.extension() << endl;

    return 0;
}

Compilar y ejecutar: ejemplo básico C++17

Como vemos el resultado de la ejecución es:

$ g++ -std=c++1z main.cpp -lstdc++fs && ./a.out
$ ./a.out

Parent path: "./path/to"
Filename: "file.txt"
Extension: ".txt"

Características de filesystem C++17

A continuación vamos a analizar algunas características que nos proporciona std::filesystem con ejemplos en C++11 y C++17, de esta forma podremos hacernos una idea de las utilidades que esta nueva librería nos trae y cómo efectivamente ayuda al desarrollador a escribir código más claro y seguro.

std::filesystem::path

Más arriba ya hemos visto un pequeño ejemplo de uso de clase std::filesystem::path. Ésta abstracción nos proporciona una ruta a ficheros y directorios multi-plataforma, utilizando el separador de directorios correspondiente a la plataforma en la que trabajamos \ en sistemas basados en Windows y / en sistemas basados en Unix.

Separador de directorios

Si quisiéramos que nuestro software utilizase el separador de directorios correcto para una plataforma, en C++11 podríamos utilizar una macro de compilación condicional:

#include <iostream>

using namespace std;

#ifdef _WIN32
const string SEP = "\\";
#else
const string SEP = "/";
#endif

int main()
{
    cout << "Separator in my system " << SEP << endl;
    return 0;
}

Compilar y ejecutar: ejemplo separador C++11

En C++17 sería algo más sencillo:

#include <experimental/filesystem>
#include <iostream>

namespace fs = std::experimental::filesystem;
using namespace std;

int main()
{
    cout << "Separator in my system " << fs::path::preferred_separator << endl;
    return 0;
}

Compilar y ejecutar: ejemplo separador C++17

Operador separador de directorios

std::filesystem::path implementa el operador /, el cual nos permite concatenar fácilmente rutas a ficheros o directorios.

Si quisiéramos construir rutas a directorios en C++11, tendríamos que implementar cierta lógica extra para detectar que no añadimos separadores extra y para utilizar el separador correcto:

#include <iostream>

using namespace std;

#ifdef _WIN32
const string SEP = "\\";
#else
const string SEP = "/";
#endif

int main()
{
    string root {"/"};
    string dir {"var/www/"};
    string index {"index.html"};

    string pathToIndex{};
    pathToIndex.append(root).append(SEP).append(dir).append(SEP).append(index);

    cout << pathToIndex << endl;
    return 0;
}

Compilar y ejecutar: ejemplo concatenar rutas C++11. Como vemos el resultado no es del todo correcto, deberíamos comprobar si las partes de la ruta ya contienen separador, para no añadirlo.

Toda esta lógica está ya implementada en std::filesystem::path, así que el código en C++17 sería algo así:

#include <experimental/filesystem>
#include <iostream>

namespace fs = std::experimental::filesystem;
using namespace std;

int main()
{
    fs::path root {"/"};
    fs::path dir {"var/www/"};
    fs::path index {"index.html"};

    fs::path pathToIndex = root / dir / index;

    cout << pathToIndex << endl;
    return 0;
}

Compilar y ejecutar: ejemplo concatenar rutas C++17. Aquí el código es más limpio y el resultado es simplemente correcto, no hay separadores duplicados.

Crear y borrar directorios

std::filesystem introduce algunas facilidades para crear y borrar directorios y ficheros, primero vamos a ver una de las formas de hacerlo en C++11.

#include <iostream>
#include <cstdio>
#include <sys/stat.h>

using namespace std;

int main()
{
    auto opts = S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH;
    mkdir("sandbox", opts);
    mkdir("sandbox/a", opts);
    mkdir("sandbox/a/b", opts);
    mkdir("sandbox/c", opts);
    mkdir("sandbox/c/d", opts);

    system("ls -la sandbox/*");

    remove("sandbox/c/d");
    remove("sandbox/a/b");
    remove("sandbox/c");
    remove("sandbox/a");
    remove("sandbox");

    system("ls -la");

    return 0;
}

Compilar y ejecutar: crear y borrar directorios C++11. Para crear y borrar directorios anidados, debemos hacerlo uno por uno. Podemos escribir este fragmento de código con menos líneas, pero aún así tendremos que tener cuidado del orden en el que creamos/borramos los directorios.

En C++17 podemos borrar y crear directorios anidados con una sola llamada.

#include <experimental/filesystem>
#include <iostream>

namespace fs = std::experimental::filesystem;
using namespace std;

int main()
{
    fs::create_directories("sandbox/a/b");
    fs::create_directories("sandbox/c/d");
    system("ls -la sandbox/*");

    cout << "Were directories removed? " << fs::remove_all("sandbox") << endl;
    system("ls -la");

    return 0;
}

Compilar y ejecutar: crear y borrar directorios C++17.

Ejemplo completo: Iterar Recursivamente por Directorios

Vamos a ver un ejemplo algo más completo, consiste en iterar recursivamente a través de directorios, filtrando los ficheros por extension.

Este es el ejemplo en C++11, sin filtrar por extension, para evitar complicarlo:

recursive-directory/filesystem.11.cpp (Código fuente)

#include <dirent.h>
#include <cstring>
#include <iostream>
#include <fstream> // std::ofstream
#include <vector>
#include <memory>
#include <system_error>
#include <sys/stat.h>

using namespace std;

const string UP_DIR = "..";
const string CURRENT_DIR = ".";
const string SEP = "/";


string path(initializer_list<string> parts)
{
    string pathTmp {};
    string separator = "";
    for (auto & part: parts)
    {
        pathTmp.append(separator).append(part);
        separator = SEP;
    }
    return pathTmp;
}

vector<string> getDirectoryFiles(const string& dir, const vector<string> & extensions)
{
    vector<string> files;
    shared_ptr<DIR> directory_ptr(opendir(dir.c_str()), [](DIR* dir){ dir && closedir(dir); });
    if (!directory_ptr)
    {
        throw system_error(error_code(errno, system_category()), "Error opening : " + dir);
    }

    struct dirent *dirent_ptr;
    while ((dirent_ptr = readdir(directory_ptr.get())) != nullptr)
    {
        const string fileName {dirent_ptr->d_name};
        if (dirent_ptr->d_type == DT_DIR)
        {
            if (CURRENT_DIR != fileName && UP_DIR != fileName)
            {
                auto subFiles = getDirectoryFiles(path({dir, fileName}), extensions);
                files.insert(end(files), begin(subFiles), end(subFiles));
            }
        }
        else if (dirent_ptr->d_type == DT_REG)
        {
            // here we should check also if filename has an extension in extensions vector
            files.push_back(path({dir, fileName}));
        }
    }
    return files;
}

int main ()
{
    auto opt = S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH;
    mkdir("sandbox", opt);
    mkdir("sandbox/a", opt);
    mkdir("sandbox/a/b", opt);

        vector<string> e_files = {
            "./sandbox/a/b/file1.rst",
            "./sandbox/a/b/file1.txt",
            "./sandbox/a/file2.RST",
            "./sandbox/file3.md",
            "./sandbox/will_be.ignored"
        };

        // create files
        for (auto &f: e_files)
        {
                ofstream of(f, ofstream::out);
                of << "test";
        }

    cout << "filtered files: " << endl;
        for (auto &f: getDirectoryFiles(".", {".rst", ".RST", ".md"})){
            cout << "\t" << f << endl;
        }

    return 0;
}

Compilar y ejecutar el ejemplo C++11.

El siguiente ejemplo filtra los ficheros por extension.

recursive-directory/filesystem.17.cpp (Código fuente)

#include <experimental/filesystem>
#include <iostream>
#include <vector>
#include <fstream>
#include <algorithm>    // std::find

namespace fs = std::experimental::filesystem;
using namespace std;

vector<string> getDirectoryFiles(const string & dir, const vector<string> & extensions)
{
        vector<string> files;
    for(auto & p: fs::recursive_directory_iterator(dir))
    {
        if (fs::is_regular_file(p))
        {
                if (extensions.empty() || find(extensions.begin(), extensions.end(), p.path().extension().string()) != extensions.end())
            {
                files.push_back(p.path().string());
            }
        }
    }
    return files;
}

int main()
{
    fs::create_directories("sandbox/a/b");
        vector<string> e_files = {
            "./sandbox/a/b/file1.rst",
            "./sandbox/a/b/file1.txt",
            "./sandbox/a/file2.RST",
            "./sandbox/file3.md",
            "./sandbox/will_be.ignored"
        };

        // create files
        for (auto &f: e_files)
        {
                ofstream(f) << "test";
        }

    cout << "filtered files: " << endl;
        for (auto &f: getDirectoryFiles(".", {".rst", ".RST", ".md"})){
            cout << "\t" << f << endl;
        }

    return 0;
}

Compilar y ejecutar el ejemplo C++17.

Comentarios

Comments powered by Disqus