Gemini Pro 基于 https://www.in-ulm.de/~mascheck/various/shebang/ 整理.
好的引言** (2001-08-13 .. 2021-10-20)
本文将深入探讨 Unix 系统中 #!
(shebang 或 hash-bang)机制的细节。内容包括:
延伸阅读
- 起源
- Unix 常见问题解答 (FAQ)
- Andries Brouwer 的发现
- 维基百科
关键问题
#!
后是否必须有空格?#!
后是否禁止有空格?- setuid 支持
- 解释器作为
#!
脚本 - 参数分割
env
工具- 注释
- 最大长度限制
- 一些有趣的代码实现
- 处理长行和多参数的解决方案
- POSIX 标准
#!
的特殊之处- 可能出现的错误
- 不同系统的测试结果
延伸阅读
起源
Dennis Ritchie 的一封旧邮件介绍了这一新特性。该邮件被引用在 4.0 BSD 的 /usr/src/sys/newsys/sys1.c
文件中。newsys
路径组件是一个选项。它也在 /usr/src/sys/sys/TODO
文件中被提及:
6. Exec fixes
Implement dmr's #! feature; pass string arguments through faster.
因此,#!
机制起源于贝尔实验室,介于 Version 7 和 Version 8 之间,并在 4.0BSD(约 1980 年 10 月)上可用,但默认未激活。
与当前实现有两个重要区别:
- 行的长度限制为 16(Research Unix)或 32(BSD)字节。
- 不传递“参数”。
它在 4.2BSD(约 1983 年 9 月)上默认实现,由 Robert Elz 在 /usr/src/sys/sys/kern_exec.c
中完成。此实现将所有 #! 参数作为单个参数传递。
在4.0BSD发布后不到一年,但在4.2BSD发布两年多以前,#!
也被添加到了2.8BSD(~07/'81),但默认没有激活.2.x BSD是一条独立的开发线,独立于4 BSD.它是一个第7版(V7)的内核,通过宏激活了修正。用于#!
代码的宏并不在makefile中出现,所以您必须自己激活它。代码措辞与4 BSD略有不同。在2.8 BSD上,#!
似乎来自门洛帕克的美国地质调查局,而不是伯克利。
(感谢Gunnar Ritter指出了4.0和4.2BSD的起源,感谢Jeremy C. Reed提到了Robert Elz,感谢Richard Kettlewell在TUHS邮件列表中发现了2.8BSD.)
在 4.3BSD Net/2 中,由于许可战争,该代码被删除,并且必须为后代(例如,NetBSD、386BSD、BSDI)重新实现。
在 Version 8(也称为第 8 版)中,#!
在 /usr/sys/sys/sys1.c
中实现,并在 exec(2)
中记录。
根据贝尔实验室的公开版本,直到SVR4('88),#!
才被添加。System III 和 SVR1 肯定还没有实现它。
根据Dennis M. Ritchie的说法,他从别处得到了这个想法,可能是来自UCB关于BSD的会议之一。而且似乎#!
最初没有名字。
Doug McIllroy在TUHS邮件列表中提到,当时在贝尔实验室,#
的俚语是"sharp"。
Unix 常见问题解答 (FAQ)
"Why do some scripts start with #! ... ?" 这一段强调了关于 shell 的历史,而不是内核。
该文档在两个细节上不正确:
#!
不是在伯克利发明的(但他们在广泛发布的版本中首先实现了它),见上文。- 关于
#
csh-hack:文档明确指出只有 csh 在 BSD 上被修改。然而,从 3BSD(1980 年 3 月)开始,Bourne shell 在 BSD 上也进行了类似的修改。
Andries Brouwer 的发现
Andries Brouwer 的文章强调了一些这里没有解释的其他内容,并采用了更通用的方法。
维基百科
维基百科用 Shebang_(Unix) 涵盖了这个主题。
关键问题
#!
后是否必须有空格?
有传言说,一些非常特殊、非常早期的 Unix 版本(特别是 4.2BSD 衍生版本)要求您用空格将 " #!
" 与后面的路径分开。但是,找到一个真正需要这样的Unix实际上是不可能的.
4.2BSD 实际上并不需要它,尽管 GNU autoconf 教程的先前版本声称需要。
相反,请参阅 4.2BSD 的 /usr/src/sys/sys/kern_exec.c
(第一个常规出现)。接受空格,但不是必需的。
这种误解的起源可能是 4.1 BSD 的一个特定版本:在 CSRG CD 上的 "4.1.snap" 快照中有一个手册页,/usr/man/man2/exec.2
(1981 年 4 月 1 日),其中提到 #!
后的空格/制表符是强制性的。然而,这是不正确的:源代码本身保持不变。
目前尚不清楚这是文档中的错误还是混乱,或者伯克利是否计划修改 BSD 源代码但最终没有修改。
DYNIX 也被提及在 autoconf 文档中。不清楚这个变体是否可能在少数版本中实现了它(可能遵循上述手册页)。至少 Dynix 3.2.0 或 Dynix PTS 1.2.0 实际上是 4.2 BSD 派生的,并且不需要空格。
#!
后是否禁止有空格?
没有证据表明有任何实现禁止在 #!
后使用空格。
Setuid (set user id) 支持
- 由于安全原因,许多系统忽略了 setuid/gid 位。这主要是由于内核启动解释器和解释器启动脚本之间的竞争条件:同时,您可以替换脚本。
SVR4 和 4.4BSD 引入了一个虚拟文件描述符文件系统,可以避免这种竞争:内核可以将打开的文件描述符(例如 /dev/fd/n)交给解释器。
但是4.4BSD还不支持setuid脚本。UNIX FAQ声称这一点,但在kern_exec.c中明确否认了。setuid对于脚本的支持已经在4.3BSD-Tahoe中被禁用了。而且4.4BSD的继任者4.4BSD-Lite由于许可战失去了它的execve()实现。相反,一个非常早期的NetBSD版本似乎是关于自由BSDs的起源.
NetBSD 已经在
exec_script.c
的第一个 cvs 条目中实现了它(1994/01/16),在 1.0 版本之前的一段时间。更早的代码已从 netbsd.org 中删除。文件描述符文件系统("fdescfs")已随 0.8 版本(1993 年 4 月)添加。NetBSD 受 386BSD 的影响,但我无法在那里找到它(包括 patchkit 0.2.4,1993 年 6 月)。FreeBSD 是 386BSD 的直接后代,也没有实现它。OpenBSD 后来(1995 年 10 月)从 NetBSD 分叉出来,因此像 NetBSD 一样实现了它。Jason Steven aka Neozeed 同时通过 cvsweb 提供了 NetBSD 0.8 和 0.9,NetBSD 0.8 的
kern_execve.c
不提供 setuid,但 NetBSD 0.9 的kern_exec.c
(1993/07/13)具有所有位(请参阅文件头部的宏SETUIDSCRIPTS
和FDCSCRIPTS
)。
以下系统通过 fd 文件系统实现 set user id 支持:
- Solaris(从一开始)
- Irix(至少从版本 5 开始)
- UnixWare(从一开始)
- NetBSD(几乎从一开始;但只有在激活内核选项
SETUIDSCRIPTS
时) - OpenBSD(从一开始;但只有在激活内核选项
SETUIDSCRIPTS
时) - MacOS X 从 10.5 / xnu-1228 / Leopard 开始,早期版本没有 fd 文件系统。请参阅 sysctl 内核变量
kern.sugid_scripts
。
以下系统也实现了 set user id 支持:
- SCO OpenServer 6.0。文档没有说明它是使用 fd 文件系统实现的。尽管文档 "SUID, SGID, and sticky bit clearing on writes" 指出,suid/sgid 位在 shell 脚本上不起作用(没有明确提及
#!
机制),但chmod(1)
和exec(s)
明确指出,如果使用#!
解释器文件,则该位有效。正如 Bela Lubkin 指出的:非常基本上,OpenServer 6 是一个具有底层 UnixWare 7.1.4 内核的 OSR 507 用户空间。 - 旁注:SVR4 shell 引入了相关的标志
-p
。如果没有此标志,如果 EUID 与 UID 不同,则 EUID 将设置回 UID。相反,ksh88 和 ksh93 在 euid/egid 不等于 uid/gid 时自动激活此标志。bash-1 不知道这个标志;bash-2 ff. 实现它并要求设置它。 现在许多系统仍然忽略
#!
机制的 setuid 位,因为您必须注意许多问题。另请参阅上面提到的 Unix FAQ 条目。关键字集合包括:- 上述关于实际调用的脚本的竞争条件(符号链接攻击)
- 一些 ksh88(在不使用 /dev/fd 机制的系统上相关)显示出这种怪癖:打开脚本时,它们会在查找当前目录之前查找 PATH。(由 Stephane Chazelas 提出)
- 后续命令中的 shell 转义
- 对所有命令中的数据流的完全控制?
- 继承的环境
- 防止 -i 攻击
- 如果使用,控制文件名扩展
- 覆盖现有文件
- 关于内部临时文件的竞争条件
- 安全理解将来其他人对脚本的维护
解释器本身作为 #!
脚本 (嵌套)
大多数情况下,没有任何贝尔实验室或伯克利衍生的 Unix 接受解释器本身是再次以 #!
开头的脚本。
然而,Linux 从 2.6.27.9 开始和 Minix 接受这一点。
小心不要混淆内核是否接受它,或者内核是否返回了 ENOEXEC
并且您的 shell 默默地尝试接管,自己解析 #!
行。
- bash-1 的行为如此(行长度随后被截断为 80 个字符,并且
argv[0]
成为调用的脚本。) - 如果编译时不存在
#!
机制(可能仅在类似 unix 的环境中,如 cygwin),bash-2、-3 和 -4 也会出)。
有关 Linux 上嵌套 #!
的更多信息,请参阅内核补丁和 binfmt_script.c
,其中包含重要的部分。Linux 最多允许 BINPRM_MAX_RECURSION
,即 4 层嵌套。
参数分割
很少有系统只提供第一个参数,一些系统像 shell 一样拆分参数以填充 argv[]
,大多数系统将所有参数作为单个字符串传递。
对于 Linux(将所有参数作为一个字符串传递),有人在 Linux 内核邮件列表上建议了一个拆分补丁,随后讨论了一些可移植性问题。
env
工具
env(1)
通常与 #!
机制一起使用以启动解释器,然后只需要解释器位于您的 PATH 中的某个位置,例如 "#!/usr/bin/env perl
"。
但是,env(1)
的位置可能会有所不同。Free-、Net-、OpenBSD 和一些 Linux 发行版(例如 Debian)仅附带 /usr/bin/env。另一方面,至少在 SCO OpenServer 5.0.6 和 Cray Unicos 9.0.2 上只有 /bin/env(尽管后者仅具有历史意义)。在一些其他 Linux 发行版(Redhat)上,它位于 /bin 中,/usr/bin/ 包含指向它的符号链接。
env 机制极大地提高了便利性,现在几乎所有系统都提供 /usr/bin/env。然而,它不能严格保证脚本的“可移植性”。
实际上,env 不应该是一个脚本。
注释
FreeBSD 4.0 引入了在参数中对 "#" 进行类似注释的处理,但 6.0 版本撤销了这一点。
MacOS X 在 10.3(/xnu-517/Panther) 版本中引入了对 "#" 进行类似注释的处理。
最大长度限制
- 最初(Research Unix 在 Version 7 和 8 之间)是 16 字节。
- 4.xBSD、386BSD、OSF1 1.0、SunOS 4 和 Ultrix 4.3 上为 32 字节。这是 "
sizeof(struct a.out)
" 或 "sizeof(struct exec)
"。原因是包含此结构 a.out(或 exec)和相同大小的字符串的union
,该字符串将包含#!
行。(在 SVR3、早期的 HP-UX 和 Unicos 上是相同的限制;但我不知道是否出于相同的原因。) - 有关 386BSD(后来免费 BSD 变体的前身)上的实现,请参阅 patchkit 0.2.3 中的补丁。可以在 386BSD-0.0 的补丁 5(树 "newer")中找到更早的建议。
- 有关 NetBSD 的历史记录,请参阅
kern_execve.v
(在 Attic 中),它从 386BSD-0.1 补丁 0.2.2 继承,并很快添加了允许一个参数。实现已移至kern/exec_script.c
(sys/param.h
中的MAXINTERP
或sys/syslimits.h
中的PATH_MAX
)。 - 对于FreeBSD的历史,请参阅
imgact_shell.c
和<sys/imgact.h>
,从6.0开始还有<machine/param.h>
和<sys/param.h>
。MAXSHELLCMDLEN
现在设置为PAGESIZE
,这又取决于架构。 - 有关 OpenBSD 的历史记录,请参阅
kern/exec_script.c
(sys/param.h
中的MAXINTERP
)。 Linux 上为 127 字节,另请参阅
linux/fs/binfmt_script.c
中load_script()
中的宏BINPRM_BUF_SIZE
、<linux/binfmts.h>
和<uapi/linux/binfmts.h>
。在 Linux 上,
#!
是在内核版本 0.09 或 0.10 中引入的(0.08 尚未实现它)。实际上,原始最大长度为 1022,请参阅 Linux 0.10 的linux/fs/exec.c
。但是从 Linux 0.12 开始,这被更改为 127。在许多其他风格上,最大长度在 _POSIX_PATH_MAX (255) 和 PATH_MAX(例如 1024)之间变化;请参阅相应系统上的
limits.h
或syslimits.h
。例外是 BIG-IP4.2 (BSD/OS4.1) 为 4096,FreeBSD 从 6.0 (PAGE_SIZE) 开始为 4096 或 8192,具体取决于架构。
Minix 也使用
PATH_MAX
字符的限制(此处为 255),但实际限制为 257 个字符,因为src/mm/exec.c
中的patch_stack()
首先使用lseek()
跳过 "#!
",然后读入其余部分。
有趣的代码实现
2.8BSD 使用多字符常量实现了对
#!
魔法的测试:#define SCRMAG '#!'
Demos(最初基于 2.9 BSD)继承了
SCRMAG
,甚至为魔法的变体添加了自己的多字符常量:# define SCRMAG2 '/*#!' # define ARGPLACE "$*"
BSD/OS (2.0,
sys/i386/i386/exec_machdep.c
) 显示了一种构建魔法的有趣方式:switch (magic) { /* interpreters (note byte order dependency) */ case '#' | '!' << 8: handler = exec_interpreter; break; case [...]
POSIX 标准
POSIX.2 或 SUSv2 / SUSv3 / SUSv4 仅将 #!
提及为可能的扩展:
Shell Introduction
[...]
If the first line of a file of shell commands starts with the
characters #!, the results are unspecified.
The construct #! is reserved for implementations wishing to provide
that extension. A portable application cannot use #! as the first
line of a shell script; it might not be interpreted as a comment.
[...]
Command Search and Execution
[...]
This description requires that the shell can execute shell
scripts directly, even if the underlying system does not support
the common #! interpreter convention. That is, if file foo contains
shell commands and is executable, the following will execute foo:
./foo
有一个工作组决议试图定义该机制。
另一方面,谈论任何 Unix 上的 "#!/bin/sh
":如果您期望 Bourne shell 家族及其后代的任何东西被调用,这是一个非常可靠且可移植的传统约定。
#!
的特殊之处
#!
是一个很棒的技巧,可以让脚本看起来和感觉像真正的可执行二进制文件。
但是,作为一个小总结,#!
有什么特别之处?(列表主要由 David Korn 提供)
- 解释器名称不得包含空格
#!
的长度远小于最大路径长度- 不搜索
$PATH
以查找解释器(除了绝对路径,#!
行也接受相对路径,并且#!interpreter
等效于#!./interpreter
,但是,它没有任何实际用途) - 解释器通常不能再次是
#!
脚本 #!
行本身中参数的处理方式各不相同- setuid 机制可能适用于或不适用于脚本
- 无法表达
#!$SHELL
处理长行和多参数的解决方案
在解释器可能位于太深的目录结构中的系统上,有一些解决方案可以处理长行(和/或多个参数):
- "sbang"(github)是一个代表原始可执行文件运行的 POSIX shell 脚本。它解析包含实际的、可能更长的解释器路径的后续行,并确保可以传递多个参数
- "long-shebang" (github)提供了一个可执行文件,以防系统不接受shebang行中的解释器,并且还确保传递多个参数
可能出现的错误
如果未找到解释器,系统将返回
ENOENT
。此错误可能会产生误导,因为许多 shell 随后会打印脚本名称而不是其#!
行中的解释器:$cat script.sh #!/bin/notexistent $ ./script.sh ./script.sh: not found
bash 从版本 3 开始随后自己读取第一行并给出关于解释器的诊断:
bash: ./script.sh: /bin/notexistent: bad interpreter: No such file or directory
如果
#!
行太长,至少会发生三件事:- 该行被截断,通常截断为允许的最大长度。
- 系统返回
E2BIG
或ENAMETOOLONG
,您会得到类似 "Arg list too long" / "Arg list or environment too large" 或 "File name too long" 的消息。 - 内核拒绝执行文件并返回
ENOEXEC
。在某些 shell 中,这会导致静默失败。其他 shell 随后会尝试自己解释脚本。
不同系统的测试结果
使用了以下程序 "showargs":
#include <stdio.h>
int main(argc, argv)
int argc; char** argv;
{
int i;
for (i=0; i<argc; i++)
fprintf(stdout, "argv[%d]: \"%s\"\n", i, argv[i]);
return(0);
}
以及一个名为 "invoker.sh" 的单行脚本来调用它,类似于:
#!/tmp/showargs -1 -2 -3
通常,上述结果如下所示:
argv[0]: "/tmp/showargs"
argv[1]: "-1 -2 -3"
argv[2]: "./invoker.sh"
但下表列出了变化。列的含义如下所述。
(表格略,因为原内容为HTML表格,直接翻译会非常冗长,建议参考原文表格。)
表格列的含义:
- "maximum length of #! line":
#!
行的最大长度。 - "cut-off(c), error(err) or ENOEXEC ( )": 截断、错误或 ENOEXEC。
第3列: 参数如何处理
- "all args in one":
argv[1]
包含所有参数。 - "no arguments": 没有参数传递给解释器。
- "only the 1st arg": 只传递第一个参数给解释器。
- "separate args": 参数被分割。
- "all args in one":
- "handle
#
like a comment": 如果#
出现在参数中,则#
和该行的其余部分将被忽略。 - "argv[0]: invoker, instead of interpreter":
argv[0]
包含调用脚本而不是解释器。 - "not full path in argv[0]":
argv[0]
包含程序的基名而不是其完整路径。 - "remove trailing whitespace": 删除尾随空格。
- "convert tabulator to space": 将制表符转换为空格。
- "accept interpreter":
#!
行中的解释器程序本身可以是脚本。 - "do not search current directory":这表示如果从
/bin
调用,#!sh
不起作用 - "no suid, or allow suid, or optional": setuid 支持。
问号表示无法测试的细节。"n/a" 表示该属性在这种情况下无关紧要。
Shebang 的命名
为什么叫 shebang?在音乐中,"#" 表示 升号。所以只需将 #!
缩短为 sharp-bang。或者它可能源自 "shell bang"。所有这些可能都受到美国俚语 "the whole shebang"(一切、全部、所涉及的一切)的影响。
有时也称为 hash-bang、pound-bang、sha-bang/shabang、hash-exclam 或 hash-pling(英国)。
根据Dennis M. Ritchie的说法,它似乎最初没有名字。而Doug McIllroy提到,当时在贝尔实验室,#
的俚语很可能是"sharp"。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。