颜林林的个人网站

Linlin Yan's Personal Website

释伴:脚本解释器定义行

2020-03-16 08:51

简介

计算机程序,按照其执行方式,可以分为两类:“编译型”和“解释型”。前者需要把程序翻译成为计算机能够读懂的二进制格式后才能执行,这个翻译工具通常称为编译器(Compiler),而后者则通过解释器对程序进行解释执行。我们通常使用的脚本,就属于后者,它们都需要对应的解释器进行支持。

脚本的运行,最基本的方法就是运行解释器,并把脚本文件作为其参数,以Perl为例:

1
$ perl foo.pl arg1 arg2

然而这种方法比较繁琐,于是我们希望简化其用法,最好直接把脚本当作一个可执行的程序来使用,而不必每次都手工输入其对应的解释器。

这就需要在脚本中定义好应该使用什么解释器,通常这个定义就写到脚本的第一行,并以“#!”字符开头,这个脚本解释器定义行,英文称为“shebang”,中文翻译为“释伴”(兼具音译和意译):

1
2
3
4
$ head -n1 foo.pl     # 显示脚本首行(释伴,这里定义需要写上解释器的全路径)
#!/usr/bin/perl
$ chmod +x foo.pl     # 设置脚本属性为可执行
$ ./foo.pl arg1 arg2  # 直接运行该脚本(注意需要在前面加上“./”,才能保证程序正确运行)

进阶

从实现方式上深究,shell在执行脚本时,不过是把首行“释伴”中的命令提取出来,将原命令拼接到其后面,真正执行的是这个拼接后的命令,例如:

$ echo '#!/bin/echo hello' >> test.script
$ chmod +x test.script
$ ./test.script 123 xyz

这里构造了一个只有首行“释伴”的测试脚本,最后一行执行该脚本,其实就相当于执行了:

1
/bin/echo hello ./test.script 123 xyz

所以如下这个例子:

1
2
3
4
5
6
$ cat <<EOF > test2.script
#!/bin/cat
Hello, world
EOF
$ chmod +x test2.script
$ ./test2.script

运行该“脚本”,相当于运行了“/bin/cat test2.script”命令,于是就把文件内容打印出来。

有时候,我们还可能看到一些特别的释伴写法,如:

#!/bin/bash --

在末尾多了两个“-”符号,通过上述研究,我们可以知道,这其实是让shell执行:

/bin/bash -- foo.sh [args...]

于是就能明白,这个“–”是为了禁用后续的参数解析,把后续“-”开头的参数都当作普通字符串解析,而不让它们干涉到bash自身的执行。

可移植性

写出尽可能适应不同运行环境的脚本,是一件值得追求的事。然而,在shebang中,要求给出解释器的全路径,否则,会出现如下报错:

1
2
3
4
$ echo '#!bash' > test.sh
$ chmod +x test.sh
$ ./test.sh
bash: ./test.sh: bash: bad interpreter: No such file or directory

这对于bash还好,因为基本上它都是位于/bin/bash不变的(这也是我们见得最多的“#!/bin/bash”;当然也有安装bash到不同位置的场景)。但如果我们写的是其他语言的脚本,比如Perl或Python呢?这些解释器有可能位于不同的目录,比如“/usr/bin/”或“/usr/local/bin/”,甚至是其他用户自定义的目录。

这时候,可以通过/usr/bin/env命令,来实现自动在PATH环境变量中搜索到可用目录,以执行对应的解释器:

#!/usr/bin/env perl

#!/usr/bin/env python

扩展

来个扩展问题,能不能通过配置shebang,把一个编译型源码程序(如C/C++)变得像脚本一样呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ cat <<EOF > test.c
#!/usr/bin/gcc $0 && ./a.out && rm -f a.out
#include <stdio.h>

int main()
{
    printf("Hello, world!\n");
    return 0;
}
$ chmod +x test.c
$ ./test.c

然而,这种方式失败了,报错如下:

gcc: error: $0 && ./a.out && rm -f a.out: No such file or directory

它把整个“$0 && ./a.out && rm -f a.out”字符串当作输入文件名扔给gcc了。

看来需要变通一下,让C源码被当作bash脚本来执行,这样,在bash中就可以更灵活地控制变量,生成临时文件,调度编译结果的执行等行为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ cat <<EOF > test.c
#!/bin/bash
sed '1,2d' $0 > /tmp/$$.c; gcc /tmp/$$.c -o /tmp/$$.out; /tmp/$$.out; rm -f /tmp/$$.{in,out}; exit
#include <stdio.h>

int main()
{
    printf("Hello, world!\n");
    return 0;
}
EOF
$ chmod +x test.c
$ ./test.c

这次终于执行成功,该C程序表现得如同脚本一样了。也就是说,想要让某个C程序变成脚本执行,只需要在其开头增加两行(上面代码框中的第2、3两行)即可。

参考