传递包含"!!!!"的字符串时,argv的奇怪行为

31

我编写了一个小程序,它从*argv []中获取一些输入参数并将它们打印出来。 在几乎所有使用情况下,我的代码都可以很好地工作。 仅当我在要传递为参数的字符串末尾使用超过一个感叹号时才会出现问题...

这个是有效的:

./program -m "Hello, world!"

这样做是不起作用的:

./program -m "Hello, world!!!!"

^^ 如果我这样做,程序的输出要么是那个字符串的两倍,要么是我输入 ./program 命令之前的命令。

但是,我绝对不理解的是:以下内容竟然可以工作:

./program -m 'Hello, world!!!!'

^^ 输出与输入相同,不包含任何额外的空格或换行符。

Hello, world!!!!

... 就像预期的那样。

所以,我的问题是:

  • 为什么在一个字符串中使用多个感叹号时会出现奇怪的行为?
  • 据我所知,在C语言中您使用“”表示字符串,使用''表示单个字符。那么,为什么当我按照我的理解应该使用“”时,使用''可以得到所需的结果,而不是使用“”
  • 我的代码有错误吗,或者我需要改变什么来能够输入任何字符串(无论使用多少标点符号)并准确地打印该字符串?

我的代码的相关部分:

// this is a simplified example that, in essence, does the same 
// as my (significantly longer) code
int main(int argc, char* argv[]) {
    char *msg = (char *)calloc(1024, sizeof(char));

    printf("%s", strcat(msg, argv[2])); // argv[1] is "-m"

    free(msg);
}

我已经尝试将argv[2]的内容复制到一个char*缓冲区中,并在其末尾添加了一个'\0',但这并没有改变任何内容。


2
为什么要使用printf("%s", strcat(msg, argv[2])),而不是printf("%s", argv[2])) - Jabberwocky
2
空格,你需要对它进行转义(escape)。而且 calloc() 在这里没有太多意义。只使用 char msg[1024] 就非常好了。当你使用单引号时,字符串会原封不动地传递。这与 argv 或 [tag:c] 编程语言无关,而是与 shell 相关。 - Iharob Al Asimi
1
@Michael Walz:因为我在msg中创建了一个更长的字符串。将argv的内容附加到其中仅是完整代码中的许多步骤中的第一步。抱歉之前没有澄清。 - ci7i2en4
6
感叹号是你的shell(可能是bash)的一个特殊字符。如果它不被放在单引号内,shell会解释!!并将其替换为其他内容(历史记录中的上一个命令)。你的程序工作正确,它打印出从命令行接收到的内容。 - axiac
2
@ci7i2en4:1)语法错误是在扩展期间创建的bash语法错误,在这种情况下,您的程序甚至没有启动,2)您的程序应负责验证输入值,但您无法“撤消”bash执行的扩展。您应该做的事情是:检查argc是否具有有效长度并检查参数是否合理,然后可能显示带有使用信息的错误消息。 - vgru
显示剩余11条评论
3个回答

68
这与您的代码无关,而是与启动它的shell有关。
在大多数shell中,"!!"是上一条命令的简写。当您使用双引号时,shell允许在字符串中进行历史扩展(以及变量替换等)。因此,当您将"!!"放在双引号字符串中时,它会替换为上次运行的命令。
对于您的程序,这意味着所有这些都发生在程序执行之前,因此程序除了检查传入的字符串是否有效外,没有太多可以做的事情。
相比之下,当您使用单引号时,shell不会进行任何替换,并且原样传递字符串给程序。
因此,您需要使用单引号来传递此字符串。如果用户不希望进行任何替换,则需要知道这一点。另一种选择是创建一个包装shell脚本,提示用户输入要传递的字符串,然后脚本将使用正确的参数调用您的程序。

我明白了,谢谢!那么我还有一个问题:是否有一种方法可以确保我的程序用户仍然可以使用“Something!!!!”作为输入参数而不会遇到这种行为?(无论他们使用什么shell) - ci7i2en4
13
那取决于你的用户如何决定。也许他们希望那种扩展发生,也可能不希望。 - dbush
2
在Bash中,使用 set +o histexpandset +H 命令可以禁用历史扩展。其他终端可能有其它设置。 - ilkkachu
8
我认为,任何改变 shell 预期行为的解决方案都可能是个坏主意。用户应该知道如何在使用的 shell 中发送输入。 - Ethan
此外,那只会改变我的bash的行为。如果其他人在他们的shell中运行我的程序,他们仍然会遇到历史扩展问题... - ci7i2en4
这篇文章也很有用:Bash 中双引号和单引号的区别 - codeforester

9

Shell在双引号字符串中进行扩展。如果您阅读Bash手册页面(假设您使用Bash,这是大多数Linux发行版的默认设置),那么如果您查看历史扩展部分,您将看到!!的含义为

参考前一个命令。

因此,在您的双引号字符串中,!!!!将扩展为前一个命令两次。

这种扩展不适用于单引号字符串。

因此,问题不在于您的程序,而是由环境(shell)调用您的程序造成的。


8

除了提供的答案之外,你应该记住 echo 是你的 shell 朋友。如果在命令前加上 "echo",你就可以看到 shell 实际发送给你的脚本。

echo ./program -m "Hello, world!!!!"

这将展示一些奇怪的东西,可能有助于引导你朝正确的方向前进。

1
“echo” 实际上并不是这种用途的最佳选择——毕竟,“echo“hello world””和“echo“hello” “world””尽管是非常不同的命令,但它们的输出完全相同。 - Charles Duffy
1
考虑使用以下代码替代:print_args() { printf '%q ' "$@"; printf '\n'; } -- 之后,print_args ./program -m "Hello, world!!!!" 将以一种使它们的解释在 echo 处理错误的情况下也不会产生歧义的方式发出参数。 - Charles Duffy
这只是Charles的观点。我很感激他展示了使用echo作为工具的不足之处,尤其是他提供了一种替代方法。对我来说,在每个Linux环境中都依赖于一个函数(或复制/粘贴)并记住该函数名称是一个糟糕的选择。现在读者有两个糟糕的选择,一个在答案中,另一个在评论中。 - UncleCarl
关于“意见”--请参阅POSIX规范中的echo,特别是应用使用部分,该部分明确建议除非传递的数据被限制为已知安全的子集,否则应改用printf - Charles Duffy
printf保证在任何地方都可用(尽管如果可移植性是目标,则可能需要不同的格式字符串,例如'<%s>\n');除非选择这样做,否则无需使用函数来包装它。也就是说——我并不特别支持我建议使用的print_args函数是一个好选择。我绝对支持echo是一个糟糕的选择,而printf是正确的替代品,并且有规范的文档支持我的观点。 - Charles Duffy

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