“/usr/bin/env node”在节点文件的开头到底做什么?

我在 nodejs中的一些例子的开头看到了这一行 #!/usr/bin/env node,我在 Google 中没有找到任何可以回答这一行的原因的主题。

文字的本质使得搜索不是那么容易。

我最近读了一些 javascriptnodejs的书,我不记得在其中的任何一本书里看到过它。

如果你想要一个例子,你可以看到 RabbitMQ的官方 教程,他们有几乎所有的例子,这里是其中之一:

#!/usr/bin/env node


var amqp = require('amqplib/callback_api');


amqp.connect('amqp://localhost', function(err, conn) {
conn.createChannel(function(err, ch) {
var ex = 'logs';
var msg = process.argv.slice(2).join(' ') || 'Hello World!';


ch.assertExchange(ex, 'fanout', {durable: false});
ch.publish(ex, '', new Buffer(msg));
console.log(" [x] Sent %s", msg);
});


setTimeout(function() { conn.close(); process.exit(0) }, 500);
});

有人能解释一下这句话的意思吗?

如果我放置或删除这一行有什么区别? 在什么情况下我需要它?

78129 次浏览

简短的回答: 它是通向解释器的路径。

编辑(长答案) : 在“ node”之前没有斜杠的原因是因为您不能总是保证 # 的可靠性!/垃圾桶/。“/env”位通过在修改后的环境中运行脚本和更可靠地查找解释程序,使程序更具有跨平台性。

您不一定需要它,但是使用它可以确保可移植性(和专业性)

#!/usr/bin/env node一个 < a href = “ https://en.wikipedia.org/wiki/Shebang _ (Unix)”rel = “ norefrer”> shebang line 的实例: 在 < strong > 类 Unix 平台 上的可执行纯文本文件中的第一行,告诉系统将文件传递给哪个解释器执行,通过神奇的 #!前缀(称为 开始吧)后面的命令行。

注意: Windows does < em > not support shebang lines,因此它们实际上是 被忽略了; 在 Windows 上,只有给定文件的 文件扩展名决定哪个可执行文件将解释它。但是,在 npm的上下文中仍然需要它们.[1]

The following, 关于蛇帮线的一般性讨论 is limited to Unix-like platforms:

在下面的讨论中,我将假设包含由 Node.js 执行的源代码的文件名为 file

  • 如果你想调用一个 Node.js 源文件 直接,那么 需要这条线本身就是一个可执行文件——这里假设这个文件已经被标记为可执行文件,比如 chmod +x ./file,这个命令允许你用例如 ./file来调用这个文件,或者,如果它位于 $PATH变量中列出的某个目录中,那么就简单地标记为 file

    • 具体来说,您需要一个 shebang 行来创建基于 Node.js 源文件的 CLI,将其作为 npm 包裹的一部分,并且 npm将根据 包的 ABC2文件中的 "bin"的值安装 CLI; 还可以参见 这个答案,了解如何使用安装了 全球范围内的软件包。脚注[1]显示了如何在 Windows 上处理这个问题。
  • 您使用 不需要这一行通过 node解释器显式地调用一个文件,例如,node ./file


可选背景资料 :

#!/usr/bin/env <executableName>随身携带指定解释器的一种方法: 简而言之,它说: 无论您(首先)在 $PATH变量中列出的目录中找到它,都执行 <executableName>(并隐式地将路径传递给手边的文件)。

这解释了为什么给定的解释器可以跨平台安装在不同的位置,对于 Node.js 二进制程序 node来说就是这种情况。

相比之下,env实用程序本身的位置可以依赖于跨平台的 一样位置,即 /usr/bin/env-并且指定可执行文件的 满了路径是以 shebang 行表示的 需要

请注意,POSIX 实用程序 env在这里是 重新利用,通过文件名定位并在 $PATH中执行一个可执行文件。
env的真正用途是为一个命令管理环境——参见 env的 POSIX 规格基思 · 汤普森的有益回答


值得注意的是,Node.js 正在为 shebang 行创建一种语法 例外,因为它们不是有效的 JavaScript 代码(#在 JavaScript 中不是注释字符,不像在类 POSIX 的 shell 和其他解释器中)。


[1]为了保持跨平台的一致性,在安装包的 package.json文件中指定的可执行文件时(通过 "bin"属性)使用 ABC0在 Windows 上创建 < em > wrapper *.cmd文件(批处理文件)。本质上,这些包装批文件 模仿 Unix shebang 功能: 它们 使用 shebang 行中指定的可执行文件显式调用目标文件-因此,您的脚本必须包含一个 shebang 行,即使您只打算在 Windows 上运行它们-参见我的 这个答案的详细信息。
由于 *.cmd文件可以在不使用 .cmd扩展名的情况下调用,因此可以实现无缝的跨平台体验: 在 Windows 和 Unix 上,您可以通过原始的、无扩展名的名称有效地调用安装了 npm的 CLI。

解释器执行的脚本通常在顶部有一个 一字排开来告诉操作系统如何执行它们。

如果您有一个名为 foo的脚本,其第一行是 #!/bin/sh,系统将读取第一行并执行相当于 /bin/sh foo的代码。因此,大多数解释器都设置为接受脚本文件的名称作为命令行参数。

#!后面的解释器名称必须是完整路径; 操作系统不会搜索 $PATH来找到解释器。

如果您有一个由 node执行的脚本,那么显而易见的编写第一行的方法是:

#!/usr/bin/node

但是如果没有在 /usr/bin中安装 node命令,那么这种方法就不起作用。

一种常见的解决方法是使用 env命令(真的不是为此目的而设计的) :

#!/usr/bin/env node

如果脚本名为 foo,则操作系统将执行相当于

/usr/bin/env node foo

env命令执行其命令行上指定名称的另一个命令,并将以下参数传递给该命令。这里使用它的原因是 env将在 $PATH中搜索该命令。因此,如果 node安装在 /usr/local/bin/node中,而 $PATH中有 /usr/local/bin,则 env命令将调用 /usr/local/bin/node foo

env命令的主要用途是使用修改后的环境执行另一个命令,在运行该命令之前添加或删除指定的环境变量。但是没有额外的参数,它只是在一个不变的环境中执行命令,这就是本例中所需的全部内容。

这种方法有一些缺点。大多数现代的类 Unix 系统都有 /usr/bin/env,但我使用的是较老的系统,其中 env命令安装在另一个目录中。使用此机制传递的其他参数可能有一些限制。如果用户 没有$PATH中有包含 node命令的目录,或者有一些名为 node的不同命令,那么它可能调用了错误的命令,或者根本无法工作。

其他方法包括:

  • 使用指定到 node命令本身的完整路径的 #!行,根据不同系统的需要更新脚本; 或者
  • 使用脚本作为参数调用 node命令。

另见 这个问题(和 我的回答)更多关于 #!/usr/bin/env技巧的讨论。

顺便说一下,在我的系统(Linux Mint 17.2)上,它是以 /usr/bin/nodejs的形式安装的。根据我的笔记,它在 Ubuntu 12.04和12.10之间从 /usr/bin/node变成了 /usr/bin/nodejs#!/usr/bin/env技巧对此没有帮助(除非您设置了一个符号链接或类似的东西)。

更新: mtraceur 的评论说(重新格式化) :

解决 nodejs vs 节点问题的方法是以 以下六行:

#!/bin/sh -
':' /*-
test1=$(nodejs --version 2>&1) && exec nodejs "$0" "$@"
test2=$(node --version 2>&1) && exec node "$0" "$@"
exec printf '%s\n' "$test1" "$test2" 1>&2
*/

这将首先尝试 nodejs,然后尝试 node,并且只 如果找不到两个错误消息,则打印它们。解释 在这些评论的范围之外,我只是把它留在这里,以防 帮助任何人处理这个问题,因为这个答案带来了 出问题了。

我最近没有使用 NodeJS。我的希望是,nodejsnode的问题已经解决了这些年来,因为我第一次张贴了这个答案。在 Ubuntu 18.04上,nodejs包安装 /usr/bin/nodejs作为到 /usr/bin/node的符号链接。在一些早期的操作系统(Ubuntu 或 Linux Mint,我不确定是哪个)中,有一个 nodejs-legacy包提供了 node作为到 nodejs的符号链接。不能保证我所有的细节都正确。

Linux 内核的 exec系统调用原生地理解 shebang (#!)

当你在 bash 上做的时候:

./something

在 Linux 上,这将使用路径 ./something调用 exec系统调用。

在传递给 exec: https://github.com/torvalds/linux/blob/v4.8/fs/binfmt_script.c#L25的文件上调用内核的这一行

if ((bprm->buf[0] != '#') || (bprm->buf[1] != '!'))

它读取文件的第一个字节,并将它们与 #!进行比较。

如果比较结果为真,那么该行的其余部分将被 Linux 内核解析,它将使用以下命令执行另一个 exec调用:

  • 可执行文件: /usr/bin/env
  • 第一个参数: node
  • 第二个参数: 脚本路径

因此等同于:

/usr/bin/env node /path/to/script.js

env是一个可执行文件,它搜索 PATH,例如查找 /usr/bin/node,然后最后调用:

/usr/bin/node /path/to/script.js

Js 解释器确实看到了文件中的 #!行,但是它必须被编程为忽略该行,即使 #在 Node 中通常不是一个有效的注释字符(不像其他许多语言,比如 Python) ,请参见: 磅号(#)作为 JavaScript 注释的开始?

是的,你可以做一个无限循环:

printf '#!/a\n' | sudo tee /a
sudo chmod +x /a
/a

Bash 发现了这个错误:

-bash: /a: /a: bad interpreter: Too many levels of symbolic links

#!碰巧是人类可读的,但这不是必需的。

如果文件以不同的字节开始,那么 exec系统调用将使用不同的处理程序。另一个最重要的内置处理程序是用于 ELF 可执行文件的: https://github.com/torvalds/linux/blob/v4.8/fs/binfmt_elf.c#L1305,它检查字节 7f 45 4c 46(这碰巧也是人类可读的 .ELF)。让我们通过读取 /bin/ls的前4个字节来确认这一点,/bin/ls是一个 ELF 可执行文件:

head -c 4 "$(which ls)" | hd

产出:

00000000  7f 45 4c 46                                       |.ELF|
00000004

因此,当内核看到这些字节时,它将获取 ELF 文件,正确地将其放入内存中,并使用它启动一个新进程。参见: 内核如何让一个可执行的二进制文件在 Linux 下运行?

最后,您可以使用 binfmt_misc机制添加自己的 shebang 处理程序。例如,可以添加 .jar文件的自定义处理程序。这种机制甚至通过文件扩展名支持处理程序。另一个应用程序是 使用 QEMU 透明地运行不同架构的可执行文件

然而,我不认为 POSIX指定了 shebang: https://unix.stackexchange.com/a/346214/32558,尽管它在基本原理部分中提到了,并且以“如果系统支持可执行脚本,那么可能会发生一些事情”的形式提到。然而,macOS 和 FreeBSD 似乎也实现了它。

PATH搜索动机

可能,shebang 存在的一个很大的动机是,在 Linux 中,我们经常希望从 PATH运行命令,就像:

basename-of-command

而不是:

/full/path/to/basename-of-command

但是,如果没有 shebang 机制,Linux 如何知道如何启动每种类型的文件呢?

在命令中对扩展进行硬编码:

 basename-of-command.js

或者在每个解释器上实现 PATH 搜索:

node basename-of-command

但是这样做的主要问题是,如果我们决定将命令重构为另一种语言,那么所有东西都会崩溃。

她很好地解决了这个问题。