Bang Bang


解释器读取并执行脚本(而外壳更像是一个厨房通道,可以执行或传递给另一个解释器)。当我们指定命令行上的解释器时,它就是将被使用的那个。例如,Rscript script.R 将使用 Rscript 解释器执行 script.R

当我们执行一个文件而没有明确指定解释器(例如,像 ./myscript.pl),那么“shebang”的任务就是告诉外壳/操作系统使用哪个解释器。shebang 是文本文件的第一行,以 #! 开头,后跟解释器路径

#!/usr/bin/perl

有时我们会看到 env 程序,它在我们路径中找到第一个 perl

#!/usr/bin/env perl

env 不分割参数,因此我们无法添加选项

#!/usr/bin/env perl -w

并且,env 并不一定位于 /usr/bin/env,因此它可以在机器/分发级别保证一些可移植性,但并不总是在分发之间。

Perl 很好

perl 与其他解释器不同——它很好,即使有挑战。 perl 检查 shebang 以确定它是否真的适用于它(如果不是,它将我们的程序传递给另一个解释器)。

例如,文件 i-am-python.pl 包含一个 Python 程序,这绝对不是 Perl

#!/usr/bin/python
import os
import time

print("I'm a snake : " + os.environ["SHELL"] + " " + os.environ["_"])

# Keep it alive to have time to inspect with ps
while True:
    time.sleep(5)

显然,我们不关心扩展名,因为它并不意味着任何类型的文件关联(尽管一些系统允许你关联它)。所以我们有一个 .pl 文件,我们用 perl 来执行它,但里面有一个 python shebang 和一些 Python 代码。很明显,这不是一个有效的 Perl 文件。

如果你不相信我,可以用一个快速的语法检查来验证 perl -c i-am-python.pl,它告诉我们它不是一个有效的 Perl

$ perl -c i-am-python.pl
syntax error at i-am-python.pl line 3, near "import time"
i-am-python.pl had compilation errors.

当我们用 perl 执行这个文件时,令人惊讶的是,一切都很顺利。这是怎么回事?perl 足够聪明,可以将脚本传递给 python

$ perl i-am-python.pl
I'm a snake : /bin/bash /usr/bin/perl

如果我们想检查哪个解释器真正运行了这个脚本,我们可以查看进程表

$ ps aux | grep "i-am-pytho[n].pl"
tduponc+  5647  0.0  0.0  33208  7024 pts/0    S    13:04   0:00 /usr/bin/python i-am-python.pl

注意 i-am-pytho[n].pl,其中方括号将 n 放在字符类中。这是一个巧妙的技巧,以便 grep 能够找到包含 python 的行,但不是 grep 进程本身,因为该模式不会与字面量 [ 匹配。

别忘了杀死程序,因为它会永远休眠!

现在,如果我们想测试相反的情况,用 python 解释器运行 Perl 代码怎么办?

#!/usr/bin/perl

my $str = "I'm a jewel";
print "$str : $ENV{SHELL} $ENV{_}\n";

while (1) { sleep 5; }

这是一个有效的 Perl 文件,但 python 解释器不会传递给 perl,而是返回一个 Python 错误

$ python i-am-perl.py
  File "iamperl.py", line 3
    my $str = "I'm a jewel";
       ^
SyntaxError: invalid syntax

这特别适用于 Python。你自己用 bash、Ruby 或其他东西试试。

我有一件事要告诉你

命令行上有正确的解释器并不意味着 shebang 完全被忽略。 perl 再次非常聪明,表现得就像我们想象的那样(DWIM)。例如,如果我们把警告开关(-w)放在 shebang 中,就像这个文件 override-bang.pl 一样

#!/usr/bin/perl -w

$str = "will produce a warning";

即使我们没有在命令行上添加 -w,我们仍然会收到警告

$ perl override-bang.pl
Name "main::str" used only once: possible typo at override-bang.pl line 3.

足够多不是瘟疫

现在,如果我们指定了一些命令行开关和一些 shebang 中的开关,会发生什么?剧透:它们只是简单地合并在一起。

当我们运行perl -c overridebang.pl来检查语法正确的文件时,我们会从命令行和shebang行获取开关。我们得到一个perl -cw执行

Name "main::str" used only once: possible typo at override-bang.pl line 5.
override-bang.pl syntax OK

如果我们有像-w启用警告和-X禁用警告这样的冲突选项怎么办?这里有一个enable-warnings.pl

#!/usr/bin/perl -w

$str = "will produce a warning";

当我们单独运行这个脚本时,我们会得到预期的警告

$ perl enable-warnings.pl
Name "main::str" used only once: possible typo at warnings.pl line 3.

当我们命令行中添加-X时,没有输出

$ perl -X enable-warnings.pl

那么将-X放在shebang行呢?这里是一个disable-warnings.pl

#!/usr/bin/perl -X

$str = "will produce a warning";

当我们用-w运行这个脚本时,我们仍然没有输出

$ perl -X enable-warnings.pl

-X总是关闭警告。

shebang(-X)的优先级高于命令行,没有报告警告。如果我们用perl -W disable-warnings.pl执行文件也是一样。

我们可以想象这是一个解决“最后看到的”参数冲突的规则,但是等等,事情并不那么简单。

那么-X和启用所有警告的-W相比,谁会赢呢?结果是定义在最后的参数会赢。我们可以在命令行上看到这一点

$ perl -W -X -e '$str = "will produce a warning"'
$ perl -X -W -e '$str = "will produce a warning"'
Name "main::str" used only once: possible typo at -e line 1.

作为读者的练习,尝试不同的taint检查选项组合:-T-U

一个魔法咒语

有时我们在Perl程序的开头看到一些奇怪的行。这是什么黑魔法?这实际上是一个非常聪明的“多语言”打开方式,对shell(有或没有shebang支持)和perl都是正确的

#!/usr/bin/perl
eval 'exec /usr/bin/perl -S $0 ${1+"$@"}'
    if $running_under_some_shell;

如果我们用perl开始脚本,任务就完成了,perl会执行

eval 'exec /usr/bin/perl -S $0 ${1+"$@"}'
if $running_under_some_shell;

$running_under_some_shell没有值,所以代码翻译成false条件。这一行被忽略,文件的其他部分正常解释。

eval 'exec /usr/bin/perl -S $0 ${1+"$@"} if 0;'

如果我们用识别shebang的shell开始脚本呢?shell将任务传递给perl,然后读取第一行(shebang然后eval ...)。然后执行流程与上面相同(魔法咒语不起作用,文件被解释)。这并不令人惊讶。

但是,如果我们用一个不识别shebang的shell开始脚本呢?没有立即发生传递。这正是这个魔法有用的地方。shell会忽略第一行,永远不会到达第三行。为什么永远不会到达第三行呢?换行符终止shell命令,exec将替换当前的执行为perl。从那个exec之后,脚本的其他部分不再重要。我们的代码从这样

#!/usr/bin/perl
eval 'exec /usr/bin/perl -S $0 ${1+"$@"}'
    if $running_under_some_shell;

有效变为这样

eval 'exec /usr/bin/perl -S $0 ${1+"$@"}'

那些$0$@是shell脚本名和参数的词,而-S告诉perl使用PATH环境变量在$0中查找值。(perldoc

-x很有趣

我们已经和perl解释器和shebang玩得很开心了,但是perl有一个-x选项,它本身就很有趣。这个选项告诉Perl要执行的程序实际上嵌入在一个更大的无关文本块中,忽略它。也许Perl程序在电子邮件消息的中间

"I do not know if it is what you want, but it is what you get.
        -- Larry Wall"

#!/usr/bin/env perl

print "perl -x ignores everything before shebang\n";
print <DATA>;

__END__

"Fortunately, it is easier to keep an old interpreter around than an
old computer.
        -- Larry Wall"

将这个作为程序执行是语法错误,因为shebang之前的Larry Wall引言不是有效的Perl。当我们用perl -x执行这段代码时,shebang之前的所有内容都被忽略,它正常工作

$ perl -x email.txt
perl -x ignores everything before shebang

"Fortunately, it is easier to keep an old interpreter around than an
old computer.
        -- Larry Wall"

出于好奇,如果我们再进一步尝试会怎样?文件中有没有多个shebang,其中一个有-x

#!/usr/bin/perl -x
#!/usr/bin/perl

但是它只产生了一个错误

Can't emulate -x on #! line.

但是有一个技巧可以实现这一点,通过使用shell的eval。现在那个perl -x是在shell进程中执行的,而不是像以前那样由perl二进制文件解释。

#!/bin/sh
eval 'exec perl -x $0 ${1+"$@"}'
die "another day"; exit 1
#!perl
print "$]\n";

startperl

这篇文章如果不讨论一下配置变量$Config{startperl}就不会完整。这个变量来自Config.pm,它提供有关配置环境的信息(你还可以用perl -V看到它)

$ perl -e 'use Config; print $Config{startperl}'
#!/usr/bin/perl

这实际上是在编译过程中从默认值或用户/供应商提供的配置中构建的。如果我们想使用不同的值呢?只需在./Configure步骤中指定此值,配置选项为-Dstartperl='...'。然后我们需要重新构建perl

$ ./Configure -des -Dstartperl='#!/my/shebang'
$ make test install

现在我们的自定义值是默认值

$ perl -e 'use Config; print $Config{startperl}'
#!/my/shebang

ExtUtils::MakeMakerModule::Build似乎也使用startperl等其它方法来修复模块的shebang。

请注意使用一个表现得像perl解释器的解释器或程序!一些CPAN模块使用startperl来写入生成的perl测试的第一行。这里仍然适用/usr/bin/env的限制。

资源

标签

Thibault Duponchelle

Thibault Duponchelle是一位软件开发者。主要兴趣在GNU/Linux、开源、Perl、C和汇编。

浏览他们的文章

反馈

这篇文章有什么问题吗?请在GitHub上通过GitHub提交一个issue或pull request来帮助我们。