为什么#!/usr/bin/env bash比#!/bin/bash更好?

308

我在很多地方看到,包括这个网站的建议(What is the preferred Bash shebang?),推荐使用#!/usr/bin/env bash而不是#!/bin/bash。我甚至看到有人建议使用#!/bin/bash是错误的,这样做会丢失bash功能。

虽然我在一个高度控制的测试环境中使用bash,其中每个驱动器基本上都是单个主驱动器的克隆品。我理解可移植性的论点,但在我的情况下并不一定适用。除了可移植性之外,是否有其他原因优先考虑#!/usr/bin/env bash而不是其他选择?在假设可移植性是一个问题时,使用它是否会破坏功能?


12
并不一定更好。请参考unix.stackexchange.com上的这个问题我的回答。(我会投票关闭这个问题,因为它是一个重复的问题,但我认为你不能跨站点这样做。) - Keith Thompson
3
除了 @zigg 的回答之外,env 可能不位于 /usr/bin。在我看来,shebang 注释本质上是一个不良的想法。如果您的默认脚本解释器不处理 shebang 注释,那么它只是一条注释。然而,如果您知道脚本解释器可以处理 shebang 注释,并且您知道 bash 的路径,那么没有理由不使用其绝对路径调用它,除非路径过长(不太可能),或者您可能将脚本移植到没有将 bash 位于 /bin 的系统中。再次强调,这种情况涉及可移植性,需要注意上述的警告事项。 - user539810
1
@KeithThompson,谢谢你提供的链接。也许在发帖之前我对寻找答案的范围有些狭窄。从这一切中我得到的启示是:(1)Linux/Unix/Posix等等是灰色的,(2)任何声称绝对拥有正确答案的人,只是针对他们特定情况下的正确答案而已。 - spugm1r3
4
在POSIX/Unix中,许多事物的行为都有明确定义,但位置并不总是那么清晰明了。例如,/etc/bin/sh等文件必须存在。对于大多数类Unix系统,bash是一种附加组件。仅在Linux系统中,bash才保证位于/bin目录下,并且很可能也链接到/bin/sh。由于Linux成为许多人现代实际使用的Unix系统,因此已经忘记了除Linux以外的其他系统也可能存在。在下面的回答中,我假设您使用的是Linux,因为您提到了bash。我曾经使用过的许多BSD系统甚至没有安装bash - Sean Perry
5
在Bash方面(与其他问题中的Python相对),OpenBSD没有/bin/bash。Bash不是默认安装的,如果您想要它,必须使用pkg install bash进行安装。一旦安装完成,它将位于/usr/local/bin/bash。在OpenBSD上,没有任何东西安装在/bin/bash位置。使用#!/bin/bash作为shebang会出错,而使用#!/usr/bin/env bash则会成功。 - jww
显示剩余5条评论
8个回答

320

#!/usr/bin/env 搜索 PATH,查找 bash。在非Linux系统上,bash 并不总是在 /bin 中。例如,在我的OpenBSD系统中,它在 /usr/local/bin 中,因为它是作为一个可选包安装的。

如果你确信 bash/bin 中并且将始终如此,那么将其直接放入您的shebang中没有什么坏处。但是我建议不要这样做,因为脚本和程序都有超出我们最初所认为的生命周期。


56
环境变量的位置呢?POSIX标准并没有强制规定。 - Julio Guerra
3
就像有一个"/usr/lib/sendmail"(或更近的版本"/usr/sbin/sendmail")可用于处理邮件一样,对于类Unix系统来说,最好有"/usr/bin/env",因为"env shebang"是如此普遍的做法,它是事实上的标准接口。 - zigg
32
@zigg 那很像 UNIX……我的意思是,它对他们有最大的利益来为 env 设定一个标准位置,但却没有为 bash 设定一个标准位置(这只需要是软链接就可以了)。更不用说,为什么 hashbang 不接受 #!bash,而使用 PATH 来做与我们使用 env 完全相同的事情。我猜这对于新手来说还不够混乱。 - ddekany
4
@XaviMontero说:我曾经使用过一个系统,其中env/bin而不是/usr/bin(不确定是哪个操作系统,可能是SunOS 4)。现在很可能会有/usr/bin/env可用,这只是因为#!/usr/bin/env的流行。 - Keith Thompson
6
起初我同意你的观点,但是后来我考虑到bash和env之间的差异。我敢打赌,一个系统拥有或支持多种类型的shell(sh、csh、tsh、zsh、fish、bash等)以及多个版本的shell(bash 3与bash 4)的可能性要比存在多个“env”程序的可能性大得多。...尽管如此,了解这些差异对于初学者深入学习并不利。 - Kevin
显示剩余6条评论

60

bash标准位置是/bin,我怀疑在所有系统上都是这样。但是,如果您不喜欢那个版本的bash怎么办?例如,我想使用bash 4.2,但我的Mac上的bash是3.2.5。

我可以尝试在/bin中重新安装bash,但那可能是一个坏主意。如果我更新操作系统,它将被覆盖。

然而,我可以在/usr/local/bin/bash中安装bash,并设置我的PATH为:

PATH="/usr/local/bin:/bin:/usr/bin:$HOME/bin"
如果我指定bash,就会使用位于/usr/local/bin的新版本而不是旧版的/bin/bash。但是我的shell脚本有!# /bin/bash shebang,这意味着当我运行shell脚本时,我会得到那个旧的差劲版本的bash,它甚至没有关联数组功能。使用 /usr/bin/env bash 将使用路径中找到的bash版本。如果我设置我的PATH,使得执行/usr/local/bin/bash,那么我的脚本将使用该版本的bash。在bash中很少见到这种情况,但在Perl和Python中经常发生:

  • 某些Unix/Linux发行版专注于稳定性,因此某些脚本语言的版本可能非常陈旧。不久前,RHEL的Perl版本是5.8.8——一个八年前的版本!如果有人想要使用更现代的功能,必须自己安装版本。
  • 像Perlbrew和Pythonbrew这样的程序允许您安装多个版本的这些语言。它们依赖于操纵你的PATH的脚本来得到所需的版本。硬编码路径意味着我无法在brew下运行我的脚本。
  • 不久之前(好吧,其实很久之前),Perl和Python不是大多数Unix系统中包含的标准软件包。这意味着你不知道这两个程序安装在哪里。是在/bin下?/usr/bin?还是/opt/bin?谁知道呢?使用#! /usr/bin/env perl意味着我不需要知道。

现在为什么不应该使用#! /usr/bin/env bash

当路径在shebang中被硬编码时,我必须使用该解释器运行。因此,#! /bin/bash强制我使用默认安装的bash版本。由于bash功能非常稳定(尝试在Python 3.x下运行Python 2.x脚本),所以我的特定BASH脚本很可能能够正常工作,而且由于我的bash脚本可能会在此系统和其他系统中使用,因此使用非标准版本的bash可能会产生不良影响。很可能我想确保我的shell脚本使用稳定的标准版本的bash,因此我可能要在我的shebang中硬编码路径。


12
这段话的意思是:“‘bash’ 的标准位置并不是‘/bin’(除非你能引用一个标准文档),更准确地说,在大多数 Linux 发行版和 macOS 上,它通常位于此处,但在 Unix 系统中并不是一个标准位置(在大多数 BSD 中也不是该位置)。” - tesch1
7
NixOS 是一个当前的 Linux 发行版,它在 /bin 目录下没有安装任何东西(包括 bash)。 - slackhacker

16

有很多系统的/bin目录中没有Bash,其中包括FreeBSD和OpenBSD。如果你的脚本需要在许多不同的Unix系统上移植,你可能需要使用#!/usr/bin/env bash代替#!/bin/bash

请注意,这不适用于sh;对于符合Bourne标准的脚本,我只使用#!/bin/sh,因为我认为几乎每个现存的Unix系统都有/bin中的sh


1
在Ubuntu 18.04的/bin目录下,我看到sh -> dash。这是一个符号链接,揭示了Ubuntu的Debian本质。运行以下三个命令字符串,以实现所有内容都归结为个人偏好:which bash,然后是which sh,最后是which dash - noobninja

14

调用bash有点过头了。除非您有多个bash二进制文件,例如您自己的~/bin中,但这也意味着您的代码依赖于$PATH具有正确的内容。

对于像python这样的东西很方便。有包装脚本和环境,导致使用替代的python二进制文件。

但是只要您确信它是您真正想要的二进制文件,使用精确的二进制文件路径不会有任何损失。


对于 python 来说非常准确。我已经数不清 python 出现的地方了。 - zigg
2
同意使用/usr/bin/env对于Python来说更加有用,特别是如果你使用virtualenv。 - Dennis

2
你的问题存在偏见,因为它假设#!/usr/bin/env bash优于#!/bin/bash。这个假设是不正确的,以下是原因: env在两种情况下非常有用:
1. 当有多个版本的解释器是不兼容的时候。例如:python 2/3、perl 4/5或php 5/7。
2. 当位置取决于PATH时,比如Python虚拟环境。
但bash并不属于这两种情况,因为:
1. Bash是相当稳定的,特别是在现代系统如Linux和BSD上,这些系统构成了大多数Bash安装。
2. 在/bin下通常只安装了一个版本的bash。在过去20年中,只有很旧的Unix(已经没有人再使用)会有不同的位置。
因此,通过/usr/bin/env变量来查找Bash并不是有用的。
此外,有三个好理由使用#!/bin/bash 1. 对于系统脚本(不使用sh)而言,在PATH变量中可能不包含/bin。例如,cron默认使用非常严格的PATH/usr/bin:/bin是可以的,但是其他上下文/环境可能由于某种奇怪的原因而不包括/bin
2. 当用户搞砸了他的PATH时,这非常普遍。
3. 在安全性方面,例如您调用一个suid程序来调用bash脚本。你不希望解释器通过PATH变量被发现,因为这完全在用户的控制范围内!
最后,有人可以说使用env生成bash的一个合法用例:当需要使用#!/usr/bin/env -S VAR=value bash传递额外的环境变量给解释器时。但对于Bash来说,这不是问题,因为当你控制shebang时,你也控制整个脚本,所以只需在脚本中添加VAR=value即可,避免env引入的上述问题。

2
 #!/usr/bin/env bash

使用环境变量查找bash可执行文件路径,这绝对是更好的选择。

前往您的Linux shell并键入

env

它将打印出您的所有环境变量。

进入您的shell脚本,键入

Original Answer翻译成:"最初的回答"

echo $BASH

它会打印出你的bash路径(根据环境变量列表),你可以在脚本中使用它来构建正确的shebang路径。"Original Answer"的翻译是"最初的回答"。

1
通常情况下,当执行#!path/to/command命令时,Bash会在调用脚本时将命令路径添加到脚本前缀。例如:
# file.sh
#!/usr/bin/bash
echo hi

./file.sh 会启动一个新的进程,脚本将像 /bin/bash ./file.sh 那样被执行。

现在

# file.sh
#!/usr/bin/env bash
echo hi

将被执行为/usr/bin/env bash ./file.sh,引用自env的man页面描述如下:

env - 在修改后的环境中运行程序

因此,env将在其PATH环境变量中查找命令bash并在单独的环境中执行,其中环境值可以像NAME=VALUE对一样传递给env

您可以使用其他解释器(如python等)测试此功能以运行不同的脚本。

#!/usr/bin/env python
# python commands

我想看看你如何像你声称的那样在shebang行中传递NAME=VALUE :) - Eric
1
@Eric 请尝试以下代码: `#!/bin/env -S A=2 bash echo $A` - gitartha
TIL -S 可以在 shebang 行上传递多个参数。谢谢! - Eric

0
我更喜欢将主程序包装在类似下面的脚本中,以检查系统上所有可用的bash。最好对它使用的版本有更多的控制。
#! /usr/bin/env bash

# This script just chooses the appropriate bash
# installed in system and executes testcode.main

readonly DESIRED_VERSION="5"

declare all_bash_installed_on_this_system
declare bash

if [ "${BASH_VERSINFO}" -ne "${DESIRED_VERSION}" ]
then
    found=0

    all_bash_installed_on_this_system="$(\
        awk -F'/' '$NF == "bash"{print}' "/etc/shells"\
        )"

    for bash in $all_bash_installed_on_this_system
    do
        versinfo="$( $bash -c 'echo ${BASH_VERSINFO}' )"
        [ "${versinfo}" -eq "${DESIRED_VERSION}" ] && { found=1 ; break;}
    done
    if [ "${found}" -ne 1 ]
    then
        echo "${DESIRED_VERSION} not available"
        exit 1
    fi
fi

$bash main_program "$@"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接