Bash检查显示文件存在但实际上不存在?

3

在 bash 中运行以下命令:

   stuff=`rpm -ql <some package> | grep dasdasdfd`

(包中不存在的文件,退出代码 = 1,标准输出为空)

  if [ -f $stuff ]; then echo "whaaat"; fi

上述命令用于检查文件是否存在,但是:
file $stuff

打印文件的使用信息...等

stat $stuff

缺少操作数...

请问有人能解释一下为什么会出现这种情况吗?这是一个bug吗?还是我做错了什么?我只是想确保包中的文件存在于文件系统中。

3个回答

6
您可能需要在引号中包含$stuff
if [ -f "$stuff" ]; then

作为一般规则,在使用路径名时,几乎总是要在其周围添加引号。
我认为将shell脚本中的变量视为“宏”更有用,这些变量在第一次使用时会被扩展为其值。这与几乎所有其他编程语言中的变量不同。
因此,如果$stuff包含“hello world”(注意空格),那么这将与您键入的内容相同:
[ -f hello world ]

这显然是一个错误。

在这种情况下,您提到正在处理一个不存在的文件,因此$stuff实际上是空的,这就像输入:

[ -f ]

这实际上是有效的,但总是成功的。这是一个有点晦涩的测试行为,从POSIX规范中我们可以看到,如果只有一个参数(在这种情况下,参数是-f),则test始终成功:

1个参数:
如果$1不为空,则退出true(0);否则退出false。

这可能是为了方便编写以下内容:

[ $variable_that_may_or_may_not_be_defined ]

如果您添加引号,那么您将传递2个参数,这样更加合理:

if [ -f "" ]; then

我简直不敢相信我没有尝试过那个... 我一定是傻傻的... 谢谢! - niken
有人能解释一下为什么 [ -ftest -f 在没有参数的情况下返回0吗?文档似乎并没有提到这种情况。 - Michael Jaros
6
[ 字符串 ] 被解释为 [ -n 字符串 ][ -f ] 等价于 [ -n -f ],始终为真:因为 -f 不是空字符串。 - John Kugelman

6

Martin Tournoij的答案DevSolar的答案都提供了正确的解决方案和有用的背景信息:其中一个是关于[ ... ],另一个是关于[[ ... ]]

由于可能不明显何时选择[[ ... ]]而不是[ ... ](及其(虚拟)别名test ...),因此让我尝试总结一下:

  • 如果您的代码必须是可移植的(符合POSIX标准),则必须使用[ ... ](或test ...
    • [ ... ]内部的标记像传递给可执行文件的参数一样解析,因此您必须双引号引用变量引用,除非您明确希望将所有Shell扩展(特别是单词拆分(自动通过空格拆分为多个标记)和globbing)应用于它们。
    • [ -f "$stuff" ] # 如果$stuff包含嵌入式空格,则需要双引号引用
  • 如果您知道您的代码将在bash中运行,则可以使用[[ ... ]]以获得更多功能和更少的意外情况
    • [[ ... ]]内部的标记在一个特殊上下文中解析,其中不应用单词拆分或路径名扩展(globbing)(尽管其他扩展,如参数扩展,发生),因此通常不需要双引号引用变量引用
    • [[ -f $stuff ]] # 双引号引用可选

请注意,kshzsh也支持[[ ... ]](可能具有不同的行为差异)。

有关更多背景信息,例如[[ ... ]]提供的其他功能,请继续阅读。


[[ ... ]]相对于[ ... ] / test ...的改进如下

下面的“RHS”表示“右手边”,即二元操作符的右操作数。

  • 通常不需要引用变量(除了在===~的RHS上指定一个字面量字符串或子字符串时)

    • f='some file'; [[ -f $f ]] # ok, double quotes optional
    • v='*'; [[ $v == '*' ]] # ok, double quotes optional
    • [[ ... ]]内部不会应用单词分割或路径名扩展,因此可以安全地使用未引用的变量引用,这些变量的值具有嵌入式空格和/或像*这样的值,这些值通常会导致全局匹配。
  • 提供使用=/==字符串模式匹配,RHS上为未引用的模式(或至少是未引用的模式元字符)

    • [[ abc == a* ]] && echo yes # matches; use of = instead of == works too
    • 注意:因此,在=/==的RHS上,如果您希望将它们的值视为字面值处理,则必须对变量引用进行双引号(或单引号文字)。
      • v='a*'; [[ abc == "$v" ]] # does NOT match
  • 提供使用=~正则表达式匹配,RHS上为未引用的扩展正则表达式(或至少是未引用的正则表达式元字符)。
    • [[ abc =~ ^a.+$ ]] && echo yes # matches
    • 注意:因此,在=~的RHS上,如果您希望将它们的值视为字面值处理,则必须对变量引用进行双引号(或单引号文字)。
      • v='a.+'; [[ abc =~ ^"$v"$ ]] # does NOT match
      • 还要注意,未引用/引用的区别仅在bash 3.2中引入-您仍然可以使用shopt -s compat31使单引号和双引号字符串也被视为正则表达式。
    • 注意=~理解的正则表达式方言是特定于平台的,因此在一个平台上有效的正则表达式可能在另一个平台上无效(这是bash行为受平台影响的少数情况之一)。例如,在Linux上,您可以使用\b\</\>进行单词边界断言,而BSD/macOS仅支持[[:<]][[:>]],而Linux则不支持-请参见我此答案
  • 提供分组和否定使用未转义的()!字符
  • 提供使用&&||布尔AND和OR

    • 关于[test的实现说明:

      • [[是shell的关键字(在bashkshzsh中支持),它允许使用不同的解析规则,如上所述。
      • 相比之下,[test是所有主要POSIX-like shell(bashkshzshdash)中的内置命令
      • 此外,根据POSIX的规定,[test都存在于外部工具中(可执行的文件,需要一个独立的进程来调用)。
        • 实际上,您需要外部实用程序版本才能在“无shell”调用场景中使用[test,例如将测试传递给find -execxargs
        • 虽然[实用程序可以被实现为指向test实用程序的符号链接(只要test知道它是如何被调用的,并在被调用为[时强制执行关闭]),但实际上它们通常(总是?)是单独的可执行文件(例如,在Linux和macOS / BSD上是真实的;在Linux上,它们的内容不同,而在macOS / BSD上,它们的内容相同(它们是同一个文件的副本))。

3

一种选择是像Carpetsmoker所说的那样将$stuff放入引号中。

但由于标记为bash,并且考虑到文件名中的空格会很烦人,您可以选择以下方式:

if [[ -f $stuff ]]

[test的别名不同,[[结构“知道”如何正确处理$stuff的内容。

很好知道,我使用[[进行逻辑等价性测试,包括!=和==。 - niken
@Nik:这是[[的附加功能之一:=〜运算符用于将字符串与正则表达式进行匹配。 - DevSolar

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