为什么在这个 git 钩子示例的末尾使用 exec(看似不必要)?

Why is exec used (seemingly unnecessarily) at the end of this git hook sample?

我正在使用我在 OSX (git version 2.24.3 (Apple Git-128)) 上的版本附带的 commit.sample 之前的 githooks。代码中有一些特殊之处,即与看似虚假的 exec.

有关

预提交示例包含以下代码(已删除不相关的 lines/blocks):

#!/bin/sh

against=HEAD

# Redirect output to stderr.
exec 1>&2

# If there are whitespace errors, print the offending file names and fail.
exec git diff-index --check --cached $against --

如果我试图通过在最后一次 exec 调用之后附加验证来修改此代码,它永远不会运行。根据 relevant AskUbuntu post,我明白 exec 是什么让这一切发生了。

但是,我不明白为什么 exec 需要首先发生。如果有尾随空格,此行的挂钩会失败,但如果我删除 exec 并直接调用 git diff-index ....

,它的行为似乎相同

换句话说,这个:

git diff-index --check --cached $against --

...看起来像这样:

exec git diff-index --check --cached $against --

...除了后者似乎更具限制性。我找不到带有或不带有 exec 除了 的文件之间的区别 exec 使得空白检查 最后发生。

为什么示例创建者会选择 exec 选项,当它看起来与表面上限制较少的直接调用相同时?

这可能是(也许是误入歧途的)提高效率的尝试。

一般来说,在shell脚本中,脚本中的return值是最后一个命令运行的值,如。所以:

#! /bin/sh
cmd1
cmd2
cmd3
exit $?

只是一种long-winded/明确的做法:

#! /bin/sh
cmd1
cmd2
cmd3

这里的一般规则是 shell 采用每个“管道”——一个管道被定义为一系列带有 | 符号的命令,这些符号相互连接——并且 运行 是主 shell 进程的 fork-then-exec 中的管道。所以:

cmd1 | cmd2
cmd3

使主 shell 分叉一次到 运行 cmd1 | cmd2 (在内部,这两个命令中的每一个都需要另一个分叉),然后再次分叉到 运行 命令 3。然后,运行 退出命令,shell 将以 $?(最后一个管道的退出状态)作为它自己的状态退出。

添加重定向,例如:

cmd1 | cmd2 > file

“意味着”shell 应该分叉,然后 运行 管道 cmd1 | cmd2 将其输出重定向到该文件。当然 cmd1 的输出已经重定向到 cmd2 的输入,所以这里只有 cmd2 的输出受到影响——但我们可以看到 cmd3 的输出是not 重定向,很明显,重定向没有发生在 shell 级别,而是发生在 sub-shell 级别分叉到 运行 管道。1

exec 关键字的作用实际上是防止分叉。即:

exec cmd > out

重定向发生在 顶层 shell,然后 运行 使用 exec 系统调用发送给定命令无需先调用 fork。此 shell 替换为 运行 命令(但会保留进程 ID 和所有打开的文件描述符,直到 运行 到此结束)。

如果我们省略命令本身,我们得到:

exec >out

这意味着没有命令获得 运行,但是重定向发生在 shell 本身 而不是某些 sub-shell。所以现在每个后续命令都会得到 fork-and-exec,并将其输出发送到文件 out.

我们在您自己的脚本中看到类似的内容:

exec 1>&2

强制所有后续命令的 stdout 转到与 stderr 相同的文件描述符。

奇怪的是,只有一个后续命令,这意味着如果目标是效率,他们可以使用:

exec git diff-index --check --cached $against -- 1>&2

将所有内容放在一行中。


1实际上,shells 实际上会提前打开文件,并且必须做很多花哨的步法来在 forkexec 调用。使用 POSIX 风格的作业控制,情况更糟:shell 必须做很多 signal-directing 工作,创建进程组,等等。编写 shell 是 困难的 ,正如 V8 Unix 和 Plan 9 人员所见,这意味着整个 OS 设计需要一些修改。


一般退出状态

如您在回复中所述:

Hence, if I have validation after a non-execed command, I'd need to make sure check for a non-0 result from the git diff-index.

是的。请注意,通常 shells(特别是 /bin/sh)具有有趣的标志,您可以从命令或 #! 行或使用 set 命令设置这些标志。这些标志之一是 e 标志,如果命令具有 non-zero 退出代码,它会使 shell 退出:2

#! /bin/sh -e
cmd1
cmd2
cmd3

大致相当于:

#! /bin/sh
cmd1 || exit
cmd2 || exit
cmd3

(我们不需要最后一个 || exit,尽管我们可以无害地使用它)。 -e 标志通常是个好主意。


2注意tested命令不会让shell立即退出,所以我们可以这样写:

if grep ...; then
    thing to run when regexp is found
else
    thing to run when regexp is not found
fi

/bin/sh 的一些早期版本中存在一个错误,它不能正常工作:我记得修复它,然后发现我要么 over-fixed 要么 under-fixed 它对于像 a && b || c 这样的情况,必须 re-fix 它。