Kapitel 4. Erstellen von Kommandozeilen-Tools
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Im Laufe des Buches stelle ich dir viele Befehle und Befehlskombinationen vor, die im Grunde auf eine Zeile passen. Diese sind als Einzeiler oder Pipelines bekannt. Die Möglichkeit, komplexe Aufgaben mit nur einem Einzeiler auszuführen, macht die Kommandozeile so mächtig. Es ist eine ganz andere Erfahrung als das Schreiben und Benutzen herkömmlicher Programme.
Manche Aufgaben führst du nur einmal aus, andere öfter. Manche Aufgaben sind sehr spezifisch, während andere verallgemeinert werden können. Wenn du einen bestimmten Einzeiler regelmäßig wiederholen musst, lohnt es sich, diesen in ein eigenes Kommandozeilen-Tool zu verwandeln. Sowohl Einzeiler als auch Kommandozeilen-Tools haben also ihren Nutzen. Die Möglichkeit zu erkennen, einen Einzeiler oder bestehenden Code in ein Kommandozeilen-Tool zu verwandeln, erfordert Übung und Geschick. Die Vorteile eines Kommandozeilen-Tools sind, dass du dir nicht den gesamten Einzeiler merken musst und dass es die Lesbarkeit verbessert, wenn du es in eine andere Pipeline einbaust. In diesem Sinne kannst du dir ein Kommandozeilen-Tool ähnlich wie eine Funktion in einer Programmiersprache vorstellen.
Der Vorteil der Arbeit mit einer Programmiersprache ist jedoch, dass der Code in einer oder mehreren Dateien steht. Das bedeutet, dass du diesen Code leicht bearbeiten und wiederverwenden kannst. Wenn der Code Parameter hat, kann er sogar verallgemeinert und auf Probleme angewendet werden, die einem ähnlichen Muster folgen.
Kommandozeilen-Tools haben das Beste aus beiden Welten: Sie können von der Kommandozeile aus verwendet werden, sie akzeptieren Parameter und müssen nur einmal erstellt werden. In diesem Kapitel wirst du dich auf zwei Arten mit der Erstellung von Kommandozeilen-Tools vertraut machen. Zunächst erkläre ich, wie du diese Einzeiler in wiederverwendbare Kommandozeilen-Tools verwandelst. Indem du deinen Befehlen Parameter hinzufügst, kannst du die gleiche Flexibilität erreichen, die eine Programmiersprache bietet. Anschließend zeige ich dir, wie du aus Code, der in einer Programmiersprache geschrieben wurde, wiederverwendbare Kommandozeilentools erstellst. Wenn du der Unix-Philosophie folgst, kannst du deinen Code mit anderen Kommandozeilentools kombinieren, die vielleicht in einer ganz anderen Sprache geschrieben wurden. In diesem Kapitel konzentriere ich mich auf drei Programmiersprachen: Bash, Python und R.
Ich bin davon überzeugt, dass das Erstellen von wiederverwendbaren Kommandozeilen-Tools dich langfristig zu einem effizienteren und produktiveren Data Scientist macht. Du wirst nach und nach deinen eigenen Data Science-Werkzeugkasten aufbauen, aus dem du bestehende Tools schöpfen und sie auf Probleme anwenden kannst, auf die du zuvor gestoßen bist.
Tipp
Um einen Einzeiler in ein Shell-Skript zu verwandeln, werde ich ein kleines bisschen Shell-Skripting verwenden. Dieses Buch zeigt nur eine kleine Teilmenge von Konzepten aus dem Shell-Skripting, darunter Variablen, Bedingungen und Schleifen. Ein vollständiger Kurs in Shell-Skripting könnte ein ganzes Buch füllen und sprengt daher den Rahmen dieses Buches. Wenn du tiefer in das Shell-Skripting eintauchen willst, empfehle ich dir das Buch Classic Shell Scripting von Arnold Robbins und Nelson H. F. Beebe (O'Reilly).
Übersicht
In diesem Kapitel erfährst du, wie du..:
-
Einzeiler in parametrisierte Shell-Skripte umwandeln
-
Verwandle vorhandenen Python- und R-Code in wiederverwendbare Kommandozeilen-Tools
Dieses Kapitel beginnt mit den folgenden Dateien:
$ cd /data/ch04 $ l total 32K -rwxr--r-- 1 dst dst 400 Jun 29 14:27 fizzbuzz.py* -rwxr--r-- 1 dst dst 391 Jun 29 14:27 fizzbuzz.R* -rwxr--r-- 1 dst dst 182 Jun 29 14:27 stream.py* -rwxr--r-- 1 dst dst 147 Jun 29 14:27 stream.R* -rwxr--r-- 1 dst dst 105 Jun 29 14:27 top-words-4.sh* -rwxr--r-- 1 dst dst 128 Jun 29 14:27 top-words-5.sh* -rwxr--r-- 1 dst dst 647 Jun 29 14:27 top-words.py* -rwxr--r-- 1 dst dst 584 Jun 29 14:27 top-words.R*
Wie du diese Dateien bekommst, erfährst du in Kapitel 2. Alle anderen Dateien werden entweder heruntergeladen oder mit Hilfe von Kommandozeilen-Tools erstellt.
Umwandlung von Einzeilern in Shell-Skripte
In diesem Abschnitt erkläre ich dir, wie du einen Einzeiler in ein wiederverwendbares Kommandozeilen-Tool verwandelst. Nehmen wir an, du möchtest die 10 am häufigsten verwendeten Wörter in einem Text herausfinden. Nimm das Buch "Alice's Adventures in Wonderland" von Lewis Carroll, das wie viele andere großartige Bücher kostenlos im Project Gutenberg erhältlich ist:
$ curl -sL "https://www.gutenberg.org/files/11/11-0.txt" | trim The Project Gutenberg eBook of Alice’s Adventures in Wonderland, by Lewis... This eBook is for the use of anyone anywhere in the United States and most other parts of the world at no cost and with almost no restrictions whatsoever. You may copy it, give it away or re-use it under the terms of the Project Gutenberg License included with this eBook or online at www.gutenberg.org. If you are not located in the United States, you will have to check the laws of the country where you are located before using this eBook. ... with 3751 more lines
Die folgende Abfolge von Werkzeugen, oder Pipeline, sollte die Arbeit erledigen:
$ curl -sL "https://www.gutenberg.org/files/11/11-0.txt" | > tr '[:upper:]' '[:lower:]' | > grep -oE "[a-z\']{2,}" | > sort | > uniq -c | > sort -nr | > head -n 10 1839 the 942 and 811 to 638 of 610 it 553 she 486 you 462 said 435 in 403 alice
Lade ein ebook mit
curl
herunter.Wandle den gesamten Text mit
tr
in Kleinbuchstaben um.1Extrahiere alle Wörter mit
grep
2 und setze jedes Wort in eine eigene Zeile.Sortiere diese Wörter in alphabetischer Reihenfolge mit
sort
.Entferne alle Duplikate und zähle mit
uniq
, wie oft jedes Wort in der Liste vorkommt.3Sortiere diese Liste mit eindeutigen Wörtern nach ihrer Anzahl in absteigender Reihenfolge mit
sort
.Behalte nur die obersten 10 Zeilen (d.h. die Wörter) mit
head
.
Diese Wörter kommen in der Tat am häufigsten im Text vor. Da diese Wörter (abgesehen von alice) in vielen englischen Texten sehr häufig vorkommen, haben sie nur wenig Bedeutung. Sie sind unter als Stoppwörter bekannt. Wenn wir sie loswerden, bleiben die häufigsten Wörter übrig, die mit dem Text zu tun haben.
Hier ist eine Liste von Stoppwörtern, die ich gefunden habe:
$ curl -sL "https://raw.githubusercontent.com/stopwords-iso/stopwords-en/master/ stopwords-en.txt" | > sort | tee stopwords | trim 20 10 39 a able ableabout about above abroad abst accordance according accordingly across act actually ad added adj adopted ae … with 1278 more lines
Mit grep
können wir die Stoppwörter herausfiltern, bevor wir mit dem Zählen beginnen:
$ curl -sL "https://www.gutenberg.org/files/11/11-0.txt" | > tr '[:upper:]' '[:lower:]' | > grep -oE "[a-z\']{2,}" | > sort | > grep -Fvwf stopwords | > uniq -c | > sort -nr | > head -n 10 403 alice 98 gutenberg 88 project 76 queen 71 time 63 king 60 turtle 57 mock 56 hatter 55 gryphon
Holen Sie sich die Muster aus einer Datei( in unserem FallStoppwörter ), ein Muster pro Zeile, mit
-f
. Interpretieren Sie diese Muster als feste Zeichenketten mit-F
. Wähle nur die Zeilen mit Übereinstimmungen aus, die ganze Wörter bilden, mit-w
. Wähle nicht übereinstimmende Zeilen mit-v
.
Tipp
Jedes Kommandozeilentool, das in diesem Einzeiler verwendet, bietet eine Manpage. Wenn du also mehr über grep
wissen möchtest, kannst du man grep
von der Kommandozeile aus aufrufen. Die Kommandozeilentools tr
, grep
, uniq
und sort
werden im nächsten Kapitel ausführlicher behandelt.
Es spricht nichts dagegen, diesen Einzeiler nur einmal auszuführen. Aber stell dir vor, du möchtest die 10 wichtigsten Wörter jedes Ebooks auf Project Gutenberg anzeigen lassen. Oder stell dir vor, du möchtest die 10 wichtigsten Wörter stündlich auf einer Nachrichten-Website anzeigen lassen. In diesen Fällen wäre es am besten, diesen Einzeiler als separaten Baustein zu verwenden, der Teil von etwas Größerem sein kann. Um diesen Einzeiler in Bezug auf die Parameter flexibler zu gestalten, machen wir ein Shell-Skript daraus.
So können wir den Einzeiler als Ausgangspunkt nehmen und ihn schrittweise verbessern. Um diesen Einzeiler in ein wiederverwendbares Kommandozeilen-Tool zu verwandeln, führe ich dich durch die folgenden sechs Schritte:
-
Kopiere den Einzeiler und füge ihn in eine Datei ein.
-
Füge Ausführungsberechtigungen hinzu.
-
Definiere einen sogenannten Shebang.
-
Entferne den festen Eingangsteil.
-
Füge einen Parameter hinzu.
-
Optional kannst du deinen PATH erweitern.
Schritt 1: Eine Datei erstellen
Der erste Schritt besteht darin, eine neue Datei zu erstellen. Du kannst deinen Lieblingseditor öffnen und den Einzeiler kopieren und einfügen. Nennen wir die Datei top-words-1.sh, um anzuzeigen, dass dies der erste Schritt zu unserem neuen Kommandozeilen-Tool ist. Wenn du lieber in der Kommandozeile bleibst, kannst du das eingebaute fc
verwenden, das für " fix command" steht und mit dem du den zuletzt ausgeführten Befehl korrigieren oder bearbeiten kannst:
$ fc
Wenn du fc
aufrufst, wird der Standardtexteditor aufgerufen, der in der Umgebungsvariablen EDITOR
gespeichert ist. Im Docker-Container ist diese auf nano
gesetzt,4 Wie du sehen kannst, enthält diese Datei unseren Einzeiler:
GNU nano 5.4 /tmp/zsh9198lv curl -sL "https://www.gutenberg.org/files/11/11-0.txt" | tr '[:upper:]' '[:lower:]' | grep -oE "[a-z\']{2,}" | sort | grep -Fvwf stopwords | uniq -c | sort -nr | head -n 10 [ Read 8 lines ] ^G Help ^O Write Out ^W Where Is ^K Cut ^T Execute ^C Location ^X Exit ^R Read File ^\ Replace ^U Paste ^J Justify ^_ Go To Line
Geben wir dieser temporären Datei einen richtigen Namen, indem wir Strg-O drücken, den temporären Dateinamen entfernen und Folgendes eingeben top-words-1.sh
:
GNU nano 5.4 /tmp/zsh9198lv curl -sL "https://www.gutenberg.org/files/11/11-0.txt" | tr '[:upper:]' '[:lower:]' | grep -oE "[a-z\']{2,}" | sort | grep -Fvwf stopwords | uniq -c | sort -nr | head -n 10 File Name to Write: top-words-1.sh ^G Help M-D DOS Format M-A Append M-B Backup File ^C Cancel M-M Mac Format M-P Prepend ^T Browse
Drücke Enter:
GNU nano 5.4 /tmp/zsh9198lv curl -sL "https://www.gutenberg.org/files/11/11-0.txt" | tr '[:upper:]' '[:lower:]' | grep -oE "[a-z\']{2,}" | sort | grep -Fvwf stopwords | uniq -c | sort -nr | head -n 10 Save file under DIFFERENT NAME? Y Yes N No ^C Cancel
Drücke Y, um zu bestätigen, dass du unter einem anderen Dateinamen speichern möchtest:
GNU nano 5.4 top-words-1.sh curl -sL "https://www.gutenberg.org/files/11/11-0.txt" | tr '[:upper:]' '[:lower:]' | grep -oE "[a-z\']{2,}" | sort | grep -Fvwf stopwords | uniq -c | sort -nr | head -n 10 [ Wrote 8 lines ] ^G Help ^O Write Out ^W Where Is ^K Cut ^T Execute ^C Location ^X Exit ^R Read File ^\ Replace ^U Paste ^J Justify ^_ Go To Line
Drücke Strg-X, um nano
zu verlassen und zu dem Ort zurückzukehren, von dem du gekommen bist.
Wir verwenden die Datei mit der Endung .sh, um zu verdeutlichen, dass wir ein Shell-Skript erstellen. Befehlszeilentools müssen jedoch keine Endung haben. Tatsächlich haben Befehlszeilentools nur selten Endungen.
Bestätige den Inhalt der Datei:
$ pwd /data/ch04 $ l total 44K -rwxr--r-- 1 dst dst 400 Jun 29 14:27 fizzbuzz.py* -rwxr--r-- 1 dst dst 391 Jun 29 14:27 fizzbuzz.R* -rw-r--r-- 1 dst dst 7.5K Jun 29 14:27 stopwords -rwxr--r-- 1 dst dst 182 Jun 29 14:27 stream.py* -rwxr--r-- 1 dst dst 147 Jun 29 14:27 stream.R* -rw-r--r-- 1 dst dst 173 Jun 29 14:27 top-words-1.sh -rwxr--r-- 1 dst dst 105 Jun 29 14:27 top-words-4.sh* -rwxr--r-- 1 dst dst 128 Jun 29 14:27 top-words-5.sh* -rwxr--r-- 1 dst dst 647 Jun 29 14:27 top-words.py* -rwxr--r-- 1 dst dst 584 Jun 29 14:27 top-words.R* $ bat top-words-1.sh ───────┬──────────────────────────────────────────────────────────────────────── │ File: top-words-1.sh ───────┼──────────────────────────────────────────────────────────────────────── 1 │ curl -sL "https://www.gutenberg.org/files/11/11-0.txt" | 2 │ tr '[:upper:]' '[:lower:]' | 3 │ grep -oE "[a-z\']{2,}" | 4 │ sort | 5 │ grep -Fvwf stopwords | 6 │ uniq -c | 7 │ sort -nr | 8 │ head -n 10 ───────┴────────────────────────────────────────────────────────────────────────
Du kannst nun bash
verwenden5 verwenden, um die Befehle in der Datei zu interpretieren und auszuführen:
$ bash top-words-1.sh 403 alice 98 gutenberg 88 project 76 queen 71 time 63 king 60 turtle 57 mock 56 hatter 55 gryphon
Das erspart dir, den Einzeiler beim nächsten Mal noch einmal zu tippen.
Da die Datei jedoch nicht eigenständig ausgeführt werden kann, ist sie noch kein echtes Kommandozeilentool. Das wollen wir im nächsten Schritt ändern.
Schritt 2: Erlaubnis zum Ausführen erteilen
Der Grund, warum wir unsere Datei nicht direkt ausführen können, ist, dass wir nicht die richtigen Zugriffsrechte haben. Insbesondere du als Benutzer musst die Berechtigung haben, die Datei auszuführen. In diesem Abschnitt ändern wir die Zugriffsrechte unserer Datei.
Um die Unterschiede zwischen den Schritten zu vergleichen, kopiere die Datei mit cp -v top-words-{1,2}.sh
nach top-words-2.sh.
Tipp
Wenn du überprüfen willst, wohin die Klammererweiterung oder eine andere Form der Dateierweiterung führt, ersetze den Befehl durch echo
bis und drucke das Ergebnis aus - zum Beispiel echo book_{draft,final}.md
oder echo agent-{001..007}
.
Um die Zugriffsrechte einer Datei zu ändern, müssen wir ein Kommandozeilentool namens chmod
verwenden,6 Es ändert die Dateimodus-Bits einer bestimmten Datei. Der folgende Befehl gibt dem Benutzer (dir) die Berechtigung, top-words-2.sh auszuführen:
$ cp -v top-words-{1,2}.sh 'top-words-1.sh' -> 'top-words-2.sh' $ chmod u+x top-words-2.sh
Das Argument u+x
besteht aus drei Zeichen: (1) u
zeigt an, dass wir die Berechtigungen für den Benutzer ändern wollen, der die Datei besitzt, also dich, weil du die Datei erstellt hast; (2) +
zeigt an, dass wir eine Berechtigung hinzufügen wollen; und (3) x
zeigt die Ausführungsberechtigung an.
Schauen wir uns nun die Zugriffsberechtigungen beider Dateien an:
$ l top-words-{1,2}.sh -rw-r--r-- 1 dst dst 173 Jun 29 14:27 top-words-1.sh -rwxr--r-- 1 dst dst 173 Jun 29 14:28 top-words-2.sh*
Die erste Spalte zeigt die Zugriffsrechte für jede Datei. Für top-words-2.sh ist dies -rwxr—r--
Das erste Zeichen, -
(Bindestrich), gibt den Dateityp an. Ein -
steht für eine normale Datei und ein d
bedeutet Verzeichnis. Die nächsten drei Zeichen, rwx
geben die Zugriffsrechte für den Benutzer an, dem die Datei gehört. Die r
und w
bedeuten Lesen bzw. Schreiben. (Wie du siehst, hat top-words-1.sh ein -
statt eines x
was bedeutet, dass wir diese Datei nicht ausführen können.) Die nächsten drei Zeichen, rw-
geben die Zugriffsberechtigungen für alle Mitglieder der Gruppe an, der die Datei gehört. Die letzten drei Zeichen in der Spalte r--
die Zugriffsberechtigungen für alle anderen Benutzer an.
Jetzt kannst du die Datei wie folgt ausführen:
$ ./top-words-2.sh 403 alice 98 gutenberg 88 project 76 queen 71 time 63 king 60 turtle 57 mock 56 hatter 55 gryphon
Wenn du versuchst, eine Datei auszuführen, für die du nicht die richtigen Zugriffsrechte hast, wie bei top-words-1.sh, siehst du folgende Fehlermeldung:
$ ./top-words-1.sh zsh: permission denied: ./top-words-1.sh
Schritt 3: Definiere ein Shebang
Obwohl wir die Datei bereits alleine ausführen können, sollten wir der Datei einen sogenannten Shebang hinzufügen. Der Shebang ist eine spezielle Zeile im Skript, die dem System mitteilt, welche ausführbare Datei es zur Interpretation der Befehle verwenden soll.
Der Name shebang kommt von den ersten beiden Zeichen: einer Raute (she) und einem Ausrufezeichen (bang): #!
. Es ist keine gute Idee, es wegzulassen, wie wir es im vorherigen Schritt getan haben, denn jede Shell hat eine andere Standardausführungskomponente. Die Z-Shell, die wir im gesamten Buch verwenden, verwendet standardmäßig die Ausführungskomponente /bin/sh, wenn kein shebang definiert ist. In diesem Fall möchte ich, dass bash
die Befehle interpretiert, denn das gibt uns etwas mehr Funktionalität als sh
.
Auch hier steht es dir frei, einen beliebigen Editor zu verwenden, aber ich werde mich an nano
halten, der im Docker-Image installiert ist:
$ cp -v top-words-{2,3}.sh 'top-words-2.sh' -> 'top-words-3.sh' $ nano top-words-3.sh
GNU nano 5.4 top-words-3.sh curl -sL "https://www.gutenberg.org/files/11/11-0.txt" | tr '[:upper:]' '[:lower:]' | grep -oE "[a-z\']{2,}" | sort | grep -Fvwf stopwords | uniq -c | sort -nr | head -n 10 [ Read 8 lines ] ^G Help ^O Write Out ^W Where Is ^K Cut ^T Execute ^C Location ^X Exit ^R Read File ^\ Replace ^U Paste ^J Justify ^_ Go To Line
Gib nun ein #!/usr/bin/env bash
und drücke die Eingabetaste. Wenn du fertig bist, drücke Strg-X zum Speichern und Beenden:
GNU nano 5.4 top-words-3.sh * #!/usr/bin/env bash curl -sL "https://www.gutenberg.org/files/11/11-0.txt" | tr '[:upper:]' '[:lower:]' | grep -oE "[a-z\']{2,}" | sort | grep -Fvwf stopwords | uniq -c | sort -nr | head -n 10 Save modified buffer? Y Yes N No ^C Cancel
Drücke Y, um anzuzeigen, dass du die Datei speichern möchtest:
GNU nano 5.4 top-words-3.sh * #!/usr/bin/env bash curl -sL "https://www.gutenberg.org/files/11/11-0.txt" | tr '[:upper:]' '[:lower:]' | grep -oE "[a-z\']{2,}" | sort | grep -Fvwf stopwords | uniq -c | sort -nr | head -n 10 File Name to Write: top-words-3.sh ^G Help M-D DOS Format M-A Append M-B Backup File ^C Cancel M-M Mac Format M-P Prepend ^T Browse
Lass uns überprüfen, wie top-words-3.sh aussieht:
$ bat top-words-3.sh ───────┬──────────────────────────────────────────────────────────────────────── │ File: top-words-3.sh ───────┼──────────────────────────────────────────────────────────────────────── 1 │ #!/usr/bin/env bash 2 │ curl -sL "https://www.gutenberg.org/files/11/11-0.txt" | 3 │ tr '[:upper:]' '[:lower:]' | 4 │ grep -oE "[a-z\']{2,}" | 5 │ sort | 6 │ grep -Fvwf stopwords | 7 │ uniq -c | 8 │ sort -nr | 9 │ head -n 10 ───────┴────────────────────────────────────────────────────────────────────────
Das ist genau das, was wir brauchen: unsere ursprüngliche Pipeline mit einem Paukenschlag davor.
Manchmal stößt man auf Skripte, die ein "Shebang" in Form von !/usr/bin/bash
, oder !/usr/bin/python
im Fall von Python (wie wir im nächsten Abschnitt sehen werden). Das funktioniert zwar im Allgemeinen, aber wenn die bash
oder python
7 an einem anderen Ort als /usr/bin installiert sind, funktioniert das Skript nicht mehr. Es ist besser, die Form zu verwenden, die ich hier vorstelle, nämlich !/usr/bin/env bash
und !/usr/bin/env python
zu verwenden, weil die env
8 weiß, wo bash
und python
installiert sind. Kurz gesagt, mit env
sind deine Skripte besser portierbar.
Schritt 4: Entfernen Sie den festen Eingang
Wir haben jetzt ein gültiges Kommandozeilentool , das wir von der Kommandozeile aus ausführen können. Aber wir können noch mehr tun. Wir können unser Kommandozeilentool wiederverwendbar machen. Der erste Befehl in unserer Datei ist curl
, mit dem den Text herunterlädt, aus dem wir die 10 meistverwendeten Wörter ermitteln wollen. So werden die Daten und die Operationen in einem zusammengefasst.
Was wäre, wenn wir die Top 10 der meistverwendeten Wörter aus einem anderen E-Book oder einem anderen Text abrufen wollten? Die Eingabedaten sind in den Tools selbst festgelegt. Es wäre besser, die Daten vom Kommandozeilen-Tool zu trennen.
Wenn wir davon ausgehen, dass der Benutzer des Kommandozeilen-Tools den Text bereitstellt, wird das Tool allgemein anwendbar. Die Lösung ist also, den Befehl curl
aus dem Skript zu entfernen. Hier ist das aktualisierte Skript namens top-words-4.sh:
$ cp -v top-words-{3,4}.sh 'top-words-3.sh' -> 'top-words-4.sh' $ sed -i '2d' top-words-4.sh $ bat top-words-4.sh ───────┬──────────────────────────────────────────────────────────────────────── │ File: top-words-4.sh ───────┼──────────────────────────────────────────────────────────────────────── 1 │ #!/usr/bin/env bash 2 │ tr '[:upper:]' '[:lower:]' | 3 │ grep -oE "[a-z\']{2,}" | 4 │ sort | 5 │ grep -Fvwf stopwords | 6 │ uniq -c | 7 │ sort -nr | 8 │ head -n 10 ───────┴────────────────────────────────────────────────────────────────────────
Das funktioniert, weil ein Skript, das mit einem Befehl beginnt, der Daten von der Standardeingabe benötigt, wie tr
, die Eingabe übernimmt, die den Befehlszeilentools gegeben wird. ZumBeispiel:
$ curl -sL 'https://www.gutenberg.org/files/11/11-0.txt' | ./top-words-4.sh 403 alice 98 gutenberg 88 project 76 queen 71 time 63 king 60 turtle 57 mock 56 hatter 55 gryphon $ curl -sL 'https://www.gutenberg.org/files/12/12-0.txt' | ./top-words-4.sh 469 alice 189 queen 98 gutenberg 88 project 72 time 71 red 70 white 67 king 63 head 59 knight $ man bash | ./top-words-4.sh 585 command 332 set 313 word 313 option 304 file 300 variable 298 bash 258 list 257 expansion 238 history
Tipp
Auch wenn wir dies in unserem Skript nicht getan haben, gilt das gleiche Prinzip für das Speichern von Daten. Im Allgemeinen ist es besser, dies dem Benutzer mit Hilfe der Ausgabeumleitung zu überlassen, als das Skript in eine bestimmte Datei schreiben zu lassen. Wenn du ein Kommandozeilentool nur für deine eigenen Projekte verwenden willst, sind dir natürlich keine Grenzen gesetzt wie spezifisch du sein kannst.
Schritt 5: Argumente hinzufügen
Es gibt noch einen weiteren Schritt, um unser Kommandozeilentool noch besser nutzbar zu machen: Parameter. In unserem Kommandozeilentool gibt es eine Reihe fester Kommandozeilenargumente - zum Beispiel -nr
für sort
und -n 10
für head
. Es ist wahrscheinlich am besten, das erste Argument fest zu halten. Es wäre jedoch sehr nützlich, verschiedene Werte für den Befehl head
zuzulassen. Damit könnte der Endbenutzer die Anzahl der am häufigsten verwendeten Wörter festlegen, die ausgegeben werden sollen. Im Folgenden siehst du, wie unsere Datei top-words-5.sh aussieht:
$ bat top-words-5.sh ───────┬──────────────────────────────────────────────────────────────────────── │ File: top-words-5.sh ───────┼──────────────────────────────────────────────────────────────────────── 1 │ #!/usr/bin/env bash 2 │ 3 │ NUM_WORDS="${1:-10}" 4 │ 5 │ tr '[:upper:]' '[:lower:]' | 6 │ grep -oE "[a-z\']{2,}" | 7 │ sort | 8 │ grep -Fvwf stopwords | 9 │ uniq -c | 10 │ sort -nr | 11 │ head -n "${NUM_WORDS}" ───────┴────────────────────────────────────────────────────────────────────────
-
Die Variable
NUM_WORDS
wird auf den Wert von$1
gesetzt, einer speziellen Variable in der Bash; sie enthält den Wert des ersten Befehlszeilenarguments, das an unser Befehlszeilentool übergeben wird. In der folgenden Tabelle sind die anderen speziellen Variablen der Bash aufgeführt. Wenn kein Wert angegeben wird, nimmtNUM_WORDS
den Wert10
an. -
Beachte, dass du ein Dollarzeichen vor den Wert der Variable
NUM_WORDS
setzen musst, um ihn zu verwenden. Wenn du sie setzt, schreibst du kein Dollarzeichen.
Wir hätten $1
auch direkt als Argument für head
verwenden können und uns nicht die Mühe machen müssen, eine zusätzliche Variable wie NUM_WORDS
zu erstellen. Bei größeren Skripten und ein paar weiteren Befehlszeilenargumenten wie $2
und $3
wird dein Code jedoch lesbarer, wenn du benannte Variablen verwendest.
Wenn wir nun die 20 meistgenutzten Wörter unseres Textes sehen wollen, rufen wir unser Kommandozeilen-Tool wie folgt auf:
$ curl -sL "https://www.gutenberg.org/files/11/11-0.txt" > alice.txt $ < alice.txt ./top-words-5.sh 20 403 alice 98 gutenberg 88 project 76 queen 71 time 63 king 60 turtle 57 mock 56 hatter 55 gryphon 53 rabbit 50 head 48 voice 45 looked 44 mouse 42 duchess 40 tone 40 dormouse 37 cat 34 march
Wenn der Benutzer keine Zahl angibt, zeigt unser Skript die 10 häufigsten Wörter an:
$ < alice.txt ./top-words-5.sh 403 alice 98 gutenberg 88 project 76 queen 71 time 63 king 60 turtle 57 mock 56 hatter 55 gryphon
Schritt 6: Erweitere deinen PATH
Nach den vorangegangenen fünf Schritten haben wir endlich ein wiederverwendbares Kommandozeilentool erstellt. Es gibt jedoch einen weiteren Schritt, der sehr nützlich sein kann. In diesem optionalen Schritt stellen wir sicher, dass du deine Kommandozeilentools von überall aus ausführen kannst.
Wenn du dein Kommandozeilentool ausführen möchtest, musst du entweder zu dem Verzeichnis navigieren, in dem es sich befindet, oder den vollständigen Pfadnamen angeben, wie in Schritt 2 gezeigt. Das ist in Ordnung, wenn das Kommandozeilentool speziell für ein bestimmtes Projekt erstellt wurde. Wenn dein Kommandozeilentool jedoch in mehreren Situationen eingesetzt werden kann, ist es nützlich, es von überall aus ausführen zu können, genau wie die Kommandozeilentools, die mit Ubuntu geliefert werden.
Um dies zu erreichen, muss die Bash wissen, wo sie nach deinen Kommandozeilen-Tools suchen muss. Dazu durchläuft sie eine Liste von Verzeichnissen, die in einer Umgebungsvariablen namens PATH
gespeichert sind. In einem frischen Docker-Container sieht PATH
wie folgt aus:
$ echo $PATH /usr/local/lib/R/site-library/rush/exec:/usr/bin/dsutils:/home/dst/.local/bin:/u sr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Die Verzeichnisse werden durch Doppelpunkte getrennt. Wir können diese als eine Liste von Verzeichnissen ausgeben, indem wir die Doppelpunkte in Zeilenumbrüche umwandeln:
$ echo $PATH | tr ':' '\n' /usr/local/lib/R/site-library/rush/exec /usr/bin/dsutils /home/dst/.local/bin /usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin
Um PATH
dauerhaft zu ändern, musst du die .bashrc- oder .profile-Datei in deinem Home-Verzeichnis bearbeiten. Wenn du alle deine benutzerdefinierten Kommandozeilen-Tools in einem Verzeichnis ablegst - z.B. ~/tools -,brauchst du PATH
nur einmal zu ändern. Dann brauchst du das ./ nicht mehr hinzuzufügen und kannst einfach den Dateinamen verwenden. Außerdem musst du dich nicht mehr daran erinnern, wo sich das Kommandozeilen-Tool befindet:
$ cp -v top-words{-5.sh,} 'top-words-5.sh' -> 'top-words' $ export PATH="${PATH}:/data/ch04" $ echo $PATH /usr/local/lib/R/site-library/rush/exec:/usr/bin/dsutils:/home/dst/.local/bin:/u sr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/data/ch04 $ curl "https://www.gutenberg.org/files/11/11-0.txt" | > top-words 10 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 170k 100 170k 0 0 223k 0 --:--:-- --:--:-- --:--:-- 223k 403 alice 98 gutenberg 88 project 76 queen 71 time 63 king 60 turtle 57 mock 56 hatter 55 gryphon
Erstellen von Kommandozeilen-Tools mit Python und R
Das Kommandozeilentool , das wir im vorigen Abschnitt erstellt haben, wurde in Bash geschrieben. (Sicherlich wurden nicht alle Funktionen der Programmiersprache Bash verwendet, aber der Interpreter war trotzdem bash
.) Wie du inzwischen weißt, ist die Kommandozeile sprachunabhängig, so dass wir für die Erstellung von Kommandozeilentools nicht unbedingt Bash verwenden müssen.
In diesem Abschnitt zeige ich, dass Kommandozeilen-Tools auch in anderen Programmiersprachen erstellt werden können. Ich konzentriere mich auf Python und R, weil dies die beiden beliebtesten Programmiersprachen in der Data Science Community sind. Ich kann keine vollständige Einführung in eine der beiden Sprachen geben, daher gehe ich davon aus, dass du mit Python und/oder R vertraut bist. Andere Programmiersprachen wie Java, Go und Julia folgen einem ähnlichen Muster, wenn es um die Erstellung von Kommandozeilen-Tools geht.
Es gibt drei Hauptgründe für die Entwicklung von Kommandozeilen-Tools in einer anderen Programmiersprache als Bash: Erstens hast du vielleicht schon Code, den du gerne von der Kommandozeile aus verwenden möchtest. Zweitens würde das Kommandozeilen-Tool mehr als hundert Zeilen Bash-Code umfassen. Drittens muss das Kommandozeilen-Tool sicherer und robuster sein (Bash fehlen viele Funktionen, wie z.B. die Typüberprüfung).
Die sechs Schritte, die ich im vorherigen Abschnitt erläutert habe, gelten im Großen und Ganzen auch für die Erstellung von Kommandozeilen-Tools in anderen Programmiersprachen. Der erste Schritt wäre jedoch nicht das Kopieren und Einfügen von der Kommandozeile, sondern das Kopieren und Einfügen des entsprechenden Codes in eine neue Datei. Kommandozeilen-Tools, die in Python und R geschrieben wurden, müssen python
und Rscript
9 als Interpreter nach demShebang angeben.
Wenn es darum geht, Kommandozeilen-Tools mit Python und R zu erstellen, gibt es zwei weitere Aspekte, die besondere Aufmerksamkeit verdienen: Erstens muss die Verarbeitung von Standardeingaben, die bei Shell-Skripten selbstverständlich ist, in Python und R explizit berücksichtigt werden.
Portierung des Shell-Skripts
Sehen wir uns zunächst an, wie wir das Shell-Skript, das wir gerade erstellt haben, sowohl auf Python als auch auf R portieren würden. Mit anderen Worten: Welcher Python- und R-Code liefert uns die am häufigsten verwendeten Wörter aus der Standardeingabe? Wir zeigen zunächst die beiden Dateien top-words.py und top-words.R und diskutieren dann die Unterschiede zum Shell-Code. In Python würde der Code etwa wie folgt aussehen:
$ cd /data/ch04 $ bat top-words.py ───────┬──────────────────────────────────────────────────────────────────────── │ File: top-words.py ───────┼──────────────────────────────────────────────────────────────────────── 1 │ #!/usr/bin/env python 2 │ import re 3 │ import sys 4 │ 5 │ from collections import Counter 6 │ from urllib.request import urlopen 7 │ 8 │ def top_words(text, n): 9 │ with urlopen("https://raw.githubusercontent.com/stopwords-iso/stopw │ ords-en/master/stopwords-en.txt") as f: 10 │ stopwords = f.read().decode("utf-8").split("\n") 11 │ 12 │ words = re.findall("[a-z']{2,}", text.lower()) 13 │ words = (w for w in words if w not in stopwords) 14 │ 15 │ for word, count in Counter(words).most_common(n): 16 │ print(f"{count:>7} {word}") 17 │ 18 │ 19 │ if __name__ == "__main__": 20 │ text = sys.stdin.read() 21 │ 22 │ try: 23 │ n = int(sys.argv[1]) 24 │ except: 25 │ n = 10 26 │ 27 │ top_words(text, n) ───────┴────────────────────────────────────────────────────────────────────────
Beachte, dass dieses Python-Beispiel keine Pakete von Drittanbietern verwendet. Wenn du fortgeschrittene Textverarbeitung betreiben willst, empfehle ich dir das NLTK-Paket.10 Wenn du mit vielen numerischen Daten arbeiten willst, empfehle ich dir das Pandas-Paket.11
In R würde der Code etwa so aussehen:
$ bat top-words.R ───────┬──────────────────────────────────────────────────────────────────────── │ File: top-words.R ───────┼──────────────────────────────────────────────────────────────────────── 1 │ #!/usr/bin/env Rscript 2 │ n <- as.integer(commandArgs(trailingOnly = TRUE)) 3 │ if (length(n) == 0) n <- 10 4 │ 5 │ f_stopwords <- url("https://raw.githubusercontent.com/stopwords-iso/sto │ pwords-en/master/stopwords-en.txt") 6 │ stopwords <- readLines(f_stopwords, warn = FALSE) 7 │ close(f_stopwords) 8 │ 9 │ f_text <- file("stdin") 10 │ lines <- tolower(readLines(f_text)) 11 │ 12 │ words <- unlist(regmatches(lines, gregexpr("[a-z']{2,}", lines))) 13 │ words <- words[is.na(match(words, stopwords))] 14 │ 15 │ counts <- sort(table(words), decreasing = TRUE) 16 │ cat(sprintf("%7d %s\n", counts[1:n], names(counts[1:n])), sep = "") 17 │ close(f_text) ───────┴────────────────────────────────────────────────────────────────────────
Überprüfen wir, ob alle drei Implementierungen (Bash, Python und R) die gleichen fünf Wörter mit der gleichen Anzahl zurückgeben:
$ time < alice.txt top-words 5 403 alice 98 gutenberg 88 project 76 queen 71 time top-words 5 < alice.txt 0.08s user 0.01s system 107% cpu 0.084 total $ time < alice.txt top-words.py 5 403 alice 98 gutenberg 88 project 76 queen 71 time top-words.py 5 < alice.txt 0.38s user 0.02s system 82% cpu 0.478 total $ time < alice.txt top-words.R 5 403 alice 98 gutenberg 88 project 76 queen 71 time top-words.R 5 < alice.txt 0.29s user 0.07s system 56% cpu 0.652 total
Wunderbar! Sicher, die Ausgabe an sich ist nicht sehr aufregend. Spannend ist aber, dass wir dieselbe Aufgabe mit mehreren Sprachen erledigen können. Schauen wir uns die Unterschiede zwischen den Ansätzen an.
Was sofort auffällt, sind die Unterschiede in der Menge des Codes. Für diese spezielle Aufgabe benötigen sowohl Python als auch R viel mehr Code als Bash. Das zeigt, dass es für manche Aufgaben besser ist, die Kommandozeile zu benutzen. Für andere Aufgaben ist es vielleicht besser, eine Programmiersprache zu verwenden. Wenn du mehr Erfahrung mit der Kommandozeile sammelst, wirst du erkennen, wann du welchen Ansatz verwenden solltest. Wenn alles ein Kommandozeilentool ist, kannst du die Aufgabe sogar in Teilaufgaben aufteilen und ein Bash-Kommandozeilentool mit einem Python-Kommandozeilentool kombinieren - je nachdem, welcher Ansatz für die jeweilige Aufgabe am besten funktioniert .
Verarbeitung von Streaming-Daten aus der Standardeingabe
In den beiden vorangegangenen Codeschnipseln lesen sowohl Python als auch R die gesamte Standardeingabe auf einmal. Auf der Kommandozeile leiten die meisten Tools die Daten in einem Streaming-Verfahren an das nächste Kommandozeilen-Tool weiter. Einige Kommandozeilen-Tools wie sort
benötigen die vollständigen Daten, bevor sie Daten auf die Standardausgabe schreiben. Das bedeutet, dass die Pipeline von diesen Tools blockiert wird. Das muss kein Problem sein, wenn die Eingabedaten endlich sind, wie z. B. eine Datei. Wenn die Eingabedaten jedoch ein ununterbrochener Strom sind, sind solche blockierenden Kommandozeilen-Tools nutzlos.
Zum Glück unterstützen Python und R die Verarbeitung von Streaming-Daten. Du kannst zum Beispiel eine Funktion zeilenweise anwenden. Hier sind zwei minimale Beispiele, die zeigen, wie das in Python bzw. R funktioniert.
Sowohl die Python- als auch die R-Tools lösen das berühmt-berüchtigte Fizz-Buzz-Problem, das wie folgt definiert ist: Drucke jede Zahl von 1 bis 100, aber wenn die Zahl durch 3 teilbar ist, drucke stattdessen "fizz"; wenn die Zahl durch 5 teilbar ist, drucke "buzz"; und wenn die Zahl durch 15 teilbar ist, drucke "fizzbuzz". Hier ist der Python-Code:12
$ bat fizzbuzz.py ───────┬──────────────────────────────────────────────────────────────────────── │ File: fizzbuzz.py ───────┼──────────────────────────────────────────────────────────────────────── 1 │ #!/usr/bin/env python 2 │ import sys 3 │ 4 │ CYCLE_OF_15 = ["fizzbuzz", None, None, "fizz", None, 5 │ "buzz", "fizz", None, None, "fizz", 6 │ "buzz", None, "fizz", None, None] 7 │ 8 │ def fizz_buzz(n: int) -> str: 9 │ return CYCLE_OF_15[n % 15] or str(n) 10 │ 11 │ if __name__ == "__main__": 12 │ try: 13 │ while (n:= sys.stdin.readline()): 14 │ print(fizz_buzz(int(n))) 15 │ except: 16 │ pass ───────┴────────────────────────────────────────────────────────────────────────
Und hier ist der R-Code:
$ bat fizzbuzz.R ───────┬──────────────────────────────────────────────────────────────────────── │ File: fizzbuzz.R ───────┼──────────────────────────────────────────────────────────────────────── 1 │ #!/usr/bin/env Rscript 2 │ cycle_of_15 <- c("fizzbuzz", NA, NA, "fizz", NA, 3 │ "buzz", "fizz", NA, NA, "fizz", 4 │ "buzz", NA, "fizz", NA, NA) 5 │ 6 │ fizz_buzz <- function(n) { 7 │ word <- cycle_of_15[as.integer(n) %% 15 + 1] 8 │ ifelse(is.na(word), n, word) 9 │ } 10 │ 11 │ f <- file("stdin") 12 │ open(f) 13 │ while(length(n <- readLines(f, n = 1)) > 0) { 14 │ write(fizz_buzz(n), stdout()) 15 │ } 16 │ close(f) ───────┴────────────────────────────────────────────────────────────────────────
Testen wir beide Tools (um Platz zu sparen, habe ich die Ausgabe an column
weitergeleitet):
$ seq 30 | fizzbuzz.py | column -x 1 2 fizz 4 buzz fizz 7 8 fizz buzz 11 fizz 13 14 fizzbuzz 16 17 fizz 19 buzz fizz 22 23 fizz buzz 26 fizz 28 29 fizzbuzz $ seq 30 | fizzbuzz.R | column -x 1 2 fizz 4 buzz fizz 7 8 fizz buzz 11 fizz 13 14 fizzbuzz 16 17 fizz 19 buzz fizz 22 23 fizz buzz 26 fizz 28 29 fizzbuzz
Diese Ausgabe sieht für mich korrekt aus! Es ist schwierig zu demonstrieren, dass diese beiden Tools tatsächlich im Streaming-Verfahren arbeiten. Du kannst das selbst überprüfen, indem du die Eingabedaten an sample -d 100
weiterleitest, bevor sie an das Python- oder R-Tool weitergeleitet werden. Auf diese Weise fügst du eine kleine Verzögerung zwischen den einzelnen Zeilen ein, sodass es einfacher ist, zu bestätigen, dass die Tools nicht auf die gesamten Eingabedaten warten, sondern stattdessen Zeile für Zeile arbeiten.
Zusammenfassung
In diesem Intermezzo-Kapitel habe ich dir gezeigt, wie du dein eigenes Kommandozeilen-Tool erstellst. In nur sechs Schritten verwandelst du deinen Code in einen wiederverwendbaren Baustein, der dich viel produktiver macht. Ich empfehle dir, nach Möglichkeiten Ausschau zu halten, deine eigenen Tools zu erstellen. Das nächste Kapitel behandelt den zweiten Schritt des OSEMN-Modells für Data Science, nämlich das Scrubbing von Daten.
Für weitere Erkundungen
-
Das Hinzufügen einer Hilfedokumentation zu deinem Tool ist wichtig, wenn es viele Optionen gibt, an die du dich erinnern musst, und noch wichtiger, wenn du dein Tool mit anderen teilen möchtest.
docopt
ist ein sprachunabhängiges Framework für die Bereitstellung von Hilfe und die Definition der Optionen, die dein Tool akzeptiert. Es gibt Implementierungen in fast jeder Programmiersprache, einschließlich Bash, Python und R. -
Wenn du mehr über die Programmierung in der Bash erfahren möchtest, empfehle ich dir Classic Shell Programming von Arnold Robbins und Nelson H. F. Beebe (O'Reilly) und Bash Cookbook, 2nd Edition von Carl Albing und JP Vossen (O'Reilly).
-
Ein robustes und sicheres Bash-Skript zu schreiben, ist ziemlich knifflig. ShellCheck ist ein Online-Tool, das deinen Bash-Code auf Fehler und Schwachstellen prüft. Ein Kommandozeilentool ist ebenfalls verfügbar.
-
Das Buch Ten Essays on Fizz Buzz von Joel Grus (Brightwalton) ist eine aufschlussreiche und lustige Sammlung von 10 verschiedenen Möglichkeiten, Fizz Buzz mit Python zu lösen.
1 Jim Meyering, tr - Translate or Delete Characters, Version 8.30, 2018, https://www.gnu.org/software/coreutils.
2 Jim Meyering, grep - Print Lines That Match Patterns, Version 3.4, 2019, https://www.gnu.org/software/grep.
3 Richard M. Stallman und David MacKenzie, uniq - Report or Omit Repeated Lines, Version 8.30, 2019, https://www.gnu.org/software/coreutils.
4 Benno Schulenberg et al., nano - Nanos ANOther Editor, inspiriert von Pico, Version 5.4, 2020, https://nano-editor.org.
5 Brian Fox und Chet Ramey, bash - GNU Bourne-Again SHell, Version 5.0.17, 2019, https://www.gnu.org/software/bash.
6 David MacKenzie und Jim Meyering, chmod - Change File Mode Bits, Version 8.30, 2018, https://www.gnu.org/software/coreutils.
7 The Python Software Foundation, python - an Interpreted, Interactive, Object-Oriented Programming Language, Version 3.8.5, 2021, https://www.python.org.
8 Richard Mlynarik, David MacKenzie, und Assaf Gordon, env - Run a Program in a Modified Environment, Version 8.32, 2020, https://www.gnu.org/software/coreutils.
9 The R Foundation for Statistical Computing, R - a Language and Environment for Statistical Computing, Version 4.0.4, 2021, https://www.r-project.org.
10 Jacob Perkins, Python Text Processing with NLTK 2.0 Cookbook (Birmingham, UK: Packt, 2010).
11 Wes McKinney, Python for Data Analysis (O'Reilly, 2017).
12 Dieser Code ist einem Python-Skript von Joel Grus entnommen.
Get Datenwissenschaft an der Kommandozeile, 2. now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.