Nehmen wir an, Sie möchten alle Exe-Dateien vom Dateipfad "c:\Programme\Autocad 2008" in unbegrenzter Verzeichnistiefe ermitteln. Da man die Anzahl der Unterordner nicht kennt, geschweige denn die Unterordner der Unterordner der Unterordner ....usw. wird es doch ein wenig komplizierter als es anfangs den Anschein hat. Man muss also einen Weg finden in die einzelnen Unterordner "einzutauchen" ohne dabei etwas zu "vergessen".
In VisualLISP gibt es für diverse Dateizugriffe einige Funktionen und hierzu schauen wir auf die beiden Funktionen...vl-file-directory-p & vl-directory-files.
vl-file-directory-p ist wie das p am Ende schon vermuten lässt, eine Funktion um zu prüfen, ob das übergebene Argument (muss ein String sein!) ein Ordner ist. Die Rückgabe ist entweder ein T oder ein nil.
vl-file-directory-p "") --> nil
(vl-file-directory-p "test") --> nil
(vl-file-directory-p "c:") --> T
(vl-file-directory-p "c:/") --> T
(vl-file-directory-p "c:\windows") --> T
Die zweite Funktion die wir uns näher betrachten wollen ist vl-directory-files.
Auszug aus der Autocad-Hilfe:
--cut in--
Lists all files in a given directory
(vl-directory-files [directory pattern directories])
Arguments
directory --> A string naming the directory to collect files for; if nil or absent, vl-directory-files uses the current directory.
pattern --> A string containing a DOS pattern for the file name; if nil or absent, vl-directory-files assumes "*.*"
directories --> An integer that indicates whether the returned list should include directory names. Specify one of the following:
-1 List directories only.
0 List files and directories (the default).
1 List files only.
--cut out--
Also, der Funktion wird ein Dateipfad als String übergeben (directory) und alle Dateien und/oder Ordner (directories) werden aufgelistet, sofern die zu suchende Zeichenfolge (pattern) übereinstimmt. Wenn keine bestimmte Zeichenfolge beachtet werden soll, einfach als Argument ein nil übergeben.
Mit diesem Wissen, können wir nun eine Funktion erstellen, die man sehr gut rekursiv lösen kann. Natürlich muss man immer bei rekursiven Funktionen darauf achten, dass sie nicht in einen infiniten Regress kommen (auch unendlicher Regress oder Endlosrekursion genannt; regressus in/ad infinitum). Das heißt im Klartext, dass die Funktion sich selbst prüfen muss, damit sie sich sauber beenden kann. Aus Performancegründen will ich innerhalb der Funktion auf das prüfen mittels vl-file-directory-p verzichten. Dieses sollte, wenn schon, im Hauptprogramm erfolgen, wobei wir uns hier überlegen, in welcher Form wir das Argument unserer Funktion übergeben wollen.
Mein Vorschlag: Gleich als Liste, da man so mehrere Ordner rekursiv durchlaufen lassen kann. Um nun zu überprüfen, ob alle Eintragungen der Liste auch wirklich Ordner auf einer Festplatte sind (hier wird vorausgesetzt, dass alle Atome der Liste Strings sind), können wir mit Hilfe der Funktion vl-every eine kleine Prüfroutine schreiben.
vl-every benötigt dabei zwei Argumente. das erste Argument ist eine Prädikats-Funktion, also eine Funktion die entweder ein True oder Nil zurückgibt und das zweite Argument ist die zu prüfende Liste.
Und so könnte die Prüfung aussehen:
(vl-every 'vl-file-directory-p '("c:/windows" "c:/temp")) => T
(vl-every 'vl-file-directory-p '("c:/windows" "Test")) => nil
Beispiel einer Baumdarstellung eines Dateiordners
Hier erstmal mein Code. Diese Funktion wird übrigens als kaskadenförmige Rekursion oder auch als Baumrekursion bezeichnet.
(defun :f-getfolderlist (#folderlist / 1st-folder)
(cond ((null #folderlist) nil)
((append (list (setq 1st-folder (car #folderlist)))
(:f-getfolderlist
(mapcar '(lambda (s) (strcat 1st-folder "\" s))
(cddr (vl-directory-files 1st-folder nil -1))
)
)
(:f-getfolderlist (cdr #folderlist))
)
)
)
)
Was passiert hier?
(cond ((null #folderlist) nil)
Wie oben schon erwähnt, muss eine rekursive Funktion dafür Sorge tragen, dass sie nicht in einem infiniten Regress kommt, weshalb hier die ersten Zeileninnerhalb cond diese Prüfung ist.
Das Argument darf nicht "leer" (nil) sein!
vl-file-directory-p als zusätzliche Prüfung wäre meiner Meinung nach ineffizient, da durch die Rekursion sowieso korrekte Ordner hinzukommen und nur die Grundliste einmal wie oben gezeigt geprüft werden sollte. Bei einer Übergabe eines(!) Ordners z.B. ist somit nur einmal ein vl-file-directory-p erforderlich, was aber durch die Rekursion hinzugefügte Ordner jedes mal(!) erneut geprüft wird, obwohl es sich wissentlich um "echte" Ordner bereits handelt (vl-directory-files fügt ja nur Ordner hinzu)!
((append (list (setq 1st-folder (car #folderlist)))
(:f-getfolderlist
(mapcar '(lambda (s) (strcat 1st-folder "\" s))
(cddr (vl-directory-files 1st-folder nil -1))
)
)
(:f-getfolderlist (cdr #folderlist))
)
)
Das Herzstück dieser Funktion. Wir erzeugen (beginnen, öffnen...) eine neue Liste mit append und setzen den ersten Eintrag des Arguments (#folderlist) an diese Liste.
((append (list (setq 1st-folder (car #folderlist)))
Als nächstes durchsuchen wir diesen Ordner rekursiv(!) nach Unterordner und tauchen dabei so tief vor, bis keine Unterordner mehr gefunden werden. Diese Rekursion ist wichtig, da sonst nur die Unterordner der obersten Stufe gefunden werden. Zusätzlich ist erforderlich, dass man den jeweiligen übergeordneten Ordner an die gefundenen Unterordner anhängt! --> (strcat 1st-folder "\" s)
(:f-getfolderlist
(mapcar '(lambda (s) (strcat 1st-folder "\" s))
(cddr (vl-directory-files 1st-folder nil -1))
)
)
Sobald der erste Rekursionsaufruf ein nil zurückgibt wird nun als letztes der eventuelle nächste Eintrag aus der Liste verarbeitet und der Prozess wiederholt sich. Erst wenn dieser letzte Funktionsaufruf auch ein nil zurückgibt, wird die neue Liste mit allen gefundenen Verzeichnisse aus dem Stack zurückgegeben, also append(!!) wird beendet. Des Weiteren wird jedes Mal nur der 3. Eintrag (cddr..) von der R&¨ckgabe der Funktion vl-directory-files verwendet, da die ersten beiden Einträge der Null-Device ist ([.] [..]).
Eh voilà, fertig ist die rekursive Verzeichnisermittlung.
Tipp: Setzen Sie vor der Klammer -->(<--:f-getfolderlist (cdr #folderlist)) einen Haltepunkt und schauen Sie sich jedes Mal die Variable #folderlist dabei an. Hier können Sie nachlesen, wie man das mit dem VisualLISP (Vlide) - Editor macht.
Vorteile dieser Rekursion:
- Elegant und geringer Codeaufwand
- Die Rückgabe der Liste erfolgt sortiert
Nachteil dieser Rekursion
- Je tiefer die Rekursion erfolgt, desto mehr Speicher (da ja alles erstmal im Stack liegt) ist erforderlich und hier im speziellen kann die Kaskadenförmige Ausdehnung (Baumstruktur) manchmal einen exponentiellen Berechnungsaufwand nach sich ziehen (siehe auch Wikipedia --> Rekursion ).
Nun schnell noch eine Bindung der R&¨ckgabe an ein Symbol
(setq folderlist (:f-getfolderlist '("c:/Programme/AutoCAD 2008")))
Da wir jetzt eine Liste haben die alle Unterordner enthält, brauchen wir zum Schluss nur noch eine Funktion, die aus den einzelnen Pfaden die zu suchenden Dateien ermittelt...
...und hier ist sie:
(defun :f-getfilelist (#folderlist #fn)
(apply 'append
(mapcar '(lambda (a / res)
(if (setq res (vl-directory-files a #fn 1))
(mapcar '(lambda (b) (strcat a "/" b)) res)
)
)
#folderlist
)
)
)
Wie Sie bestimmt schon am Code gesehen haben, hänge ich natürlich gleich den Pfad an die gefundenen Dateien an, denn ohne den Pfad ist die Rückgabe i.d.R. nutzlos. Mit apply 'append erreiche ich gleich zwei Eigenschaften. Zum einen werden die Listen, die durch mapcar entstehen, zu einer Liste zusammengefügt und zum anderen eventuelle nils (falls im Pfad keine Datei gefunden wurde, die dem Suchkriterium entsprechen) ausgelassen. Weiterhin ist es sinnvoll, bei der Funktion vl-directory-files das Argument directories zu setzen (1 = List files only), da sonst beim pattern mit "*.*" bzw. "*" oder nil der Null-Device ([.] [..]) mit aufgenommen wird.
Aufruf: (:f-getfilelist folderlist "*.exe") =>
("c:/Programme/AutoCAD 2008/acad.exe"
"c:/Programme/AutoCAD 2008/acgge.exe"
"c:/Programme/AutoCAD 2008/AcSignApply.exe"
"c:/Programme/AutoCAD 2008/addplwiz.exe"
"c:/Programme/AutoCAD 2008/AdMigrator.exe"
"c:/Programme/AutoCAD 2008/AdRefMan.exe"
"c:/Programme/AutoCAD 2008/AdSubAware.exe"
"c:/Programme/AutoCAD 2008/DwgCheckStandards.exe"
"c:/Programme/AutoCAD 2008/HPSETUP.exe"
"c:/Programme/AutoCAD 2008/pc3exe.exe"
"c:/Programme/AutoCAD 2008/senddmp.exe"
"c:/Programme/AutoCAD 2008/sfxfe32.exe"
"c:/Programme/AutoCAD 2008/slidelib.exe"
"c:/Programme/AutoCAD 2008/styexe.exe"
"c:/Programme/AutoCAD 2008/styshwiz.exe"
"c:/Programme/AutoCAD 2008/Express/alias.exe"
"c:/Programme/AutoCAD 2008/Express/dumpshx.exe"
"c:/Programme/AutoCAD 2008/Express/lspsurf.exe"
"c:/Programme/AutoCAD 2008/Setup/AcDelTree.exe"
"c:/Programme/AutoCAD 2008/Setup/Setup.exe"
)
Mit freundlicher Genehmigung von Rolf Wischnewski. Originalbeitrag im Juli 2009, CADmaro.de