释伴:脚本解释器定义行
简介
计算机程序,按照其执行方式,可以分为两类:“编译型”和“解释型”。前者需要把程序翻译成为计算机能够读懂的二进制格式后才能执行,这个翻译工具通常称为编译器(Compiler),而后者则通过解释器对程序进行解释执行。我们通常使用的脚本,就属于后者,它们都需要对应的解释器进行支持。
脚本的运行,最基本的方法就是运行解释器,并把脚本文件作为其参数,以Perl为例:
1$ perl foo.pl arg1 arg2然而这种方法比较繁琐,于是我们希望简化其用法,最好直接把脚本当作一个可执行的程序来使用,而不必每次都手工输入其对应的解释器。
这就需要在脚本中定义好应该使用什么解释器,通常这个定义就写到脚本的第一行,并以“#!”字符开头,这个脚本解释器定义行,英文称为“shebang”,中文翻译为“释伴”(兼具音译和意译):
1$ head -n1 foo.pl # 显示脚本首行(释伴,这里定义需要写上解释器的全路径)
2#!/usr/bin/perl
3$ chmod +x foo.pl # 设置脚本属性为可执行
4$ ./foo.pl arg1 arg2 # 直接运行该脚本(注意需要在前面加上“./”,才能保证程序正确运行)进阶
从实现方式上深究,shell在执行脚本时,不过是把首行“释伴”中的命令提取出来,将原命令拼接到其后面,真正执行的是这个拼接后的命令,例如:
1$ echo '#!/bin/echo hello' >> test.script
2$ chmod +x test.script
3$ ./test.script 123 xyz这里构造了一个只有首行“释伴”的测试脚本,最后一行执行该脚本,其实就相当于执行了:
1/bin/echo hello ./test.script 123 xyz所以如下这个例子:
1$ cat <<EOF > test2.script
2#!/bin/cat
3Hello, world
4EOF
5$ chmod +x test2.script
6$ ./test2.script运行该“脚本”,相当于运行了“/bin/cat test2.script”命令,于是就把文件内容打印出来。
有时候,我们还可能看到一些特别的释伴写法,如:
1#!/bin/bash --在末尾多了两个“-”符号,通过上述研究,我们可以知道,这其实是让shell执行:
1/bin/bash -- foo.sh [args...]于是就能明白,这个“–”是为了禁用后续的参数解析,把后续“-”开头的参数都当作普通字符串解析,而不让它们干涉到bash自身的执行。
可移植性
写出尽可能适应不同运行环境的脚本,是一件值得追求的事。然而,在shebang中,要求给出解释器的全路径,否则,会出现如下报错:
1$ echo '#!bash' > test.sh
2$ chmod +x test.sh
3$ ./test.sh
4bash: ./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环境变量中搜索到可用目录,以执行对应的解释器:
1#!/usr/bin/env perl或
1#!/usr/bin/env python扩展
来个扩展问题,能不能通过配置shebang,把一个编译型源码程序(如C/C++)变得像脚本一样呢?
1$ cat <<EOF > test.c
2#!/usr/bin/gcc $0 && ./a.out && rm -f a.out
3#include <stdio.h>
4
5int main()
6{
7 printf("Hello, world!\n");
8 return 0;
9}
10$ chmod +x test.c
11$ ./test.c然而,这种方式失败了,报错如下:
1gcc: 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$ cat <<EOF > test.c
2#!/bin/bash
3sed '1,2d' $0 > /tmp/$$.c; gcc /tmp/$$.c -o /tmp/$$.out; /tmp/$$.out; rm -f /tmp/$$.{in,out}; exit
4#include <stdio.h>
5
6int main()
7{
8 printf("Hello, world!\n");
9 return 0;
10}
11EOF
12$ chmod +x test.c
13$ ./test.c这次终于执行成功,该C程序表现得如同脚本一样了。也就是说,想要让某个C程序变成脚本执行,只需要在其开头增加两行(上面代码框中的第2、3两行)即可。