用 Shebang 执行 Python 脚本(翻译)

本文最后更新于:2023年11月13日 晚上

翻译信息

原文链接:Executing Python Scripts With a Shebang

作者:Bartosz Zaczyński

译者:muzing

翻译时间:2023.11

译文有删节。允许转载,转载必须保留全部翻译信息,且不能对正文有任何修改。

在阅读他人编写的 Python 代码时,经常会看到一行神秘的特殊代码——总是出现在文件顶部、并以独特的 shebang(#!)字符开头。它看起来像是一行没什么用的注释,也不包含在之前所学的 Python 知识里。不禁让人感到好奇,这是什么,有什么作用,为什么只出现在某些 Python 模块中?

本文包含以下内容:

  • 何为 shebang
  • 何时在 Python 脚本中使用 shebang
  • 如何定义跨系统可移植的 shebang
  • 如何向 shebang 中定义的命令传递参数
  • shebang 的局限性及其替代方案

继续学习前,确保已经具备了必要的背景知识:基本熟悉命令行,并知道如何从命令行运行 Python 脚本。还可以下载本教程的辅助材料,以便跟读代码示例:realpython/materials/python-shebang

何为Shebang,何时使用

一言以蔽之,shebang 是一种特殊的注释,可以在源代码中加入这种注释,来告诉操作系统的 shell 该到何处寻找适用于该源码文件的解释器:

1
2
3
#!/usr/bin/python3

print("Hello, World!")

如果要使用 shebang,它必须出现在脚本的第一行,并且必须以哈希符号 (#,也称“井号”) 开头,后面紧跟一个感叹号 (! 。选择 # 作为这个特殊字符序列的首字符并非偶然,因为许多脚本语言都将其用于内联注释。

译者注:shebang 有时也被写作 sha-bangsh-bang 等。这个名字的准确由来已不可考证。一种说法是 sh 来自于 shell,而另一种说法是 # 代表高音符号,读作 sharp(类似于编程语言 C# 中的情况),所以对应于 shesha;在漫画书中经常用 Bang!!! 这样的字眼形象化表达惊人的巨响,所以对应于感叹号 !。shebang 的一种中文译名是“释伴”,来自于 Linux 中国翻译组的 GOLinux,是“解释伴随行”的缩写,也是 shebang 的音译。

若想让 shebang 能被识别并正常工作,就必须确保在 shebang 行之前不要添加其他注释。在感叹号之后,指定一个代码解释器(比如 Python 解释器)的绝对路径。使用相对路径则不会有任何效果。

Note: shebang 仅被运行在类 Unix 操作系统(macOS、Linux 发行版等)上的 shell(比如 Z shellBash)所识别,而在 Windows 终端中没有特殊含义,会被作为普通注释忽略。

可以通过安装带有 Unix shell 的 WSL 来实现在 Windows 上使用 shebang。另外,Windows 还允许通过在扩展名(比如 .py)和应用程序(比如 Python 解释器)之间建立全局文件关联,以实现类似的效果。

译者注:在 Windows 平台可以借助于 Python Launcher for Windows 来实现正确识别 shebang 并自动使用对应解释器的效果,详情参考官方文档

把 shebang 和 name-main 语句组合使用也很常见,name-main 语句块中的代码将不会在被另一个模块导入时运行:

1
2
3
4
#!/usr/bin/python3

if __name__ == "__main__":
print("Hello, World!")

有了这条条件语句,Python 会只在将此模块作为脚本直接运行时才调用 print() 函数——具体来说就像下面这样,通过向 Python 解释器提供模块的路径来运行:

1
2
$ python3 /path/to/your/script.py
Hello, World!

只要脚本内容以正确定义的 shebang 行开始,并且系统用户有对应文件的执行权限,就可以省略 python3 命令直接运行该脚本:

1
2
$ /path/to/your/script.py
Hello, World!

shebang 只适用于可运行脚本,尤其是那些期望在无需明确指明运行程序的情况下即可执行的脚本。而对于那些只包含用于为其他模块导入提供函数和类定义的 Python 模块,则一般不会使用 shebang。

Note: 在以前的 Python 中,shebang 行有时会和另一个特殊格式的注释一同出现(像下面这段代码中的第 2 行),这是一种编码声明,用于显式声明当前源码文件使用何种编码保存。更多详细内容可以参考 PEP 263 – Defining Python Source Code Encodings

1
2
3
4
5
#!/usr/bin/python3
# -*- coding: utf-8 -*-

if __name__ == "__main__":
print("Grüß Gott")

译者注:由于历史原因,在 Python 2 中默认编码为 ASCII,所以无法处理中文等非拉丁字符,必须用编码声明显式将编码修改为 UTF-8 或 GBK 等;而目前 Python 3 中的默认编码即为 UTF-8,可以完美支持包括 emoji 表情在内的几乎所有字符,所以无需再添加编码声明。除非有非常特殊的目的,否则不应将 Python 3 源码保存为非 UTF-8 编码并添加对应的编码声明。原文作者认为应该把源码中复杂的字符替换为其 Unicode 编码表示形式,译者认为这样做反而极大降低了可阅读性,在此保留不同意见。

现在已经对 shebang 是什么以及何时使用有了一些表层了解,是时候对它进行更深入的探索了。在下一小节中,将仔细讨论其工作原理。

Shebang工作原理

通常,要在终端运行程序,必须指定一个二进制可执行文件的完整路径,或 PATH 环境变量所列目录中的命令名称。在该路径或命令后面可以有一个或多个命令行参数:

1
2
3
4
5
$ /usr/bin/python3 -c 'print("Hello, World!")'
Hello, World!

$ python3 -c 'print("Hello, World!")'
Hello, World!

这里以非交互模式调用 Python 解释器,运行通过 -c 选项传递的单行程序。在第一种情况下,指明了 python3 的绝对路径;在第二种情况下,则是因为 python3 的父文件夹 /usr/bin/ 包含在默认搜索路径中。即使不提供完整路径,shell 也能通过 PATH 变量中的目录找到 Python 可执行文件。

Note: 如果在 PATH 变量所列出的多个目录中存在同名的命令,那么 shell 将执行它能找到的第一个。因此,如果没有明确指定相应路径,运行命令的结果可能会出乎意料,取决于 PATH 变量中目录的顺序。不过,恰当使用这个特性会非常实用,稍后详细说明。

在实践中,大部分 Python 程序都是由分散在多个模块中的多行代码组成的。一个程序通常只有一个可运行的入口点——一个可以传递给 Python 解释器执行的脚本:

1
2
$ python3 /path/to/your/script.py
Hello, World!

到目前为止,这个调用并没有惊人之处。不过还是请注意,此处运行的仍然是一个二进制可执行文件,它携带有适用于当前平台和计算机架构的机器码,又由这个二进制可执行文件反过来解释 Python 代码:

1
2
3
4
5
6
7
8
9
10
11
$ hexdump -C /usr/bin/python3 | head
00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
00000010 02 00 3e 00 01 00 00 00 00 aa 5f 00 00 00 00 00 |..>......._.....|
00000020 40 00 00 00 00 00 00 00 38 cf 53 00 00 00 00 00 |@.......8.S.....|
00000030 00 00 00 00 40 00 38 00 0d 00 40 00 20 00 1f 00 |....@.8...@. ...|
00000040 06 00 00 00 04 00 00 00 40 00 00 00 00 00 00 00 |........@.......|
00000050 40 00 40 00 00 00 00 00 40 00 40 00 00 00 00 00 |@.@.....@.@.....|
00000060 d8 02 00 00 00 00 00 00 d8 02 00 00 00 00 00 00 |................|
00000070 08 00 00 00 00 00 00 00 03 00 00 00 04 00 00 00 |................|
00000080 18 03 00 00 00 00 00 00 18 03 40 00 00 00 00 00 |..........@.....|
00000090 18 03 40 00 00 00 00 00 1c 00 00 00 00 00 00 00 |..@.............|

在许多 Linux 发行版上,python3 是一个已编译为ELF(可执行和可链接格式)的可执行文件的别名,可以使用 hexdump 命令查看该文件。

不过,shell 也可以执行脚本——也就是包含 Python、PerlJavaScript 等高级解释语言源码的文本文件。因为执行脚本,特别是来源不可信任的脚本,可能会造成有害的副作用,所以默认情况下文件是不可执行的。如果不事先将 Python 脚本设置为可执行就尝试运行,则会在终端中出现如下错误信息:

1
2
$ ./script.py
bash: ./script.py: Permission denied

一般来说,可以将执行指定文件的权限授予文件的所有者、属于与该文件相关的用户组的用户或其他人。要让任何人都能执行脚本,可以使用 chmod 命令更改文件模式位:

1
chmod +x script.py

Unix 文件权限使用符号表示法,其中字母 x 表示执行权限,加号(+)打开相关位。在某些终端上,这个操作还会改变显示可执行脚本的颜色,这样一眼就能区分出来。

虽然现在应该可以运行脚本了,但仍不能按预期方式工作:

1
2
3
$ ./script.py
./script.py: line 1: syntax error near unexpected token `"Hello, World!"'
./script.py: line 1: `print("Hello, World!")'

除非在文件开头加上 shebang,否则 shell 会假定该脚本是用相应的 shell 语言编写的。例如,如果使用 Bash shell,那么 shell 就会期望从文件中读出 Bash 命令。因此,当它遇到脚本中的 Python 函数 print()时,并不能理解。注意文件的扩展名(比如 .py)在此过程中是完全不起任何作用的!

只有当在脚本中使用 shebang 来提供 Python 解释器的绝对路径时,shell 才会知道将该脚本传递到何处:

1
2
3
4
5
6
$ cat script.py
#!/usr/bin/python3
print("Hello, World!")

$ ./script.py
Hello, World!

这非常方便,现在可以编写可直接运行的 Python 脚本了。但仍有不足之处,在 shebang 中硬编码绝对路径的做法不利于在不同系统间的移植,即使在 Unix 家族中也是如此。如果 Python 安装在不同的位置,或者 python3 命令被替换成了 python,该怎么办?如果不满足于操作系统默认的 Python 解释器,想使用虚拟环境pyenv 又该怎么办?在下一小节中,将通过改进 shebang 和探索一些替代方案来解决这些问题。

定义可移植的Shebang

在 shebang 中指定一个固定的绝对路径意味着该脚本可能无法适用于所有系统,因为系统间会有细微的差别。

再次强调,不能在 shebang 中使用相对路径而必须使用绝对路径。由于此种限制,许多开发者采用了一种变通办法,即使用 /user/bin/env 命令。这个命令可以找出 Python 解释器的实际路径:

1
2
3
#!/usr/bin/env python3

print("Hello, World!")

在不带任何参数的情况下,/usr/bin/env 命令将显示 shell 中定义的 环境变量。当然,它的主要用途是,暂时覆盖性地修改某些变量,然后在这个修改后的临时环境中运行程序。例如,可以通过设置 LANG 变量来更改指定程序的语言:

1
2
3
4
5
$ /usr/bin/env LANG=es_ES.UTF_8 git status
En la rama master
Tu rama está actualizada con 'origin/master'.

nada para hacer commit, el árbol de trabajo está limpio

一般情况下,git status 命令会以默认语言显示此消息,但在此处要求使用西班牙语。(注意并非所有程序都支持多语言,并且可能需要先在操作系统上安装一个额外的语言包才能生效。)

/usr/bin/env 的优势在于,运行命令时无需修改任何环境变量:

1
2
3
4
$ /usr/bin/env python3
Python 3.11.2 (main, Feb 13 2023, 19:48:40) [GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

这个命令的副作用之一是,它会在 PATH 变量中找到第一个出现的指定可执行文件,比如 python3,并运行该可执行文件。考虑到当激活 Python 虚拟环境时,会修改当前终端会话中的 PATH 变量(在最前方添加激活的 Python 可执行文件的父文件夹),这就非常有用了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ which python3
/usr/bin/python3

$ python -m venv venv/
$ source venv/bin/activate

(venv) $ which python3
/home/realpython/venv/bin/python3

(venv) $ echo $PATH
/home/realpython/venv/bin
⮑:/home/realpython/.local/bin:/usr/local/sbin:/usr/local/bin
⮑:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games
⮑:/snap/bin:/home/realpython/.local/bin:

如果当前 shell 中没有已激活的虚拟环境,则 python3 命令就是 /usr/bin/python3 的缩写。然而,一旦创建并激活了新的虚拟环境,python3 命令就会指向当前 venv/ 文件夹中的 Python 可执行文件。究其原因,PATH 变量已经以该文件夹开始,所以优先于全局安装的 Python 解释器。

Note: 切勿在 shebang 中使用裸 python 命令,因为根据操作系统及其配置的差异,它可能会映射到 python2python3。仅有的例外情况是,使用向后兼容的方式编写脚本,并且期望它在两个 Python 版本中都能运行。在 PEP 394 中可以找到更多关于推荐做法的信息。

/usr/bin/env 的一个缺点是它不支持向底层命令传递任何参数。因此,如果想通过运行 python3 -i 使得 Python 解释器在执行完脚本后停留在交互模式,这种做法就不太可行了:

1
2
3
$ ./script.py
/usr/bin/env: ‘python3 -i’: No such file or directory
/usr/bin/env: use -[v]S to pass options in shebang lines

幸运的是,根据错误消息的提示,有一种快速解决的办法。可以使用 /usr/bin/env 命令的 -S 选项来将后面的字符串分割成独立的参数传递给解释器:

1
2
3
#!/usr/bin/env -S python3 -i

print("Hello, World!")

脚本运行后,将进入交互式 Python REPL,在这种状态下可以查看各变量的值,进行事后调试。

Note: 在某些系统中,shebang 行可能有一定字符数的长度限制,因此要让它保持在合理的长度内。

归根结底,shebang 是一种相对简单的制作可直接运行的 Python 脚本的方法,但它需要一定的 shell、环境变量以及当前操作系统的背景知识。此外,它在可移植性方面并不完美,因为它主要适用于类 Unix 系统。

如果不想手动设置 shebang,可以借助 setuptoolsPoetry 等工具来完成这项工作。它们可以通过常规函数为项目配置便捷的入口点。或者,可以创建一个特殊的 __main__.py 文件来将 Python 模块变成一个可运行的单元。也可以在 Python 中构建一个可执行的 ZIP 应用程序,从而避免使用 shebang。这些都是值得一试的替代方案,避开了 shebang 的一些不足之处。

更多Shebang示例

上面的例子中,已经使用 shebang 为脚本指定了 Python 解释器的版本。然而,在某些情况下,可能有不止一种解释器能够理解和处理同一份代码。例如,可以编写一个同时兼容 Python 2 和 Python 3 的脚本:

1
2
3
4
5
$ /usr/bin/env python2 script.py
Hello, World!

$ /usr/bin/env python3 script.py
Hello, World!

在 Python 2 中,print 语句包裹的括号会被忽略,所以只要继续使用这种保守的语法风格,就可以安全地使用 python 命令,而无需在 Shebang 中使用明确的版本:

1
2
3
#!/usr/bin/env python

print("Hello, World!")

甚至可以使用 Perl 解释器来运行这段代码,Perl 中 print 函数的行为也与 Python 中的类似:

1
2
3
#!/usr/bin/env perl

print("Hello, World!")

因为 shell 并不关心文件扩展名,所以甚至不需要修改扩展名,只更新 Shebang 行,就能使用之前安装过的 Perl 解释器运行此脚本:

1
2
$ ./script.py
Hello, World!$

脚本的运行效果几乎一模一样,只是 Python 添加了一个尾随换行,而 Perl 没有。这是一个最简单的多语言程序的例子——数种编程语言都能理解它,并产生相同的输出结果。

用 Perl 编写的正确版 Hello, World! 程序大概是这样的:

1
2
3
#!/usr/bin/env perl

print("Hello, World!\n");

在 Perl 中,虽然语法上并不要求,但建议在函数参数周围使用括号。注意在字符串文字中有换行符(\n),以及在行尾有终止语句的分号(;)。

如果电脑上有 Node.js,那么可以直接在终端上运行 JavaScript。下面是一个类似的 Hello, World! 脚本,使用 JavaScript 这种曾是网络浏览器领域专属的语言:

1
2
3
#!/usr/bin/env node

console.log("Hello, World!")

尽管 # 在 JavaScript 中不是有效的语法,但 Node.js 服务器会识别出独特的 shebang 序列,并在执行源码文件的其余部分前忽略整个 shebang 行。

稍加努力,甚至可以用 Java 来编写脚本(虽然严格来讲 Java 并不是一种脚本语言)。Java 需要将其高级代码编译成 Java 虚拟机(JVM)字节码,这类似于 Python 解释器,只不过是二进制操作码(opcodes)。

译者注:对于 Python 代码的运行,其实也存在先从高级代码编译至字节码,然后进行解释运行的过程。在运行 Python 3 源码后,在同级目录中出现的 __pycache__ 目录中的 .pyc 文件,正是字节码缓存文件。

为了在 Java 程序中使用 shebang,需要进行如下几步操作:

  1. 确保 Java 源码文件不使用传统的 .java 扩展名。可以为文件设置中性的扩展名,比如 .j。正如 Stack Overflow 上的这个回答,这种扩展名可以确保 Java 忽略不合法的 # 字符。
  2. 使用 java 命令而不是 javac 命令运行源码文件。
  3. 通过 --source 开关显式指明使用 Java 11 版本。

下面是一个完整的 Java “脚本” 示例:

1
2
3
4
5
6
7
#!/usr/bin/env -S java --source 11

public class Hello {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}

相比于用真正的解释语言编写的 Hello, World! 程序,运行这个脚本所需的时间明显更长。因为每一次调用时都会进行额外的编译步骤。

Note: Spring Boot 框架可以巧妙地在二进制压缩包的开头加上一个 shebang 和一个 shell 脚本,并将 Java 类与之结合,从而为 Unix 系统创建一个完全可执行的 JAR 文件。这使得部署此类 Java 网络应用程序变得轻而易举!

只要能够让 shell 找到正确的解释器,就能为任何语言编写可直接运行的脚本,甚至包括自己的 DSL(domain-specific language,特定领域语言)。

译者注:原文此处还有一个小节,展示了如何使用 Python 实现一个“神秘语言” brainf**k 的解释器,并用 shebang 调用这个解释器实现可直接运行的 .b 脚本。由于和主题关系不太密切,翻译时删去了这一节。感兴趣的朋友可以访问原文查看。

Shebang最佳实践

总之,为在 Python 脚本中恰当地使用 shebang,应遵守以下几条规则:

  • 牢记 shebang 只适用于类 Unix 操作系统上的可运行脚本。
  • 如果没有指定 shebang,shell 将认为脚本是用其对应的 shell 语言编写的,尝试解释运行。
  • 不要在仅用于导入而不会执行的 Python 模块中使用 shebang。
  • 确保对脚本文件有执行权限。
  • 可以考虑将 shebang 和 if __name__ == "__main__": 组合使用。
  • 将 shebang 行放在脚本的最开始,前面不要加任何其他注释。
  • shebang 要以 #! 字符起始,以区别于普通注释。
  • 使用 /usr/bin/env python3 命令,避免硬编码指向任何特定 Python 解释器的绝对路径。
  • 避免使用裸 python 命令,除非该脚本可以向后兼容 Python 2。一般情况下,应该使用含义更明确的 python3
  • 使用 -S 标识来向解释器传递额外参数——例如 #!/usr/bin/env -S python3 -i
  • 注意 shebang 行中的字符数。

最后,反问一下自己,是否需要手动添加 shebang,还是可以通过一些工具链中的工具来自动生成或替换成一些更高层次的抽象。


用 Shebang 执行 Python 脚本(翻译)
https://muzing.top/posts/d52d9f46/
作者
muzing
发布于
2023年10月24日
更新于
2023年11月13日
许可协议