es scheint, dass Bash eine Turing-vollständige Sprache ist
Das Konzept der Turing-Vollständigkeit ist völlig unabhängig von vielen anderen Konzepten, die in einer Programmiersprache im Großen und Ganzen nützlich sind : Benutzerfreundlichkeit, Ausdruckskraft, Verständlichkeit, Geschwindigkeit usw.
Wenn Turing-Vollständigkeit alle waren wir, würden wir alle Programmiersprachen nicht haben überhaupt , nicht einmal Assemblersprache . Computerprogrammierer würden alle nur in Maschinencode schreiben , da unsere CPUs auch Turing-komplett sind.
Warum wird Bash fast ausschließlich zum Schreiben relativ einfacher Skripte verwendet?
Große, komplexe Shell-Skripte - wie die configure
von GNU Autoconf ausgegebenen Skripte - sind aus vielen Gründen untypisch:
Bis vor relativ kurzer Zeit konnte man nicht überall mit einer POSIX-kompatiblen Shell rechnen .
Viele Systeme, vor allem ältere, haben technisch eine POSIX-kompatible Shell irgendwo auf dem System, aber es kann nicht wie an einem vorhersagbaren Ort sein /bin/sh
. Wenn Sie ein Shell-Skript schreiben und es auf vielen verschiedenen Systemen laufen muss, wie schreiben Sie dann die Shebang-Zeile ? Eine Möglichkeit ist, fortzufahren und zu verwenden /bin/sh
, aber Sie können sich auf den Bourne-Shell-Dialekt vor POSIX beschränken, falls er auf einem solchen System ausgeführt wird.
In Bourne-Shells vor POSIX ist nicht einmal eine Arithmetik integriert. Sie müssen anrufen expr
oder bc
um das zu erledigen.
Selbst bei einer POSIX-Shell fehlen Ihnen assoziative Arrays und andere Funktionen, die wir in Unix-Skriptsprachen erwartet haben, seit Perl in den frühen 1990er-Jahren zum ersten Mal populär wurde .
Diese Tatsache aus der Geschichte bedeutet, dass es eine jahrzehntelange Tradition gibt, viele der leistungsstarken Funktionen moderner Shell-Skript-Interpreter der Bourne-Familie zu ignorieren, nur weil Sie nicht darauf zählen können, dass sie überall verfügbar sind.
Das geht bis heute so: Bash hat bis zur Version 4 keine assoziativen Arrays erhalten , aber Sie werden überrascht sein, wie viele Systeme noch auf Bash 3 basieren. Apple liefert Bash 3 2017 noch mit macOS aus - anscheinend für Lizenzgründe - und Unix / Linux-Server laufen oft sehr lange, aber unberührt in der Produktion, sodass Sie möglicherweise ein stabiles altes System haben, auf dem Bash 3 ausgeführt wird, wie beispielsweise eine CentOS 5-Box. Wenn Sie solche Systeme in Ihrer Umgebung haben, können Sie in Shell-Skripten, die darauf ausgeführt werden müssen, keine assoziativen Arrays verwenden.
Wenn Sie auf dieses Problem antworten, dass Sie nur Shellskripte für "moderne" Systeme schreiben, müssen Sie damit rechnen, dass der letzte gemeinsame Bezugspunkt für die meisten Unix-Shells der POSIX-Shell-Standard ist , der seither weitgehend unverändert ist Es gibt viele verschiedene Schalen, die auf diesem Standard basieren, aber alle sind in unterschiedlichem Maße von diesem Standard abgewichen. Um wieder assoziative Arrays zu nehmen bash
, zsh
und ksh93
alle haben diese Funktion, aber es gibt mehrere Implementierung Inkompatibilitäten. Sie haben dann die Wahl, nur Bash oder nur Zsh oder nur Bash zu verwenden ksh93
.
Wenn Ihre Antwort auf dieses Problem lautet: "Installieren Sie einfach Bash 4" oder ksh93
was auch immer, warum installieren Sie dann nicht stattdessen "nur" Perl, Python oder Ruby? Das ist in vielen Fällen inakzeptabel. Standardeinstellungen sind wichtig.
Keine der Shell-Skriptsprachen der Bourne-Familie unterstützt Module .
In einem Shell-Skript können Sie einem Modulsystem am nächsten kommen, indem Sie den .
Befehl eingeben - auch bekannt als source
bei neueren Bourne-Shell-Varianten -, der auf mehreren Ebenen im Vergleich zu einem geeigneten Modulsystem fehlschlägt. Das grundlegendste davon ist der Namespace .
Unabhängig von der Programmiersprache beginnt das menschliche Verständnis zu schwinden, wenn eine einzelne Datei in einem größeren Gesamtprogramm mehrere tausend Zeilen überschreitet. Der eigentliche Grund, warum wir große Programme in viele Dateien strukturieren, besteht darin, dass wir ihren Inhalt zu höchstens ein oder zwei Sätzen zusammenfassen können. Datei A ist der Befehlszeilenparser, Datei B ist die Netzwerk-E / A-Pumpe, Datei C ist die Schnittstelle zwischen Bibliothek Z und dem Rest des Programms usw. Wenn Ihre einzige Methode zum Zusammenstellen vieler Dateien in einem einzigen Programm die Texteinbeziehung ist Sie haben eine Grenze gesetzt, wie groß Ihre Programme angemessen wachsen können.
Zum Vergleich wäre es so, als hätte die Programmiersprache C keinen Linker, nur #include
Anweisungen. Ein solcher C-Lite-Dialekt würde keine Schlüsselwörter wie extern
oder benötigen static
. Diese Funktionen sind vorhanden, um Modularität zu ermöglichen.
POSIX definiert keine Möglichkeit , Variablen auf eine einzelne Shell-Skriptfunktion zu beschränken, geschweige denn auf eine Datei.
Dadurch werden alle Variablen effektiv global , was wiederum die Modularität und Zusammensetzbarkeit beeinträchtigt.
Es gibt Lösungen für dieses in Post-POSIX - Schalen - sicherlich in bash
, ksh93
und zsh
zumindest - aber das bringt Sie gerade zurück zu Punkt 1 oben.
Sie können die Auswirkung davon in den Styleguides beim Schreiben von GNU Autoconf-Makros sehen, in denen empfohlen wird , Variablennamen den Namen des Makros als Präfix voranzustellen, was zu sehr langen Variablennamen führt, um die Wahrscheinlichkeit einer Kollision auf ein akzeptables Maß zu reduzieren Null.
Sogar C ist in dieser Hinsicht um eine Meile besser. Die meisten C-Programme werden nicht nur hauptsächlich mit funktionslokalen Variablen geschrieben, sondern C unterstützt auch das Block-Scoping, sodass mehrere Blöcke innerhalb einer einzigen Funktion Variablennamen ohne Kreuzkontamination wiederverwenden können.
Shell-Programmiersprachen haben keine Standardbibliothek.
Man kann argumentieren, dass die Standardbibliothek einer Shell-Skriptsprache der Inhalt von ist PATH
, aber das besagt nur, dass ein Shell-Skript ein anderes ganzes Programm aufrufen muss, wahrscheinlich eines, das in einer mächtigeren Sprache geschrieben wurde, um Konsequenzen zu erzielen anfangen mit.
Es gibt auch kein weit verbreitetes Archiv von Shell-Dienstprogrammbibliotheken wie das von Perl verwendete CPAN . Ohne eine große verfügbare Bibliothek mit Utility-Code von Drittanbietern muss ein Programmierer mehr Code von Hand schreiben, damit er weniger produktiv ist.
Selbst wenn man die Tatsache ignoriert, dass die meisten Shell-Skripte auf externen Programmen basieren, die normalerweise in C geschrieben sind, um nützliche Aufgaben zu erledigen, entstehen all diese pipe()
→ fork()
→ exec()
Aufrufketten. Dieses Muster ist unter Unix ziemlich effizient im Vergleich zu IPC und dem Starten von Prozessen unter anderen Betriebssystemen, aber hier ersetzt es effektiv das, was Sie mit einem Unterprogrammaufruf in einer anderen Skriptsprache tun würden , was noch weitaus effizienter ist. Dadurch wird die Höchstgeschwindigkeit für die Ausführung von Shell-Skripten stark begrenzt.
Shell-Skripte sind nur wenig in der Lage, ihre Leistung durch parallele Ausführung zu steigern.
Bourne - Shells hat &
, wait
und Pipelines für diese, aber das ist weitgehend nur nützlich für mehrere Programme zu komponieren, nicht für die CPU zu erreichen oder E / A - Parallelität. Sie sind wahrscheinlich nicht in der Lage, die Kerne zu fixieren oder ein RAID-Array nur mit Shell-Scripting zu sättigen, und wenn Sie dies tun, könnten Sie wahrscheinlich eine viel höhere Leistung in anderen Sprachen erzielen.
Insbesondere Pipelines sind schwache Möglichkeiten, die Leistung durch parallele Ausführung zu steigern. Es können nur zwei Programme gleichzeitig ausgeführt werden, und eines der beiden Programme wird wahrscheinlich zu einem bestimmten Zeitpunkt für die E / A von oder zu dem anderen gesperrt .
Es gibt heutzutage Wege, wie zum Beispiel xargs -P
und GNUparallel
, aber dies widmet sich nur Punkt 4 oben.
Shell-Skripte können Multi-Prozessor-Systeme praktisch nicht voll ausnutzen und sind daher immer langsamer als gut geschriebene Programme in einer Sprache, die alle Prozessoren des Systems nutzen kann. Um dieses GNU Autoconf- configure
Skriptbeispiel noch einmal zu verwenden, führt eine Verdoppelung der Anzahl der Kerne im System kaum zu einer Verbesserung der Geschwindigkeit, mit der es ausgeführt wird.
Shell-Skriptsprachen haben keine Zeiger oder Referenzen .
Dies verhindert, dass Sie eine Reihe von Aufgaben in anderen Programmiersprachen erledigen können.
Zum einen bedeutet die Unfähigkeit, indirekt auf eine andere Datenstruktur im Programmspeicher zu verweisen, dass Sie auf die integrierten Datenstrukturen beschränkt sind . Ihre Shell verfügt möglicherweise über assoziative Arrays , aber wie werden sie implementiert? Es gibt verschiedene Möglichkeiten mit unterschiedlichen Kompromissen: Rot-Schwarz-Bäume , AVL-Bäume und Hash-Tabellen sind am häufigsten, aber es gibt auch andere. Wenn Sie einen anderen Satz von Kompromissen benötigen, stecken Sie fest, weil Sie ohne Referenzen nicht die Möglichkeit haben, viele Arten von erweiterten Datenstrukturen von Hand zu rollen. Sie stecken fest mit dem, was Ihnen gegeben wurde.
Es kann aber auch vorkommen, dass Sie eine Datenstruktur benötigen, in deren Shell-Skript-Interpreter nicht einmal eine geeignete Alternative integriert ist, z. B. ein gerichtetes azyklisches Diagramm , das Sie möglicherweise benötigen, um ein Abhängigkeitsdiagramm zu modellieren . Ich programmiere seit Jahrzehnten und die einzige Möglichkeit, die mir in einem Shell-Skript einfällt, besteht darin, das Dateisystem zu missbrauchen und Symlinks als falsche Referenzen zu verwenden. Das ist die Art von Lösung, die Sie erhalten, wenn Sie sich nur auf die Vollständigkeit von Turing verlassen, die Ihnen nichts darüber aussagt, ob die Lösung elegant, schnell oder einfach zu verstehen ist.
Fortgeschrittene Datenstrukturen sind nur eine Verwendung für Zeiger und Referenzen. Es gibt unzählige andere Anwendungen für sie , die in einer Shell-Skriptsprache der Bourne-Familie einfach nicht einfach ausgeführt werden können.
Ich könnte weiter und weiter gehen, aber ich denke, Sie verstehen es hier. Einfach ausgedrückt, es gibt viele leistungsfähigere Programmiersprachen für Unix-Systeme.
Dies ist ein großer Vorteil, der in einigen Fällen die Mittelmäßigkeit der Sprache selbst ausgleichen könnte.
Sicher, und genau deshalb verwendet GNU Autoconf für seine configure
Skriptausgaben eine absichtlich eingeschränkte Teilmenge der Bourne-Familie von Shell-Skriptsprachen, sodass die configure
Skripts praktisch überall ausgeführt werden.
Sie werden wahrscheinlich keine größere Gruppe von Gläubigen finden, die an die Nützlichkeit des Schreibens in einem hoch portierbaren Bourne-Shell-Dialekt glauben als die Entwickler von GNU Autoconf, aber ihre eigene Kreation ist hauptsächlich in Perl geschrieben, plus einige m4
und nur ein bisschen Shell Skript; Nur die Ausgabe von Autoconf ist ein reines Bourne-Shell-Skript. Wenn das nicht die Frage aufwirft, wie nützlich das Konzept "Bourne everywhere" ist, weiß ich nicht, wie es sein wird.
Gibt es eine Grenze für die Komplexität solcher Programme?
Technisch gesehen nein, wie Ihre Turing-Vollständigkeitsbeobachtung nahelegt.
Das ist aber nicht dasselbe wie zu sagen, dass beliebig große Shell-Skripte angenehm zu schreiben, einfach zu debuggen oder schnell auszuführen sind.
Ist es möglich, beispielsweise einen Dateikomprimierer / Dekomprimierer in reiner Bash zu schreiben?
"Pure" Bash, ohne irgendwelche Aufrufe zu Dingen in der PATH
? Der Kompressor ist wahrscheinlich mit echo
und hex Escape Sequenzen machbar , aber es wäre ziemlich schmerzhaft zu tun. Der Dekomprimierer kann möglicherweise nicht auf diese Weise geschrieben werden, da er keine Binärdaten in der Shell verarbeiten kann . Am Ende würden Sie u. U. od
Binärdaten in das Textformat übersetzen, die native Art der Shell, mit Daten umzugehen.
Sobald Sie anfangen, Shell-Skripte so zu verwenden, wie es beabsichtigt war, um andere Programme zu PATH
steuern, öffnen sich die Türen, da Sie sich nur noch auf das beschränken, was in anderen Programmiersprachen möglich ist habe überhaupt keine Grenzen. Ein Shell-Skript, das seine ganze Kraft PATH
entfaltet, indem es andere Programme in der ausführt, läuft nicht so schnell wie monolithische Programme, die in leistungsstärkeren Sprachen geschrieben sind, aber es läuft .
Und genau darum geht es. Wenn Sie ein Programm benötigen, um schnell zu arbeiten, oder wenn es von sich aus leistungsfähig sein muss, anstatt sich die Leistung von anderen zu leihen, schreiben Sie es nicht in Shell.
Ein einfaches Videospiel?
Hier ist Tetris in der Schale . Andere solche Spiele sind verfügbar, wenn Sie auf der Suche sind.
Es gibt nur sehr begrenzte Debugging-Tools
Ich würde die Unterstützung für das Debugging-Tool auf den 20. Platz in der Liste der Funktionen setzen, die zur Unterstützung der Programmierung im Großen erforderlich sind. Eine ganze Reihe von Programmierern verlassen sich viel stärker auf das printf()
Debuggen als auf die richtigen Debugger, unabhängig von der Sprache.
In der Shell haben Sie echo
und set -x
, die zusammen ausreichen, um sehr viele Probleme zu debuggen.
sh
Skript,configure
das im Rahmen des Erstellungsprozesses für sehr viele un * x-Pakete verwendet wird, ist nicht "relativ einfach".