在 Mac 上,将通配符模式放在单引号中,可防止默认的 zsh shell 在命令运行前将其展开。例如,printf '%s\n' '*.csv' 会将字面文本 *.csv 作为一个参数传递。引号会被 shell 移除;星号则会传递给程序。如果程序本身使用模式匹配,它可能会在随后解析该星号。

为什么通配符会在命令运行前发生变化

当你在“终端”中输入命令时,shell 会在启动程序之前处理其参数。在 zsh 中,未加引号的 * 会触发文件名生成(filename generation):shell 会用匹配的文件名替换该模式。如果没有匹配项,zsh 通常会报错,不过 shell 选项可以更改这种行为。zsh 手册介绍了文件名生成及其无匹配时的行为。

Apple 将 zsh 指定为“终端”中的默认 shell,不过 Mac 也可以配置为使用其他 shell。Apple 的“终端”使用手册说明了默认 shell 以及如何更改它。下面介绍的单引号方法适用于 zsh 和 Bash;如果你使用的是其他 shell,请查阅其手册,因为引用规则可能会有所不同。

使用单引号保留通配符字面值

将通配符模式输入在单引号之间,并保留属于它的任何其他字符。例如:

printf '%s\n' '*.csv'

无论当前目录是否包含 CSV 文件,输出都是字面文本 *.csv。printf 将该模式作为参数接收并将其打印出来;shell 不会展开星号。引号属于 shell 语法,因此它们不包含在参数中。Apple 的 shell 脚本指南解释说,单引号会禁用所引文本内的 shell 展开。Apple:流程控制、展开与解析(Flow Control, Expansion, and Parsing)。

当程序本身需要自行处理模式匹配时,这一点尤为重要。例如在 macOS 上,find 可以使用提供给 -name 的模式来匹配文件名:

find . -type f -name '*.log'

在此处,shell 将 *.log 按原样传递给 find;然后 find 对当前目录下的文件应用其自身的文件名匹配逻辑。加引号可以防止 shell 在 find 使用该模式之前,就将其替换为当前目录中的文件名。模式的含义取决于接收它的程序:shell 引用控制的是 shell 展开,而不是程序如何解释其参数。

何时改用反斜杠

对于较短的模式,反斜杠可以在引号之外保护单个通配符:

printf '%s\n' \*.csv

这同样会按原样传递星号。当模式包含多个特殊字符、空格或方括号时,使用单引号通常更易于阅读。例如,'chapter [0-9]*.txt' 可以防止方括号和通配符被 shell 解释。Apple 在其 shell 脚本指南中记录了反斜杠转义和单引号的行为。

常见的加引号错误

双引号并不能可靠地保护每一个 shell 字符。它们可以在 zsh 中阻止通配符展开,但仍然允许某些展开,包括变量(如 $name)和命令替换(如 $(command))。对于需要完全按原样传递的模式,除非参数确实需要上述某种展开,否则请使用单引号。

单引号不能作为普通字符直接出现在单引号引起的字符串内部。如果模式本身包含单引号(撇号),请结束当前引用部分,使用其他引用方式添加该单引号,然后再重新打开单引号;Apple 在其指南中展示了这种 shell 引用技巧。对于不常见的模式,在运行之前请仔细检查确切的命令。

此外,还要将 shell 展开与接收程序的行为区分开来。给 *.log 加上引号并不会强制程序将星号视为普通字符;它只能确保 shell 将其原样传递。诸如 find -name 之类的实用工具可能会将其解释为模式,而另一个命令则可能将其视为普通字面文本。当这种区别至关重要时,请查阅该命令自身的相关文档。

按 Return 键前的快速检查

想清楚程序应该接收什么内容。如果它需要一个通配符模式,请将该模式放在单引号中,并确认命令的选项已设置为按预期解释模式。如果程序应该接收实际的文件名,请在适当的情况下保留 shell 展开,或者直接提供文件名。关键在于:当你希望程序接收通配符字符本身时,在 shell 层面做好引用保护。

参考来源