Shell的秘密
VAR=1
if [ $VAR == "2" ]
then
echo "T"
else
echo "F"
fi
今天遇到一件奇怪的事,以上的脚本在不同的执行模式下会有不同的输出:
// --------------- /bin/sh --> dash
❯ ./test.sh
./test.sh: 2: [: 1: unexpected operator
F
❯ /bin/sh ./test.sh
./test.sh: 2: [: 1: unexpected operator
F
// ----------------- /bin/zsh
❯ echo $SHELL
/bin/zsh
❯ source ./test.sh
./test.sh:2: = not found
❯ . ./test.sh
./test.sh:2: = not found
❯ /bin/zsh ./test.sh
./test.sh:2: = not found
// ----------- /bin/bash
❯ bash ./test.sh
F
可以看到,以上错误的本质原因是不同shell对语法的支持情况不同,导致该脚本只能在bash中顺利执行。通过echo $SHELL
可以看到当前的SHELL的版本是zsh。但是为什么除了显式执行的情况,有些情况会触发其他SHELL的启动?
Script脚本的执行
首先,我们通过strace来跟踪./test.sh
发生了什么事?
❯ strace ./test.sh
execve("./test.sh", ["./test.sh"], 0x7ffde468a700 /* 29 vars */) = -1 ENOEXEC (Exec format error)
strace: exec: Exec format error
+++ exited with 1 +++
可以看到本质就是对execve的系统调用?Wait,execve系统调用可以处理脚本?是的,Linux支持注册各种格式,每个格式会提供一个fmt
->
load_binar
的方法尝试加载程序,execve会将程序尝试按序用所有注册的格式进行加载,加载成功即进入运行。检查的代码可见如下:
通过register_binfmt的跟踪可以发现linux支持经典的elf格式,以及可以看到最下方的script格式。
从script格式的load_binary可以看到其会首先检查有没有shabang(#!),#!用于指定脚本的解释器,若没有,linux无法知道用哪种解释器,则会运行失败。
通过下面一个简单的程序可以验证这一点,test.sh确实无法被成功启动。test.sh必须被显式传入一个解释器(如:/bin/sh test.sh
)进行执行。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
static char *newargv[] = { NULL};
static char *newenviron[] = { NULL };
execve("./test.sh",newargv,newenviron);
printf("Fail to execve\n");
exit(EXIT_FAILURE);
}
.和source
.和source是shell的内置命令,通常用于在当前的shell环境执行脚本。因此通过.和source执行test.sh得到的输出与使用/bin/zsh执行的结果一致。
./test.sh
最诡异的情况是./test.sh
,从strace的追逐结果看,execve
确实是返回失败,但是我们仍能从标准输出中得到结果,这说明该脚本至少被执行过一次。
❯ ./test.sh
./test.sh: 2: [: 1: unexpected operator
F
这个问题我在:
- https://stackoverflow.com/questions/12296308/shell-script-working-fine-without-shebang-line-why
- https://github.com/zsh-users/zsh/blob/a66e92918568881af110a3e2e3018b317c054e4a/Src/exec.c#L630
找到答案:当zsh运行失败时,会判断执行文件是否有可执行权限,若有则检查其是否为一个缺少shabang的脚本文件,若是则默认使用/bin/sh
执行该脚本。