it-swarm.com.de

Warum wird die Verwendung einer Shell-Schleife zum Verarbeiten von Text als schlechte Praxis angesehen?

Wird die Verwendung einer while-Schleife zum Verarbeiten von Text in POSIX-Shells allgemein als schlechte Praxis angesehen?

Wie Stéphane Chazelas betonte , sind einige der Gründe für die Nichtverwendung der Shell-Schleife konzeptionell , Zuverlässigkeit , Lesbarkeit , Leistung und Sicherheit .

Diese Antwort erklärt die Zuverlässigkeit und Lesbarkeit Aspekte:

while IFS= read -r line <&3; do
  printf '%s\n' "$line"
done 3< "$InputFile"

Für Leistung sind die Schleife while und read beim Lesen aus einer Datei oder einer Pipe enorm langsam. weil das eingebaute Shell lesen jeweils ein Zeichen liest.

Wie wäre es mit konzeptionellen und Sicherheitsaspekten ?

207
cuonglm

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

Im Allgemeinen von Shell Scripting Anfängern. Dies sind naive wörtliche Übersetzungen dessen, was Sie in imperativen Sprachen wie C oder Python tun würden, aber so machen Sie es 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 sind Bausteine ​​nur eine Ebene über den Computeranweisungen. Sie sagen Ihrem Prozessor, was zu tun ist und was als nächstes zu tun ist. Sie nehmen Ihren Prozessor bei der Hand und verwalten ihn mikro: 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 sind vor allen Befehlszeilendolmetschern. Die Arbeit 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, war die Pipe Und die Standard-Streams stdin/stdout/stderr, die alle Befehle standardmäßig verarbeiten.

In 50 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 (öffnen Sie die Dateien, richten Sie die Pipes ein, rufen Sie die Befehle auf) und wenn alles fertig ist, fließt sie einfach, ohne dass die Shell etwas unternimmt. Die Tools erledigen ihre Arbeit gleichzeitig, effizient in ihrem eigenen Tempo und mit genügend Pufferung, damit nicht eines das andere blockiert, sondern einfach nur schön und doch so einfach ist.

Das Aufrufen eines Tools ist jedoch mit Kosten verbunden (und wir werden dies in Bezug auf den Leistungspunkt entwickeln). Diese Tools können mit Tausenden von Anweisungen in C geschrieben werden. Es muss ein Prozess erstellt, das Tool geladen, initialisiert, dann bereinigt, der Prozess zerstört und gewartet werden.

Das Aufrufen von cut ist wie das Öffnen der Küchenschublade, das Messer nehmen, benutzen, waschen, trocknen, wieder in die Schublade legen. Wenn Sie das tun:

while read line; do
  echo $line | cut -c3
done < file

Es ist so, als würde man für jede Zeile der Datei das Werkzeug read aus der Küchenschublade holen (sehr ungeschickt, weil es wurde nicht dafür entwickelt ), eine Zeile lesen, die Lektüre waschen Werkzeug, legen Sie es wieder in die Schublade. Planen Sie dann eine Besprechung für das Tool 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 Shells enthalten, aber das macht hier kaum einen Unterschied, da echo und cut noch vorhanden sein müssen in separaten Prozessen ausführen.

Es ist, als würde man eine Zwiebel schneiden, aber das Messer waschen und zwischen den einzelnen Scheiben wieder in die Küchenschublade legen.

Hier ist der naheliegende Weg, Ihr cut Werkzeug aus der Schublade zu holen, Ihre ganze Zwiebel in Scheiben zu schneiden und sie nach Abschluss der gesamten Arbeit wieder in die Schublade zu legen.

IOW, in Shells, insbesondere zum Verarbeiten von Text, rufen Sie so wenige Dienstprogramme wie möglich auf und lassen sie an der Aufgabe zusammenarbeiten. Führen Sie nicht Tausende von Tools nacheinander aus, die darauf warten, dass jedes gestartet, ausgeführt und bereinigt wird, bevor Sie das nächste ausführen.

Lesen Sie weiter in Bruce's feine Antwort . Die internen Tools für die Textverarbeitung auf niedriger Ebene in Shells (außer möglicherweise 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. Eine enorme Kosten, wenn dieser Befehl nicht eingebaut ist, aber selbst wenn sie eingebaut sind, sind die Kosten hoch.

Und Shells sind nicht dafür ausgelegt, so zu laufen, sie haben keinen Anspruch darauf, performante Programmiersprachen zu sein. Sie sind nicht, sie sind nur Befehlszeilendolmetscher. Daher wurde an dieser Front wenig optimiert.

Außerdem führen die Shells Befehle in separaten Prozessen aus. Diese Bausteine ​​haben keinen gemeinsamen Speicher oder Status. Wenn Sie in C eine fgets() oder fputs() 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 kostspielige Systemaufrufe zu oft ausgeführt werden.

Die entsprechenden sogar eingebauten Shell-Dienstprogramme (read, echo, printf) können dies nicht. read soll eine Zeile lesen. Wenn es über das Zeilenumbruchzeichen hinaus liest, bedeutet dies, dass der nächste Befehl, den Sie ausführen, es verfehlt. read muss also die Eingabe byteweise lesen (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 immer noch viel weniger ist als bei Textdienstprogrammen.

Auf der Ausgabeseite kann echo seine Ausgabe nicht einfach puffern, sondern muss sie sofort ausgeben, da der nächste Befehl, den Sie ausführen, diesen Puffer nicht gemeinsam nutzt.

Wenn Sie Befehle nacheinander ausführen, müssen Sie natürlich auf sie warten. Es ist ein kleiner Scheduler-Tanz, der die Kontrolle von der Shell über die Tools bis hin zur Rückseite ermöglicht. Dies bedeutet auch (im Gegensatz zur Verwendung lang laufender Instanzen von Tools in einer Pipeline), dass Sie nicht mehrere Prozessoren gleichzeitig nutzen können, wenn diese verfügbar sind.

Zwischen dieser while read - Schleife und dem (angeblich) äquivalenten cut -c3 < file Gibt es in meinem Schnelltest in meinen Tests ein CPU-Zeitverhältnis von etwa 40000 (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über 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 Tool, das viele verschiedene Dinge tun kann. Es kann Eingaben des Benutzers lesen und in Wörter aufteilen, um sie in verschiedenen Variablen zu speichern. read line Liest nicht liest eine Eingabezeile oder liest eine Zeile auf eine ganz besondere Weise. Es liest tatsächlich Wörter von der Eingabe jene Wörter, die durch $IFS Getrennt sind und wobei Backslash verwendet werden kann, um die Trennzeichen oder das Zeilenumbruchzeichen zu umgehen.

Mit dem Standardwert $IFS Bei einer Eingabe wie:

   foo\/bar \
baz
biz

read line Speichert "foo/bar baz" In $line Und nicht wie erwartet in " foo\/bar \".

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 dazu gedacht waren, so verwendet zu werden.

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. Sie benötigen stattdessen printf hier.

Und natürlich gibt es das typische Vergessen, Ihre Variable zu zitieren, in das jeder fällt. Es ist also mehr:

while IFS= read -r line; do
  printf '%s\n' "$line" | cut -c3
done < file

Nun noch ein paar Einschränkungen:

  • mit Ausnahme von zsh funktioniert dies nicht, wenn die Eingabe NUL-Zeichen enthält, während mindestens GNU Textdienstprogramme das Problem nicht haben würden).
  • 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 von stdin gelesen werden.
  • bei den Befehlen innerhalb der Schleifen achten wir nicht darauf, ob sie erfolgreich sind oder nicht. Normalerweise werden Fehlerbedingungen (Festplatte voll, Lesefehler ...) schlecht behandelt, normalerweise schlechter als mit dem Äquivalent richtig.

Wenn wir einige der oben genannten Probleme angehen möchten, wird dies zu:

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 beim Übergeben von Daten an Befehle über die Argumente oder beim Abrufen ihrer Ausgabe in Variablen:

  • die Begrenzung der Größe von Argumenten (einige Textdienstprogrammimplementierungen haben auch dort eine Begrenzung, obwohl die Auswirkungen der erreichten Argumente im Allgemeinen weniger problematisch sind)
  • das NUL-Zeichen (auch ein Problem mit Textdienstprogrammen).
  • argumente, die als Optionen verwendet werden, wenn sie mit - beginnen (oder manchmal mit +)
  • verschiedene Macken verschiedener Befehle, die typischerweise in diesen Schleifen verwendet werden, wie expr, test...
  • die (eingeschränkten) Textmanipulationsoperatoren verschiedener Shells, die Multi-Byte-Zeichen auf inkonsistente Weise verarbeiten.
  • ...

Sicherheitsüberlegungen

Wenn Sie mit Shell Variablen und Argumente für Befehle arbeiten, geben Sie ein Minenfeld ein.

Wenn Sie vergessen, Ihre Variablen zu zitieren , das Ende der Optionsmarkierung vergessen, in Gebietsschemas mit Mehrbyte-Zeichen arbeiten (die Norm heutzutage), werden Sie sicher einführen Fehler, die früher oder später zu Sicherheitslücken werden.

Wenn Sie Schleifen verwenden möchten.

TBD

271

In Bezug auf Konzeption und Lesbarkeit interessieren sich Shells normalerweise für Dateien. Ihre "adressierbare Einheit" ist die Datei, und die "Adresse" ist der Dateiname. Shells verfügen über alle Arten von Testmethoden für Dateiexistenz, Dateityp und Dateinamenformatierung (beginnend mit Globbing). Shells haben nur sehr wenige Grundelemente für den Umgang mit Dateiinhalten. Shell-Programmierer müssen ein anderes Programm aufrufen, um den Dateiinhalt zu verarbeiten.

Aufgrund der Ausrichtung von Datei und Dateinamen ist die Textmanipulation in der Shell, wie Sie bereits bemerkt haben, sehr langsam, erfordert jedoch auch einen unklaren und verzerrten Programmierstil.

43
Bruce Ediger

Es gibt einige komplizierte Antworten, die den Geeks unter uns viele interessante Details geben, aber es ist wirklich ganz einfach - die Verarbeitung einer großen Datei in einer Shell-Schleife ist einfach zu langsam.

Ich denke, der Fragesteller ist an einer typischen Art von Shell-Skript interessiert, das mit einer Befehlszeilenanalyse, Umgebungseinstellungen, dem Überprüfen von Dateien und Verzeichnissen und etwas mehr Initialisierung beginnen kann, bevor er zu seiner Hauptaufgabe übergeht: Durchlaufen eines großen Skripts zeilenorientierte Textdatei.

Für die ersten Teile (initialization) spielt es normalerweise keine Rolle, dass Shell-Befehle langsam sind - es werden nur ein paar Dutzend Befehle ausgeführt, möglicherweise mit ein paar kurzen Schleifen. Selbst wenn wir diesen Teil ineffizient schreiben, dauert es normalerweise weniger als eine Sekunde, um die gesamte Initialisierung durchzuführen, und das ist in Ordnung - es passiert nur einmal.

Aber wenn wir mit der Verarbeitung der großen Datei beginnen, die Tausende oder Millionen von Zeilen enthalten kann, ist dies der Fall nicht gut Für das Shell-Skript dauert es für jede Zeile einen Bruchteil einer Sekunde (selbst wenn es nur ein paar Dutzend Millisekunden sind), da dies zu Stunden führen kann.

Dann müssen wir andere Tools verwenden, und das Schöne an Unix Shell-Skripten ist, dass sie uns dies sehr einfach machen.

Anstatt eine Schleife zu verwenden, um jede Zeile zu betrachten, müssen wir die gesamte Datei durchlaufen eine Pipeline von Befehlen. Dies bedeutet, dass die Shell die Befehle nicht tausend- oder millionenfach aufruft, sondern nur einmal aufruft. Es ist wahr, dass diese Befehle Schleifen haben, um die Datei Zeile für Zeile zu verarbeiten, aber sie sind keine Shell-Skripte und sie sind so konzipiert, dass sie schnell und effizient sind.

Unix verfügt über viele wunderbare integrierte Tools, von einfachen bis hin zu komplexen Tools, mit denen wir unsere Pipelines erstellen können. Normalerweise beginne ich mit den einfachen und verwende nur bei Bedarf komplexere.

Ich würde auch versuchen, mich an Standardtools zu halten, die auf den meisten Systemen verfügbar sind, und versuchen, meine Nutzung portabel zu halten, obwohl dies nicht immer möglich ist. Und wenn Ihre Lieblingssprache Python oder Ruby) ist, macht es Ihnen vielleicht nichts aus, wenn Sie sicherstellen, dass sie auf jeder Plattform installiert ist, auf der Ihre Software ausgeführt werden muss :-)

Einfache Werkzeuge umfassen head, tail, grep, sort, cut, tr, sed, join (beim Zusammenführen von 2 Dateien) und awk unter anderem Einzeiler. Es ist erstaunlich, was manche Leute mit Pattern Matching und sed Befehlen machen können.

Wenn es komplexer wird und Sie wirklich eine Logik auf jede Zeile anwenden müssen, ist awk eine gute Option - entweder ein Einzeiler (einige Leute setzen ganze awk-Skripte in 'eine Zeile', obwohl dies nicht der Fall ist sehr gut lesbar) oder in einem kurzen externen Skript.

Da awk eine interpretierte Sprache ist (wie Ihre Shell), ist es erstaunlich, dass sie zeilenweise so effizient verarbeitet werden kann, aber sie wurde speziell dafür entwickelt und ist wirklich sehr schnell.

Und dann gibt es Perl und eine große Anzahl anderer Skriptsprachen, die sehr gut Textdateien verarbeiten können und außerdem viele nützliche Bibliotheken enthalten.

Und schließlich gibt es ein gutes altes C, wenn Sie es brauchen maximale Geschwindigkeit und hohe Flexibilität (obwohl die Textverarbeitung etwas mühsam ist). Aber es ist wahrscheinlich eine sehr schlechte Zeit, ein neues C-Programm für jede andere Dateiverarbeitungsaufgabe zu schreiben, auf die Sie stoßen. Ich arbeite viel mit CSV-Dateien, daher habe ich in C mehrere allgemeine Dienstprogramme geschrieben, die ich in vielen verschiedenen Projekten wiederverwenden kann. Tatsächlich erweitert dies den Bereich der 'einfachen, schnellen Unix-Tools', die ich aus meinen Shell-Skripten aufrufen kann, sodass ich die meisten Projekte nur durch Schreiben von Skripten bearbeiten kann. Dies ist viel schneller als das Schreiben und Debuggen von maßgeschneidertem C-Code jedes Mal!

Einige letzte Hinweise:

  • vergessen Sie nicht, Ihr Haupt-Shell-Skript mit export LANG=C zu starten, da sonst viele Tools Ihre einfachen alten ASCII-Dateien als Unicode behandeln und sie dadurch viel langsamer machen
  • ziehen Sie auch die Einstellung von export LC_ALL=C in Betracht, wenn sort unabhängig von der Umgebung eine konsistente Reihenfolge erzeugen soll!
  • wenn Sie Ihre Daten sort benötigen, wird dies wahrscheinlich mehr Zeit (und Ressourcen: CPU, Speicher, Festplatte) als alles andere in Anspruch nehmen. Versuchen Sie daher, die Anzahl der Befehle sort und die Größe zu minimieren der Dateien, die sie sortieren
  • wenn möglich, ist eine einzelne Pipeline in der Regel am effizientesten. Das Ausführen mehrerer Pipelines nacheinander mit Zwischendateien ist möglicherweise besser lesbar und debuggbar, verlängert jedoch die Zeit, die Ihr Programm benötigt
26

Ja aber...

Das richtige Antwort von Stéphane Chazelas basiert auf dem Shell Konzept, jede Textoperation an bestimmte Binärdateien wie grep, awk zu delegieren , sed und andere.

Da bash in der Lage ist, viele Dinge selbst zu erledigen, kann das Löschen von Gabeln schneller werden (selbst wenn ein anderer Dolmetscher ausgeführt wird, um alle Aufgaben zu erledigen).

Schauen Sie sich zum Beispiel diesen Beitrag an:

https://stackoverflow.com/a/38790442/1765658

und

https://stackoverflow.com/a/7180078/1765658

testen und vergleichen ...

Na sicher

Es gibt keine Überlegungen zu Benutzereingaben und Sicherheit!

Schreiben Sie keine Webanwendung unter bash !!

Bei vielen Serververwaltungsaufgaben, bei denen bash anstelle von Shell verwendet werden kann, kann die Verwendung von Builtins bash sehr effizient sein.

Meine Bedeutung:

Das Schreiben von Tools wie bin utils ist nicht die gleiche Arbeit wie die Systemadministration.

Also nicht die gleichen Leute!

Wo Sysadmins Shell wissen müssen, könnten sie Prototypen schreiben, indem sie seine bevorzugten (und bekanntestes) Werkzeug.

Wenn dieses neue Dienstprogramm (Prototyp) wirklich nützlich ist, könnten einige andere Leute ein spezielles Werkzeug entwickeln, indem sie eine geeignetere Sprache verwenden.

15
F. Hauri