我一直认为 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-output
和fresh-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 的观察有误。)