小心地模仿bash中的Argv[0]

6

我正在尝试编写一个Bash包装脚本,非常仔细地模仿argv[0]/$0的值。 我使用exec -a来使用包装器的argv [0]值执行单独的程序。 我发现有时Bash的$0无法给出与C程序的argv [0]相同的值。 这里是一个简单的测试程序,展示了C和Bash中的差异:

int main(int argc, char* argv[0])
{
    printf("Argv[0]=%s\n", 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] 的目标?

编辑:错别字。

1个回答

2
这里展示的是 bashexecve 的文档化行为(至少在 LinuxFreeBSD 上有文档记录;我想其他系统也有类似的文档),并反映了构建 argv[0] 的不同方式。
Bash(像任何其他 shell 一样)从提供的命令行构建 argv,在执行各种扩展、必要时重新分割单词等操作后。最终结果是当你输入
printargv

"argv"被构造为{"printargv", NULL},当你输入时。
to/printargv

argv被构建为{"to/printargv", NULL}。所以没有什么意外的。

(在两种情况下,如果有命令行参数,它们将从位置1开始出现在argv中。)

但是在那一点上,执行路径分叉。当命令行中的第一个单词包含/时,则被认为是文件名,可以是相对或绝对路径。Shell不进行进一步处理;它只是使用提供的文件名作为其filename参数和先前构建的argv数组作为其argv参数调用execve。在这种情况下,argv[0]恰好对应于filename

但是当命令没有斜杠时:

printargv

外壳程序要做更多的工作:
  • 首先,它检查名称是否是用户定义的外壳函数。如果是,它将执行该函数,并使用已构建的 argv 数组中的 $1...$n。(尽管如此,$0 仍然是来自脚本调用的 argv[0]。)

  • 然后,它会检查名称是否为内置的 bash 命令。如果是,它将执行该命令。内置命令如何与命令行参数交互不在本答案的范围之内,并且并不真正可见。

  • 最后,它尝试找到与命令对应的外部实用程序,通过搜索 $PATH 的组件并查找可执行文件。如果找到一个,则调用 execve,并将找到的路径作为 filename 参数传递,但仍然使用由命令单词组成的 argv 数组。因此,在这种情况下,filenameargv[0] 不匹配。

因此,在这两种情况下,shell 最终都会调用 execve 函数,将文件路径(可能是相对路径)作为 filename 参数,将经过单词分割的命令作为 argv 参数。
如果指定的文件是可执行映像,则实际上没有更多要说的了。该映像被加载到内存中,并使用提供的 argv 向量调用其 main 函数。 argv[0] 将是一个单词或相对或绝对路径,这仅取决于最初键入的内容。
但是,如果指定的文件是脚本,则加载器将产生错误,execve 将检查文件是否以 shebang(#!)开头。(自 Posix 2008 起,execve 还会尝试使用系统 shell 运行文件作为脚本,就好像它具有 #!/bin/sh 作为 shebang 行一样。)
以下是 Linux 上 execve 的文档:

An interpreter script is a text file that has execute permission enabled and whose first line is of the form:

      #! interpreter [optional-arg]

The interpreter must be a valid pathname for an executable file. If the filename argument of execve() specifies an interpreter script, then interpreter will be invoked with the following arguments:

      interpreter [optional-arg] filename arg...

where arg... is the series of words pointed to by the argv argument of execve(), starting at argv[1].

请注意,上述中的filename参数是execve函数的filename参数。给定shebang行#!/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 设置为给定的文件名参数,在我们的示例中为 to/printargv/path/to/printargv,并将 $1...$n 设置为剩余的参数,这些参数是从原始命令行中的命令行参数复制而来的。
总之,如果您使用不带斜杠的文件名调用命令:
  • 如果文件名包含可执行映像,则它将把 argv[0] 视为键入的命令名称。

  • 如果文件名包含具有 shebang 行的 bash 脚本,则脚本将把 $0 视为脚本文件的实际路径。

如果您使用带斜杠的文件名调用命令,则在两种情况下它都会将 argv [0] 视为键入的文件名(可能是相对的,但显然总是有一个斜杠)。
另一方面,如果您通过明确调用shell解释器(bash printargv)来调用脚本,则脚本将把$0视为键入的文件名,这不仅可能是相对路径,而且可能没有斜杠。
所有这些意味着,如果您知道要模拟的调用脚本的形式,那么您只能“小心地模仿argv [0]”。 (这也意味着脚本永远不应该依赖于argv [0]的值,但这是一个不同的话题。)
如果您正在进行单元测试,您应该提供一个选项来指定要提供给argv [0]的值。许多尝试分析$0的shell脚本都假定它是文件路径。他们不应该这样做,因为它可能不是,但事实就是如此。如果您想排除这些实用程序,您需要提供一些垃圾值作为$0。否则,您的最佳选择是提供脚本文件的路径作为默认值。

谢谢回复。printargs.sh确实有一个shebang。我发布了它的两行源代码。这仍然留下了问题的重要部分:用bash脚本准确模拟$argv[0]的正确方法是什么? - Jason Hiser
@jason,是的,抱歉,我被打断了。我会在几个小时内完成回答。 - rici
@jason:好的,我重写了答案,希望能够有所帮助。我认为没有“正确的方法”,因为argv [0]可能是这个或那个,理想情况下,bash脚本将使用任何一个都可以。因此,如果你正在测试,你应该尝试使用不同的$0值来测试脚本。如果你只是想给它一个合理的东西,我的建议是使用完整的绝对文件名。 - rici
感谢您详细的回复。这不是为了测试。我真的需要准确地传递argv[0],就像(ELF)可执行文件一样。基本上,根据您所说的、文档和一些实验,我发现由于bash脚本的执行方式不同,它们根本无法访问在执行ELF可执行文件时使用的字符串。最终,我用一个小型可执行文件包装了bash脚本,正确地传递了参数,包括argv[0]。这意味着bash脚本无法透明地替换可执行文件。我觉得这很恼人。 - Jason Hiser

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