[EasyLinux-Ubuntu] Re: Skript für doppelte Dateien

Franz Deuzer franz.deuzer at aon.at
Mon Okt 2 19:59:12 CEST 2006


Dennis Neumeier schrieb am 28. September 2006:

> Hallo Liste!
> 
> Ich habe auf meinen Kubuntu Dapper-PC ein Verzeichnis, in dem ich alle Songs 
> meines IPods als Sicherung daruf habe. Die Ablage-Struktur sieht 
> folgendermaßen aus:
> 
> .../ipod-backup/bandname/albumname/liedname.mp3
> 
> Nun habe ich bemerkt, daß ich aus irgendwelchen Gründen einige Lieder 
einiger 
> Alben doppelt vorhanden habe. Ich würde nun gerne ein Skript haben, daß den 
> ganzen ipod-backup-Pfad rekursiv nach unten durchsucht und mir dann 
entwender 
> direkt in einer Shell oder - besser noch - in einer Datei alle doppelt 
> vorkommenden Lieder (top wäre hier der ganze Pfad!) ausgibt.
> 
> Kann man sowas überhaupt mit einem Shell-Skript machen? Wenn ja, wie?
> 
> Gruß,
> Dennis

Hallo Dennis!

Ich möchte im Folgenden ein "Skript" vorstellen, mit dem Du Deine Aufgabe 
bewältigen kannst. Es handelt sich dabei aber weder um ein Shell-, noch Awk-, 
noch Perl- oder sonstiges Skript. Ich muß dazu noch sagen, "sowas mit einem 
Shell-Skript (zu) machen", halte ich - eben wegen der von Dir angesprochenen 
Rekursion und der doch eher bescheidenen Mittel einer Shell - für keine so 
gute Idee. D.h.: das Problem liegt in der Verarbeitung; man müßte sich jeden 
Treffer (d.i. Datei) merken - sagen wir mal, in einer Liste. Dann müßte man 
für jede neu hinzukommende Datei die Liste durchmustern, ob sie bereits 
vorhanden ist oder nicht. Ein ziemlich aufwendiges Unterfangen, aber mehr gibt 
eine Shell nicht her. (Ich muß aber eingestehen, daß Shellprogrammierung bei 
mir schon länger zurückliegt.)

Aber nun zur Lösung, geschrieben in Common Lisp:

;;;
;;;   F. Deuzer; 30.5.2001
;;;

(defparameter *ht* (make-hash-table :test #'string=))

(defun find-files (directory comparison &key recursive)
  "Startet im gegebenen DIRECTORY und vergleicht alle dort befindlichen
 Dateien mittels COMPARISON, das eine Funktion mit einem Argument sein muss,
 nämlich um eine(n) Datei(pfad) aufzunehmen. Ist RECURSIVE t, dann werden
 sämtliche Unterverzeichnisse auch in die Suche miteinbezogen.
 Befüllt wird eine Hashtabelle mit den Treffern."
  (labels ((rec (directory comparison recursive-p)
                 (dolist (path (directory directory))
                   (when (and (file-directory-p path) recursive-p)
                     (rec (namestring path) comparison recursive-p))
                   (when (and (not (file-directory-p path))
                                      (funcall comparison path))
                     (multiple-value-bind (val found)
                         (gethash (file-namestring path) *ht*)
                       (if found
                           (push path (gethash (file-namestring path) *ht*))
                         (setf (gethash (file-namestring path) *ht*)
                                 (list path))))))))
    ;; um das Ausgangsdirectory zu merken
    (let ((current-dir (cd ".")))
      (cd directory)
      (rec directory comparison recursive)
      (cd current-dir))))

;; Aufruf:
(find-files "<pfad-zu>/ipod-backup/"
               #'(lambda (path)
                    (string= (pathname-type path) "mp3"))
               :recursive t)

Nun zu den einzelnen Teilen:
-) Mit "defparameter" wird, wie der Name schon andeutet, eine Variable
   angelegt, die eine Hashtabelle referenziert (im Jargon: eine Bindung wird
   eingerichtet).
-) Nun zur Funktion "find-files": dieser wird ein Startverzeichnis übergeben,
   eine Funktion, die die vorgefundenen Dateien nach einem vom Benutzer zu
   bestimmenden Kriterium filtern soll und weiters, ob in die Tiefe des
   Verzeichnisbaumes hinabgestiegen werden soll oder nicht. Soll z.B. gar
   nicht gefiltert werden, gibt man statt des lambda-Ausdrucks die Funktion
   "identity" an. (Das ließe sich auch als optionaler Parameter einrichten.)
   Der String ab der zweiten Zeile ist ein sogenannter Dokumentationsstring,
   der beschreibt, was die Funktion so tun soll; dieser ist optional.
   Dann geht es los: "labels" definiert lokale Funktionen, in unserem Fall die
   Funktion "rec", die die Hauptaufgabe leistet. Dazu später mehr.
   Schlußendlich kommt der Hauptteil des Programms, eingeleitet durch eine
   "let"-Form. Der Teil ist erstaunlich kurz:
    x) man merkt sich das Verzeichnis, in dem man gerade sitzt - um dann am
        Ende wieder dorthin zu wechseln (auch nicht unbedingt notwendig)
    x) dann wechselt man in das beim Funktionsaufruf angegebene Vezeichnis
        "directory" und
    x) ruft die lokal definierte Funktion "rec" auf.
    x) ganz zum Schluß wechselt man wieder zurück in das Ausgangsverzeichnis.

Nun sollte die Hashtabelle alle gefundenen (mp3-)Dateien enthalten. Sie müssen
nur noch ausgelesen werden. Das erledigt die Funktion "maphash".
(maphash #'(lambda (key val)
                      (when (cdr val)   ;; (*)
                        (format t "Mehrmaliges vorkommen von:~%")
                        (format t "~{   ~s~^~%~}" val)))
                 *ht*)

(*) Man benötigt wirklich nicht die gesamte Länge einer Liste, bloß um zu 
erfahren, ob sie mehr als ein Element enthält!

Maphash benötigt wieder ein funktionales Objekt ("lambda,,,") und eine 
Hashtabelle, die Eintrag für Eintrag abgearbeitet wird. Da nur die Einträge 
interessieren, die mehrere Treffer repräsentieren, so werden auch nur diese 
ausgegeben. (Das läßt sich sehr einfach - mit Hilfe des Macros
"with-open-file" - in eine Datei ausgeben. Ebenfalls wie gewünscht.)

Nun wie versprochen zum Hauptteil - "rec": dies ist eine rekursive Funktion, 
deren Aufbau die Datenstruktur widerspiegelt; gemeint ein Dateiverzeichnis, 
das als Baumstruktur vorliegt.
a) Man holt sich aus einem Dateiverzeichnis sämtliche Einträge und geht sie
    der Reihe nach durch
b) Handelt es sich bei einem Eintrag um ein Verzeichnis und wurde dem Key-
    word Parameter "recursiv" "t" (true) übergeben, so taucht man in dieses
    Unterverzeichnis ab.
c) Handelt es sich um eine normale Datei, so wird geprüft, ob sie dem ange-
    gebenen Kriterium entspricht ("funcall comparison ..."). Ist das der Fall,
    so wird der Dateiverzeichniseintrag (der gesamte Pfad) in der Hashtabelle
    in einer Liste gespeichert. Als Schlüssel (ein String) dient der Dateiname
    (ohne Pfad!).
d) Das wird solange fortgesetzt, bis sämtliche Einträge im Ausgangsverzeichnis
    abgearbeitet sind.
e) Das war auch schon alles. Am Ende hat man dann eine gefüllte Hashtabelle,
    die es noch auszugeben gilt.

Anmerkungen:

-) Bezüglich Erklärungen zu "format" verweise ich auf die "Hyperspec":
   http://www.lisp.org/HyperSpec/; und bzgl. Lisp auf die "Association of Lisp
   Users", kurz ALU: http://www.lisp.org/alu/home. Obiges Programm sollte ohne
   Probleme unter Lispworks laufen.

-) Die beiden "when" Formen im Körper der "rec" Funktion mögen nicht allzu
   glücklich erscheinen. Sie sind aber nicht falsch und drücken m.E. besser
   aus,  was man will. Es ist ja ein leichtes sie durch eine "cond"-Form zu
   ersetzen.
   (Auch eine verschachtelte "if"-Form täte es: da hätte aber die zweite
   kein "else", ist also semantisch wieder ein "when". Also mir persönlich
   gefällt das nicht, "cond" wollte ich jetzt nicht erklären und "when" liest
   sich irgendwie leichter. Ist aber bloß mein persönlicher Geschmack.)

-) Aufpassen muß man nur bei der Funktion, die sämtliche Einträge eines
   Dateiverzeichnisses liefert. Die beiden speziellen Einträge "." und ".."
   dürfen nicht mitspielen, man landet sonst im Hamsterrad: Endlosrekursion.

-) Ich habe das etwas ausführlicher beschrieben, weil zur Zeit im LinuxUser
   ein Artikel über Python läuft und ein klitzekleines Programm wie das
   vorliegende  vielleicht dort (noch) aufgegriffen werden könnte. Oder aber -
   wie ich hoffe - Dir Anreiz gibt, es selbst zu probieren?! Auch ein Umsetzen
   nach Ruby, Perl, Java, etc. (oder was die Sprache Deiner Wahl halt ist)
   sollte nicht länger als ein, zwei Stündchen in Anspruch nehmen. Die
   "Spezifikation" habe ich ja bereits vorgelegt.

-) obiges Programm ist noch viel leistungsfähiger: man kann z.B.: mit der
   comparison-Funktion auch nach Dateien filtern, die jünger als ein gegebenes
   Datum sind: #'(lambda (file)
                            (<= (encode-universal-time 0 0 0 20 9 2006)
                                     (file-write-date file)))

Da würde ich aber die Hashtabelle weglassen und nur mit Listen arbeiten. So 
war und ist diese Funktion auch heute noch - ich habe sie bloß für die 
vorliegende Aufgabe etwas modifiziert. Meine Intention damals (ist ja schon 
einige Jährchen her) war, alle möglichen Shell-Tools in einige wenige Lisp 
Funktionen zu packen, was ja speziell unter MS-Windows enorm hilfreich ist.

[   Man kann natürlich auch alles zusammen haben:
    #'(lambda (file)
         (and (<= (encode-universal-time 0 0 0 20 9 2006)
                         (file-write-date file))
                 (string= (pathname-type path) "mp3")))

Jetzt muß ich mich aber einbremsen :-)   ]


Mit freundlichen Grüßen
Franz

P.S. Hoffe, die Formatierung hat unter KMail nicht allzu sehr gelitten.