Sonderzeichen in Dateinamen bereiten vielen Shell-Tools Probleme. Mit Xargs umschiffen Sie die Hürden elegant.
Komplexe Befehle auf der Kommandozeile erfordern es häufig, die Ausgabe eines Befehls als Eingabe für einen anderen zu verwenden. Ein ganz typisches Beispiel ist das Kommando find: Es listet rekursiv alle den Parametern entsprechenden Dateien auf. So findet der Befehl aus Listing 1 alle Ogg-Vorbis-Dateien im Ordner Podcasts.
Listing 1
$ find Podcasts -name '*.ogg' Podcasts/dh-20130204-ausgabe-045.ogg Podcasts/dh-20130107-ausgabe-044.ogg
Möchten Sie nur wissen, ob es sich dabei wirklich ausschließlich um Ogg-Vorbis-Dateien handelt, so prüfen Sie dies mit dem Befehl file nach. Er erhält die Namen der Dateien als Parameter – im Beispiel zwar nur zwei, doch das Tippen der entsprechenden Befehle macht dennoch Arbeit. Dabei gibt es doch die verkehrten Anführungszeichen, englisch “Backticks”. Die Ausgabe des Befehls zwischen den Backticks ergibt dabei den Parameter für den äußeren Befehl (Listing 2).
Listing 2
$ file `find Podcasts -name '*.ogg'` Podcasts/dh-20130204-ausgabe-045.ogg: Ogg data, Vorbis audio, stereo, 44100 Hz, ~91840 bps Podcasts/dh-20130107-ausgabe-044.ogg: Ogg data, Vorbis audio, stereo, 44100 Hz, ~91840 bps
Stolperfallen
So weit, so gut. Die Funktion trennt die Ausgabe des inneren Befehls an sämtlichen Leerzeichen, inklusive Zeilenumbruch und Tabulator. Das schreit nach der Frage, was passiert, wenn find eine Datei herauspickt, die Leerzeichen im Dateinamen hat. Die Antwort darauf zeigt Listing 3.
Listing 3
$ find Musik -name '*.ogg' Musik/Roxette - June Afternoon.ogg Musik/Roxette - I Don't Want to Get Hurt.ogg $ file `find Musik -name '*.ogg'` Musik/Roxette: ERROR: cannot open `Musik/Roxette' (No such file or directory) ^C
Mehrere Dinge spielen sich dabei ab: Die Shell hat die Dateinamen an den Leerzeichen aufgetrennt und jeden Teil für sich als einzelnen Parameter verwendet. Das Minuszeichen im Dateinamen übergibt sie ebenfalls als einzelnen Parameter. Der Befehl file interpretiert dies derart, dass er von da ab auf der Standardeingabe auf Daten wartet, um diese zu analysieren. Im vorliegenden Fall bleibt nichts anderes übrig, als den “hängenden” Befehl mit [Strg]+[C] abzubrechen.
Eine weitere Stolperfalle taucht auf, wenn Sie Backticks verschachteln möchten. In der Praxis passiert dies, wenn Sie zuerst alle Verzeichnisse auflisten, die Sie durchsuchen möchten. Listing 4 zeigt ein Kommando, das alle Verzeichnisse durchforstet, die Podcasts heißen.
Listing 4
$ file `find \`find . -name Podcasts -type d\` -name '*.ogg'` ./lang/Podcasts/dh-20130204-ausgabe-045.ogg: Ogg data, Vorbis audio, stereo, 44100 Hz, ~91840 bps ./lang/Podcasts/dh-20130107-ausgabe-044.ogg: Ogg data, Vorbis audio, stereo, 44100 Hz, ~91840 bps ./kurz/Podcasts/dh-20121015-kurz-018.ogg: Ogg data, Vorbis audio, stereo, 44100 Hz, ~91840 bps
Backticks erlauben es zwar, Kommandos zu verschachteln, aber Sie müssen einen verkehrten Schrägstrich, englisch Backslash, vor die inneren Backticks stellen, damit die Shell sie nicht als schließendes Element zum ersten Backtick ansieht. Spätestens noch eine Ebene tiefer gerät die Sache meist außer Kontrolle: Sie müssten dann den zweiten Backslash noch mit einem dritten Rückstrich maskieren.
Die schnelle Lösung hierfür lautet, das Konstrukt $(...) anstatt Backticks zu verwenden, da diese Variante durch unterschiedliche Zeichen am Anfang und Ende deutlich übersichtlicher ausfällt (Listing 5). Das Problem mit den Leer- und Sonderzeichen in Dateinamen sind Sie damit aber noch nicht los.
Listing 5
$ file $(find $(find . -name Podcasts -type d) -name '*.ogg') ./lang/Podcasts/dh-20130204-ausgabe-045.ogg: Ogg data, Vorbis audio, stereo, 44100 Hz, ~91840 bps ./lang/Podcasts/dh-20130107-ausgabe-044.ogg: Ogg data, Vorbis audio, stereo, 44100 Hz, ~91840 bps ./kurz/Podcasts/dh-20121015-kurz-018.ogg: Ogg data, Vorbis audio, stereo, 44100 Hz, ~91840 bps
Xargs zur Rettung!
Das Umleiten der Ausgabe als Eingabe für einen anderen Befehl zu verwenden, ist viel einfacher, da das zweite Programm die Eingabe direkt verarbeitet. Xargs bietet also die schwierigere Variante zum Preis der einfacheren (Listing 6).
Listing 6
$ find Podcasts -name '*.ogg' | xargs file Podcasts/dh-20130204-ausgabe-045.ogg: Ogg data, Vorbis audio, stereo, 44100 Hz, ~91840 bps Podcasts/dh-20130107-ausgabe-044.ogg: Ogg data, Vorbis audio, stereo, 44100 Hz, ~91840 bps
Xargs hat hier die Ausgabe von Find als Eingabe gelesen, das als Parameter angegebene Programm aufgerufen und ihm die erhaltenen Dateinamen als Parameter mitgegeben. Dabei trennt es die auf der Standardeingabe übergebenen Zeichenketten ebenfalls an Leerzeichen einschließlich Zeilenumbrüchen auf (Listing 7).
Listing 7
$ find Musik -name '*.ogg' | xargs file xargs: unmatched single quote; by default quotes are special to xargs unless you use the -0 option Musik/Roxette: ERROR: cannot open `Musik/Roxette' (No such file or directory) /dev/stdin: empty June: ERROR: cannot open `June' (No such file or directory) Afternoon.ogg: ERROR: cannot open `Afternoon.ogg' (No such file or directory) Musik/Roxette: ERROR: cannot open `Musik/Roxette' (No such file or directory) /dev/stdin: empty I: ERROR: cannot open `I' (No such file or directory)
Auf den ersten Blick hat sich nichts verbessert. Der Vorteil von Xargs liegt aber darin, dass Sie mit der Kommandozeilenoption -d festlegen dürfen, an welchem Zeichen es trennt. In Listing 7 waren die Dateinamen mit Zeilenumbrüchen getrennt. Diese kommen nun als Trenner zum Einsatz (Listing 8).
Listing 8
$ find Musik -name '*.ogg' | xargs -d '\n' file Musik/Roxette - June Afternoon.ogg: Ogg data, Vorbis audio, [...] Musik/Roxette - I Don't Want to Get Hurt.ogg: Ogg data, Vorbis audio, [...] Musik/Roxette - Joyride.ogg: Ogg data, Vorbis audio, [...] Musik/Roxette - Crash! Boom! Bang!.ogg: Ogg data, Vorbis audio, [...] Musik/Roxette - Almost Unreal.ogg: Ogg data, Vorbis audio, [...]
Enthalten die Dateinamen einen Zeilenumbruch, verwenden Sie eines der beiden einzigen Zeichen als Trenner, die nie in einem Posix-konformen Dateinamen vorkommen dürfen: das Nullzeichen mit ASCII-Code 0. Weil dies ein so typischer Fall ist, haben die Entwickler sowohl Find als auch Xargs gleich passende Optionen spendiert (Listing 9).
Listing 9
$ find . -type f -print0 | xargs -0 file ./Datei mit Zeilenumbrüchen.txt: ASCII text
Die Option -print0 veranlasst Find, bei der Ausgabe jeden Dateinamen mit einem Nullzeichen abzuschließen; mittels -0 trennt Xargs die Eingabe nur an solchen. Es handelt sich bei den Zeichen in den Optionen jeweils um die Ziffer Null.
Xargs-Varianten
Xargs ist in praktisch jeder Linux- oder BSD-Distribution enthalten und auch Mac OS X hat Xargs mit an Bord. Unter Linux kommt fast immer die Implementation des GNU-Projekts zum Einsatz. Das Programm zählt dort zusammen mit Find und Locate zu den GNU Find Utilities [1].
Allerdings sind nicht alle Implementationen identisch. Sowohl die von FreeBSD [2] und NetBSD [3] (sie kommt auch unter Mac OS X [4] zum Einsatz) als auch die von GNU kennen die Option -0, obwohl diese nicht im Posix-Standard [5] vorkommt. Die Option -d findet sich dagegen nur in der Implementation des GNU-Projekts [6].
Schachteln ohne Schachteln
Aus Verschachtelungen macht Xargs einfach mehrere Umleitungen. Sie dürfen aber bei Find die zu durchsuchenden Verzeichnisse nicht einfach ans Ende anhängen. Per Option -I legen Sie daher eine Zeichenkette fest, die Sie durch den jeweiligen Dateinamen ersetzen möchten. In diesem Fall lautet sie HIER (Listing 10).
Listing 10
$ find . -name Podcasts -type d | xargs -I HIER find HIER -name '*.ogg' | xargs file ./lang/Podcasts/dh-20130204-ausgabe-045.ogg: Ogg data, Vorbis audio, stereo, 44100 Hz, ~91840 bps ./lang/Podcasts/dh-20130107-ausgabe-044.ogg: Ogg data, Vorbis audio, stereo, 44100 Hz, ~91840 bps ./kurz/Podcasts/dh-20121015-kurz-018.ogg: Ogg data, Vorbis audio, stereo, 44100 Hz, ~91840 bps
Dies ist praktisch, wenn Sie mit Xargs die Befehle mv oder cp aufrufen möchten, die bei mehr als zwei Parametern das Zielverzeichnis immer als letzten Parameter haben wollen.
Linux vor Version 2.6.23 (Oktober 2007) und manche andere Unix-Systeme haben eine Beschränkung hinsichtlich der Anzahl der Kommandozeilenparameter, die Sie einem Programm auf einmal übergeben dürfen. Kommen dort Backticks oder das Konstrukt $(...) zum Einsatz, und gibt der innere Befehl mehr als 1024 Worte aus, so bricht die Shell den Befehl wegen zu vielen Parametern ab.
Auch hier hilft Xargs: Es weiß von diesem Limit und zerlegt zu große Parameterlisten in fürs Betriebssystem verträgliche Häppchen.
Einzeln behandelt
Es gibt viele Programme für die Kommandozeile, die nur eine Datei pro Aufruf verarbeiten, GnuPG zum Beispiel. Deswegen besteht die Möglichkeit, Xargs mit der Option -n mitzuteilen, wie viele Parameter pro Aufruf es übergibt (Listing 11). Geben Sie zusätzlich noch die Xargs-Option -P und eine Zahl an, so führt die Software entsprechend viele Instanzen des als Parameter übergebenen Programms parallel aus.
Listing 11
$ find . -name '*.gpg' | xargs -n 1 gpg [...]
Nichtstun
Auch Nichtstun ist manchmal wichtig: Viele Programme mögen es gar nicht, wenn Sie sie komplett ohne Parameter aufrufen, und melden einen Fehler – GNU Xargs ruft das übergebene Programm auch dann einmal auf, wenn es keine Parameter dafür bekam (Listing 12). Mit der Option -r teilen Sie Xargs mit, dass es den Befehl gar nicht erst auszuführen braucht, falls es keinen Inhalt auf der Standardeingabe bekommt. Die BSD-Implementationen verhalten sich bereits in der Standardeinstellung so.
Listing 12
$ find . -name '*.bla'
$ find . -name '*.bla' | xargs file
Usage: file [-bchikLlNnprsvz0] [--apple] [--mime-encoding] [--mime-type]
[-e testname] [-F separator] [-f namefile] [-m magicfiles] file ...
file -C [-m magicfiles]
file [--help]
$ find . -name '*.bla' | xargs -r file
Zusammenarbeit
Nicht nur Find arbeitet gut mit Xargs zusammen, sondern auch Grep. Mit der Option -l zeigt das Programm lediglich die Namen der Dateien an, die einen Treffer enthalten. In Listing 13 sucht die Software zunächst in allen Dateien im aktuellen Verzeichnis nach dem Zeichen < und reicht danach die gefundenen Dateinamen an Xargs weiter. Das wiederum füttert File damit und ermittelt den jeweiligen Dateityp.
Listing 13
$ grep -l '<' * | xargs file bar.html: HTML document, ASCII text foo.xml: XML document text
GNU Grep ab Version 2.4 kennt sogar die Option -Z (wie englisch “Zero” für null), die Dateinamen mit einem Nullzeichen statt eines Zeilenumbruchs abschließt, mit den bereits genannten Vorteilen (Listing 14).
Listing 14
$ grep -l foo * | xargs file Datei: ERROR: cannot open `Datei' (No such file or directory) mit: ERROR: cannot open `mit' (No such file or directory) Zeilenumbrüchen.txt: ERROR: cannot open `Zeilenumbrüchen.txt' (No such file or directory) bar.html: HTML document, ASCII text foo.xml: XML document text $ grep -lZ foo * | xargs -0 file Datei mit Zeilenumbrüchen.txt: ASCII text bar.html: HTML document, ASCII text foo.xml: XML document text
Das Programm Prips [7] arbeitet ebenfalls ausgezeichnet mit Xargs zusammen. Der Name steht für “Print IPs”, und das Tool gibt für einen als Parameter angegebenen IP-Bereich sämtliche enthaltenen IP-Adressen aus. Ein Beispiel zeigt Listing 15, wo wieder die Option -n 1 zum Einsatz kommt: Der Befehl host verarbeitet ebenfalls nur eine IP-Adresse pro Aufruf.
Listing 15
$ prips 192.33.96.0/30 192.33.96.0 192.33.96.1 192.33.96.2 192.33.96.3 $ prips 192.33.96.0/30 | xargs -n 1 host 0.96.33.192.in-addr.arpa domain name pointer phys-hpx-dock-1.ethz.ch. 1.96.33.192.in-addr.arpa domain name pointer rou-hpx-1-phys-hpx-dock-1.ethz.ch. 2.96.33.192.in-addr.arpa domain name pointer floo.ethz.ch. 3.96.33.192.in-addr.arpa domain name pointer aragog.ethz.ch.
Fazit
Backticks auf der Kommandozeile und Sonderzeichen in Dateinamen vertragen sich praktisch nie. Mit Backticks geht außerdem bei verschachtelten Konstrukten sehr schnell die Übersicht verloren. Glücklicherweise hilft Xargs in beinahe allen Fällen weiter, in denen Backticks an ihre Grenzen stoßen.
Danksagung
Der Autor bedankt sich bei Frank Hofmann und Benjamin Schieder für Anmerkungen und Kommentare im Vorfeld dieses Artikels.
Infos
[1] GNU Find Utilities: https://www.gnu.org/software/findutils/
[2] FreeBSD Xargs (Manpage): http://www.freebsd.org/cgi/man.cgi?query=xargs
[3] NetBSD Xargs (Manpage): http://netbsd.gw.com/cgi-bin/man-cgi?xargs
[4] Mac OS X Xargs (Quellcode): https://opensource.apple.com/source/shell_cmds/shell_cmds-17.1/xargs/
[5] Xargs im Posix-Standard: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/xargs.html
[6] Optionen von GNU Xargs: https://www.gnu.org/software/findutils/manual/html_node/find_html/xargs-options.html
[7] Prips: http://devel.ringlet.net/sysutils/prips/





