- Überblick
- Text vs. Was auch immer
- Doppelte Anführungszeichen um Argumente
- List Unique Species
- Lösung
- Warum Befehle in der History aufzeichnen, bevor man sie ausführt?
- Lösung
- Diese Arbeitsweise ermöglicht es den Leuten, das, was sie über ihre Daten und ihren Arbeitsablauf herausgefunden haben, mit einem Aufruf von history und ein wenig Bearbeitung wiederzuverwenden, um die Ausgabe zu bereinigen und als Shell-Skript zu speichern: Erstellen eines Skripts
- Variablen in Shell-Skripten
- Lösung
- Finde die längste Datei mit einer bestimmten Erweiterung
- Lösung
- Script Reading Comprehension
- Lösungen
- Debugging Scripts
- Lösung
- Schlüsselpunkte
Überblick
Unterricht: 30 min
Übungen: 15 minFragen
Wie kann ich Befehle speichern und wiederverwenden?
Lernziele
Schreiben Sie ein Shell-Skript, das einen Befehl oder eine Reihe von Befehlen für eine festgelegte Gruppe von Dateien ausführt.
Starten Sie ein Shell-Skript von der Kommandozeile aus.
Schreiben Sie ein Shell-Skript, das eine Reihe von Dateien bearbeitet, die vom Benutzer in der Befehlszeile definiert wurden.
Erstellen Sie Pipelines, die Shell-Skripte enthalten, die Sie und andere geschrieben haben.
Wir sind nun endlich bereit zu sehen, was die Shell zu einer so mächtigen Programmierumgebung macht.Wir werden die Befehle, die wir häufig wiederholen, in Dateien speichern, so dass wir diese Operationen später mit einem einzigen Befehl erneut ausführen können.Aus historischen Gründen wird ein Bündel von Befehlen, die in einer Datei gespeichert sind, gewöhnlich als Shell-Skript bezeichnet, aber täuschen Sie sich nicht: Es handelt sich dabei um kleine Programme.
Beginnen wir damit, zu molecules/
zurückzukehren und eine neue Datei middle.sh
zu erstellen, die unser Shell-Skript werden soll:
$ cd molecules$ nano middle.sh
Der Befehl nano middle.sh
öffnet die Datei middle.sh
im Texteditor ’nano‘ (der innerhalb der Shell läuft).Wir können den Texteditor verwenden, um die Datei direkt zu bearbeiten – wir fügen einfach die folgende Zeile ein:
head -n 15 octane.pdb | tail -n 5
Dies ist eine Abwandlung der Pipe, die wir zuvor konstruiert haben: sie wählt die Zeilen 11-15 der Datei octane.pdb
aus.Denken Sie daran, dass wir sie noch nicht als Befehl ausführen, sondern die Befehle in eine Datei schreiben.
Dann speichern wir die Datei (Ctrl-O
in nano) und beenden den Texteditor (Ctrl-X
in nano).Überprüfen Sie, dass das Verzeichnis molecules
jetzt eine Datei namens middle.sh
enthält.
Nachdem wir die Datei gespeichert haben, können wir die Shell bitten, die darin enthaltenen Befehle auszuführen.
$ bash middle.sh
ATOM 9 H 1 -4.502 0.681 0.785 1.00 0.00ATOM 10 H 1 -5.254 -0.243 -0.537 1.00 0.00ATOM 11 H 1 -4.357 1.252 -0.895 1.00 0.00ATOM 12 H 1 -3.009 -0.741 -1.467 1.00 0.00ATOM 13 H 1 -3.172 -1.337 0.206 1.00 0.00
Die Ausgabe unseres Skripts entspricht genau dem, was wir erhalten würden, wenn wir die Pipeline direkt ausführen würden.
Text vs. Was auch immer
Gewöhnlich bezeichnen wir Programme wie Microsoft Word oder LibreOffice Writer als „Texteditoren“, aber wir müssen ein bisschen vorsichtiger sein, wenn es um Programmierung geht. Microsoft Word verwendet standardmäßig
.docx
-Dateien, um nicht nur Text zu speichern, sondern auch Formatierungsinformationen über Schriftarten, Überschriften und so weiter. Diese zusätzlichen Informationen werden nicht als Zeichen gespeichert und haben für Tools wiehead
keine Bedeutung: Sie erwarten, dass Eingabedateien nur die Buchstaben, Ziffern und Satzzeichen einer Standardtastatur enthalten. Wenn Sie Programme editieren, müssen Sie daher entweder einen Klartext-Editor verwenden oder darauf achten, Dateien als Klartext zu speichern.
Was ist, wenn wir Zeilen aus einer beliebigen Datei auswählen wollen?Wir könnten middle.sh
jedes Mal editieren, um den Dateinamen zu ändern, aber das würde wahrscheinlich länger dauern, als den Befehl noch einmal in die Shell zu tippen und ihn mit einem neuen Dateinamen auszuführen.Stattdessen bearbeiten wir middle.sh
und machen es vielseitiger:
$ nano middle.sh
Ersetzen Sie nun in „nano“ den Text octane.pdb
durch die spezielle Variable :
head -n 15 "" | tail -n 5
In einem Shell-Skript bedeutet „der erste Dateiname (oder ein anderes Argument) in der Befehlszeile“.Wir können nun unser Skript wie folgt ausführen:
$ bash middle.sh octane.pdb
ATOM 9 H 1 -4.502 0.681 0.785 1.00 0.00ATOM 10 H 1 -5.254 -0.243 -0.537 1.00 0.00ATOM 11 H 1 -4.357 1.252 -0.895 1.00 0.00ATOM 12 H 1 -3.009 -0.741 -1.467 1.00 0.00ATOM 13 H 1 -3.172 -1.337 0.206 1.00 0.00
oder in einer anderen Datei wie dieser:
$ bash middle.sh pentane.pdb
ATOM 9 H 1 1.324 0.350 -1.332 1.00 0.00ATOM 10 H 1 1.271 1.378 0.122 1.00 0.00ATOM 11 H 1 -0.074 -0.384 1.288 1.00 0.00ATOM 12 H 1 -0.048 -1.362 -0.205 1.00 0.00ATOM 13 H 1 -1.183 0.500 -1.412 1.00 0.00
Doppelte Anführungszeichen um Argumente
Aus demselben Grund, aus dem wir die Schleifenvariable in doppelte Anführungszeichen setzen, umgeben wir
mit doppelten Anführungszeichen, falls der Dateiname zufällig Leerzeichen enthält.
Augenblicklich müssen wir middle.sh
jedes Mal bearbeiten, wenn wir den Bereich der zurückgegebenen Zeilen anpassen wollen. Das können wir ändern, indem wir unser Skript so konfigurieren, dass es stattdessen drei Befehlszeilenargumente verwendet. Nach dem ersten Befehlszeilenargument () ist jedes weitere Argument, das wir angeben, über die speziellen Variablen
,
,
zugänglich, die sich auf das erste, zweite bzw. dritte Befehlszeilenargument beziehen.
Da wir dies wissen, können wir zusätzliche Argumente verwenden, um den Bereich der Zeilen zu definieren, die an head
bzw. tail
übergeben werden sollen:
$ nano middle.sh
head -n "" "" | tail -n ""
Wir können nun starten:
$ bash middle.sh pentane.pdb 15 5
ATOM 9 H 1 1.324 0.350 -1.332 1.00 0.00ATOM 10 H 1 1.271 1.378 0.122 1.00 0.00ATOM 11 H 1 -0.074 -0.384 1.288 1.00 0.00ATOM 12 H 1 -0.048 -1.362 -0.205 1.00 0.00ATOM 13 H 1 -1.183 0.500 -1.412 1.00 0.00
Indem wir die Argumente für unseren Befehl ändern, können wir das Verhalten unseres Skripts ändern:
$ bash middle.sh pentane.pdb 20 5
ATOM 14 H 1 -1.259 1.420 0.112 1.00 0.00ATOM 15 H 1 -2.608 -0.407 1.130 1.00 0.00ATOM 16 H 1 -2.540 -1.303 -0.404 1.00 0.00ATOM 17 H 1 -3.393 0.254 -0.321 1.00 0.00TER 18 1
Das funktioniert, aber die nächste Person, die middle.sh
liest, braucht vielleicht einen Moment, um herauszufinden, was es tut.Wir können unser Skript verbessern, indem wir oben einige Kommentare einfügen:
$ nano middle.sh
# Select lines from the middle of a file.# Usage: bash middle.sh filename end_line num_lineshead -n "" "" | tail -n ""
Ein Kommentar beginnt mit einem #
-Zeichen und läuft bis zum Ende der Zeile.Der Computer ignoriert Kommentare, aber sie sind von unschätzbarem Wert, wenn es darum geht, anderen Menschen (einschließlich Ihrem zukünftigen Ich) zu helfen, Skripte zu verstehen und zu benutzen.Die einzige Einschränkung ist, dass Sie jedes Mal, wenn Sie das Skript ändern, überprüfen sollten, ob der Kommentar noch korrekt ist: eine Erklärung, die den Leser in die falsche Richtung schickt, ist schlimmer als gar keine.
Was, wenn wir viele Dateien in einer einzigen Pipeline verarbeiten wollen?Wenn wir zum Beispiel unsere .pdb
Dateien nach Länge sortieren wollen, würden wir eingeben:
$ wc -l *.pdb | sort -n
weil wc -l
die Anzahl der Zeilen in den Dateien auflistet (erinnern Sie sich, dass wc
für „Wortanzahl“ steht, das Hinzufügen der Option -l
bedeutet stattdessen „Zeilenanzahl“) und sort -n
die Dinge numerisch sortiert.Wir könnten dies in eine Datei schreiben, aber dann würde sie immer nur eine Liste von .pdb
Dateien im aktuellen Verzeichnis sortieren.Wenn wir in der Lage sein wollen, eine sortierte Liste anderer Arten von Dateien zu erhalten, brauchen wir eine Möglichkeit, all diese Namen in das Skript zu bekommen.Wir können nicht ,
usw. verwenden, weil wir nicht wissen, wie viele Dateien es gibt.Stattdessen verwenden wir die spezielle Variable
$@
, die bedeutet: „Alle Befehlszeilenargumente für das Shell-Skript“.Wir sollten $@
auch in Anführungszeichen setzen, um den Fall von Argumenten mit Leerzeichen zu behandeln ("$@"
ist eine spezielle Syntax und entspricht ""
""
…).
Hier ist ein Beispiel:
$ nano sorted.sh
# Sort files by their length.# Usage: bash sorted.sh one_or_more_filenameswc -l "$@" | sort -n
$ bash sorted.sh *.pdb ../creatures/*.dat
9 methane.pdb12 ethane.pdb15 propane.pdb20 cubane.pdb21 pentane.pdb30 octane.pdb163 ../creatures/basilisk.dat163 ../creatures/minotaur.dat163 ../creatures/unicorn.dat596 total
List Unique Species
Leah hat mehrere hundert Datendateien, von denen jede wie folgt formatiert ist:
2013-11-05,deer,52013-11-05,rabbit,222013-11-05,raccoon,72013-11-06,rabbit,192013-11-06,deer,22013-11-06,fox,12013-11-07,rabbit,182013-11-07,bear,1
Ein Beispiel für diesen Dateityp findet sich in
data-shell/data/animal-counts/animals.txt
.Wir können den Befehl
cut -d , -f 2 animals.txt | sort | uniq
verwenden, um die einzelnen Arten inanimals.txt
zu erzeugen. Um zu vermeiden, dass diese Befehlsfolge jedes Mal eingegeben werden muss, kann ein Wissenschaftler stattdessen ein Shell-Skript schreiben.Schreiben Sie ein Shell-Skript mit dem Namen
species.sh
, das eine beliebige Anzahl von Dilennamen als Befehlszeilenargumente annimmt und eine Variation des obigen Befehls verwendet, um eine Liste der eindeutigen Arten zu drucken, die in jeder dieser Dateien einzeln vorkommen.Lösung
# Script to find unique species in csv files where species is the second data field# This script accepts any number of file names as command line arguments# Loop over all filesfor file in $@doecho "Unique species in $file:"# Extract species namescut -d , -f 2 $file | sort | uniqdone
Angenommen, wir haben gerade eine Reihe von Befehlen ausgeführt, die etwas Nützliches getan haben – zum Beispiel ein Diagramm erstellt, das wir in einem Aufsatz verwenden möchten.Anstatt sie erneut einzutippen (und sie möglicherweise falsch zu machen), können wir Folgendes tun:
$ history | tail -n 5 > redo-figure-3.sh
Die Datei redo-figure-3.sh
enthält jetzt:
297 bash goostats NENE01729B.txt stats-NENE01729B.txt298 bash goodiff stats-NENE01729B.txt /data/validated/01729.txt > 01729-differences.txt299 cut -d ',' -f 2-3 01729-differences.txt > 01729-time-series.txt300 ygraph --format scatter --color bw --borders none 01729-time-series.txt figure-3.png301 history | tail -n 5 > redo-figure-3.sh
Nachdem wir einen Moment in einem Editor gearbeitet haben, um die Seriennummern der Befehle zu entfernen und die letzte Zeile, in der wir den Befehl history
aufgerufen haben, zu entfernen, haben wir eine völlig genaue Aufzeichnung darüber, wie wir diese Figur erstellt haben.
Warum Befehle in der History aufzeichnen, bevor man sie ausführt?
Wenn man den Befehl:
$ history | tail -n 5 > recent.sh
ausführt, ist der letzte Befehl in der Datei der
history
Befehl selbst, d.h.,die Shell hathistory
zum Befehlsprotokoll hinzugefügt, bevor sie ihn tatsächlich ausführt. Tatsächlich fügt die Shell immer Befehle zum Protokoll hinzu, bevor sie ausgeführt werden. Warum macht sie das wohl?Lösung
Wenn ein Befehl einen Absturz oder ein Hängen verursacht, könnte es nützlich sein, zu wissen, welcher Befehl das war, um das Problem zu untersuchen.Würde der Befehl erst nach seiner Ausführung aufgezeichnet werden, hätten wir im Falle eines Absturzes keine Aufzeichnung des zuletzt ausgeführten Befehls.
In der Praxis entwickeln die meisten Leute Shell-Skripte, indem sie Befehle an der Shell-Eingabeaufforderung ein paar Mal ausführen, um sicherzustellen, dass sie das Richtige tun, und sie dann in einer Datei zur Wiederverwendung speichern.
Diese Arbeitsweise ermöglicht es den Leuten, das, was sie über ihre Daten und ihren Arbeitsablauf herausgefunden haben, mit einem Aufruf von history
und ein wenig Bearbeitung wiederzuverwenden, um die Ausgabe zu bereinigen und als Shell-Skript zu speichern: Erstellen eines Skripts
Nelles Vorgesetzter bestand darauf, dass alle ihre Analysen reproduzierbar sein müssen. Der einfachste Weg, alle Schritte festzuhalten, ist ein Skript.
Zunächst kehren wir in Nelles Datenverzeichnis zurück:
$ cd ../north-pacific-gyre/2012-07-03/
Sie startet den Editor und schreibt Folgendes:
# Calculate stats for data files.for datafile in "$@"do echo $datafile bash goostats $datafile stats-$datafiledone
Sie speichert dies in einer Datei mit dem Namen do-stats.sh
, so dass sie nun die erste Stufe ihrer Analyse durch Eintippen wiederholen kann:
$ bash do-stats.sh NENE*.txt
Sie kann auch dies tun:
$ bash do-stats.sh NENE*.txt | wc -l
so dass die Ausgabe nur die Anzahl der verarbeiteten Dateien ist und nicht die Namen der verarbeiteten Dateien.
Eine Sache, die man bei Nelles Skript beachten sollte, ist, dass es die Person, die es ausführt, entscheiden lässt, welche Dateien verarbeitet werden sollen.Sie hätte es wie folgt schreiben können:
# Calculate stats for Site A and Site B data files.for datafile in NENE*.txtdo echo $datafile bash goostats $datafile stats-$datafiledone
Der Vorteil ist, dass sie immer die richtigen Dateien auswählt: Sie muss nicht daran denken, die „Z“-Dateien auszuschließen.Der Nachteil ist, dass es immer nur diese Dateien auswählt – sie kann es nicht auf alle Dateien (einschließlich der „Z“-Dateien) oder auf die „G“- oder „H“-Dateien anwenden, die ihre Kollegen in der Antarktis produzieren, ohne das Skript zu bearbeiten.Wenn sie abenteuerlustiger sein wollte, könnte sie ihr Skript so modifizieren, dass es nach Befehlszeilenargumenten sucht und NENE*.txt
verwendet, wenn keine vorhanden sind, was natürlich einen weiteren Kompromiss zwischen Flexibilität und Komplexität mit sich bringt.
Variablen in Shell-Skripten
Stellen Sie sich vor, Sie haben im Verzeichnis
molecules
ein Shell-Skript namensscript.sh
, das die folgenden Befehle enthält:head -n tail -n
Während Sie sich im Verzeichnis
molecules
befinden, geben Sie den folgenden Befehl ein:bash script.sh '*.pdb' 1 1
Welche der folgenden Ausgaben würden Sie erwarten?
- Alle Zeilen zwischen der ersten und der letzten Zeile jeder Datei, die auf
.pdb
im Verzeichnismolecules
endet- Die erste und die letzte Zeile jeder Datei, die auf
.pdb
im Verzeichnismolecules
endet Verzeichnis- Die erste und die letzte Zeile jeder Datei im
molecules
Verzeichnis- Ein Fehler wegen der Anführungszeichen um
*.pdb
Lösung
Die richtige Antwort ist 2.
Die speziellen Variablen $1, $2 und $3 stehen für die Befehlszeilenargumente, die dem Skript übergeben werden, so dass folgende Befehle ausgeführt werden:
$ head -n 1 cubane.pdb ethane.pdb octane.pdb pentane.pdb propane.pdb$ tail -n 1 cubane.pdb ethane.pdb octane.pdb pentane.pdb propane.pdb
Die Shell expandiert
'*.pdb'
nicht, da es in Anführungszeichen eingeschlossen ist, so dass das erste Argument des Skripts'*.pdb'
ist, das im Skript durchhead
undtail
erweitert wird.
Finde die längste Datei mit einer bestimmten Erweiterung
Schreibe ein Shell-Skript namens
longest.sh
, das den Namen eines Verzeichnisses und eine Dateierweiterung als Argumente annimmt und den Namen der Datei mit den meisten Zeilen in diesem Verzeichnis mit dieser Erweiterung ausgibt. Zum Beispiel:$ bash longest.sh /tmp/data pdb
gibt den Namen der Datei
.pdb
in/tmp/data
mit den meisten Zeilen aus.Lösung
# Shell script which takes two arguments:# 1. a directory name# 2. a file extension# and prints the name of the file in that directory# with the most lines which matches the file extension.wc -l /*. | sort -n | tail -n 2 | head -n 1
Der erste Teil der Pipeline,
wc -l /*. | sort -n
, zählt die Zeilen in jeder Datei und sortiert sie numerisch (die größte zuletzt). Bei mehr als einer Datei gibtwc
auch eine abschließende Übersichtszeile aus, die die Gesamtzahl der Zeilen in allen Dateien angibt. Wir verwendentail-n 2 | head -n 1
, um diese letzte Zeile wegzuwerfen.Mit
wc -l /*. | sort -n | tail -n 1
sehen wir die endgültige Zusammenfassungszeile: Wir können unsere Pipeline stückweise aufbauen, um sicher zu sein, dass wir die Ausgabe verstehen.
Script Reading Comprehension
Betrachten wir für diese Frage noch einmal das Verzeichnis
data-shell/molecules
.Erläutern Sie, was jedes der folgenden drei Skripte tun würde, wenn es alsbash script1.sh *.pdb
,bash script2.sh *.pdb
bzw.bash script3.sh *.pdb
ausgeführt würde.# Script 1echo *.*
# Script 2for filename in do cat $filenamedone
# Script 3echo [email protected]
Lösungen
In jedem Fall expandiert die Shell den Platzhalter in
*.pdb
, bevor sie die resultierende Liste von Dateinamen als Argumente an das Skript übergibt.Skript 1 würde eine Liste aller Dateien ausgeben, die einen Punkt in ihrem Namen enthalten.
Die Argumente, die dem Skript übergeben werden, werden nirgends im Skript verwendet.
Skript 2 würde den Inhalt der ersten drei Dateien mit einer
.pdb
-Dateierweiterung ausgeben.,
und
beziehen sich auf das erste, zweite bzw. dritte Argument.
Script 3 würde alle Argumente für das Skript ausgeben (d.h. alle
.pdb
-Dateien), gefolgt von.pdb
.$@
bezieht sich auf alle Argumente, die einem Shell-Skript übergeben werden.cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb.pdb
Debugging Scripts
Angenommen, Sie haben das folgende Skript in einer Datei namens
do-errors.sh
im Verzeichnisnorth-pacific-gyre/2012-07-03
von Nelle gespeichert:# Calculate stats for data files.for datafile in "$@"do echo $datfile bash goostats $datafile stats-$datafiledone
Wenn Sie es ausführen:
$ bash do-errors.sh NENE*.txt
ist die Ausgabe leer.Um herauszufinden, warum, führen Sie das Skript mit der Option
-x
erneut aus:bash -x do-errors.sh NENE*.txt
Was zeigt Ihnen die Ausgabe? Welche Zeile ist für den Fehler verantwortlich?
Lösung
Die Option
-x
bewirkt, dassbash
im Debug-Modus ausgeführt wird, wodurch jeder Befehl während seiner Ausführung ausgedruckt wird, was Ihnen hilft, Fehler zu finden.In diesem Beispiel können wir sehen, dassecho
nichts ausgibt. Wir haben uns im Namen der Schleifenvariablen vertippt, und die Variabledatfile
existiert nicht, so dass eine leere Zeichenkette zurückgegeben wird.
Schlüsselpunkte
Speichern Sie Befehle in Dateien (gewöhnlich Shell-Skripte genannt) zur Wiederverwendung.
bash
führt die in einer Datei gespeicherten Befehle aus.
$@
bezieht sich auf alle Befehlszeilenargumente eines Shell-Skripts.
,
, usw., beziehen sich auf das erste Befehlszeilenargument, das zweite Befehlszeilenargument usw.
Variablen in Anführungszeichen setzen, wenn die Werte Leerzeichen enthalten könnten.
Den Benutzer entscheiden zu lassen, welche Dateien verarbeitet werden sollen, ist flexibler und konsistenter mit den eingebauten Unix-Befehlen.