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 月)上可用,但默认未激活。

与当前实现有两个重要区别:

  1. 行的长度限制为 16(Research Unix)或 32(BSD)字节。
  2. 不传递“参数”。

它在 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 的历史,而不是内核。

该文档在两个细节上不正确:

  1. #! 不是在伯克利发明的(但他们在广泛发布的版本中首先实现了它),见上文。
  2. 关于 # 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)具有所有位(请参阅文件头部的宏 SETUIDSCRIPTSFDCSCRIPTS)。

以下系统通过 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.csys/param.h 中的 MAXINTERPsys/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.csys/param.h 中的 MAXINTERP)。
  • Linux 上为 127 字节,另请参阅 linux/fs/binfmt_script.cload_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.hsyslimits.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
  • 如果 #! 行太长,至少会发生三件事:

    • 该行被截断,通常截断为允许的最大长度。
    • 系统返回 E2BIGENAMETOOLONG,您会得到类似 "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": 参数被分割。
  • "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-bangpound-bangsha-bang/shabanghash-exclamhash-pling(英国)。

根据Dennis M. Ritchie的说法,它似乎最初没有名字。而Doug McIllroy提到,当时在贝尔实验室,#的俚语很可能是"sharp"。


题叶
17.3k 声望2.7k 粉丝

Calcit 语言作者