Eine interessante kleine Aufgabenstellung: ein Skript hat versehentlich tausende von Dateien mit einer verkehrten Extension generiert. Alle Dateien heissen .jpgjpg
statt einfach nur .jpg
. Gestandene Shell-Benutzer reparieren sowas natürlich mit einem formschönen Einzeiler. Das ist einfach, und eigentlich lohnt es sich nicht da lange drüber nachzudenken. Es sei denn man kommt auf die Idee, das zeitlich zu optimieren…
Im konkreten Fall — es geht um etwa 6000 Dateien — dauert die Optimierung länger als das was an Laufzeitgewinn rauskommt. Wenn wir also schon keine Zeit gewinnen, dann wenigstens Erkenntnis.
Also, mein erster Ansatz ist es intuitiv, alle Dateien zu suchen, für jede Datei den neuen Namen festzulegen und dann umzubenennen. Machen wir eine Trockenübung bei laufender Stopuhr:
1 |
time ( find . -name \*.jpgjpg | while read i; do echo mv $i $(echo $i | sed -e "s/.jpgjpg/.jpg/"); done ) > /dev/null |
Das führt zu einer Laufzeit von erstaunlichen 21,443 Sekunden!
Halbwegs moderne Shells wie die Bash beherrschen Stringmanipulation, dazu muss man nicht zwingend auf sed zurückgreifen. Im TLDP steht wie es geht, so finde ich dort unter ‚Substring Removal‘ den richtigen Ansatz:
1 |
time ( find . -name \*.jpgjpg | while read i; do echo mv $i ${i%jpg}; done ) | /dev/null |
Das drückt die Zeit für die gleichen Testdaten schon auf 0,811 Sekunden. Soweit ich weiss haben wir uns damit zwar einen Bashismus eingehandelt, aber das ist heutzutage vielleicht schon egal.
Wenn wir aber schon Kompatibilität aufgeben können wir — schliesslich sind wir in einer hochmodernen Zsh — gleich in die Vollen gehen:
1 |
time ( for i in **/*.jpgjpg; do echo mv $i ${i%jpg}; done ) | /dev/null |
Ich habe noch nie rekursives Globbing (die zsh-lovers-Manpage hat mir verraten wie das geht) benutzt, aber nach nur 0,165 Sekunden war klar: es funktioniert.
Oh, da man hier auch denken könnte dass das Betriebssystem den Dateibaum cached: nein, das ist nicht so. Wenn ich die Einzeiler mehrfach absetze, auch in anderen Reihenfolgen, kommen immer wieder vergleichbare Ergebnisse raus.
Da die effizienteste Methode auch gleichzeitig die am schnellsten zu tippende ist denke ich, dass die — wenn ich auswendig gewusst hätte wie das geht — in jeder Hinsicht die erste Wahl gewesen wäre. Aber ich kenne mich: wenn ich das ein paar Tage nicht benutze muss ich erst wieder recherchieren. Und dann falle ich doch wieder auf das bewährte sed zurück…
würde mich einmal interessieren, wie der folgende Vorschlag mit den beiden Änderungen konkurriert:
time ( find . -name \*.jpgjpg -exec mv {} $(basename {} jpg) \; )
(ungetested)
basename halte ich in diesem Zusammenhang für logisch, weil es in erster Linie zur Dateinamensmanipulation geschaffen wurde, und auch das -exec vom find zu benutzen finde ich recht „generisch“. Und insgesamt doch bestimmt viel weniger kryptisch, oder ;) ?
Kann aber gut sein, dass wir hier pro Datei zwei weitere Subshells aufmachen, das ist dann natürlich nicht so kostenoptimiert.
Das kommt hier mit den gleichen Daten auf 8,518 Sekunden. Der Haken ist aber: es funktioniert nicht. Ich hatte im Hinterkopf dass man bei
find -exec
nur einmal die geschweiften Klammern setzen kann. Da scheine ich mich getaeuscht zu haben.Aber die Subshell wird nur einmal ausgefuehrt, noch bevor find ueberhaupt startet. Das
$(basename {} jpg)
wird also durch{}
ersezt. Folglich bleibt nurfind . -name \*.jpgjpg -exec echo mv {} {} \;
stehen, das benennt nichts um.Aber Danke fuer den Hinweis mit basename. Wenn ich das anstelle von sed in meine erste Variante einbaue laeuft die nur noch 12,103 Sekunden, macht also fast 50% Gewinn. Und ja, das ist auch intuitiver als sed.
sed wiederum ist generischer. wenn man das einmal auf dem Kasten hat…
dass ich im exec keine subshell aufmachen kann, klar, ist natürlich doof.
schon erstaunlich, dass uns selbst nach so vielen Jahren Linux so viele gute Lösungen für das Problem einfallen.
Da gilt wie in Perl: TIMTOWTDI.
https://de.wikipedia.org/wiki/Perl_%28Programmiersprache%29#Mehrere_Wege