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" |  1
> tr '[:upper:]' '[:lower:]' |  2
> grep -oE "[a-z\']{2,}" |  3
> sort |  4
> uniq -c |  5
> sort -nr |  6
> head -n 10  7
   1839 the
    942 and
    811 to
    638 of
    610 it
    553 she
    486 you
    462 said
    435 in
    403 alice
1

Lade ein ebook mit curl herunter.

2

Wandle den gesamten Text mit tr in Kleinbuchstaben um.1

3

Extrahiere alle Wörter mit grep2 und setze jedes Wort in eine eigene Zeile.

4

Sortiere diese Wörter in alphabetischer Reihenfolge mit sort.

5

Entferne alle Duplikate und zähle mit uniq, wie oft jedes Wort in der Liste vorkommt.3

6

Sortiere diese Liste mit eindeutigen Wörtern nach ihrer Anzahl in absteigender Reihenfolge mit sort.

7

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 |  1
> 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
1

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:

  1. Kopiere den Einzeiler und füge ihn in eine Datei ein.

  2. Füge Ausführungsberechtigungen hinzu.

  3. Definiere einen sogenannten Shebang.

  4. Entferne den festen Eingangsteil.

  5. Füge einen Parameter hinzu.

  6. 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 bashverwenden5 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, rwxgeben 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 xwas 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 python7 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 pythonzu verwenden, weil die env8 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, nimmt NUM_WORDS den Wert 10 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 Rscript9 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.