我正在尝试编写一个非常仔细地模仿argv[0]/$0值的bash包装器脚本。 我正在使用 exec -a 来执行一个带有包装器的 argv[0] 值的单独程序。 我发现有时 bash 的 0 美元并不能提供我在 C 程序的 argv[0] 中获得的相同值。 下面是一个简单的测试程序,演示了 C 和 bash 的区别:
int main(int argc, char* argv[0])
{
printf("Argv[0]=%sn", argv[0]);
return 0;
}
和
#!/bin/bash
echo $0=$0
当使用二进制文件的完整(绝对或相对)路径运行这些程序时,它们的行为相同:
$ /path/to/printargv
Argv[0]=/path/to/printargv
$ /path/to/printargv.sh
$0=/path/to/printargv.sh
$ to/printargv
Argv[0]=to/printargv
$ to/printargv.sh
$0=to/printargv.sh
但是当调用它们时,就好像它们在路径中一样,我得到不同的结果:
$ printargv
Arv[0]=printargv
$ printargv.sh
$0=/path/to/printargv.sh
两个问题:
1)这是可以解释的预期行为,还是一个错误? 2)实现精心模仿argv[0]的目标的"正确"方法是什么?
编辑:错别字。
你在这里看到的是记录在案的bash
和execve
的行为(至少,它记录在 Linux 和 FreeBSD 上;我认为其他系统也有类似的文档),并反映了argv[0]
构建的不同方式。
Bash(与任何其他 shell 一样)在执行各种扩展后从提供的命令行构造argv
,根据需要重新拆分单词等。最终结果是,当您键入时
printargv
argv
构造为{ "printargv", NULL }
和键入时
to/printargv
argv
构造为{ "to/printargv", NULL }
.所以没有惊喜。
(在这两种情况下,如果有命令行参数,它们就会出现在从位置 1 开始的argv
中。
但是在这一点上,执行路径会有所不同。当命令行中的第一个单词包含/时,它被视为文件名,无论是相对文件名还是绝对文件名。外壳不进行进一步处理;它只是使用提供的文件名作为其filename
参数调用execve
,并将先前构造的argv
数组作为其argv
参数。在这种情况下,argv[0]
精确对应于filename
但是当命令没有斜杠时:
printargv
外壳做了更多的工作:
首先,它检查名称是否是用户定义的 shell 函数。如果是这样,它将执行它,并从已经构造的
argv
数组中获取$1...$n
。(不过,$0
继续从脚本调用中argv[0]
。然后,它会检查名称是否为内置 bash 命令。如果是这样,它将执行它。内置如何与命令行参数交互超出了此答案的范围,并且不是真正的用户可见的。
最后,它尝试通过搜索
$PATH
的组件并查找可执行文件来查找与命令对应的外部实用程序。如果找到一个,它会调用execve
,给它找到的路径作为filename
参数,但仍使用由命令中的单词组成的argv
数组。所以在这种情况下,filename
和argv[0]
不对应。
因此,在这两种情况下,shell 最终都会调用execve
,提供一个文件路径(可能是相对的)作为filename
参数,并提供单词拆分命令作为argv
参数。
如果指示的文件是可执行映像,则真的没有什么可说的了。图像被加载到内存中,并使用提供的argv
向量调用其main
。argv[0]
将是单个单词或相对或绝对路径,仅取决于最初键入的内容。
但是,如果指示的文件是脚本,则加载器将产生错误,execve
将检查文件是否以 shebang (#!
) 开头。 (从 Posix 2008 开始,execve
还将尝试使用系统 shell 将文件作为脚本运行,就好像它#!/bin/sh
为一个 shebang 行一样。
以下是 Linux 上execve
的文档:
解释器脚本是启用了执行权限的文本文件,其第一行的格式为:
#! interpreter [optional-arg]
解释器必须是可执行文件的有效路径名。 如果 execve() 的文件名参数指定了解释器脚本,则解释器将使用以下参数调用:
interpreter [optional-arg] filename arg...
其中
arg...
是execve()
的argv
论点所指向的一系列词,从argv[1]
开始。
请注意,在上面,filename
参数是要execve
filename
参数。鉴于舍邦线#!/bin/bash
我们现在有
/bin/bash to/printargv # If the original invocation was to/printargv
或
/bin/bash /path/to/printargv # If the original invocation was printargv
请注意,argv[0]
实际上已经消失了。
然后bash
运行文件中的脚本。在执行脚本之前,它会$0
设置为给定的 filename 参数(在我们的示例中为to/printargv
或/path/to/printargv
),并将$1...$n
设置为其余参数,这些参数是从原始命令行中的命令行参数复制的。
总之,如果使用不带斜杠的文件名调用命令:
如果文件名包含可执行映像,它将看到
argv[0]
作为键入的命令名称。如果文件名包含带有 shebang 行的 bash 脚本,则脚本将
$0
视为脚本文件的实际路径。
如果您使用带斜杠的文件名调用该命令,在这两种情况下,它都会看到 argv[0] 作为键入的文件名(这可能是相对的,但显然总是有一个斜杠)。
另一方面,如果通过显式调用 shell 解释器来调用脚本 (bash printargv
),脚本将$0
视为键入的文件名,这不仅可能是相对的,而且可能没有斜杠。
所有这些都意味着,只有当你知道你想要模仿的脚本调用形式时,你才能"仔细模仿argv[0]"。(这也意味着脚本永远不应该依赖于argv[0]
的值,但这是一个不同的主题。
如果这样做是为了单元测试,则应提供一个选项来指定要作为 argv[0] 提供的值。许多试图分析的 shell 脚本$0
假定它是一个文件路径。他们不应该这样做,因为它可能不是,但它就在那里。如果你想把这些公用设施抽出来,你需要提供一些垃圾价值作为$0
。否则,作为默认值的最佳选择是提供脚本文件的路径。