Jak to RecursiveIteratorIteratordziała?
Podręcznik PHP nie ma nic dobrze udokumentowanego ani wyjaśnionego. Jaka jest różnica między IteratorIteratori RecursiveIteratorIterator?
Jak to RecursiveIteratorIteratordziała?
Podręcznik PHP nie ma nic dobrze udokumentowanego ani wyjaśnionego. Jaka jest różnica między IteratorIteratori RecursiveIteratorIterator?
RecursiveIteratorIteratordziała, czy już zrozumiałeś, jak IteratorIteratordziała? Mam na myśli to, że jest w zasadzie taki sam, tylko interfejs, który jest używany przez te dwa, jest inny. Czy jesteś bardziej zainteresowany kilkoma przykładami, czy chcesz zobaczyć różnice między implementacją kodu C?
IteratorIteratormapy Iteratori IteratorAggregatedo miejsca Iterator, gdzie REcusiveIteratorIteratorjest używany do przemierzania recusivly aRecursiveIterator
Odpowiedzi:
RecursiveIteratorIteratorjest konkretnym Iteratorwdrażaniu przechodzenie drzewa . Umożliwia programiście przechodzenie przez obiekt kontenera, który implementuje RecursiveIteratorinterfejs. Ogólne zasady, typy, semantyka i wzorce iteratorów można znaleźć w sekcji Iterator w Wikipedii .
W odróżnieniu od IteratorIteratorkonkretnej Iteratorimplementacji przechodzenia przez obiekt w porządku liniowym (i domyślnie akceptującej dowolny rodzaj Traversablew swoim konstruktorze), RecursiveIteratorIteratorumożliwia pętlę po wszystkich węzłach uporządkowanego drzewa obiektów, a jego konstruktor przyjmuje plik RecursiveIterator.
W skrócie: RecursiveIteratorIteratorpozwala na pętlę po drzewie,IteratorIterator pozwala na zapętlenie listy. Pokażę to wkrótce z kilkoma przykładami kodu poniżej.
Technicznie rzecz biorąc, działa to przez wyrwanie się z liniowości przez przejście przez wszystkie elementy potomne węzłów (jeśli takie istnieją). Jest to możliwe, ponieważ z definicji wszystkie dzieci węzła są ponownie RecursiveIterator. Górny poziom Iteratornastępnie wewnętrznie układa różne stosy RecursiveIteratorwedług ich głębokości i utrzymuje wskaźnik na bieżącym aktywnym podrzędnym Iteratorcelu przejścia.
Pozwala to na odwiedzenie wszystkich węzłów drzewa.
Podstawowe zasady są takie same jak w przypadku IteratorIterator: Interfejs określa typ iteracji, a podstawowa klasa iteratora jest implementacją tej semantyki. Porównaj z poniższymi przykładami, w przypadku pętli liniowej foreachzwykle nie myśl o szczegółach implementacji, chyba że musisz zdefiniować nowy Iterator(np. Gdy jakiś konkretny typ sam w sobie nie jestTraversable ).
W przypadku przemierzania rekurencyjnego - chyba że nie używasz predefiniowanej Traversaliteracji przechodzenia, która już ma rekurencyjną iterację przechodzenia - zwykle musisz utworzyć wystąpienie istniejącej RecursiveIteratorIteratoriteracji lub nawet napisać rekurencyjną iterację przechodzenia, która jest Traversabletwoją własną, aby mieć tego typu iterację przechodzenia foreach.
Wskazówka: prawdopodobnie nie zaimplementowałeś jednego ani drugiego własnego, więc może to być coś, co warto zrobić, aby poznać praktyczne różnice, jakie mają. Na końcu odpowiedzi znajdziesz sugestię zrób to sam.
Krótko mówiąc, różnice techniczne:
IteratorIteratorpobiera dowolne Traversabledla przechodzenia liniowego, RecursiveIteratorIteratorpotrzebuje bardziej szczegółowej RecursiveIteratorpętli po drzewie.IteratorIteratoreksponuje swoją główną Iteratorprzelotkę getInnerIerator(), RecursiveIteratorIteratorudostępnia bieżące aktywne sub- Iteratortylko za pomocą tej metody.IteratorIteratornie jest świadomy niczego takiego jak rodzic lub dzieci, RecursiveIteratorIteratorwie również, jak zdobyć i przemierzać dzieci.IteratorIteratornie potrzebuje stosu iteratorów, RecursiveIteratorIteratorma taki stos i zna aktywny pod-iterator.IteratorIteratorma swoją kolejność ze względu na liniowość i nie ma wyboru, RecursiveIteratorIteratorma wybór dla dalszego przejścia i musi zdecydować dla każdego węzła (decyduje tryb naRecursiveIteratorIterator ).RecursiveIteratorIteratorma więcej metod niż IteratorIterator.Podsumowując: RecursiveIteratorto konkretny typ iteracji (pętla po drzewie), który działa na własnych iteratorach, a mianowicie RecursiveIterator. Jest to ta sama podstawowa zasada jak w przypadku IteratorIerator, ale typ iteracji jest inny (kolejność liniowa).
Idealnie byłoby również stworzyć własny zestaw. Jedyną konieczną rzeczą jest to, że Twój iterator implementuje, Traversableco jest możliwe za pośrednictwem Iteratorlub IteratorAggregate. Następnie możesz go używać z foreach. Na przykład jakiś obiekt iteracji rekurencyjnej przechodzenia przez drzewo trójskładnikowe wraz z odpowiednim interfejsem iteracji dla obiektów kontenera.
Przyjrzyjmy się kilku przykładom z życia, które nie są tak abstrakcyjne. Pomiędzy interfejsami, konkretnymi iteratorami, obiektami kontenerów i semantyką iteracji może nie jest to zły pomysł.
Weźmy jako przykład listę katalogów. Weź pod uwagę, że masz na dysku następujące drzewo plików i katalogów:
Podczas gdy iterator z porządkiem liniowym po prostu przechodzi przez folder i pliki najwyższego poziomu (lista pojedynczego katalogu), iterator rekurencyjny przechodzi również przez podfoldery i wyświetla listę wszystkich folderów i plików (lista katalogów z listą jego podkatalogów):
Non-Recursive Recursive
============= =========
[tree] [tree]
├ dirA ├ dirA
└ fileA │ ├ dirB
│ │ └ fileD
│ ├ fileB
│ └ fileC
└ fileA
Możesz łatwo porównać to, z IteratorIteratorktórym nie ma rekursji przy przechodzeniu po drzewie katalogów. I RecursiveIteratorIteratorktóry może przejść do drzewa, jak pokazuje lista Rekurencyjna.
Na początek bardzo prosty przykład z DirectoryIteratorimplementacją, Traversablektóra pozwala foreachna iterację :
$path = 'tree';
$dir = new DirectoryIterator($path);
echo "[$path]\n";
foreach ($dir as $file) {
echo " ├ $file\n";
}
Przykładowy wynik dla powyższej struktury katalogów to:
[tree]
├ .
├ ..
├ dirA
├ fileA
Jak widać, nie jest to jeszcze używane IteratorIteratorani RecursiveIteratorIterator. Zamiast tego po prostu używa foreachtego, który działa na Traversableinterfejsie.
Ponieważ foreachdomyślnie zna tylko typ iteracji o nazwie porządek liniowy, możemy chcieć jawnie określić typ iteracji. Na pierwszy rzut oka może się to wydawać zbyt rozwlekłe, ale dla celów demonstracyjnych (i aby różnica była RecursiveIteratorIteratorwidoczna później), określmy liniowy typ iteracji, wyraźnie określając IteratorIteratortyp iteracji dla listingu katalogów:
$files = new IteratorIterator($dir);
echo "[$path]\n";
foreach ($files as $file) {
echo " ├ $file\n";
}
Ten przykład jest prawie identyczny z pierwszym, różnica polega na tym, że $filesjest to teraz IteratorIteratortyp iteracji dla Traversable $dir:
$files = new IteratorIterator($dir);
Jak zwykle czynność iteracji jest wykonywana przez foreach:
foreach ($files as $file) {
Wynik jest dokładnie taki sam. Więc co się zmieniło? Inny jest obiekt używany w foreach. W pierwszym przykładzie jest to DirectoryIteratoraw drugim przykładzie jest to IteratorIterator. Pokazuje to elastyczność, jaką mają iteratory: możesz je zastąpić innymi, kod wewnątrz foreachpo prostu nadal działa zgodnie z oczekiwaniami.
Zacznijmy od całej listy, łącznie z podkatalogami.
Ponieważ określiliśmy teraz typ iteracji, rozważmy zmianę go na inny typ iteracji.
Wiemy, że musimy teraz przejść całe drzewo, a nie tylko pierwszy poziom. Aby wykonać tę pracę z prostym foreach, potrzebujemy innego typu iteratora:RecursiveIteratorIterator . I to można tylko iterować po obiektach kontenerów, które mają RecursiveIteratorinterfejs .
Interfejs jest umową. Każda klasa implementująca ją może być używana razem z RecursiveIteratorIterator. Przykładem takiej klasy jestRecursiveDirectoryIterator , która jest czymś w rodzaju rekurencyjnej odmiany DirectoryIterator.
Zobaczmy pierwszy przykład kodu przed napisaniem jakiegokolwiek innego zdania ze słowem I:
$dir = new RecursiveDirectoryIterator($path);
echo "[$path]\n";
foreach ($dir as $file) {
echo " ├ $file\n";
}
Ten trzeci przykład jest prawie identyczny z pierwszym, ale tworzy inne wyniki:
[tree]
├ tree\.
├ tree\..
├ tree\dirA
├ tree\fileA
Okej, nie tak inaczej, nazwa pliku zawiera teraz nazwę ścieżki z przodu, ale reszta również wygląda podobnie.
Jak pokazuje przykład, nawet obiekt katalogu już implementuje RecursiveIteratorinterfejs, ale to nie wystarczy, aby foreachprzejść przez całe drzewo katalogów. Tutaj RecursiveIteratorIteratorzaczyna się akcja. Przykład 4 pokazuje, jak:
$files = new RecursiveIteratorIterator($dir);
echo "[$path]\n";
foreach ($files as $file) {
echo " ├ $file\n";
}
Użycie RecursiveIteratorIteratorzamiast tylko poprzedniego $dirobiektu spowoduje foreachprzechodzenie przez wszystkie pliki i katalogi w sposób rekurencyjny. Następnie wyświetla listę wszystkich plików, ponieważ typ iteracji obiektu został określony teraz:
[tree]
├ tree\.
├ tree\..
├ tree\dirA\.
├ tree\dirA\..
├ tree\dirA\dirB\.
├ tree\dirA\dirB\..
├ tree\dirA\dirB\fileD
├ tree\dirA\fileB
├ tree\dirA\fileC
├ tree\fileA
Powinno to już pokazać różnicę między przechodzeniem płaskim i przechodzeniem po drzewie. RecursiveIteratorIteratorJest w stanie przechodzić żadnego drzewiastą strukturę w postaci listy elementów. Ponieważ jest więcej informacji (takich jak poziom, na którym obecnie odbywa się iteracja), można uzyskać dostęp do obiektu iteratora podczas iteracji po nim i na przykład wcinać dane wyjściowe:
echo "[$path]\n";
foreach ($files as $file) {
$indent = str_repeat(' ', $files->getDepth());
echo $indent, " ├ $file\n";
}
Wynik przykładu 5 :
[tree]
├ tree\.
├ tree\..
├ tree\dirA\.
├ tree\dirA\..
├ tree\dirA\dirB\.
├ tree\dirA\dirB\..
├ tree\dirA\dirB\fileD
├ tree\dirA\fileB
├ tree\dirA\fileC
├ tree\fileA
Oczywiście to nie wygrywa konkursu piękności, ale pokazuje, że w przypadku iteratora rekurencyjnego dostępnych jest więcej informacji niż tylko liniowy porządek klucza i wartości . Nawet foreachmoże wyrazić ten rodzaj liniowości, dostęp do samego iteratora pozwala uzyskać więcej informacji.
Podobnie jak w przypadku metainformacji, istnieją również różne sposoby poruszania się po drzewie, a tym samym porządkowania wyników. To jest tryb programuRecursiveIteratorIterator i można go ustawić za pomocą konstruktora.
W następnym przykładzie RecursiveDirectoryIteratorpolecenie usunie kropki ( .i ..), ponieważ ich nie potrzebujemy. Ale także tryb rekursji zostanie zmieniony, aby najpierw zająć element nadrzędny (podkatalog) (SELF_FIRST ) przed dziećmi (pliki i podkatalogi w podkatalogu):
$dir = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS);
$files = new RecursiveIteratorIterator($dir, RecursiveIteratorIterator::SELF_FIRST);
echo "[$path]\n";
foreach ($files as $file) {
$indent = str_repeat(' ', $files->getDepth());
echo $indent, " ├ $file\n";
}
Dane wyjściowe pokazują teraz poprawnie wymienione pozycje podkatalogów, jeśli porównasz z poprzednim wyjściem, których tam nie było:
[tree]
├ tree\dirA
├ tree\dirA\dirB
├ tree\dirA\dirB\fileD
├ tree\dirA\fileB
├ tree\dirA\fileC
├ tree\fileA
Dlatego tryb rekurencyjny kontroluje, co i kiedy zwracane jest ramię lub liść w drzewie, dla przykładu katalogu:
LEAVES_ONLY (domyślnie): Tylko lista plików, bez katalogów.SELF_FIRST (powyżej): Lista katalogu, a następnie plików w nim.CHILD_FIRST (bez przykładu): Najpierw wyświetla listę plików w podkatalogu, a następnie w katalogu.Dane wyjściowe przykładu 5 z dwoma innymi trybami:
LEAVES_ONLY CHILD_FIRST
[tree] [tree]
├ tree\dirA\dirB\fileD ├ tree\dirA\dirB\fileD
├ tree\dirA\fileB ├ tree\dirA\dirB
├ tree\dirA\fileC ├ tree\dirA\fileB
├ tree\fileA ├ tree\dirA\fileC
├ tree\dirA
├ tree\fileA
Porównując to ze standardowym przechodzeniem, wszystkie te rzeczy nie są dostępne. Dlatego iteracja rekurencyjna jest nieco bardziej złożona, gdy trzeba ją owinąć, jednak jest łatwa w użyciu, ponieważ zachowuje się jak iterator, umieszcza się ją w a foreachi gotowe.
Myślę, że to wystarczające przykłady na jedną odpowiedź. Możesz znaleźć pełny kod źródłowy, a także przykład, aby wyświetlić ładnie wyglądające drzewa ascii w tym skrócie: https://gist.github.com/3599532
Zrób to sam: stwórz
RecursiveTreeIteratorpracę wiersz po wierszu.
Przykład 5 wykazał, że są dostępne metainformacje o stanie iteratora. Jednak zostało to celowo zademonstrowane w ramach foreachiteracji. W prawdziwym życiu to naturalnie należy doRecursiveIterator .
Lepszym przykładem jest RecursiveTreeIterator, zajmuje się wcięciami, przedrostkami i tak dalej. Zobacz następujący fragment kodu:
$dir = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS);
$lines = new RecursiveTreeIterator($dir);
$unicodeTreePrefix($lines);
echo "[$path]\n", implode("\n", iterator_to_array($lines));
RecursiveTreeIteratorPrzeznaczony jest do pracy przy linii linii, wyjście jest dość proste z jednym małym problemem:
[tree]
├ tree\dirA
│ ├ tree\dirA\dirB
│ │ └ tree\dirA\dirB\fileD
│ ├ tree\dirA\fileB
│ └ tree\dirA\fileC
└ tree\fileA
W połączeniu z a RecursiveDirectoryIteratorwyświetla całą nazwę ścieżki, a nie tylko nazwę pliku. Reszta wygląda dobrze. Dzieje się tak, ponieważ nazwy plików są generowane przez SplFileInfo. Zamiast tego powinny być wyświetlane jako nazwa basenowa. Żądane wyjście jest następujące:
/// Solved ///
[tree]
├ dirA
│ ├ dirB
│ │ └ fileD
│ ├ fileB
│ └ fileC
└ fileA
Utwórz klasę dekoratora, której można używać RecursiveTreeIteratorzamiast RecursiveDirectoryIterator. Powinien zawierać nazwę podstawową prądu SplFileInfozamiast ścieżki. Ostateczny fragment kodu mógłby wtedy wyglądać następująco:
$lines = new RecursiveTreeIterator(
new DiyRecursiveDecorator($dir)
);
$unicodeTreePrefix($lines);
echo "[$path]\n", implode("\n", iterator_to_array($lines));
Te fragmenty, w tym, $unicodeTreePrefixsą częścią istoty w Dodatku: Zrób to sam: Stwórz RecursiveTreeIteratorpracę wiersz po wierszu. .
RecursiveIteratorIteratorponieważ jest to wspólne z innymi typami, ale podałem kilka informacji technicznych, jak to faktycznie działa. Myślę, że przykłady dobrze pokazują różnice: rodzaj iteracji jest główną różnicą między nimi. Nie mam pojęcia, jeśli kupisz typ iteracji, który ubijesz trochę inaczej, ale IMHO nie jest łatwe z semantyką typów iteracji.
Jaka jest różnica między
IteratorIteratoriRecursiveIteratorIterator?
Aby zrozumieć różnicę między tymi dwoma iteratorami, należy najpierw trochę zrozumieć stosowane konwencje nazewnictwa i co rozumiemy przez iteratory „rekurencyjne”.
PHP ma nie „rekurencyjne” iteratory, takie jak ArrayIteratori FilesystemIterator. Istnieją również „rekurencyjne” iteratory, takie jak RecursiveArrayIteratoriRecursiveDirectoryIterator . Ci drudzy mają metody umożliwiające ich wnikanie, ci pierwsi nie.
Gdy instancje tych iteratorów są zapętlone samodzielnie, nawet te rekurencyjne, wartości pochodzą tylko z „najwyższego” poziomu, nawet jeśli są zapętlone po zagnieżdżonej tablicy lub katalogu z podkatalogami.
Iteratory rekurencyjne implementują zachowanie rekurencyjne (via hasChildren(), getChildren()), ale tego nie robią wykorzystują .
Lepiej byłoby myśleć o iteratorach rekurencyjnych jako o iteratorach „rekurencyjnych”, mają one rozszerzenie zdolność do iteracji rekurencyjnej, ale zwykłe iterowanie po instancji jednej z tych klas tego nie zrobi. Aby wykorzystać zachowanie rekurencyjne, czytaj dalej.
Tutaj właśnie RecursiveIteratorIteratorpojawia się gra. Posiada wiedzę na temat wywoływania „rekurencyjnych” iteratorów w taki sposób, aby drążyć strukturę w normalnej, płaskiej pętli. Wprowadza rekurencyjne zachowanie do działania. Zasadniczo wykonuje pracę polegającą na przechodzeniu przez każdą z wartości w iteratorze, sprawdzaniu, czy są „dzieci”, do których można się powrócić, czy nie, oraz wchodzeniu i wychodzeniu z tych zbiorów dzieci. Wbijasz wystąpienie RecursiveIteratorIteratorw foreach, a on nurkuje w strukturze, więc nie musisz tego robić.
Gdyby RecursiveIteratorIteratornie zostało użyte, musiałbyś napisać własne pętle rekurencyjne, aby wykorzystać zachowanie rekurencyjne, sprawdzając iterator „rekurencyjny” hasChildren()i używając getChildren().
Więc to jest krótkie omówienie RecursiveIteratorIterator, czym się różni od IteratorIterator? Cóż, w zasadzie zadajesz to samo pytanie, co Jaka jest różnica między kotkiem a drzewem? To, że oba pojawiają się w tej samej encyklopedii (lub podręczniku dla iteratorów), nie oznacza, że powinieneś się pomylić między nimi.
Zadaniem tego IteratorIteratorjest pobranie dowolnego Traversableobiektu i owinięcie go tak, aby spełniał wymagania Iteratorinterfejsu. Służy do tego możliwość zastosowania zachowania specyficznego dla iteratora na obiekcie niebędącym iteratorem.
Aby dać praktyczny przykład, DatePeriodklasa jest, Traversableale nie jest Iterator. W związku z tym możemy zapętlić jego wartości za pomocą, foreach()ale nie możemy zrobić innych rzeczy, które normalnie wykonalibyśmy za pomocą iteratora, takich jak filtrowanie.
ZADANIE : Powtarzaj w poniedziałki, środy i piątki przez następne cztery tygodnie.
Tak, jest to trywialne foreachomijanie DatePeriodi używanie if()w pętli; ale nie o to chodzi w tym przykładzie!
$period = new DatePeriod(new DateTime, new DateInterval('P1D'), 28);
$dates = new CallbackFilterIterator($period, function ($date) {
return in_array($date->format('l'), array('Monday', 'Wednesday', 'Friday'));
});
foreach ($dates as $date) { … }
Powyższy fragment kodu nie zadziała, ponieważ CallbackFilterIteratoroczekuje wystąpienia klasy implementującej Iteratorinterfejs, a DatePeriodnie. Ponieważ jednak tak jest Traversable, możemy łatwo spełnić to wymaganie, używając IteratorIterator.
$period = new IteratorIterator(new DatePeriod(…));
Jak widać, nie ma to nic wspólnego z iteracją po klasach iteratorów ani rekursją, i na tym polega różnica między IteratorIteratori RecursiveIteratorIterator.
RecursiveIteraratorIteratorsłuży do iteracji po RecursiveIterator(„rekurencyjnym” iteratorze), wykorzystując dostępne zachowanie rekurencyjne.
IteratorIteratorsłuży do stosowania Iteratorzachowania do Traversableobiektów , które nie są iteratorami .
IteratorIteratortylko standardowy typ przechodzenia w kolejności liniowej dla Traversableobiektów? Te, które mogłyby być użyte bez tego, tak foreachjak jest? A nawet dalej, czy nie jest RecursiveIterator zawsze a, Traversablea zatem nie tylko, IteratorIteratorale także RecursiveIteratorIteratorzawsze „do stosowania Iteratorzachowania do obiektów nie-iteracyjnych, przemieszczalnych” ? (Powiedziałbym teraz, że foreachstosuje typ iteracji za pośrednictwem obiektu iteratora na obiektach kontenera, które implementują interfejs typu iteratora, więc są to obiekty-kontenera-iteratora, zawsze Traversable)
IteratorIteratorto klasa, która polega na zawijaniu Traversableobiektów w plik Iterator. Nic więcej . Wydaje się, że używasz tego terminu bardziej ogólnie.
Recursivein RecursiveIteratorsugeruje zachowanie, podczas gdy bardziej odpowiednia byłaby nazwa opisująca zdolności, np RecursibleIterator.
W przypadku użycia z iterator_to_array(), RecursiveIteratorIteratorbędzie rekurencyjnie przeszukiwać tablicę, aby znaleźć wszystkie wartości. Oznacza to, że spłaszczy oryginalną tablicę.
IteratorIterator zachowa pierwotną strukturę hierarchiczną.
Ten przykład jasno pokaże różnicę:
$array = array(
'ford',
'model' => 'F150',
'color' => 'blue',
'options' => array('radio' => 'satellite')
);
$recursiveIterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($array));
var_dump(iterator_to_array($recursiveIterator, true));
$iterator = new IteratorIterator(new ArrayIterator($array));
var_dump(iterator_to_array($iterator,true));
new IteratorIterator(new ArrayIterator($array))to znaczy new ArrayIterator($array), że strona zewnętrzna IteratorIteratornic nie robi. Co więcej, spłaszczanie wyniku nie ma z tym nic wspólnego iterator_to_array- po prostu przekształca iterator w tablicę. Spłaszczenie jest właściwością sposobu, w jaki RecursiveArrayIteratorprzechodzi jego wewnętrzny iterator.
RecursiveDirectoryIterator wyświetla całą nazwę ścieżki, a nie tylko nazwę pliku. Reszta wygląda dobrze. Dzieje się tak, ponieważ nazwy plików są generowane przez SplFileInfo. Zamiast tego powinny być wyświetlane jako nazwa basenowa. Żądane wyjście jest następujące:
$path =__DIR__;
$dir = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS);
$files = new RecursiveIteratorIterator($dir,RecursiveIteratorIterator::SELF_FIRST);
while ($files->valid()) {
$file = $files->current();
$filename = $file->getFilename();
$deep = $files->getDepth();
$indent = str_repeat('│ ', $deep);
$files->next();
$valid = $files->valid();
if ($valid and ($files->getDepth() - 1 == $deep or $files->getDepth() == $deep)) {
echo $indent, "├ $filename\n";
} else {
echo $indent, "└ $filename\n";
}
}
wynik:
tree
├ dirA
│ ├ dirB
│ │ └ fileD
│ ├ fileB
│ └ fileC
└ fileA