Czy wiesz, że jesteśmy również na Slacku? Dołącz do nas już teraz klikając tutaj!

Jak iterować po katalogach w C++?


2019-05-16, 00:00

Jak byście zaimplementowali funkcję która ma przeglądnąć katalogi i znaleźć plik z zadanym rozszerzeniem? Na przykład wyszukać wszystkie pliki .txt lub .cpp? Aby rozwiązać ten problem musicie posiadać dostęp do iteracji po plikach. Czy jest to możliwe w C++? Przyjrzyjmy się dostępnym technikom oraz zobaczmy, co jest dostępne od nowego standardu C++17.

Wstęp

Załóżmy, że dysponujemy następującą strukturą katalogów:

books\
    cppbookA.txt
    cppbookA.pdf
    pythonbookX.txt
    pythonbookX.epub
    stlboob.txt
    stlbook.mobi
sources\
    licence.txt
    example1.cpp
    example2.cpp

Jak wyfiltrować wszystkie pliki *.txt z katalogu books/ lub wszystkie pliki *.cpp z katalogu sources/?

Podstawowym pomysłem tutaj jest iterowanie po katalogu, sprawdzenie czy pozycja jest regularnym plikiem oraz czy posiada odpowiednie rozszerzenie.

Przed C++17 nie było jednego standardowego sposobu na implementację powyższego zadania. W dalszej części wpisu pokażę Wam kilka możliwych API, które są obecnie dostępne. Na przykład:

  • funkcje z rodziny POSIX
  • funkcje systemu Windows
  • Qt
  • POCO
  • BOOST

Następnie przeniesiemy się do C++17.

Zacznijmy od C/POSIX

Na systemach z rodziny Linux, korzystając tylko za pomocą funkcji POSIX, możecie posłużyć się strukturą dirent:

#include <stdio.h>
#include <dirent.h>

int main(int argc, const char**argv) {
    struct dirent *entry = nullptr;
    DIR *dp = nullptr;

    dp = opendir(argc > 1 ? argv[1] : "/");
    if (dp != nullptr) {
        while ((entry = readdir(dp)))
            printf ("%s\n", entry->d_name);
    }

    closedir(dp);
    return 0;
}

Możesz uruchomić ten kod na @Coliru

Jak widzicie, mamy tutaj podstawowe API oraz trzy funkcje wykorzystywane do iterowania po katalogu:

  • opendir() do inicjalizacji szukania i znalezienia pierwszego wpisu
  • readdir() do znalezienia następnego wpisu
  • closedir() do zakończenia przeszukiwania

Podczas iterowania korzystamy ze struktury dirent entry, która jest zadeklarowana w następujący sposób:

struct dirent {
    ino_t          d_ino;       /* inode number */
    off_t          d_off;       /* offset to the next dirent */
    unsigned short d_reclen;    /* length of this record */
    unsigned char  d_type;      /* type of file; not supported
                                   by all file system types */
    char           d_name[256]; /* filename */
};

Więcej na temat dirent znajdziecie tutaj.

Jak możecie zauważyć, jest to niskopoziomowe API, którego raczej nie chcielibyśmy używać programując w nowoczesnym C++ :)

Do wyfiltrowania plików o zadanym rozszerzeniu, musielibyśmy wyciągnąć rozszerzenie z pełnej nazwy pliku, która jest zwykłą tablicą znaków.

Jeżeli potrzebujecie wersji rekursywnej, to możecie spróbować wykorzystać funkcję ftw() - “File Tree Walk” - zobaczcie jej dokumentację tutaj.

System Windows, WinApi

Windows nie jest systemem z rodziny POSIX. Pomimo tego możemy użyć funkcji dirent, która dostępna jest w paczkach MinGW oraz Cygwin. Znalazłem również samodzielny helper, który możecie zainstalować osobno: https://github.com/tronkko/dirent

Jednak, jeżeli chcecie, możecie skorzystać z natywnego Windows API.

Podstawowy przykład na tej platformie wykorzystuje takie funkcje jak FindFirstFile, FindNextFile and FindClose (w rzeczywistości działające podobnie do dirent).

WIN32_FIND_DATA FindFileData;
HANDLE hFind = FindFirstFile(/*path*/, &FindFileData);
if (hFind == INVALID_HANDLE_VALUE) {
    printf ("FindFirstFile failed (%d)\n", GetLastError());
    return;
}

do {
    if (FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
        _tprintf(TEXT("  %s   <DIR>\n"), FindFileData.cFileName);
    else
        _tprintf(TEXT("  %s \n"), FindFileData.cFileName);
} while (FindNextFile(hFind, &FindFileData) != 0);

FindClose(hFind);

W trakcie iterowania mamy dostęp do właściwości struktury WIN32_FIND_DATA, która wystawia nam takie pola jak typ ścieżki, nazwa pliku (CHAR cFileName[MAX_PATH];), datę ostatniego dostępu do pliku, rozmiar itp.

Rozszerzenie uzyskujemy wykorzystując funkcję PathFindExtension.

Więcej na ten temat tej metody możecie przeczytać tutaj: Listing the Files in a Directory - Windows applications | Microsoft Docs

Mam nadzieję, że już rozumiecie, w jaki sposób możemy iterować po katalogu. Ciągle jednak nie jest to nowoczesny C++, a wywołania niskopoziomowych funkcji systemowych. Co powiecie na kod wyższego poziomu?

Biblioteki zewnętrzne

W tej sekcji przyjżymy się trzem bibliotekom zewnętrznym, które enkapsulują natywne API, przedstawiając znacznie ładniejszy interfejs. Są one multiplatformowe, dzięki czemu nasz kod będzie działał tak samo na Windowsie oraz Linuksie.

Qt

Qt jest bardzo obszernym framworkiem, który dostarcza nam oddzielne komponenty do pracy z systemem plików. Do iterowania po katalogach wykorzystamy QDirIterator.

Tutaj mamy prosty przykład (z dokumentacji) Qt:

QDirIterator it("/etc", QDirIterator::Subdirectories);
while (it.hasNext()) {
    qDebug() << it.next();
}

Tylko trzy linijki kodu! Co więcej, ten kod jest zgodny z RAII (nie ma potrzeby zamknięcia przeszukiwania katalogu, ponieważ jest to zamknięte wewnątrz implementacji QDirIterator).

Tak oto możemy otrzymać nazwę pliku oraz pełen zestaw informacji o pliku, z poziomu iteratora.

Poco

W Poco, które jest multiplatformowym frameworkiem przeznaczonym do tworzenia aplikacji sieciowych istnieje komponent DirectoryIterator:

#include <Poco/DirectoryIterator.h>
...

for (DirectoryIterator it(path); it != DirectoryIterator{}; ++it) {

}

Jak widzicie, mamy tutaj dokładnie taki sam wzorzec jak w Qt: iterator (forward iterator), który pozwala na przewinięcie listy plików.

BOOST Filesystem

Ostatnią biblioteką, o której wspomnę jest Boost Filesystem. Jest ona bardzo potężna i dobrze odbierana przez społeczność.

Jeżeli potrzebujecie szybkiego przeglądu, to tutaj macie główny tutorial: boost.org: Boost Filesystem tutorial.

Podstawowy przykład iteracji po katalogach:

#include <boost/filesystem.hpp>
using namespace boost::filesystem;

for (directory_entry& entry : directory_iterator(inputPath))
    std::cout << entry.path() << '\n';

Tym razem, w przeciwieństwie do poprzednich rozwiązań, widzimy iterator, który opakowuje wszystkie nisko-poziomowe wywołania. Każdy wpis daje nam np. podgląd pełnej ścieżki.

Zauważmy, że ten directory_iterator posiada metody begin oraz end, co oznacza, że może być wykorzystywany w pętli for o alternatywnej składni.

Wspomniałem o Boost, ponieważ jest to bardzo znana i najczęściej wykorzystywana biblioteka, która jest fundamentem standardu Filesystem TS opublikowanego przed C++17… i ostatecznie włączona przez komisję do standardu.

Podsumowując biblioteki zewnętrzne

Jak możecie zauważyć, interfejsy bibliotek zewnętrznych są bardziej czytelne niż rozwiazania natywne. Nasze zadanie możemy zaimplementować stosując zaledwie kilka linijek kodu. Jednakże jest tutaj jedna wada: włączamy do projektu duży framework. I tak oto do projektu włączamy cały ekosystem Qt lub linkujemy do siebie wszystkie biblioteki BOOST.

Skorzystajmy z C++17!

Jak dotąd, zobaczyliśmy kilka opcji, które pozwalają na iterowanie po plikach. We wszystkich tych przykładach musieliśmy polegać na natywnym API systemowym lub kodzie zewnętrznym. Ale w końcu, od roku 2017 i wraz ze standardem C++17, możemy polegać na Bibliotece Standardowej!

Możemy skorzystać z std::filesystem, który został zaadaptowany bezpośrednio z BOOST filesystem.

Nasz kod ostatecznie będzie więc podobny do wersji BOOST, zobaczcie sami:

#include <filesystem>

namespace fs = std::filesystem;

const fs::path pathToShow{ argc >= 2 ? argv[1] : fs::current_path() };

for (const auto& entry : fs::directory_iterator(pathToShow)) {
    const auto filenameStr = entry.path().filename().string();
    if (entry.is_directory()) {
        std::cout << "dir:  " << filenameStr << '\n';
    }
    else if (entry.is_regular_file()) {
        std::cout << "file: " << filenameStr << '\n';
    }
    else
        std::cout << "??    " << filenameStr << '\n';
}

Uruchom ten kod na @Coliru

Jest jedna ważna uwaga (cytat z cppreference):

The iteration order is unspecified, except that each directory entry is visited only once. The special pathnames dot and dot-dot are skipped.

Wersja Rekursywna

Klasa directory_iterator działa tylko wewnątrz pojedynczego katalogu. Jest jednak inna klasa, która zezwala na iterowanie po całym drzewie katalogów: recursive_directory_iterator.

Możemy skorzystać z metody depth(), aby zmienić aktualny poziom rekursji. To rozwiązanie może być pomocne, kiedy będziemy chcieli utworzyć ładniejszy output, na przykład przez dodanie wcięć:

for(auto itEntry = fs::recursive_directory_iterator(pathToShow);
         itEntry != fs::recursive_directory_iterator(); 
         ++itEntry ) {
    const auto filenameStr = iterEntry->path().filename().string();
    std::cout << std::setw(iterEntry.depth()*3) << "";
    std::cout << "dir:  " << filenameStr << '\n';
}

Uruchom ten kod na @Coliru

Możemy również zaimplementować własny rekursywny algorytm, po czym wykorzystamy iterator pojedynczego katalogu:

Na przykład:

void DisplayDirectoryTree(const fs::path& pathToScan, int level = 0) {
    for (const auto& entry : fs::directory_iterator(pathToScan)) {
        const auto filenameStr = entry.path().filename().string();
        if (entry.is_directory()) {
            std::cout << std::setw(level * 3) << "" << filenameStr << '\n';
            DisplayDirectoryTree(entry, level + 1);
        }
        else if (entry.is_regular_file()) {
            std::cout << std::setw(level * 3) << "" << filenameStr
                << ", size " << entry.file_size() << " bytes\n";
        }
        else
            std::cout << std::setw(level * 3) << "" << " [?]" << filenameStr << '\n';
    }
}

Uruchom ten kod na @Coliru

Zauważmy, że obydwa te iteratory są iteratorami zewnętrznymi, zatem nie będą one spełniały wymagać algorytmów współbieżnch (forward iterators).

Rozszerzenia Plików

Jeśli chodzi o nasze główne zadanie - filtrowanie plików po rozszerzeniach - z nowym standardem staje się to proste!

C++17 daje nam dostęp do pełnej ścieżki do pliku, z której bardzo prosto możemy uzyskać rozszerzenie. Po prostu użyjemy: path::extension().

Na przykład:

std::filesystem::path("C:\\temp\\hello.txt").extension();

Wsparcie kompilatorów

Jeżeli korzystamy z GCC (przed wersją 9.0) lub Clang, to nie zapomnijmy dodać flagę -lstdc++fs, która podlinkuje nam kod biblioteki. Wraz z GCC w wersji 9.0 biblioteka filesystem jest włączona do libstdc++.

Funkcjonalność GCC Clang MSVC
Filesystem 8.0 7.0 VS 2017 15.7

Już od wersji GCC 5.3, Clang 3.9 oraz VS 2012 możemy cieszyć się wersją eksperymentalną - implementacją TS (include <experimental/filesystem>).

Podsumowanie

W tym wpisie zobaczyliście kilka opcji, które pozwalają na iterację wewnątrz katalogu w C++. Przed C++17 musieliśmy polegać na zewnętrznych bibliotekach lub wywołaniach API systemowego. Teraz możemy uzyskać ten sam efekt, korzystając z std::filesystem::directory_iterator.

Nie przedstawiłem finalnego kodu, który iteruje oraz odfiltrowywuje pliki o niedopasowanym rozszerzeniu. Czy potraficie to zaimplementować? Może chcielibyście podzielić się własnymi doświadczeniami związanymi z katalogami w C++?



Bartłomiej Filipek

Programista i pasjonat C++ z ponad 11-letnim doświadczeniem. Bloguje od wielu lat, głównie o naszym ulubionym języku programowania. Autor ksiązki C++17 In Detail.

Blog Bartka
Profil na LinkedIn


Podobne wpisy


Pssst! Używamy Cookies. Poprzez używanie naszego serwisu zgadzasz się na odczytywanie i zapisywanie Cookies w swojej przeglądarce.
Polityka Prywatności