Sistema de Ficheros en C++17

Vamos a analizar con un ejemplo la forma de recorrer directorios de manera recursiva a partir de C++17

28/05/2017 Available in en C++C++11C++17IOFilesystem

Introducción

A partir de C++17 se añadirán nuevas abstracciones sobre el sistema de ficheros. De momento están disponibles como parte de las Características Experimentales de C++. Si queréis profundizar aquí está el borrador final de la Especificación Técnica del Sistema de Ficheros.

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;
}
$ 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:

Separador de directorios independiente de la plataforma en C++11
#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;
}
Separador de directorios independiente de la plataforma en C++17. Más sencillo y claro.
#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;
}

Operador de 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:

Concatenar paths en C++11
#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;
}

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 ya está implementada en std::filesystem::path, así que el código en C++17 sería algo así:

Concatenar paths en C++17
#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;
}

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.

Crear y borrar directorios anidados 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;
}
g++-4.9 -std=c++11 main.cpp -lm && ./a.out
sandbox/a:
total 12
drwxr-xr-x 3 2001 2000 4096 May 28 12:27 .
drwxr-xr-x 4 2001 2000 4096 May 28 12:27 ..
drwxr-xr-x 2 2001 2000 4096 May 28 12:27 b

sandbox/c:
total 12
drwxr-xr-x 3 2001 2000 4096 May 28 12:27 .
drwxr-xr-x 4 2001 2000 4096 May 28 12:27 ..
drwxr-xr-x 2 2001 2000 4096 May 28 12:27 d
total 8012
drwxrwxrwx 2 2001 2000    4096 May 28 12:27 .
drwxrwxrwx 3 2002 2000 8175616 May 28 12:27 ..
-rwxr-xr-x 1 2001 2000    8168 May 28 12:27 a.out
-rw-rw-rw- 1 2001 2000     517 May 28 12:27 main.cpp

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;
}
g++ -std=c++1z -fconcepts -fgnu-tm  -O2 -Wall -Wextra -pedantic -pthread -pedantic-errors main.cpp -lm  -latomic -lstdc++fs && ./a.out
sandbox/a:
total 12
drwxr-xr-x 3 2001 2000 4096 May 28 16:45 .
drwxr-xr-x 4 2001 2000 4096 May 28 16:45 ..
drwxr-xr-x 2 2001 2000 4096 May 28 16:45 b

sandbox/c:
total 12
drwxr-xr-x 3 2001 2000 4096 May 28 16:45 .
drwxr-xr-x 4 2001 2000 4096 May 28 16:45 ..
drwxr-xr-x 2 2001 2000 4096 May 28 16:45 d
Were directories removed? 1
total 10132
drwxrwxrwx 2 2001 2000    4096 May 28 16:45 .
drwxrwxrwx 3 2002 2000 8175616 May 28 16:45 ..
-rwxr-xr-x 1 2001 2000 2170976 May 28 16:45 a.out
-rw-rw-rw- 1 2001 2000     393 May 28 16:45 main.cpp

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.

C++11

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

filesystem.11.cpp
#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;
}
g++ -std=c++11 -O2 -Wall -Wextra -pedantic -pthread -pedantic-errors main.cpp -lm  -latomic -lstdc++fs && ./a.out
filtered files:
	./main.cpp
	./sandbox/file3.md
	./sandbox/will_be.ignored
	./sandbox/a/b/file1.rst
	./sandbox/a/b/file1.txt
	./sandbox/a/file2.RST
	./a.out

C++17

El siguiente ejemplo filtra los ficheros por extension.

filesystem.17.cpp
#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;
}
g++ -std=c++11 -O2 -Wall -Wextra -pedantic -pthread -pedantic-errors main.cpp -lm  -latomic -lstdc++fs && ./a.out
filtered files:
	./main.cpp
	./sandbox/file3.md
	./sandbox/will_be.ignored
	./sandbox/a/b/file1.rst
	./sandbox/a/b/file1.txt
	./sandbox/a/file2.RST
	./a.out