"/usr/bin/env node"在Node.js文件开头具体是做什么的?

177

我在一些nodejs的例子开头看到过这个行 #!/usr/bin/env node,并且做过搜索但没有找到解释这行的原因的相关主题。

这些单词的性质使得搜索它并不那么容易。

最近我读了一些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);
});

可以有人解释一下这行的意义吗?

如果我加上或者去掉这行有什么不同?在什么情况下需要用它?


3
它基本上采用调用Shell的环境,然后将该环境填充到指定的任何应用程序中。在这种情况下,是 node - Marc B
实际上不是,我不是来自Windows,但感谢您更新您的答案。我只是在等待看看是否有其他人有不同的意见。有一件事情我认为您没有在您的答案中提到,我刚刚几个小时前发现了它。他们在这里提到的东西似乎很重要,但对我来说还不够清楚。https://dev59.com/K23Xa4cB1Zd3GeqPhKdL(如果您想更新,可以更新,我会非常感激,但不要感觉像是义务,您的答案已经足够好了)。 - Gepser Hoil
@Gepser:明白了。简而言之,如果你想让npm将Node.js源脚本安装为(可能是全局可用的)_CLI_,你必须使用shebang行 - npm甚至会在Windows上使其工作;请参见我再次更新的答案。 - mklement0
这些词的性质使得搜索它并不容易 - 你可能想尝试使用https://duckduckgo.com来进行特定的搜索。 - Ricardo
可能是为什么人们在Python脚本的第一行写#!/usr/bin/env python shebang?的重复问题。同样的问题,不同的解释器。 - jww
4个回答

219
#!/usr/bin/env nodeshebang行的一个实例:在Unix-like平台上,它是可执行纯文本文件中的第一行,告诉系统将该文件传递给哪个解释器进行执行,通过命令行跟随魔术 #! 前缀(称为 shebang)。

注意:Windows不支持shebang行,因此在Windows上它们被有效地忽略;在Windows上,仅给定文件的文件名扩展名决定了哪个可执行文件将解释它。然而,你仍然需要它们在 npm 的上下文中。[1]

以下是关于 shebang 行的普遍讨论,仅限于 Unix-like 平台:

在接下来的讨论中,我会假设包含由Node.js执行的源代码的文件简单地命名为 file

  • 如果你想要直接将一个Node.js源文件作为可执行文件来调用,那么必须有这一行,假设该文件已经被标记为可执行文件,并使用命令(例如chmod +x ./file)进行了标记,然后你可以使用./file来调用该文件,或者如果它位于$PATH变量中列出的某个目录中,则可以简单地使用file

    • 具体而言,你需要一个shebang行以创建基于Node.js源文件的CLI作为npm包的一部分,CLI将根据"bin" key在包的package.json文件中的值npm安装;另请参见this answer以了解全局安装包的工作原理。脚注[1]显示了Windows上如何处理此问题。
  • 如果通过node解释器显式调用文件,例如node ./file,则不需要这一行。


可选背景信息:

#!/usr/bin/env <executableName> 是一种便携式指定解释器的方法:简而言之,它表示:在列在 $PATH 变量中的目录中找到 <executableName>(首次出现的位置),并隐含地将其路径传递给正在处理的文件。

这就解释了在不同平台上可能会在不同位置安装同一解释器的情况,这在 Node.js 二进制文件中绝对是个问题。

相比之下,env 实用程序本身的位置可以保证在各个平台上都相同,即 /usr/bin/env,因此在 shebang 行中需要指定可执行文件的完整路径。

请注意,此处正在重新使用 POSIX 实用程序 env 来按文件名查找并执行位于 $PATH 中的可执行文件。
env 的真正目的是为命令管理环境-请参见 env 的 POSIX 规范Keith Thompson 的有用答案
值得注意的是,Node.js针对shebang行做了语法例外,因为它们不是有效的JavaScript代码(与类似POSIX的shell和其他解释器不同,#不是JavaScript中的注释字符)。
为了实现跨平台一致性,当安装包中指定的可执行文件(通过“bin”属性)在一个包的package.json文件中时,npm会在Windows上创建批处理文件(wrapper *.cmd文件)。这些包装批处理文件模拟了Unix shebang功能:它们显式地使用在shebang行中指定的可执行文件调用目标文件,因此,即使您只打算在Windows上运行脚本,您的脚本也必须包含shebang行。详情请参见我的this answer
由于*. cmd文件可以在没有.cmd扩展名的情况下被调用,这使得跨平台体验非常流畅:在Windows和Unix上,您都可以通过其原始、无扩展名的名称有效地调用一个npm安装的CLI。

你能为像我这样的小白提供一个解释或摘要吗? - Andrew Lam
7
在Windows系统中,例如.cmd.py这样的文件名扩展名决定了将用哪个程序来执行这些文件。而在Unix系统中,则是通过shebang行来进行执行。为了使npm在所有支持的平台上正常工作,即使在Windows系统中也需要使用shebang行。 - mklement0

40

通常需要由解释器执行的脚本在顶部都会有shebang line,以告诉操作系统如何执行它们。

如果你有一个名为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”的原因是,“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的评论说(重新格式化):

A workaround for the nodejs vs node problem is to start the file with the following six lines:

#!/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
*/

This will first try nodejs and then try node, and only print the error messages if both of them are not found. An explanation is out of scope of these comments, I'm just leaving it here in case it helps anyone deal with the problem since this answer brought the problem up.

我最近没有使用NodeJS。希望自我发布此回答以来,nodejsnode的问题已得到解决。在Ubuntu 18.04上,nodejs软件包将/usr/bin/nodejs安装为指向/usr/bin/node的符号链接。在早期的某些操作系统(Ubuntu或Linux Mint,我不确定),有一个名为nodejs-legacy的软件包,它提供了将node作为指向nodejs的符号链接。不能保证我对所有细节都正确。


一个非常详尽的答案,解释了事情的原因。 - Suraj Jain
1
解决 nodejsnode 冲突的方法是在文件开头添加以下六行代码:1) #!/bin/sh -, 2) ':' /*- 3) test1=$(nodejs --version 2>&1) && exec nodejs "$0" "$@" 4) test2=$(node --version 2>&1) && exec node "$0" "$@", 5) exec printf '%s\n' "$test1" "$test2" 1>&2 6) */。这将首先尝试使用 nodejs,然后再尝试使用 node,只有当两者都未找到时才会打印错误消息。这里不再详细解释,只是为了帮助其他人解决问题而留下这个答案。 - mtraceur
@mtraceur:我已经将你的评论纳入我的答案中了。为什么在 #! 行上要加上 - - Keith Thompson
#!/bin/sh - 中的 - 只是一种习惯,确保在极为狭窄和不太可能出现的情况下,shell 能正确运行脚本名称或相对路径以 - 开头的情况。(另外,是的,看起来每个主流发行版都已经重新将 node 作为主要名称。我在发表评论时没有深入挖掘,但据我所知,只有 Debian 发行版家族使用了 nodejs,而且看起来它们都回到了支持 node 的状态,一旦 Debian 这样做了。) - mtraceur
从技术上讲,作为第一个参数的单破折号并不意味着“选项结束”-它最初的意思是“关闭-x-v”,但自从早期的 Bourne-like 只解析第一个参数作为可能的选项,并且由于 shell 以这些选项关闭的状态启动,所以这是可以被滥用的,以便导致 shell 不尝试解析脚本名称,因为原始的行为在现代 Bourne-like 中仍然保持兼容性。如果我记得所有 Bourne 历史和可移植性细节正确的话。 - mtraceur
我认为每个现代的 Bourne-like shell 都支持更可识别的约定,即使用 -- 来表示选项的结束,但我患有一种罕见的脑部疾病,迫使我喜欢那些适用于最广泛的 Bourne-like shells 的 shell 脚本习惯,除非有令人信服的理由。我倾向于不记得或思考这些习惯之间的相互依赖关系,这就是为什么我坚持使用 - 而不是 --,即使我使用了 $( 替换而不是反引号,并且在没有处理古老的无参数情况下执行了 "$@" - mtraceur

6
Linux内核的exec系统调用可以原生地理解shebangs(#!

当你在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

Node.js解释器确实会识别文件中的#!行,但它必须被编程忽略该行,即使在Node中#通常不是有效的注释字符(与许多其他语言(如Python)不同),请参见:Pound Sign (#) As Comment Start In JavaScript? 是的,你可以使用以下代码创建一个无限循环:
printf '#!/a\n' | sudo tee /a
sudo chmod +x /a
/a

Bash识别错误:
-bash: /a: /a: bad interpreter: Too many levels of symbolic links
< p > #! 只是可读的,但这不是必需的。 < / p > < p > 如果文件以不同的字节开头,则 exec 系统调用将使用不同的处理程序。另一个最重要的内置处理程序是针对 ELF 可执行文件的:https://github.com/torvalds/linux/blob/v4.8/fs/binfmt_elf.c#L1305 它检查字节 7f 45 4c 46(这也恰好是人类可读的 .ELF)。让我们通过读取 ELF 可执行文件 /bin/ls 的前四个字节来确认: < / p >
head -c 4 "$(which ls)" | hd 

输出:

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

当内核看到这些字节时,它会将ELF文件正确地放入内存中,并使用它启动一个新进程。参见:Linux下内核如何运行可执行二进制文件? 最后,您可以使用binfmt_misc机制添加自己的shebang处理程序。例如,您可以添加一个.jar文件的自定义处理程序。该机制甚至支持按文件扩展名进行处理程序。另一个应用是使用QEMU透明地运行不同架构的可执行文件
我认为POSIX没有规定shebangs,但是在其基本原理和形式中提到了它,如“如果系统支持可执行脚本,则可能会发生某些事情”。不过,macOS和FreeBSD似乎也实现了它。https://unix.stackexchange.com/a/346214/32558 PATH搜索动机 很可能,shebang存在的一个重要动机是,在Linux中,我们经常希望像这样从PATH运行命令:
basename-of-command

代替:

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

但是,如果没有shebang机制,Linux如何知道如何启动每种类型的文件?
在命令中硬编码扩展名:
 basename-of-command.js

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

node basename-of-command

使用脚本解释器是一种可能性,但是如果我们决定将命令重构为另一种语言,这会导致一切都无法正常运行。

而 shebang 则完美地解决了这个问题。


1
你提到了 /usr/bin/node /path/to/script.py,但我认为你实际上是指 /usr/bin/node /path/to/script.js。请更正代码片段。目前编辑队列已满。否则我可以进行编辑。 - Ashishkel

0

简短回答: 这是解释器的路径。

编辑(长回答): 之所以在“node”之前没有斜杠,是因为无法始终保证“#!/bin/”的可靠性。 "/env" 部分通过在修改后的环境中运行脚本,并更可靠地找到解释器程序,使程序更具跨平台性。

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


1
/usr/bin/env 这一部分并不会修改环境。它只是一个在(大多数情况下)已知位置的命令,用于调用作为参数给出的另一个命令,并搜索 $PATH 来找到它。关键在于 #! 行需要被调用命令的完整路径,而你并不一定知道 node 安装在哪里。 - Keith Thompson
那就是我想表达的,谢谢你澄清! - IsaacDorenkamp

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