【问题标题】:Why is exec used (seemingly unnecessarily) at the end of this git hook sample?为什么在这个 git hook 示例的末尾使用 exec(似乎没有必要)?
【发布时间】:2020-09-04 14:36:44
【问题描述】:

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

预提交示例包含以下代码(删除了不相关的行/块):

#!/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 选项,而它的行为似乎与表面上限制较少的直接调用相同?

【问题讨论】:

  • 好吧,正如您自己发现的那样,exec 调用永远不会返回。事实上,它用执行程序 (git) 替换了当前进程 (bash)。进程的退出代码将是 git 的退出代码。如果您不执行 exec,则 git 命令将作为单独的进程执行。如果在这种情况下,您只是添加一个“echo”进行测试,退出代码将是 echo 命令之一。
  • 啊,脚本的返回值就是最后一条命令的返回值?因此,如果我在非execed 命令之后进行验证,我需要确保检查git diff-index 的非0 结果。谢谢!

标签: bash git exec


【解决方案1】:

这可能是(也许是被误导的)提高效率的尝试。

通常,在 shell 脚本中,脚本的返回值是最后一次运行命令的返回值,如 Ronald noted in a comment。所以:

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

只是一种冗长/明确的做法:

#! /bin/sh
cmd1
cmd2
cmd3

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

cmd1 | cmd2
cmd3

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

添加重定向,如:

cmd1 | cmd2 > file

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

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

exec cmd > out

重定向是否发生在 顶级 shell 中,然后它使用exec 系统调用运行给定的命令,而无需先调用fork。这替换 shell 为正在运行的命令(但挂在进程 ID 和所有打开的文件描述符上,直到此处运行的命令完成)。

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

exec >out

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

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

exec 1>&2

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

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

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

将所有内容放在一行中。


1在实践中,shell 实际上会提前打开文件,并且必须在forkexec 调用之间做很多花哨的工作来改变文件描述符。使用 POSIX 风格的作业控制,情况更糟:shell 必须做大量的信号引导工作、创建进程组等等。编写 shell 很难,正如 V8 Unix 和 Plan 9 的人所见,这意味着整个操作系统设计需要重新设计。


一般退出状态

正如您在回复中所说:

因此,如果我在未执行的命令之后进行验证,我需要确保检查来自 git diff-index 的非 0 结果。

是的。请注意,通常的 shell(尤其是 /bin/sh)具有有趣的标志,您可以从命令或 #! 行或使用 set 命令设置。这些标志之一是e 标志,如果命令具有非零退出代码,则使 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 的一些早期版本中存在一个错误:我记得修复了它,然后发现对于像 a && b || c 这样的情况我要么过度修复要么修复不足重新修复它。

【讨论】:

    猜你喜欢
    • 2016-02-02
    • 2016-02-06
    • 2015-11-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-11-18
    • 1970-01-01
    相关资源
    最近更新 更多