Ja, wir sehen eine Reihe von Dingen wie:
while read line; do
echo $line | cut -c3
done
Oder schlimmer:
for line in `cat file`; do
foo=`echo $line | awk '{print $2}'`
echo whatever $foo
done
(nicht lachen, ich habe viele davon gesehen).
In der Regel von Shell-Scripting-Anfängern. Das sind naive wörtliche Übersetzungen dessen, was Sie in imperativen Sprachen wie C oder Python tun würden, aber so tun Sie Dinge nicht in Shells, und diese Beispiele sind sehr ineffizient, völlig unzuverlässig (was möglicherweise zu Sicherheitsproblemen führt) und falls Sie es jemals schaffen Um die meisten Fehler zu beheben, wird Ihr Code unleserlich.
Konzeptionell
In C oder den meisten anderen Sprachen liegen die Bausteine nur eine Ebene über den Computeranweisungen. Sie teilen Ihrem Prozessor mit, was als Nächstes zu tun ist. Sie nehmen Ihren Prozessor bei der Hand und verwalten ihn im Mikromodus: Sie öffnen diese Datei, Sie lesen so viele Bytes, Sie tun dies, Sie tun das damit.
Muscheln sind eine höhere Sprache. Man kann sagen, es ist nicht einmal eine Sprache. Sie stehen vor allen Befehlszeileninterpreten. Die Aufgabe wird von den Befehlen erledigt, die Sie ausführen, und die Shell soll sie nur orchestrieren.
Eines der großartigen Dinge, die Unix eingeführt hat, waren die Pipe und die Standard-Streams stdin / stdout / stderr, die standardmäßig von allen Befehlen verarbeitet werden.
In 45 Jahren haben wir keine bessere API gefunden, um die Leistungsfähigkeit von Befehlen zu nutzen und sie bei einer Aufgabe zusammenarbeiten zu lassen. Das ist wahrscheinlich der Hauptgrund, warum die Leute heute noch Muscheln benutzen.
Sie haben ein Schneidwerkzeug und ein Transliterationswerkzeug und können einfach Folgendes tun:
cut -c4-5 < in | tr a b > out
Die Shell erledigt nur die Installation (Dateien öffnen, Pipes einrichten, Befehle aufrufen) und wenn alles fertig ist, fließt sie einfach, ohne dass die Shell etwas unternimmt. Die Werkzeuge erledigen ihre Arbeit gleichzeitig und effizient in ihrem eigenen Tempo, wobei genügend Puffer vorhanden sind, damit nicht einer den anderen blockiert. Es ist einfach wunderschön und doch so einfach.
Das Aufrufen eines Tools ist jedoch mit Kosten verbunden (und diese werden wir im Hinblick auf die Leistung entwickeln). Diese Tools können mit Tausenden von Anweisungen in C geschrieben sein. Es muss ein Prozess erstellt, das Tool geladen, initialisiert, dann bereinigt, der Prozess zerstört und gewartet werden.
Beim Aufrufen cut
wird die Küchenschublade geöffnet. Nehmen Sie das Messer, verwenden Sie es, waschen Sie es, trocknen Sie es und legen Sie es wieder in die Schublade. Wenn Sie das tun:
while read line; do
echo $line | cut -c3
done < file
Es ist, als würde man für jede Zeile der Datei das read
Werkzeug aus der Küchenschublade holen (ein sehr ungeschicktes, weil es nicht dafür vorgesehen ist ), eine Zeile lesen, das Lesewerkzeug waschen und es wieder in die Schublade legen. Planen Sie dann eine Besprechung für das Werkzeug echo
und cut
, holen Sie sie aus der Schublade, rufen Sie sie auf, waschen Sie sie, trocknen Sie sie, legen Sie sie wieder in die Schublade und so weiter.
Einige dieser Werkzeuge ( read
und echo
) sind in den meisten Schalen gebaut, aber das macht kaum einen Unterschied hier , da echo
und cut
müssen noch in separaten Prozessen ausgeführt werden.
Es ist, als würde man eine Zwiebel schneiden, aber man wäscht das Messer und legt es zwischen die Scheiben in die Küchenschublade zurück.
Hier ist es naheliegend, das cut
Werkzeug aus der Schublade zu holen , die ganze Zwiebel in Scheiben zu schneiden und nach Beendigung der gesamten Arbeit wieder in die Schublade zu legen.
In Shells, insbesondere zum Verarbeiten von Text, rufen Sie so wenige Dienstprogramme wie möglich auf und lassen sie bei der Ausführung der Aufgabe zusammenarbeiten. Führen Sie nicht Tausende von Tools nacheinander aus, während Sie darauf warten, dass jedes gestartet, ausgeführt und bereinigt wird, bevor Sie das nächste ausführen.
Lesen Sie weiter in Bruce's feiner Antwort . Die internen Tools für die einfache Textverarbeitung in Shells (mit Ausnahme von zsh
) sind begrenzt, umständlich und im Allgemeinen nicht für die allgemeine Textverarbeitung geeignet.
Performance
Wie bereits erwähnt, ist das Ausführen eines Befehls mit Kosten verbunden. Ein enormer Aufwand, wenn dieser Befehl nicht eingebaut ist, aber selbst wenn er eingebaut ist, sind die Kosten hoch.
Und Shells sind nicht so konzipiert, sie geben keinen Anspruch darauf, performante Programmiersprachen zu sein. Sie sind nicht, sie sind nur Befehlszeileninterpreter. In dieser Hinsicht wurde wenig optimiert.
Außerdem führen die Shells Befehle in separaten Prozessen aus. Diese Bausteine haben keinen gemeinsamen Speicher oder Status. Wenn Sie ein fgets()
oder fputs()
in C ausführen, ist dies eine Funktion in stdio. stdio speichert interne Puffer für die Ein- und Ausgabe aller stdio-Funktionen, um zu vermeiden, dass teure Systemaufrufe zu oft ausgeführt werden.
Der entsprechende sogar eingebauten Schale Utilities ( read
, echo
, printf
) , kann das nicht tun. read
soll eine Zeile lesen. Wenn es nach dem Zeilenumbruchzeichen steht, wird es beim nächsten Ausführen des Befehls übersehen. So read
muss die Eingabe ein Byte nach dem anderen gelesen werden (einige Implementierungen haben eine Optimierung, wenn die Eingabe eine reguläre Datei ist, indem sie Chunks lesen und zurücksuchen, aber das funktioniert nur für reguläre Dateien und bash
liest zum Beispiel nur 128-Byte-Chunks, was bedeutet noch viel weniger als Textdienstprogramme).
Das Gleiche gilt für die Ausgabeseite. Sie echo
kann nicht nur ihre Ausgabe puffern, sondern muss sie sofort ausgeben, da der nächste Befehl, den Sie ausführen, diesen Puffer nicht freigibt.
Wenn Sie Befehle nacheinander ausführen, müssen Sie natürlich auf sie warten. Es ist ein kleiner Scheduler-Tanz, der die Steuerung von der Shell über die Tools bis hin zu den Werkzeugen übernimmt. Dies bedeutet auch, dass Sie (im Gegensatz zur Verwendung lang laufender Instanzen von Tools in einer Pipeline) nicht mehrere Prozessoren gleichzeitig nutzen können, wenn diese verfügbar sind.
Zwischen dieser while read
Schleife und dem (angeblich) Äquivalent cut -c3 < file
gibt es in meinem Schnelltest ein CPU-Zeitverhältnis von ungefähr 40000 in meinen Tests (eine Sekunde gegenüber einem halben Tag). Aber auch wenn Sie nur Shell-Builtins verwenden:
while read line; do
echo ${line:2:1}
done
(hier mit bash
), das ist immer noch ungefähr 1: 600 (eine Sekunde gegen 10 Minuten).
Zuverlässigkeit / Lesbarkeit
Es ist sehr schwer, diesen Code richtig zu machen. Die Beispiele, die ich gegeben habe, werden zu oft in freier Wildbahn gesehen, aber sie haben viele Fehler.
read
ist ein praktisches Werkzeug, das viele verschiedene Dinge kann. Es kann Eingaben vom Benutzer lesen, in Wörter aufteilen und in verschiedenen Variablen speichern. read line
ist nicht eine Eingabezeile gelesen, oder vielleicht liest er eine Linie auf eine ganz besondere Art und Weise. Es liest tatsächlich Wörter aus der Eingabe, die durch einen $IFS
Backslash getrennt sind und mit denen die Trennzeichen oder das Newline-Zeichen ausgeblendet werden können.
Mit dem Standardwert von $IFS
, bei einer Eingabe wie:
foo\/bar \
baz
biz
read line
speichert "foo/bar baz"
in $line
, nicht " foo\/bar \"
wie man erwarten würde.
Um eine Zeile zu lesen, benötigen Sie tatsächlich:
IFS= read -r line
Das ist nicht sehr intuitiv, aber so ist es, denken Sie daran, dass Muscheln nicht so verwendet werden sollten.
Gleiches gilt für echo
. echo
erweitert Sequenzen. Sie können es nicht für beliebige Inhalte wie den Inhalt einer zufälligen Datei verwenden. Du brauchst printf
stattdessen hier.
Und natürlich gibt es das typische Vergessen, eine Variable zu zitieren, in die jeder hineinfällt. Es ist also mehr:
while IFS= read -r line; do
printf '%s\n' "$line" | cut -c3
done < file
Nun noch ein paar Vorsichtsmaßnahmen:
- mit der Ausnahme
zsh
, dass dies nicht funktioniert, wenn die Eingabe NUL-Zeichen enthält, während zumindest GNU-Textdienstprogramme das Problem nicht hätten.
- Wenn nach dem letzten Zeilenumbruch Daten vorhanden sind, werden diese übersprungen
- Innerhalb der Schleife wird stdin umgeleitet, sodass Sie darauf achten müssen, dass die darin enthaltenen Befehle nicht aus stdin lesen.
- Bei den Befehlen in den Schleifen achten wir nicht darauf, ob sie erfolgreich sind oder nicht. Normalerweise werden Fehlerzustände (Datenträger voll, Lesefehler ...) schlecht behandelt, normalerweise schlechter als mit dem richtigen Äquivalent.
Wenn wir einige der oben genannten Probleme angehen möchten, wird dies folgendermaßen aussehen:
while IFS= read -r line <&3; do
{
printf '%s\n' "$line" | cut -c3 || exit
} 3<&-
done 3< file
if [ -n "$line" ]; then
printf '%s' "$line" | cut -c3 || exit
fi
Das wird immer weniger lesbar.
Es gibt eine Reihe anderer Probleme bei der Übergabe von Daten an Befehle über die Argumente oder beim Abrufen ihrer Ausgabe in Variablen:
- die Begrenzung der Größe von Argumenten (einige Implementierungen von Textdienstprogrammen haben auch dort eine Begrenzung, obwohl die Auswirkungen, die erreicht werden, im Allgemeinen weniger problematisch sind)
- das NUL-Zeichen (auch ein Problem mit Textdienstprogrammen).
- Argumente, die als Optionen verwendet werden, wenn sie mit
-
(oder +
manchmal) beginnen
- verschiedene Eigenheiten der verschiedenen Befehle , die typischerweise in diesen Schleifen verwendet , wie
expr
, test
...
- Die (eingeschränkten) Textmanipulationsoperatoren verschiedener Shells, die Mehrbyte-Zeichen auf inkonsistente Weise verarbeiten.
- ...
Sicherheitsaspekte
Wenn Sie anfangen, mit Shell- Variablen und Argumenten für Befehle zu arbeiten , geben Sie ein Minenfeld ein.
Wenn Sie vergessen, Ihre Variablen in Anführungszeichen zu setzen , das Ende der Optionsmarkierung zu vergessen , in Gebietsschemata mit Mehrbyte-Zeichen zu arbeiten (heutzutage die Norm), werden Sie mit Sicherheit Fehler einführen, die früher oder später zu Schwachstellen werden.
Wenn Sie Loops verwenden möchten.
TBD
yes
schreibt man so schnell in eine Datei?