Nach einigen sehr schnellen Recherchen scheint Bash eine Turing-vollständige Sprache zu sein .
Ich frage mich, warum wird Bash fast ausschließlich zum Schreiben relativ einfacher Skripte verwendet? Da eine Bash-Shell mit Linux geliefert wird, können Sie Shell-Skripte ohne externen Interpreter oder Compiler ausführen, wie dies für andere gängige Computersprachen erforderlich ist. Dies ist ein großer Vorteil, der in einigen Fällen die Mittelmäßigkeit der Sprache selbst ausgleichen könnte.
Gibt es eine Grenze für die Komplexität solcher Programme? Wird reines Bash verwendet, um komplexe Programme zu schreiben? Ist es möglich, beispielsweise einen Dateikomprimierer / Dekomprimierer in Pure Bash zu schreiben? Ein Compiler? Ein einfaches Videospiel?
Wird es nur deshalb so sparsam verwendet, weil es nur sehr begrenzte Debugging-Tools gibt?
quelle
sh
Skript,configure
das im Rahmen des Erstellungsprozesses für sehr viele un * x-Pakete verwendet wird, ist nicht "relativ einfach".m4
Makros.configure
Skripte sind auch langsam, erledigen eine ganze Reihe von nutzlosen Arbeiten und waren Gegenstand einiger amüsanter Ausschreitungen. Natürlich kann die Shell für große Programme verwendet werden, aber andererseits haben die Leute auch Computer aus Conways Game of Life und Minecraft gemacht, und es gibt auch Programmiersprachen wie Brainf ** k und Hexagony . Anscheinend bauen manche Leute gerne etwas aus wirklich kleinen und verwirrenden Atomen. Sie können sogar Spiele mit dieser Idee verkaufen ...Antworten:
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.
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
oderbc
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
undksh93
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 verwendenksh93
.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 alssource
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 wieextern
oder benötigenstatic
. 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
undzsh
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.
Sicher, und genau deshalb verwendet GNU Autoconf für seine
configure
Skriptausgaben eine absichtlich eingeschränkte Teilmenge der Bourne-Familie von Shell-Skriptsprachen, sodass dieconfigure
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.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.
"Pure" Bash, ohne irgendwelche Aufrufe zu Dingen in der
PATH
? Der Kompressor ist wahrscheinlich mitecho
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 KraftPATH
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.
Hier ist Tetris in der Schale . Andere solche Spiele sind verfügbar, wenn Sie auf der Suche sind.
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
undset -x
, die zusammen ausreichen, um sehr viele Probleme zu debuggen.quelle
&
Sie Prozesse parallel ausführen. Sie könnenwait
die untergeordneten Prozesse abschließen. Sie können Pipelines und komplexere Netzwerke von Pipes mithilfe von Named Pipes einrichten. Am wichtigsten ist, dass es einfach ist, die Parallelverarbeitung auf die richtige Art und Weise mit sehr wenig Boilerplate-Code durchzuführen und die Risiken und Schwierigkeiten von Multithreading mit gemeinsamem Speicher zu vermeiden.Wir können überall gehen oder schwimmen. Warum beschäftigen wir uns dann mit Fahrrädern, Autos, Zügen, Booten, Flugzeugen und anderen Fahrzeugen? Sicher, Laufen oder Schwimmen kann anstrengend sein, aber es ist von großem Vorteil, keine zusätzliche Ausrüstung zu benötigen.
Zum einen kann bash, obwohl es sich um Turing-complete handelt, nur ganze Zahlen (nicht zu groß), Zeichenfolgen (eindimensionale) Arrays von Zeichenfolgen und endliche Abbildungen von Zeichenfolgen zu Zeichenfolgen verarbeiten. Für jede andere Art von Daten ist eine lästige Codierung erforderlich, die das Schreiben des Programms erschwert und häufig zu einer Leistung führt, die in der Praxis nicht gut genug ist. Beispielsweise sind Gleitkommaoperationen in bash hart und langsam.
Darüber hinaus hat bash nur sehr wenige Möglichkeiten, mit seiner Umgebung zu interagieren. Es kann Prozesse ausführen, es kann einige einfache Arten von Dateizugriffen ausführen (durch Umleitung), und das war's auch schon. Bash verfügt auch über einen clientseitigen Netzwerkclient. Bash kann leicht genug Null-Bytes ausgeben (
printf \\0
), aber keine Null-Bytes in seiner Eingabe analysieren, was es ungeeignet macht, Binärdaten zu lesen. Bash kann nicht direkt andere Dinge tun: Es muss dafür externe Programme aufrufen. Und das ist in Ordnung: Shells sind in erster Linie für die Ausführung externer Programme konzipiert! Shells sind die Klebesprache, um Programme miteinander zu kombinieren. Wenn Sie jedoch ein externes Programm ausführen, bedeutet dies, dass das Programm verfügbar sein muss - und dann verringern Sie den Portabilitätsvorteil:).Bash hat keine Funktion, die es einfacher macht, robuste Programme zu schreiben, abgesehen von
set -e
. Es gibt keine (nützlichen) Typen, Namespaces, Module oder verschachtelten Datenstrukturen. Fehler sind die größte Schwierigkeit beim Programmieren. Während die Leichtigkeit, fehlerfreie Programme zu schreiben, nicht immer der entscheidende Faktor bei der Auswahl einer Sprache ist, wird bash in dieser Hinsicht schlecht bewertet. Bash steht auch in Bezug auf die Leistung schlecht da, wenn es darum geht, Programme nicht miteinander zu kombinieren.Bash lief lange nicht unter Windows, und selbst heute ist es in einer Windows-Standardinstallation nicht vorhanden, und es läuft nicht vollständig nativ (auch nicht in WSL) in dem Sinne, dass es keine Schnittstellen zu hat Windows native Funktionen. Bash läuft nicht unter iOS und wird unter Android nicht standardmäßig installiert. Wenn Sie also keine reine Unix-Anwendung schreiben, ist bash überhaupt nicht portierbar.
Das Erfordernis eines Compilers ist für die Portabilität kein Problem. Der Compiler läuft auf dem Entwicklerrechner. Das Erfordernis eines Interpreters oder von Bibliotheken von Drittanbietern kann ein Problem sein, aber unter Linux ist es ein durch Distributionspakete gelöstes Problem, und unter Windows, Android und iOS werden Komponenten von Drittanbietern im Allgemeinen in ihrem Anwendungspaket gebündelt. Die Art der Portabilitätsbedenken, die Sie haben, sind also keine praktischen Bedenken für Standardanwendungen.
Meine Antwort bezieht sich auf andere Muscheln als Bash. Einige Details variieren von Shell zu Shell, aber die allgemeine Idee ist dieselbe.
quelle
Einige Gründe, warum ich keine Shell-Skripte für große Programme verwenden sollte:
mkdir
odergrep
intern leisten .zsh
hat etwas Unterstützung. Dies liegt auch daran, dass die Schnittstelle für externe Programme zumeist textbasiert\0
ist und als Trennzeichen verwendet wird.bash -c ...
oderssh -c ...
).quelle