在脚本运行时解析脚本对于 shell 来说是普遍存在的还是存在于其他解释器中?它是如何工作的?

在脚本运行时解析脚本对于 shell 来说是普遍存在的还是存在于其他解释器中?它是如何工作的?

我一直认为 shell 会解析整个脚本,构建 AST,然后从内存中执行该 AST。然而我刚刚读到斯特凡·查泽拉斯 (Stéphane Chazelas) 的评论,并测试执行此脚本,edit-while-executing.sh:

#!/bin/bash

echo start
sleep 10

然后在它睡觉的时候运行:

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

它的作用是让它在最后打印“end”。

但是,当尝试修改此内容时:

#!/bin/bash

while true; do
  echo yes
done

通过做:

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

没用,一直打印“yes”。

我还想知道其他非 shell 解释器是否也像这样工作,并尝试了与 python 的第一个脚本等效的操作,但它不起作用。不过,也许 python 不再是解释器,而更像是 JIT 编译器。

因此,重申我的问题,这是 shell 中普遍存在且仅限于它们的行为,还是也存在于其他解释器(那些不被视为 shell 的解释器)中?另外,这是如何工作的,这样我可以进行第一次修改,但不能进行第二次修改?

答案1

其他提供所谓read eval print loop. LISP 是一种非常古老的语言,具有这样的功能,Common LISP 有一个read函数,可以在这里读取表达式(+ 2 2),然后将其传递给eval求值(尽管在实际代码中,出于各种安全原因,您可能不希望这样做):

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

SEXP
* (print (eval sexp))

4
4

我们还可以定义我们自己的非常简单的 REPL,而无需太多功能或调试或几乎任何其他内容,但这确实显示了 REPL 部分:

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

YAREPL
* (yarepl)
(* 4 2)

8
(print "hi")

"hi"
"hi"

基本上就像铭牌上所说的那样,数据被读入、评估、打印,然后(假设没有发生任何崩溃,并且仍然有电或其他设备为设备供电)它循环回到读取状态,无需提前构建 AST。 (出于显示原因,SBCL 需要force-outputfresh-line添加,其他 Common LISP 实现可能有也可能没有。)

REPL 的其他内容包括 TCL(“被放射性 LISP 咬住的壳”),其中包括 Tk 的图形内容

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

或者FORTH在这里定义一个函数f>c来进行温度转换(“ok”是由添加的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

答案2

因此,这会在 Bash/dash/ksh/zsh 中无限期地运行(或者至少直到磁盘填满为止):

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

需要注意的是,只有在 shell 读取的最后一行之后添加到脚本文件中的内容才有意义 shell 不会返回重新读取前面的部分,如果输入是管道,它们甚至无法做到这一点。

类似的构造在 Perl 中不起作用,它在运行之前读取整个文件。

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

我们可以看到,当通过管道给出输入时,它也会这样做。 1 秒后会出现语法错误(并且仅此):

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

而同一个脚本通过管道传输到 Bash 等,打印hello,然后在一秒钟后抛出语法错误。

Python 看起来与带有管道输入的 Perl 类似,尽管解释器在交互时运行读取-评估-打印循环。


除了逐行读取输入脚本之外,Bash 和 dash 至少eval一次处理一行参数:

$ 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 和 ksh 立即给出错误。

类似地,对于源脚本,这次 Zsh 也逐行运行,Bash 和 dash 也是如此:

$ cat sourceme.sh
echo hello
fi
$ zsh -c '. ./sourceme.sh'
hello
./sourceme.sh:2: parse error near `fi'

答案3

至少有一种贝壳,即鱼,不会表现出这种行为(但鱼在其他方面是不寻常的):

% 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

(此答案的先前版本对 Python 和 Ruby 的观察有误。)

相关内容