KAPITEL 8

Einen frechen Einzeiler schreiben

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:

  1. Erfinden eines Befehl, der einen Teil des Rätsels löst.
  2. Ausführen des Befehls und Prüfen der Ausgabe.
  3. Zurückrufen des Befehls aus der History und Bearbeiten des Befehls.
  4. Wiederholen der Schritte 2 und 3, bis der Befehl das gewünschte Ergebnis liefert.

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.

image

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.

Machen Sie sich bereit, frech zu sein

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.

Seien Sie flexibel

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?

Denken Sie darüber nach, wo Sie anfangen sollten

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:

$ echo 2021-{01..12}-01 | xargs -n1 date +%B -d

January

February

March

...

December

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.

Lernen Sie Ihre Testwerkzeuge kennen

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!

Einen Dateinamen in eine Sequenz einfügen

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

image

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:

  1. Generieren Sie die Befehlsargumente als Listen auf der Standardausgabe.
  2. Geben Sie die Listen mit paste und einer Prozesssubstitution nebeneinander aus.
  3. Setzen Sie mit sed einen Befehlsnamen voran, indem Sie das Zeilenanfangszeichen (^) durch einen Programmnamen und ein Leerzeichen ersetzen.
  4. Schicken Sie die Ergebnisse mit einer Pipeline an die bash.

Zusammengehörende Dateipaare prüfen

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:

$ diff <(ls *.jpg | cut -d. -f1) <(ls *.txt | cut -d. -f1) \

| grep '^[<>]'

< blue_jay

> oriole

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

Ein CDPATH aus Ihrem Home-Verzeichnis generieren

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:

$ (cd && ls -d */)

Family/ Finances/ Linux/ Music/ Work/

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:..

Testdateien generieren

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:

  1. Die Wörterbuchdatei zufällig mischen.
  2. Eine willkürliche Anzahl an Zeilen aus der Wörterbuchdatei auswählen.
  3. Eine Ausgabedatei für die Ergebnisse erzeugen.
  4. Ihre Lösung tausendmal ausführen.

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.

Leere Dateien generieren

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

...

Zusammenfassung

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.