Eine Semantik für Bash-Skripte?

87

Mehr als jede andere Sprache, die ich kenne, habe ich Bash jedes Mal durch Googeln "gelernt", wenn ich etwas brauche. Folglich kann ich kleine Skripte zusammenfügen, die anscheinend funktionieren. Allerdings glaube ich nicht wirklich wissen , was los ist , und ich hatte gehofft , für eine formelle Einführung in die Bash als Programmiersprache. Zum Beispiel: Wie lautet die Bewertungsreihenfolge? Was sind die Scoping-Regeln? Was ist die Schreibdisziplin, zB ist alles eine Zeichenfolge? Wie ist der Status des Programms? Ist es eine Schlüsselwertzuweisung von Zeichenfolgen zu Variablennamen? Gibt es mehr als das, zB den Stapel? Gibt es einen Haufen? Und so weiter.

Ich dachte, ich würde das GNU Bash-Handbuch für diese Art von Einsicht konsultieren, aber es scheint nicht das zu sein, was ich will. Es ist eher eine Wäscheliste mit syntaktischem Zucker als eine Erklärung des semantischen Kernmodells. Die millionenfachen "Bash-Tutorials" im Internet sind nur noch schlimmer. Vielleicht sollte ich zuerst shBash als syntaktischen Zucker studieren und darüber hinaus verstehen? Ich weiß jedoch nicht, ob dies ein genaues Modell ist.

Irgendwelche Vorschläge?

EDIT: Ich wurde gebeten, Beispiele dafür zu liefern, wonach ich im Idealfall suche. Ein ziemlich extremes Beispiel für eine "formale Semantik" ist dieses Papier über "die Essenz von JavaScript" . Ein etwas weniger formelles Beispiel ist vielleicht der Haskell 2010-Bericht .

Jamesfischer
quelle
3
Ist der Advanced Bash Scripting Guide einer der "Millionen und Eins"?
Choroba
2
Ich bin nicht davon überzeugt, dass Bash ein "semantisches Kernmodell" hat (na ja, vielleicht "fast alles ist eine Zeichenfolge"); Ich denke, es ist wirklich syntaktischer Zucker.
Gordon Davisson
4
Was Sie "eine Wäscheliste mit syntaktischem Zucker" nennen, ist eigentlich das semantische Expansionsmodell - ein äußerst wichtiger Teil der Ausführung. 90% der Fehler und Verwirrungen sind darauf zurückzuführen, dass das Erweiterungsmodell nicht verstanden wurde.
andere Typ
4
Ich kann sehen, warum jemand denkt, dass dies eine allgemeine Frage ist, wenn Sie sie so lesen, wie schreibe ich ein Shell-Skript ? Die eigentliche Frage ist jedoch, welche formale Semantik und Grundlage insbesondere für Shell-Sprache und Bash besteht. und es ist eine gute Frage mit einer einzigen zusammenhängenden Antwort. Abstimmung zur Wiedereröffnung.
Kojiro
1
Ich habe auf linuxcommand.org einiges gelernt und es gibt sogar ein kostenloses PDF mit einem ausführlicheren Buch in der Kommandozeile und dem Schreiben von Shell-Skripten
Samrap

Antworten:

107

Eine Shell ist eine Schnittstelle für das Betriebssystem. Es ist normalerweise eine mehr oder weniger robuste Programmiersprache für sich, aber mit Funktionen, die es einfach machen, spezifisch mit dem Betriebssystem und dem Dateisystem zu interagieren. Die Semantik der POSIX-Shell (im Folgenden nur als "Shell" bezeichnet) ist ein bisschen wie ein Köter, der einige Funktionen von LISP (S-Ausdrücke haben viel mit der Aufteilung von Shell- Wörtern gemeinsam ) und C (ein Großteil der arithmetischen Syntax der Shell) kombiniert Semantik kommt von C).

Die andere Wurzel der Syntax der Shell liegt in ihrer Erziehung als Mischmasch einzelner UNIX-Dienstprogramme. Die meisten der häufig in der Shell integrierten Funktionen können tatsächlich als externe Befehle implementiert werden. Es wirft viele Shell-Neophyten für eine Schleife, wenn sie erkennen, dass dies /bin/[auf vielen Systemen vorhanden ist.

$ if '/bin/[' -f '/bin/['; then echo t; fi # Tested as-is on OS X, without the `]`
t

wat?

Dies ist viel sinnvoller, wenn Sie sich ansehen, wie eine Shell implementiert ist. Hier ist eine Implementierung, die ich als Übung gemacht habe. Es ist in Python, aber ich hoffe, das ist für niemanden ein Auflegen. Es ist nicht besonders robust, aber lehrreich:

#!/usr/bin/env python

from __future__ import print_function
import os, sys

'''Hacky barebones shell.'''

try:
  input=raw_input
except NameError:
  pass

def main():
  while True:
    cmd = input('prompt> ')
    args = cmd.split()
    if not args:
      continue
    cpid = os.fork()
    if cpid == 0:
      # We're in a child process
      os.execl(args[0], *args)
    else:
      os.waitpid(cpid, 0)

if __name__ == '__main__':
  main()

Ich hoffe, dass das Obige deutlich macht, dass das Ausführungsmodell einer Shell ziemlich genau ist:

1. Expand words.
2. Assume the first word is a command.
3. Execute that command with the following words as arguments.

Erweiterung, Befehlsauflösung, Ausführung. Die gesamte Semantik der Shell hängt mit einem dieser drei Dinge zusammen, obwohl sie weitaus umfangreicher sind als die Implementierung, die ich oben geschrieben habe.

Nicht alle Befehle fork. Tatsächlich gibt es eine Handvoll Befehle, die als extern nicht sinnvoll sinnvoll sind (so dass sie es müssten)fork ), aber selbst diese sind häufig als externe für die strikte POSIX-Konformität verfügbar.

Bash baut auf dieser Basis auf und fügt neue Funktionen und Schlüsselwörter hinzu, um die POSIX-Shell zu verbessern. Es ist nahezu kompatibel mit sh und bash ist so allgegenwärtig, dass einige Skriptautoren Jahre damit verbringen, ohne zu bemerken, dass ein Skript möglicherweise nicht auf einem POSIX-strengen System funktioniert. (Ich frage mich auch, wie die Leute sich so sehr um die Semantik und den Stil einer Programmiersprache kümmern können und so wenig um die Semantik und den Stil der Shell, aber ich bin anderer Meinung.)

Reihenfolge der Bewertung

Dies ist eine Trickfrage: Bash interpretiert Ausdrücke in seiner primären Syntax von links nach rechts, aber in seiner arithmetischen Syntax folgt sie der C-Priorität. Ausdrücke unterscheiden sich jedoch von Erweiterungen . Aus dem EXPANSIONAbschnitt des Bash-Handbuchs:

Die Reihenfolge der Erweiterungen lautet: Klammererweiterung; Tilde-Erweiterung, Parameter- und Variablenerweiterung, arithmetische Erweiterung und Befehlssubstitution (von links nach rechts); Wortteilung; und Pfadnamenerweiterung.

Wenn Sie Wortsplitting, Pfadnamenerweiterung und Parametererweiterung verstehen, sind Sie auf dem besten Weg, die meisten Funktionen von Bash zu verstehen. Beachten Sie, dass die Pfadnamenerweiterung nach dem Aufteilen von Wörtern von entscheidender Bedeutung ist, da dadurch sichergestellt wird, dass eine Datei mit einem Leerzeichen im Namen weiterhin von einem Glob abgeglichen werden kann. Aus diesem Grund ist die gute Verwendung von Glob-Erweiterungen besser als das Parsen von Befehlen im Allgemeinen .

Umfang

Funktionsumfang

Ähnlich wie altes ECMAscript hat die Shell einen dynamischen Bereich, es sei denn, Sie deklarieren explizit Namen innerhalb einer Funktion.

$ foo() { echo $x; }
$ bar() { local x; echo $x; }
$ foo

$ bar

$ x=123
$ foo
123
$ bar

$ 

Umwelt und Prozess "Umfang"

Subshells erben die Variablen ihrer übergeordneten Shells, andere Arten von Prozessen erben jedoch keine nicht exportierten Namen.

$ x=123
$ ( echo $x )
123
$ bash -c 'echo $x'

$ export x
$ bash -c 'echo $x'
123
$ y=123 bash -c 'echo $y' # another way to transiently export a name
123

Sie können diese Bereichsregeln kombinieren:

$ foo() {
>   local -x bar=123 # Export foo, but only in this scope
>   bash -c 'echo $bar'
> }
$ foo
123
$ echo $bar

$

Schreibdisziplin

Ähm, Typen. Ja. Bash hat wirklich keine Typen und alles wird zu einer Zeichenfolge erweitert (oder vielleicht wäre ein Wort besser geeignet). Aber lassen Sie uns die verschiedenen Arten von Erweiterungen untersuchen.

Saiten

So ziemlich alles kann als String behandelt werden. Barwörter in Bash sind Zeichenfolgen, deren Bedeutung vollständig von der darauf angewendeten Erweiterung abhängt.

Keine Erweiterung

Es kann sich lohnen zu zeigen, dass ein bloßes Wort wirklich nur ein Wort ist und dass Zitate daran nichts ändern.

$ echo foo
foo
$ 'echo' foo
foo
$ "echo" foo
foo
Teilstringerweiterung
$ fail='echoes'
$ set -x # So we can see what's going on
$ "${fail:0:-2}" Hello World
+ echo Hello World
Hello World

Weitere Informationen zu Erweiterungen finden Sie im Parameter ExpansionAbschnitt des Handbuchs. Es ist ziemlich mächtig.

Ganzzahlen und arithmetische Ausdrücke

Sie können Namen mit dem Integer-Attribut versehen, um die Shell anzuweisen, die rechte Seite von Zuweisungsausdrücken als arithmetisch zu behandeln. Wenn der Parameter dann erweitert wird, wird er als ganzzahlige Mathematik ausgewertet, bevor er zu… einem String erweitert wird.

$ foo=10+10
$ echo $foo
10+10
$ declare -i foo
$ foo=$foo # Must re-evaluate the assignment
$ echo $foo
20
$ echo "${foo:0:1}" # Still just a string
2

Arrays

Argumente und Positionsparameter

Bevor Sie über Arrays sprechen, sollten Sie die Positionsparameter diskutieren. Die Argumente für ein Shell - Skript kann mit nummerierten Parameter, die zugegriffen werden soll $1, $2, $3, etc. Sie alle diese Parameter auf einmal zugreifen können "$@", die Erweiterung viele Dinge gemeinsam mit Arrays hat. Sie können die Positionsparameter mithilfe der setoder integrierten shiftFunktionen oder einfach durch Aufrufen der Shell oder einer Shell-Funktion mit den folgenden Parametern festlegen und ändern :

$ bash -c 'for ((i=1;i<=$#;i++)); do
>   printf "\$%d => %s\n" "$i" "${@:i:1}"
> done' -- foo bar baz
$1 => foo
$2 => bar
$3 => baz
$ showpp() {
>   local i
>   for ((i=1;i<=$#;i++)); do
>     printf '$%d => %s\n' "$i" "${@:i:1}"
>   done
> }
$ showpp foo bar baz
$1 => foo
$2 => bar
$3 => baz
$ showshift() {
>   shift 3
>   showpp "$@"
> }
$ showshift foo bar baz biz quux xyzzy
$1 => biz
$2 => quux
$3 => xyzzy

Das Bash-Handbuch wird manchmal auch $0als Positionsparameter bezeichnet. Ich finde das verwirrend, weil es es nicht in die Argumentanzahl einbezieht $#, aber es ist ein nummerierter Parameter, also meh. $0ist der Name der Shell oder des aktuellen Shell-Skripts.

Arrays

Die Syntax von Arrays wird nach Positionsparametern modelliert, daher ist es meistens sinnvoll, sich Arrays als benannte Art von "externen Positionsparametern" vorzustellen, wenn Sie möchten. Arrays können mit den folgenden Ansätzen deklariert werden:

$ foo=( element0 element1 element2 )
$ bar[3]=element3
$ baz=( [12]=element12 [0]=element0 )

Sie können über den Index auf Array-Elemente zugreifen:

$ echo "${foo[1]}"
element1

Sie können Arrays in Scheiben schneiden:

$ printf '"%s"\n' "${foo[@]:1}"
"element1"
"element2"

Wenn Sie ein Array als normalen Parameter behandeln, erhalten Sie den nullten Index.

$ echo "$baz"
element0
$ echo "$bar" # Even if the zeroth index isn't set

$ 

Wenn Sie Anführungszeichen oder Backslashes verwenden, um das Aufteilen von Wörtern zu verhindern, behält das Array das angegebene Aufteilen von Wörtern bei:

$ foo=( 'elementa b c' 'd e f' )
$ echo "${#foo[@]}"
2

Der Hauptunterschied zwischen Arrays und Positionsparametern ist:

  1. Positionsparameter sind nicht spärlich. Wenn $12eingestellt ist, können Sie sicher sein, dass auch $11eingestellt ist. (Es könnte auf die leere Zeichenfolge gesetzt werden, wird aber $#nicht kleiner als 12 sein.) Wenn "${arr[12]}"gesetzt ist, gibt es keine Garantie, "${arr[11]}"die gesetzt ist, und die Länge des Arrays könnte so klein wie 1 sein.
  2. Das nullte Element eines Arrays ist eindeutig das nullte Element dieses Arrays. In Positionsparametern ist das nullte Element nicht das erste Argument , sondern der Name der Shell oder des Shell-Skripts.
  3. Zu shifteinem Array müssen Sie es in Scheiben schneiden und neu zuweisen, wie z arr=( "${arr[@]:1}" ). Sie könnten es auch tun unset arr[0], aber das würde das erste Element bei Index 1 ergeben.
  4. Arrays können implizit von Shell-Funktionen als Globals gemeinsam genutzt werden. Sie müssen jedoch Positionsparameter explizit an eine Shell-Funktion übergeben, damit diese angezeigt werden.

Es ist oft praktisch, Pfadnamenerweiterungen zu verwenden, um Arrays von Dateinamen zu erstellen:

$ dirs=( */ )

Befehle

Befehle sind der Schlüssel, aber sie werden auch ausführlicher behandelt, als ich es im Handbuch kann. Lesen Sie den SHELL GRAMMARAbschnitt. Die verschiedenen Arten von Befehlen sind:

  1. Einfache Befehle (zB $ startx)
  2. Pipelines (zB $ yes | make config) (lol)
  3. Listen (zB $ grep -qF foo file && sed 's/foo/bar/' file > newfile)
  4. Verbindung Befehle (zB $ ( cd -P /var/www/webroot && echo "webroot is $PWD" ))
  5. Coprozesse (komplex, kein Beispiel)
  6. Funktionen (Ein benannter zusammengesetzter Befehl, der als einfacher Befehl behandelt werden kann)

Ausführungsmodell

Das Ausführungsmodell umfasst natürlich sowohl einen Heap als auch einen Stack. Dies ist in allen UNIX-Programmen endemisch. Bash hat auch einen Aufrufstapel für Shell-Funktionen, der über die verschachtelte Verwendung von sichtbar istcaller Systems .

Verweise:

  1. Der SHELL GRAMMARAbschnitt des Bash-Handbuchs
  2. Die Dokumentation zur XCU Shell Command Language
  3. Der Bash Guide im Greycat-Wiki.
  4. Erweiterte Programmierung in der UNIX-Umgebung

Bitte machen Sie Kommentare, wenn Sie möchten, dass ich in eine bestimmte Richtung weiter expandiere.

Kojiro
quelle
16
+1: Tolle Erklärung. Schätzen Sie die Zeit, die Sie für das Schreiben aufgewendet haben, anhand von Beispielen.
Jaypal Singh
+1 für yes | make config;-) Aber im Ernst, eine sehr gute Zusammenfassung.
Digitales Trauma
Ich habe gerade angefangen, das zu lesen. Schön. wird einige Kommentare hinterlassen. 1) Eine noch größere Überraschung kommt, wenn Sie das sehen /bin/[und /bin/testist oft die gleiche Anwendung. 2) "Angenommen, das erste Wort ist ein Befehl." - Erwarten Sie, wenn Sie Aufgabe tun ...
Karoly Horvath
@KarolyHorvath Ja, ich habe die Zuweisung absichtlich von meiner Demo-Shell ausgeschlossen, da Variablen ein kompliziertes Durcheinander sind. Diese Demo-Shell wurde nicht mit dieser Antwort geschrieben - sie wurde viel früher geschrieben. Ich nehme an, ich könnte es schaffen execleund die ersten Wörter in die Umgebung interpolieren, aber das würde es noch ein bisschen komplizierter machen.
Kojiro
@kojiro: nein das würde es nur überkomplizieren, das war sicher nicht meine absicht! aber die Zuordnung funktioniert etwas anders (x), und meiner Meinung nach sollten Sie es irgendwo im Text erwähnen. (x): und eine Quelle der Verwirrung ... Ich kann nicht einmal mehr zählen, wie oft ich Leute gesehen habe, die sich darüber beschwert haben, dass sie a = 1nicht arbeiten).
Karoly Horvath
5

Die Antwort auf Ihre Frage "Was ist die Schreibdisziplin, z. B. ist alles eine Zeichenfolge?" Bash-Variablen sind Zeichenfolgen. Bash erlaubt jedoch arithmetische Operationen und Vergleiche von Variablen, wenn Variablen Ganzzahlen sind. Die Ausnahme für Regel-Bash-Variablen sind Zeichenfolgen, wenn diese Variablen gesetzt oder anderweitig deklariert sind

$ A=10/2
$ echo "A = $A"           # Variable A acting like a String.
A = 10/2

$ B=1
$ let B="$B+1"            # Let is internal to bash.
$ echo "B = $B"           # One is added to B was Behaving as an integer.
B = 2

$ A=1024                  # A Defaults to string
$ B=${A/24/STRING01}      # Substitute "24"  with "STRING01".
$ echo "B = $B"           # $B STRING is a string
B = 10STRING01

$ B=${A/24/STRING01}      # Substitute "24"  with "STRING01".
$ declare -i B
$ echo "B = $B"           # Declaring a variable with non-integers in it doesn't change the contents.
B = 10STRING01

$ B=${B/STRING01/24}      # Substitute "STRING01"  with "24".
$ echo "B = $B"
B = 1024

$ declare -i B=10/2       # Declare B and assigning it an integer value
$ echo "B = $B"           # Variable B behaving as an Integer
B = 5

Optionsbedeutungen deklarieren:

  • -a Variable ist ein Array.
  • -f Nur Funktionsnamen verwenden.
  • -i Die Variable ist als Ganzzahl zu behandeln. Die arithmetische Auswertung wird durchgeführt, wenn der Variablen ein Wert zugewiesen wird.
  • -p Zeigt die Attribute und Werte jeder Variablen an. Wenn -p verwendet wird, werden zusätzliche Optionen ignoriert.
  • -r Variablen schreibgeschützt machen. Diesen Variablen können dann weder durch nachfolgende Zuweisungsanweisungen Werte zugewiesen noch sie können nicht gesetzt werden.
  • -t Geben Sie jeder Variablen das Trace-Attribut.
  • -x Markiert jede Variable für den Export in nachfolgende Befehle über die Umgebung.
Keith Reynolds
quelle
1

Die Bash-Manpage enthält viel mehr Informationen als die meisten Manpages und enthält einige der gewünschten Informationen. Nach mehr als einem Jahrzehnt Scripting Bash gehe ich davon aus, dass es aufgrund seiner Geschichte als Erweiterung von sh eine funky Syntax hat (um die Abwärtskompatibilität mit sh aufrechtzuerhalten).

FWIW, meine Erfahrung war wie deine; Obwohl die verschiedenen Bücher (z. B. O'Reilly "Learning the Bash Shell" und ähnliches) bei der Syntax helfen, gibt es viele seltsame Möglichkeiten, verschiedene Probleme zu lösen, und einige davon sind nicht im Buch enthalten und müssen gegoogelt werden.

Philwalk
quelle