Erinnern Sie sich noch an diesen langen, aufwendig gewundenen Befehl aus der Einführung?
$ paste <(echo {1..10}.jpg | sed 's/ /\n/g') \
<(echo {0..9}.jpg | sed 's/ /\n/g') \
| sed 's/^/mv /' \
| bash
Solche magischen Aufrufe sind freche Einzeiler (»brash One-liners«).1 Nehmen wir ihn einmal auseinander, um zu verstehen, was er macht und wie er funktioniert. Die innersten echo-Befehle nutzen eine Klammererweiterung, um Listen aus JPEG-Dateinamen zu generieren:
$ echo {1..10}.jpg
1.jpg 2.jpg 3.jpg...10.jpg
$ echo {0..9}.jpg
0.jpg 1.jpg 2.jpg...9.jpg
Durch das Weiterleiten der Dateinamen in der Pipeline an sed werden die Leerzeichen durch Newlines ersetzt:
$ echo {1..10}.jpg | sed 's/ /\n/g'
1.jpg
2.jpg
...
10.jpg
$ echo {0..9}.jpg | sed 's/ /\n/g'
0.jpg
1.jpg
...
9.jpg
Der paste-Befehl gibt die beiden Listen nebeneinander aus. Eine Prozesssubstitution erlaubt es paste, die beiden Listen zu lesen, als wären es Dateien:
$ paste <(echo {1..10}.jpg | sed 's/ /\n/g') \
<(echo {0..9}.jpg | sed 's/ /\n/g')
1.jpg 0.jpg
2.jpg 1.jpg
...
10.jpg 9.jpg
Durch das Voranstellen von mv auf jeder Zeile wird eine Abfolge von Strings ausgegeben, bei denen es sich um mv-Befehle handelt:
$ paste <(echo {1..10}.jpg | sed 's/ /\n/g') \
<(echo {0..9}.jpg | sed 's/ /\n/g') \
| sed 's/^/mv /'
mv 1.jpg 0.jpg
mv 2.jpg 1.jpg
...
mv 10.jpg 9.jpg
Der Zweck des Befehls ist nun klar: Er generiert zehn Befehle zum Umbenennen der Bilddateien 1.jpg bis 10.jpg. Die neuen Namen lauten 0.jpg bis 9.jpg. Mit der Pipeline der Ausgabe an bash werden die mv-Befehle ausgeführt:
$ paste <(echo {1..10}.jpg | sed 's/ /\n/g') \
<(echo {0..9}.jpg | sed 's/ /\n/g') \
| sed 's/^/mv /' \
| bash
Freche Einzeiler sind wie Geduldsspiele oder Rätsel. Sie stehen einem Problem gegenüber, etwa dem Umbenennen einer Gruppe von Dateien, und nutzen Ihren Werkzeugkasten, um einen Linux-Befehl zu konstruieren, der das Problem löst. Freche Einzeiler fordern Ihre Kreativität heraus und stärken Ihre Kenntnisse und Fähigkeiten.
In diesem Kapitel werden Sie mithilfe der folgenden Zauberformel Schritt für Schritt solche Einzeiler zusammenbauen:
Dieses Kapitel wird recht anspruchsvoll. Lassen Sie sich nicht entmutigen, falls die Beispiele Sie manchmal überfordern. Gehen Sie einfach langsam und schrittweise vor und probieren Sie die Befehle auf einem Computer aus.
Hinweis Manche der Einzeiler in diesem Kapitel sind zu lang für eine einzige Zeile. Ich habe sie deshalb mithilfe von Backslashs auf mehrere Zeilen verteilt. Das ist jedoch kein Grund, sie freche Zweizeiler (oder gar freche Siebenzeiler) zu nennen. |
Bevor Sie sich auf die frechen Einzeiler stürzen, sollten Sie sich einen Augenblick Zeit nehmen, um in die richtige Stimmung zu kommen:
Ich werde jeden dieser Gedanken gleich genauer diskutieren.
Ein Schlüssel zum Schreiben frecher Einzeiler ist Flexibilität. Sie haben inzwischen schon ein paar erstaunliche Werkzeuge kennengelernt – eine Reihe wichtiger Linux-Programme (und die unterschiedlichsten Methoden, um sie auszuführen) sowie die Befehls-History, Kommandozeilen-Editing und mehr. Sie können diese Werkzeuge vielfältig kombinieren, und für ein bestimmtes Problem gibt es meist mehrere Lösungen.
Selbst die einfachsten Linux-Aufgaben lassen sich auf vielerlei Art bewältigen. Denken Sie nur einmal darüber nach, wie Sie in Ihrem aktuellen Verzeichnis .jpg-Dateien auflisten würden. Ich wette, 99,9 % der Linux-Benutzer würden einen solchen Befehl ausführen:
$ ls *.jpg
Dabei ist dies nur eine Lösung von vielen. Sie könnten zum Beispiel alle Dateien in dem Verzeichnis auflisten lassen und dann mit grep nur die erfassen, deren Namen auf .jpg enden:
$ ls | grep '\.jpg$'
Wieso sollten Sie diese Lösung wählen? Nun, Sie sahen ein Beispiel in »Lange Argumentlisten« auf Seite 145, als ein Verzeichnis so viele Dateien enthielt, dass diese nicht durch Pattern Matching aufgelistet werden konnten. Die Technik des »Greppens« nach einer Dateinamenserweiterung ist ein robuster, allgemeiner Ansatz zum Lösen aller möglichen Probleme. Wichtig ist es hier, flexibel zu sein und die Werkzeuge zu verstehen, damit Sie immer das am besten geeignete für jeden Fall anwenden können. Das ist eine echte Fähigkeit, die man braucht, um mit frechen Einzeilern zu zaubern.
Die folgenden Befehle listen .jpg-Dateien im aktuellen Verzeichnis auf. Versuchen Sie, herauszufinden, wie die einzelnen Befehle funktionieren:
$ echo $(ls *.jpg)
$ bash -c 'ls *.jpg'
$ cat <(ls *.jpg)
$ find . -maxdepth 1 -type f -name \*.jpg -print
$ ls > tmp && grep '\.jpg$' tmp && rm -f tmp
$ paste <(echo ls) <(echo \*.jpg) | bash
$ bash -c 'exec $(paste <(echo ls) <(echo \*.jpg))'
$ echo 'monkey *.jpg' | sed 's/monkey/ls/' | bash
$ python -c 'import os; os.system("ls *.jpg")'
Sind die Ergebnisse identisch, oder verhalten sich manche Befehle ein bisschen anders? Können Sie sich weitere passende Befehle denken?
Jeder freche Einzeiler beginnt mit der Ausgabe eines einfachen Befehls. Diese Ausgabe könnte der Inhalt einer Datei, ein Teil einer Datei, ein Verzeichnislisting, eine Sequenz aus Zahlen oder Buchstaben, eine Liste von Benutzern, es könnten ein Datum und eine Uhrzeit oder auch andere Daten sein. Ihre erste Herausforderung besteht deshalb darin, die Anfangsdaten für Ihren Befehl zu erzeugen.
Falls Sie zum Beispiel den 17. Buchstaben des englischen Alphabets wissen wollen, könnten Ihre Anfangsdaten die 26 Buchstaben sein, die durch eine Klammererweiterung erzeugt werden:
$ echo {A..Z}
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
Sobald Sie diese Ausgabe erzeugen können, müssen Sie im nächsten Schritt entscheiden, wie Sie sie so umformen, dass sie zu Ihrem Ziel passt. Müssen Sie die Ausgabe nach Zeilen oder Spalten zerlegen? Müssen Sie sie mit anderen Informationen zusammenbringen? Ist es notwendig, die Ausgabe auf kompliziertere Weise umzuformen? Betrachten Sie die Programme in den Kapiteln 1 und 5, die so etwas können, wie grep und sed und cut, und wenden Sie sie mithilfe der Techniken aus Kapitel 7 an.
Sie könnten zum Beispiel das 17. Feld mit awk ausgeben oder mit sed die Leerzeichen entfernen und das 17. Zeichen mit cut lokalisieren:
$ echo {A..Z} | awk '{print $(17)}'
Q
$ echo {A..Z} | sed 's/ //g' | cut -c17
Q
Falls Sie in einem anderen Beispiel die Monate des Jahres ausgeben wollen, könnten Ihre Anfangsdaten die Zahlen von 1 bis 12 sein, die wieder durch Klammererweiterung erzeugt werden:
$ echo {1..12}
1 2 3 4 5 6 7 8 9 10 11 12
Nun bauen Sie die Klammererweiterung so aus, dass sie Daten für den ersten Tag jedes Monats bildet (von 2021-01-01 bis 2021-12-01). Dann führen Sie date -d auf jeder Zeile aus, um Monatsnamen zu erzeugen:
Oder nehmen wir an, Sie wollten die Länge des längsten Dateinamens im aktuellen Verzeichnis wissen. Ihre Anfangsdaten könnten ein Verzeichnislisting sein:
$ ls
animals.txt cartoon-mascots.txt...zebra-stripes.txt
Jetzt generieren Sie mit awk Befehle zum Zählen der Zeichen in den einzelnen Dateinamen mit wc -c:
$ ls | awk '{print "echo -n", $0, "| wc -c"}'
echo -n "animals.txt" | wc -c
echo -n "cartoon-mascots.txt | wc -c"
...
echo -n "zebra-stripes.txt | wc -c"
(Die Option -n verhindert, dass echo Newline-Zeichen ausgibt, die jeden Zähler verfälschen würden.) Schließlich schicken Sie die Befehle mit einer Pipeline an bash zur Ausführung, sortieren die numerischen Ergebnisse vom größten zum kleinsten und greifen sich den Maximalwert (die erste Zeile) mit head -n1 heraus:
$ ls | awk '{print "echo -n", $0, "| wc -c"}' | bash | sort -nr | head -n1
23
Dieses letzte Beispiel war nicht ganz einfach: Es wurden Pipelines als Strings erzeugt und dann an eine weitere Pipeline geleitet. Dennoch ist das allgemeine Prinzip das gleiche: Sie ermitteln Ihre Anfangsdaten und manipulieren sie so, dass sie Ihren Anforderungen entsprechen.
Das Erzeugen von frechen Einzeilern läuft vermutlich über das stete Ausprobieren und Ändern von Befehlen, um auftretende Fehler auszubügeln. Die folgenden Werkzeuge und Techniken helfen Ihnen, schnell unterschiedliche Lösungen zu testen:
Verwenden Sie die Befehls-History und Kommandozeilen-Editing.
Tippen Sie Befehle nicht neu ein, wenn Sie experimentieren. Nutzen Sie Techniken aus Kapitel 3, um frühere Befehle zurückzuholen, sie zu verändern und sie dann erneut auszuführen.
Fügen Sie echo hinzu, um Ihre Ausdrücke zu testen.
Sind Sie sich nicht sicher, wie ein Ausdruck ausgewertet wird, geben Sie ihn zuvor mit echo aus, um die ausgewerteten Ergebnisse auf der Standardausgabe anschauen zu können.
Benutzen Sie ls oder fügen Sie echohinzu, um destruktive Befehle zu testen.
Falls Ihr Befehl rm, mv, cp oder andere Befehle aufruft, die Dateien überschreiben oder entfernen könnten, setzen Sie echo davor, um zu bestätigen, welche Dateien betroffen sein werden. (Statt rm führen Sie also echo rm aus.) Eine andere Sicherheitstaktik besteht darin, rm durch ls zu ersetzen, um die Dateien aufzulisten, die entfernt werden würden.
Fügen Sie ein tee ein, um Zwischenergebnisse zu betrachten.
Falls Sie mitten in einer langen Pipeline die Ausgabe (Standardausgabe) anschauen wollen, fügen Sie den Befehl tee ein. Damit wird die Ausgabe zur Untersuchung in einer Datei gespeichert. Der folgende Befehl sichert die Ausgabe von befehl3 in der Datei outfile, während er dieselbe Ausgabe in einer Pipeline an befehl4 weiterleitet:
$ befehl1 | befehl2 | befehl3 | tee outfile | befehl4 | befehl5
$ less outfile
Okay, bauen wir uns einige freche Einzeiler!
Dieser freche Einzeiler ist vergleichbar mit demjenigen am Anfang dieses Kapitels (zum Umbenennen von .jpg-Dateien), aber detaillierter. Außerdem spiegelt er eine wirkliche Situation wider, der ich mich beim Schreiben dieses Buchs gegenübersah. Wie der vorherige Einzeiler kombiniert er zwei Techniken aus Kapitel 7: Prozesssubstitution und das Leiten einer Pipeline an die bash. Das Ergebnis ist ein reproduzierbares Muster zum Lösen vergleichbarer Probleme.
Ich habe dieses Buch auf einem Linux-Computer mithilfe einer Auszeichnungssprache namens AsciiDoc (https://asciidoc.org) geschrieben. Die Einzelheiten der Sprache sind hier nicht wichtig; entscheidend ist, dass sich jedes Kapitel in einer eigenen Datei befand und es ursprünglich zehn davon gab:
$ ls
ch01.asciidoc ch03.asciidoc ch05.asciidoc ch07.asciidoc ch09.asciidoc
ch02.asciidoc ch04.asciidoc ch06.asciidoc ch08.asciidoc ch10.asciidoc
Irgendwann entschied ich mich, zwischen den Kapiteln 2 und 3 ein elftes Kapitel einzufügen. Das bedeutete, dass einige Dateien umbenannt werden mussten. Aus den Kapiteln 3 bis 10 mussten die Kapitel 4 bis 11 werden, damit eine Lücke für das neue Kapitel 3 entstehen konnte (ch03.asciidoc). Ich hätte die Dateien manuell umbenennen können, beginnend mit ch11.asciidoc und rückwärts vorgehend:2
$ mv ch10.asciidoc ch11.asciidoc
$ mv ch09.asciidoc ch10.asciidoc
$ mv ch08.asciidoc ch09.asciidoc
...
$ mv ch03.asciidoc ch04.asciidoc
Diese Methode ist aber lästig (stellen Sie sich vor, es wären 1.000 Dateien anstelle von 11!), stattdessen generierte ich die erforderlichen mv-Befehle und schickte sie in einer Pipeline an bash. Werfen Sie einen genauen Blick auf die vorhergehenden mv-Befehle und denken Sie einen Augenblick lang nach, wie Sie sie erzeugt hätten.
Konzentrieren Sie sich zuerst auf die ursprünglichen Dateinamen ch03.asciidoc bis ch10.asciidoc. Sie könnten sie mit einer Klammererweiterung wie ch{10..03}.asciidoc ausgeben, genau wie im ersten Beispiel in diesem Kapitel. Um aber unsere Flexibilität ein wenig zu üben, nutzen Sie den Befehl seq -w zum Ausgeben der Zahlen:
$ seq -w 10 -1 3
10
09
08
...
03
Verwandeln Sie diese numerische Sequenz dann in Dateinamen, indem Sie sie in einer Pipeline an sed senden:
$ seq -w 10 -1 3 | sed 's/\(.*\)/ch\1.asciidoc/'
ch10.asciidoc
ch09.asciidoc
...
ch03.asciidoc
Sie haben jetzt eine Liste der Originaldateinamen. Wiederholen Sie dies nun für die Kapitel 4 bis 11, um die Zieldateinamen zu erzeugen:
$ seq -w 11 -1 4 | sed 's/\(.*\)/ch\1.asciidoc/'
ch11.asciidoc
ch10.asciidoc
...
ch04.asciidoc
Um die mv-Befehle zu bilden, müssen Sie die originalen und die neuen Dateinamen nebeneinander ausgeben. Das erste Beispiel in diesem Kapitel löste dieses »Nebeneinander«-Problem mit paste und verwendete eine Prozesssubstitution, um die beiden ausgegebenen Listen als Dateien zu behandeln. Machen Sie das hier ebenfalls:
$ paste <(seq -w 10 -1 3 | sed 's/\(.*\)/ch\1.asciidoc/') \
<(seq -w 11 -1 4 | sed 's/\(.*\)/ch\1.asciidoc/')
ch10.asciidoc ch11.asciidoc
ch09.asciidoc ch10.asciidoc
...
ch03.asciidoc ch04.asciidoc
Tipp Der gezeigte Befehl sieht aus, als müssten Sie da wahnsinnig viel tippen, doch mit der Befehls-History und einem Kommandozeilen-Editing im Emacs-Stil ist das gar nicht so schlimm. Um von der einen »seq und sed«-Zeile zum paste-Befehl zu kommen: |
|
|
1. Rufen Sie den vorherigen Befehl mit der Pfeil-nach-oben-Taste aus der History zurück. |
|
2. Drücken Sie Strg-A und dann Strg-K, um die ganze Zeile auszuschneiden. |
|
3. Tippen Sie das Wort paste ein, gefolgt von einem Leerzeichen. |
|
4. Drücken Sie zweimal Strg-Y, um zwei Kopien der Befehle seq und sed zu erzeugen. |
|
5. Benutzen Sie Tastenkürzel zum Bewegen und Bearbeiten, um die zweite Kopie zu modifizieren. |
|
6. Und so weiter. |
Stellen Sie jeder Zeile mv voran, indem Sie die Ausgabe mit einer Pipeline an sed weiterleiten, wobei genau die mv-Befehle ausgegeben werden, die Sie brauchen:
$ paste <(seq -w 10 -1 3 | sed 's/\(.*\)/ch\1.asciidoc/') \
<(seq -w 11 -1 4 | sed 's/\(.*\)/ch\1.asciidoc/') \
| sed 's/^/mv /'
mv ch10.asciidoc ch11.asciidoc
mv ch09.asciidoc ch10.asciidoc
...
mv ch03.asciidoc ch04.asciidoc
Im letzten Schritt leiten Sie die Befehle in einer Pipeline zur Ausführung an die bash:
$ paste <(seq -w 10 -1 3 | sed 's/\(.*\)/ch\1.asciidoc/') \
<(seq -w 11 -1 4 | sed 's/\(.*\)/ch\1.asciidoc/') \
| sed 's/^/mv /' \
| bash
Ich habe genau diese Lösung für mein Buch benutzt. Nachdem die mv-Befehle ausgeführt worden waren, hatte ich als Ergebnis die Kapitel 1 und 2 sowie 4 bis 11 erhalten, sodass eine Lücke für ein neues Kapitel 3 blieb:
$ ls ch*.asciidoc
ch01.asciidoc ch04.asciidoc ch06.asciidoc ch08.asciidoc ch10.asciidoc
ch02.asciidoc ch05.asciidoc ch07.asciidoc ch09.asciidoc ch11.asciidoc
Das Muster, das ich gerade vorgestellt habe, lässt sich in allen möglichen Situationen reproduzieren, um eine Abfolge verwandter Befehle auszuführen:
Dieser freche Einzeiler ist durch den tatsächlichen Einsatz bei Mediawiki inspiriert, der Software hinter Wikipedia und Tausenden anderer Wikis. Mediawiki erlaubt es Benutzern, Bilder zur Anzeige hochzuladen. Die meisten Anwender folgen einem manuellen Vorgehen über Webformulare: Man klickt auf Choose File, um einen Dateidialog zu öffnen, navigiert zu einer Bilddatei und wählt diese aus, fügt in dem Formular einen beschreibenden Kommentar hinzu und klickt auf Upload. Wiki-Administratoren verwenden eher eine automatisierte Methode: ein Skript, das ein ganzes Verzeichnis liest und dessen Bilder hochlädt. Jede Bilddatei (sagen wir, bald_eagle.jpg) wird mit einer Textdatei (bald_eagle.txt) kombiniert, die einen beschreibenden Kommentar über das Bild enthält.
Stellen Sie sich vor, Sie hätten ein Verzeichnis, das mit Hunderten von Bild- und Textdateien gefüllt wäre. Sie möchten bestätigen, dass jede Bilddatei eine passende Textdatei hat und umgekehrt. Hier ist eine kleinere Version dieses Verzeichnisses:
$ ls
bald_eagle.jpg blue_jay.jpg cardinal.txt robin.jpg wren.jpg
bald_eagle.txt cardinal.jpg oriole.txt robin.txt wren.txt
Entwickeln wir doch einfach zwei unterschiedliche Lösungen zum Identifizieren nicht zugeordneter Dateien. Für die erste Lösung erzeugen Sie zwei Listen, eine für die JPEG- und eine für die Textdateien, und benutzen dann cut, um deren Dateierweiterungen .txt bzw. .jpg zu entfernen:
$ ls *.jpg | cut -d. -f1
bald_eagle
blue_jay
cardinal
robin
wren
$ ls *.txt | cut -d. -f1
bald_eagle
cardinal
oriole
robin
wren
Dann vergleichen Sie die Listen mit diff mithilfe der Prozesssubstitution:
$ diff <(ls *.jpg | cut -d. -f1) <(ls *.txt | cut -d. -f1)
2d1
< blue_jay
3a3
> oriole
Sie könnten hier anhalten, weil die Ausgabe anzeigt, dass die erste Liste einen zusätzlichen blue_jay aufweist (also eine Datei blue_jay.jpg) und die zweite Liste einen zusätzlichen oriole (das heißt: oriole.txt). Nichtsdestotrotz möchten wir genauere Ergebnisse haben. Eliminieren Sie unerwünschte Zeilen, indem Sie mit grep nach den Zeichen < und > am Anfang der einzelnen Zeilen greifen:
Benutzen Sie dann awk, um an jeden Dateinamen die korrekte Dateierweiterung anzuhängen ($2), die davon abhängt, ob vor dem Dateinamen jeweils ein < oder ein > steht:
$ diff <(ls *.jpg | cut -d. -f1) <(ls *.txt | cut -d. -f1) \
| grep '^[<>]' \
| awk '/^</{print $2 ".jpg"} /^>/{print $2 ".txt"}'
blue_jay.jpg
oriole.txt
Sie haben nun eine Liste nicht zusammengehörender Dateien. Allerdings enthält diese Lösung einen kleinen Bug. Angenommen, im aktuellen Verzeichnis steht der Dateiname yellow.canary.jpg, der zwei Punkte enthält. Der vorangegangene Befehl würde eine falsche Ausgabe erzeugen:
blue_jay.jpg
oriole.txt
yellow.jpg Das ist falsch.
Dieses Problem tritt auf, weil die zwei cut-Befehle Zeichen vom ersten Punkt an entfernen statt vom zweiten, sodass yellow.canary.jpg auf yellow abgeschnitten wird statt auf yellow.canary. Um dieses Problem zu beheben, ersetzen Sie cut durch sed, um Zeichen vom letzten Punkt bis zum Ende des Strings zu entfernen:
$ diff <(ls *.jpg | sed 's/\.[^.]*$//') \
<(ls *.txt | sed 's/\.[^.]*$//') \
| grep '^[<>]' \
| awk '/</{print $2 ".jpg"} />/{print $2 ".txt"}'
blue_jay.txt
oriole.jpg
yellow.canary.txt
Die erste Lösung ist nun fertig. Die zweite Lösung verfolgt einen anderen Ansatz. Anstatt diff auf zwei Listen anzuwenden, generieren Sie eine einzige Liste und entfernen zusammengehörende Dateinamenpaare. Beginnen Sie damit, dass Sie die Dateierweiterungen mit sed entfernen (wobei Sie dasselbe sed-Skript benutzen wie eben) und die Vorkommen der einzelnen Strings mit uniq -c zählen:
$ ls *.{jpg,txt} \
| sed 's/\.[^.]*$//' \
| uniq -c
2 bald_eagle
1 blue_jay
2 cardinal
1 oriole
2 robin
2 wren
1 yellow.canary
Jede Zeile der Ausgabe enthält entweder die Zahl 2, die ein zusammengehörendes Paar aus Dateinamen repräsentiert, oder die Zahl 1, die für einen unzusammenhängenden Dateinamen steht. Isolieren Sie mit awk Zeilen, die mit Whitespace und einer 1 beginnen, und geben Sie nur das zweite Feld aus:
$ ls *.{jpg,txt} \
| sed 's/\.[^.]*$//' \
| uniq -c \
| awk '/^ *1 /{print $2}'
blue_jay
oriole
yellow.canary
Wie können Sie für den letzten Schritt die fehlenden Dateierweiterungen hinzufügen? Geben Sie sich nicht mit komplizierten String-Manipulationen ab. Benutzen Sie einfach ls, um die tatsächlichen Dateien im aktuellen Verzeichnis aufzulisten. Kleben Sie mit awk einen Asterisk (ein Wildcard-Zeichen) an das Ende der einzelnen Ausgabezeilen:
$ ls *.{jpg,txt} \
| sed 's/\.[^.]*$//' \
| uniq -c \
| awk '/^ *1 /{print $2 "*"}'
blue_jay*
oriole*
yellow.canary*
und übergeben Sie die Zeilen dann über eine Befehlssubstitution an ls. Die Shell führt ein Pattern Matching durch, und ls listet die nicht zugeordneten Dateinamen auf. Fertig!
$ ls -1 $(ls *.{jpg,txt} \
| sed 's/\.[^.]*$//' \
| uniq -c \
| awk '/^ *1 /{print $2 "*"}')
blue_jay.jpg
oriole.txt
yellow.canary.jpg
Im Abschnitt »Organisieren Sie Ihr Home-Verzeichnis für eine schnelle Navigation« auf Seite 75 haben Sie von Hand eine komplizierte CDPATH-Zeile geschrieben. Sie begann mit $HOME, gefolgt von all den Unterverzeichnissen von $HOME, und sie endete mit dem relativen Pfad .. (dem übergeordneten Verzeichnis):
CDPATH=$HOME:$HOME/Work:$HOME/Family:$HOME/Finances:$HOME/Linux:$HOME/Music:..
Bauen wir einen frechen Einzeiler, der diese CDPATH-Zeile automatisch erzeugt, sodass sie sich für das Einfügen in eine bash-Konfigurationsdatei eignet. Beginnen Sie mit der Liste der Unterverzeichnisse in $HOME. Verwenden Sie dabei eine Subshell, um zu verhindern, dass der Befehl cd das aktuelle Verzeichnis Ihrer Shell ändert:
Fügen Sie mit sed vor jedem Verzeichnis $HOME/ hinzu:
$ (cd && ls -d */) | sed 's/^/$HOME\//g'
$HOME/Family/
$HOME/Finances/
$HOME/Linux/
$HOME/Music/
$HOME/Work/
Das gezeigte sed-Skript ist ein bisschen kompliziert, weil der Ersetzungsstring, $HOME/, einen Schrägstrich enthält und sed-Substitutionen ebenfalls Schrägstriche als Trennzeichen verwenden. Deshalb habe ich meinen Schrägstrich geschützt: $HOME\/. Um die Dinge ein wenig zu vereinfachen, sollten Sie sich aus »Substitution und Schrägstriche« (siehe Seite 112) ins Gedächtnis zurückrufen, dass sed jedes genehme Zeichen als Trennzeichen akzeptiert. Benutzen wir also at-Zeichen (@) anstelle der Schrägstriche, denn auf diese Weise ist kein zusätzlicher Schutz erforderlich:
$ (cd && ls -d */) | sed 's@^@$HOME/@g'
$HOME/Family/
$HOME/Finances/
$HOME/Linux/
$HOME/Music/
$HOME/Work/
Als Nächstes schneiden wir den letzten Schrägstrich mit einem weiteren sed-Ausdruck ab:
$ (cd && ls -d */) | sed -e 's@^@$HOME/@' -e 's@/$@@'
$HOME/Family
$HOME/Finances
$HOME/Linux
$HOME/Music
$HOME/Work
Geben Sie die Ausgabe mittels echo und einer Befehlssubstitution auf einer einzigen Zeile aus. Beachten Sie, dass Sie nun keine runden Klammern mehr um cd und ls herum benötigen, um explizit eine Subshell zu erzeugen, weil die Befehlssubstitution eine eigene Subshell anlegt:
$ echo $(cd && ls -d */ | sed -e 's@^@$HOME/@' -e 's@/$@@')
$HOME/Family $HOME/Finances $HOME/Linux $HOME/Music $HOME/Work
Fügen Sie das erste Verzeichnis $HOME und das letzte relative Verzeichnis .. hinzu:
$ echo '$HOME' \
$(cd && ls -d */ | sed -e 's@^@$HOME/@' -e 's@/$@@') \
..
$HOME $HOME/Family $HOME/Finances $HOME/Linux $HOME/Music $HOME/Work ..
Ändern Sie die Leerzeichen in Doppelpunkte, indem Sie die gesamte bisher verfügbare Ausgabe mit einer Pipeline an tr leiten:
$ echo '$HOME' \
$(cd && ls -d */ | sed -e 's@^@$HOME/@' -e 's@/$@@') \
.. \
| tr ' ' ':'
$HOME:$HOME/Family:$HOME/Finances:$HOME/Linux:$HOME/Music:$HOME/Work:..
Fügen Sie zum Schluss die Umgebungsvariable CDPATH hinzu, und Sie haben eine Variablendefinition generiert, die Sie in eine bash-Konfigurationsdatei einfügen können. Speichern Sie diesen Befehl in einem Skript, damit Sie die Zeile jederzeit generieren können – etwa wenn Sie ein neues Unterverzeichnis zu $HOME hinzufügen:
$ echo 'CDPATH=$HOME' \
$(cd && ls -d */ | sed -e 's@^@$HOME/@' -e 's@/$@@') \
.. \
| tr ' ' ':'
CDPATH=$HOME:$HOME/Family:$HOME/Finances:$HOME/Linux:$HOME/Music:$HOME/Work:..
Eine übliche Aufgabe in der Softwarebranche ist das Testen – das heißt das Einspeisen einer großen Vielfalt an Daten in ein Programm, um zu bestätigen, dass das Programm sich so verhält, wie es vorgesehen ist. Der nächste freche Einzeiler generiert 1.000 Dateien, die beliebigen Text enthalten, die beim Testen von Software benutzt werden können. Die Zahl 1.000 ist rein zufällig; Sie können so viele Dateien generieren, wie Sie wollen.
Die Lösung wählt zufällig Wörter aus einer großen Textdatei aus und erzeugt 1.000 kleinere Dateien mit zufälligen Inhalten und Längen. Eine perfekte Datei ist das Systemwörterbuch /usr/share/dict/words, das 102.305 Wörter enthält, die jeweils auf einer eigenen Zeile stehen.
$ wc -l /usr/share/dict/words
102305 /usr/share/dict/words
Um diesen frechen Einzeiler zu erzeugen, müssen Sie vier Rätsel lösen:
Um das Wörterbuch in eine zufällige Reihenfolge zu bringen, benutzen Sie den Befehl shuf. Jeder Durchlauf des Befehls shuf /usr/share/dict/words erzeugt mehr als 1.000 Ausgabezeilen. Werfen Sie mit head einen Blick auf die ersten Zeilen der zufälligen Ausgabe:
$ shuf /usr/share/dict/words | head -n3
evermore
shirttail
tertiary
$ shuf /usr/share/dict/words | head -n3
interactively
opt
perjurer
Ihr erstes Rätsel ist gelöst. Wie können Sie nun eine willkürlich große Menge an Zeilen aus dem gemischten Wörterbuch auswählen? shuf besitzt eine Option -n zum Ausgeben einer bestimmten Anzahl an Zeilen, allerdings wollen Sie, dass sich der Wert für jede der Ausgabedateien, die Sie erzeugen, ändert. Glücklicherweise kennt bash die Variable RANDOM, die einen zufälligen positiven Integer-Wert zwischen 0 und 32.767 enthält. Ihr Wert ändert sich jedes Mal, wenn Sie auf die Variable zugreifen:
$ echo $RANDOM $RANDOM $RANDOM
7855 11134 262
Führen Sie deshalb shuf mit der Option -n $RANDOM aus, um eine zufällige Anzahl an Zeilen auszugeben. Auch hier könnte die vollständige Ausgabe sehr lang sein, leiten Sie deshalb die Ergebnisse in einer Pipeline an wc -l, um zu bestätigen, dass die Zeilenzahl bei jeder Ausführung anders ist:
$ shuf -n $RANDOM /usr/share/dict/words | wc -l
9922
$ shuf -n $RANDOM /usr/share/dict/words | wc -l
32465
Sie haben das zweite Rätsel gelöst. Nun brauchen Sie 1.000 Ausgabedateien oder, genauer gesagt, 1.000 unterschiedliche Dateinamen. Zum Generieren von Dateinamen führen Sie das Programm pwgen aus, das zufällige Strings aus Buchstaben und Ziffern erzeugt:
$ pwgen
eng9nooG ier6YeVu AhZ7naeG Ap3quail poo2Ooj9 OYiuri9m iQuash0E voo3Eph1
IeQu7mi6 eipaC2ti exah8iNg oeGhahm8 airooJ8N eiZ7neez Dah8Vooj dixiV1fu
Xiejoti6 ieshei2K iX4isohk Ohm5gaol Ri9ah4eX Aiv1ahg3 Shaew3ko zohB4geu
...
Fügen Sie die Option -N1 hinzu, um nur einen einzigen String zu generieren, und geben Sie die String-Länge (10) als Argument an:
$ pwgen -N1 10
ieb2ESheiw
Lassen Sie den String optional eher wie den Namen einer Textdatei aussehen. Dabei hilft Ihnen eine Befehlssubstitution:
$ echo $(pwgen -N1 10).txt
ohTie8aifo.txt
Das dritte Rätsel ist ebenfalls gelöst! Sie haben nun alle Werkzeuge, um eine einzelne zufällige Textdatei zu generieren. Verwenden Sie die Option -o mit shuf, um dessen Ausgabe in einer Datei zu speichern:
$ mkdir -p /tmp/randomfiles && cd /tmp/randomfiles
$ shuf -n $RANDOM -o $(pwgen -N1 10).txt /usr/share/dict/words
und überprüfen Sie die Ergebnisse:
$ ls Listet die neue Datei auf.
Ahxiedie2f.txt
$ wc -l Ahxiedie2f.txt Wie viele Zeilen enthält sie?
13544 Ahxiedie2f.txt
$ head -n3 Ahxiedie2f.txt Ein Blick auf die ersten Zeilen.
saviors
guerillas
forecaster
Sieht gut aus! Das letzte Rätsel besteht darin, herauszufinden, wie sich der gezeigte shuf-Befehl tausendmal ausführen lässt. Sie könnten sicher eine Schleife verwenden:
for i in {1..1000}; do
shuf -n $RANDOM -o $(pwgen -N1 10).txt /usr/share/dict/words
done
Aber das macht nicht so viel Spaß wie das Herstellen eines frechen Einzeilers. Stattdessen bereiten wir die Befehle als Strings vor und leiten sie mit einer Pipeline an die bash. Als Test geben Sie den gewünschten Befehl einmal mit echo aus. Fügen Sie einfache Anführungszeichen hinzu, um sicherzustellen, dass $RANDOM nicht ausgewertet wird und pwgen nicht läuft:
$ echo 'shuf -n $RANDOM -o $(pwgen -N1 10).txt /usr/share/dict/words'
shuf -n $RANDOM -o $(pwgen -N1 10).txt /usr/share/dict/words
Dieser Befehl kann leicht mit einer Pipeline zur Ausführung an die bash weitergeleitet werden:
$ echo 'shuf -n $RANDOM -o $(pwgen -N1 10).txt /usr/share/dict/words' | bash
$ ls
eiFohpies1.txt
Geben Sie nun den Befehl tausendmal aus. Dazu verwenden Sie den Befehl yes, der in einer Pipeline an head geleitet wird, und leiten die Ergebnisse dann in einer Pipeline an bash weiter. Sie haben das vierte Rätsel gelöst:
$ yes 'shuf -n $RANDOM -o $(pwgen -N1 10).txt /usr/share/dict/words' \
| head -n 1000 \
| bash
$ ls
Aen1lee0ir.txt IeKaveixa6.txt ahDee9lah2.txt paeR1Poh3d.txt
Ahxiedie2f.txt Kas8ooJahK.txt aoc0Yoohoh.txt sohl7Nohho.txt
CudieNgee4.txt Oe5ophae8e.txt haiV9mahNg.txt uchiek3Eew.txt
...
Hätten Sie anstelle der Textdateien lieber 1.000 zufällige Bilddateien, nutzen Sie dieselbe Technik (yes, head und bash) und ersetzen shuf durch einen Befehl, der ein zufälliges Bild generiert. Hier ist ein Einzeiler, den ich aus einer Lösung von Mark Setchell aus Stack Overflow (https://oreil.ly/ruDwG) adaptiert habe. Diese führt den Befehl convert aus dem Grafikpaket ImageMagick aus, um zufällige Bilder der Größe 100 x 100 Pixel zu erzeugen, die aus farbigen Quadraten bestehen:
$ yes 'convert -size 8x8 xc: +noise Random -scale 100x100 $(pwgen -N1 10).png' \
| head -n 1000 \
| bash
$ ls
Bahdo4Yaop.png Um8ju8gie5.png aing1QuaiX.png ohi4ziNuwo.png
Eem5leijae.png Va7ohchiep.png eiMoog1kou.png ohnohwu4Ei.png
Eozaing1ie.png Zaev4Quien.png hiecima2Ye.png quaepaiY9t.png
...
$ display Bahdo4Yaop.png Das erste Bild anschauen.
Manchmal brauchen Sie zum Testen nur einfach eine große Menge an Dateien mit unterschiedlichen Namen, selbst wenn sie leer sind. Das Generieren von 1.000 leeren Dateien, die file0001.txt bis file1000.txt benannt sind, ist ganz einfach:
$ mkdir /tmp/empties Anlegen eines Verzeichnisses für die Dateien
$ cd /tmp/empties
$ touch file{01..1000}.txt Generieren der Dateien
Falls Sie interessantere Dateinamen bevorzugen, entnehmen Sie diese willkürlich dem Systemwörterbuch. Verwenden Sie grep, um die Namen aus Gründen der Einfachheit auf Kleinbuchstaben zu beschränken (wodurch Sie Leerzeichen, Apostrophe und andere Zeichen vermeiden, die für die Shell eine spezielle Bedeutung haben würden):
$ grep '^[a-z]*$' /usr/share/dict/words
a
aardvark
aardvarks
...
Mischen Sie die Namen mit shuf und geben Sie die ersten 1.000 mit head aus:
$ grep '^[a-z]*$' /usr/share/dict/words | shuf | head -n1000
triplicating
quadruplicates
podiatrists
...
Leiten Sie die Ergebnisse schließlich mit einer Pipeline an xargs, um mit touch die Dateien zu erzeugen:
$ grep '^[a-z]*$' /usr/share/dict/words | shuf | head -n1000 | xargs touch
$ ls
abases distinctly magnolia sadden
abets distrusts maintaining sales
aboard divided malformation salmon
...
Ich hoffe, dass Ihnen die Beispiele in diesem Kapitel geholfen haben, Ihre Fähigkeiten beim Schreiben von frechen Einzeilern zu stärken. Einige davon bieten reproduzierbare Muster, die Ihnen auch in anderen Situationen nützlich sein könnten.
Einen Hinweis habe ich aber noch: Freche Einzeiler sind nicht die einzige Lösung, die es gibt. Sie sind nur ein Ansatz zum effizienten Arbeiten auf der Kommandozeile. Manchmal ist es sinnvoller, ein Shell-Skript zu schreiben. Und dann wieder kann es sein, dass Sie bessere Lösungen finden, wenn Sie auf eine Programmiersprache wie Perl oder Python setzen. Dennoch ist das Schreiben von Einzeilern eine entscheidende Fertigkeit, um wichtige Aufgaben schnell und stilvoll zu erledigen.