Ist das Parsen von Skripten zur Laufzeit von Skripten für Shells allgegenwärtig oder in anderen Interpreten vorhanden, und wie funktioniert das?

7

Ich hatte immer gedacht, dass Shells ganze Skripte analysieren, einen AST erstellen und diesen AST dann aus dem Speicher ausführen. Ich habe jedoch gerade einen Kommentar von Stéphane Chazelas gelesen und die Ausführung dieses Skripts getestet: edit-while-executing.sh:

#!/bin/bash

echo start
sleep 10

und dann, während es beim Laufen schlief:

$ echo "echo end" >> edit-while-executing.sh

und es hat funktioniert, damit es am Ende "Ende" druckt.

Wenn Sie jedoch versuchen, dies zu ändern:

#!/bin/bash

while true; do
  echo yes
done

indem Sie tun:

$ printf "%s" "no " | dd of=edit-while-executing.sh conv=notrunc seek=35 bs=1

Es hat nicht funktioniert und immer wieder "Ja" gedruckt.

Ich habe mich auch gefragt, ob andere Nicht-Shell-Interpreter auch so funktionieren, und habe das Äquivalent des ersten Skripts mit Python ausprobiert, aber es hat nicht funktioniert. Vielleicht ist Python kein Interpreter mehr und eher ein JIT-Compiler.

Um meine Frage zu wiederholen: Ist dies ein Verhalten, das für Muscheln allgegenwärtig und auf sie beschränkt ist oder auch bei anderen Dolmetschern (die nicht als Muscheln angesehen werden) vorhanden ist? Wie funktioniert das auch so, dass ich die erste Änderung vornehmen kann, aber nicht die zweite?

JoL
quelle
Die Shell hatte in Ihrem Schleifenbeispiel bereits den vollständigen zusammengesetzten Befehl (die forSchleife) analysiert , sodass die Änderung an der Datei des Shell-Skripts keine Auswirkungen hatte.
Kusalananda
1
Wenn Shells ihre gesamte Eingabe lesen müssten, bevor sie etwas tun könnten, könnten Sie sie nicht interaktiv verwenden. Shells sollen in erster Linie interaktiv betrieben werden. Skripte sind zweitrangig.
Stéphane Chazelas
@ StéphaneChazelas Ich hatte nicht gedacht, dass Python vielleicht etwas anderes macht, um interaktiv zu laufen.
JoL
1
In einigen Fällen analysieren Shells Skripte vollständig, bevor sie etwas tun. Dies gilt z. B. für den Punktbefehl, . myscriptwird also zuerst myscriptals eine zusammengesetzte Anweisung analysiert und dann ausgeführt. Dies ist beispielsweise ein Grund, warum in einem solchen Skript definierte Aliase im Skript noch nicht aktiv sind.
schily
1
@schily, das hängt sehr stark von der Shell ab. Dies ist beispielsweise bei Asche, Pdksh oder Zsh nicht der Fall.
Stéphane Chazelas

Antworten:

2

Dies läuft also auf unbestimmte Zeit in Bash / dash / ksh / zsh (oder zumindest bis Ihre Festplatte voll ist):

#!/bin/sh
s=$0
foo() { echo "hello"; echo "foo" >> $s; sleep .1; }
foo

Das , was zu beachten ist, dass nur Sachen hängten zur Skriptdatei nach der letzten Zeile hinzugefügt die Shell - Angelegenheiten gelesen hat. Die Shells gehen nicht zurück, um die früheren Teile erneut zu lesen, was sie selbst nicht tun könnten, wenn die Eingabe eine Pipe wäre.

Das ähnliche Konstrukt funktioniert in Perl nicht, es liest die gesamte Datei ein, bevor es ausgeführt wird.

#!/usr/bin/perl -l    
open $fd, ">>", $0;
sub foo { print "hello"; print $fd 'foo;' }
foo;

Wir können sehen, dass dies auch bei Eingabe über eine Pipe der Fall ist. Dies gibt nach 1 Sekunde einen Syntaxfehler (und nur diesen):

$ (echo 'printf "hello\n";' ; sleep 1 ; echo 'if' ) | perl 

Während dasselbe Skript z. B. an Bash weitergeleitet wird, hellowird eine Sekunde später der Syntaxfehler gedruckt und ausgelöst.

Python ähnelt Perl mit Piped-Eingabe, obwohl der Interpreter interaktiv eine Lese-Auswertungs-Druckschleife ausführt.


Zusätzlich zum zeilenweisen Lesen des Eingabeskripts werden mindestens Bash- und Dash-Prozessargumente evalzeilenweise verarbeitet:

$ cat evaltest.sh
var='echo hello
fi'
eval "$var"
$ bash evaltest.sh
hello
evaltest.sh: eval: line 4: syntax error near unexpected token `fi'
evaltest.sh: eval: line 4: `fi'

Zsh und ksh geben den Fehler sofort aus.

In ähnlicher Weise wird Zsh bei Skripts mit Sourcing-Funktion ebenso wie Bash und Dash zeilenweise ausgeführt:

$ cat sourceme.sh
echo hello
fi
$ zsh -c '. ./sourceme.sh'
hello
./sourceme.sh:2: parse error near `fi'
ilkkachu
quelle
"Die Muscheln gehen nicht zurück, um die früheren Teile noch einmal zu lesen" - bedenken Sie jedochperl -e 'if(fork()){exec qw/sh/}else{while(1){sleep 1;sysseek STDIN,0,0}}' < foo.sh
am
@roaima, ah ja natürlich. mein Fehler.
Ilkkachu
1
@ Thrig, Argh! Das ist nicht wirklich die Hülle, die "zurück" geht, aber ich schätze die Schrecklichkeit der Idee.
Ilkkachu
3

Diese Funktion ist in anderen Interpreten vorhanden, die das anbieten, was als a bezeichnet wird read eval print loop. LISP ist eine ziemlich alte Sprache mit einer solchen Funktion, und Common LISP verfügt über eine readFunktion, die hier den Ausdruck einliest, (+ 2 2)der dann evalzur Auswertung übergeben werden kann (obwohl Sie dies in echtem Code aus verschiedenen Sicherheitsgründen möglicherweise nicht auf diese Weise tun möchten ):

% sbcl
* (defparameter sexp (read))
(+ 2 2)

SEXP
* (print (eval sexp))

4
4

Wir können auch unsere eigene sehr einfache REPL definieren, ohne viel an Funktionen oder Debugging oder so ziemlich allem anderen, aber dies zeigt die REPL-Teile:

* (defun yarepl () (loop (print (eval (read))) (force-output) (fresh-line)))

YAREPL
* (yarepl)
(* 4 2)

8
(print "hi")

"hi"
"hi"

Grundsätzlich werden, wie auf dem Typenschild angegeben, Daten eingelesen, ausgewertet, gedruckt und dann (vorausgesetzt, es stürzt nichts ab und es gibt immer noch Strom oder etwas, das das Gerät mit Strom versorgt) wieder zum Lesevorgang zurückgeschaltet. Es ist nicht erforderlich, im Voraus einen AST aufzubauen. (SBCL benötigt die force-outputund fresh-lineErgänzungen aus Anzeigegründen, andere Common LISP-Implementierungen können oder können nicht.)

Andere Dinge mit REPL sind TCL ("eine von einem radioaktiven LISP gebissene Hülle"), die Grafikmaterial mit Tk enthält

% wish
wish> set msg "hello"
hello
wish> pack [label .msg -textvariable msg]
wish> wm geometry . 500x500
wish> exit

Oder FORTH hier, um eine Funktion f>cfür die Temperaturumwandlung zu definieren (die "ok" werden hinzugefügt durch gforth):

% gforth
Gforth 0.7.3, Copyright (C) 1995-2008 Free Software Foundation, Inc.
Gforth comes with ABSOLUTELY NO WARRANTY; for details type `license'
Type `bye' to exit
: f>c ( f -- c ) 32 - 5 9 */ cr . cr ;  ok
-40 f>c
-40
 ok
100 f>c
37
 ok
bye
Thrig
quelle
Auch in tcl beim Aufruf als tclsh < file/ expect < file(nicht tclsh file/ expect file).
Stéphane Chazelas
@ StéphaneChazelas Ich frage mich jetzt, ob LISP der erste war oder ob irgendetwas eine Antwort davor hatte ... aber das ist wahrscheinlich eher eine Retro-Computing-Frage
am
1

Mindestens eine Muschel, Fisch, zeigt dieses Verhalten nicht (aber Fisch ist auf andere Weise ungewöhnlich):

% for sh in zsh mksh fish dash bash tcsh; do echo 'sleep 5' > foo.$sh; $sh foo.$sh & sleep 1; echo 'echo $0' >> foo.$sh; fg; done
[2] 7381
[2]  - 7381 running    $sh foo.$sh
foo.zsh
[2] 7385
[2]  - 7385 running    $sh foo.$sh
foo.mksh
[2] 7387
[2]  - 7387 running    $sh foo.$sh
[2] 7390
[2]  - 7390 running    $sh foo.$sh
foo.dash
[2] 7393
[2]  - 7393 running    $sh foo.$sh
foo.bash
[2] 7415
[2]  - 7415 running    $sh foo.$sh
foo.tcsh

(Eine frühere Version dieser Antwort hatte falsche Beobachtungen von Python und Ruby gemacht.)

muru
quelle
Es funktioniert nicht mit mir mit der gleichen Python-Version. Ich denke, der Unterschied ist, dass ich die Anweisung Sekunden später hinzufüge und Sie es fast sofort tun. Wahrscheinlich wurde die Anweisung hinzugefügt, bevor Python das Parsen der Datei abgeschlossen hat.
JoL