Jak iterować po katalogach w C++?
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 wpisureaddir()
do znalezienia następnego wpisuclosedir()
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++?