如何在bash中转义通配符/星号字符?

169
例如:
me$ FOO="BAR * BAR"
me$ echo $FOO
BAR file1 file2 file3 file4 BAR

使用转义字符\

me$ FOO="BAR \* BAR"
me$ echo $FOO
BAR \* BAR

我显然在做一些愚蠢的事情。

如何得到输出 BAR * BAR

7个回答

176

在设置$FOO时仅使用引号是不够的。您还需要引用变量引用:

me$ FOO="BAR * BAR"
me$ echo "$FOO"
BAR * BAR

12
这很神秘,为什么会这样?发生了什么事情? - tofutim
1
由于变量扩展,因此... - Daniel
1
这不仅仅是$FOO变量扩展,还包括特殊字符*扩展(请参见https://tldp.org/LDP/abs/html/special-chars.html),或者阅读下面的https://dev59.com/KXVD5IYBdhLWcg3wE3No#104023以充分理解细微差别。 - Chris Beck
我花了几秒钟才弄清楚me$是什么意思。 - Ömer An
我花了几秒钟才弄清楚me$是什么意思。 - undefined

126

简短回答

像其他人说的那样-为了防止奇怪的行为,您应该始终引用变量。因此,请使用echo "$foo"而不是echo $foo

详细回答

我认为这个例子需要进一步解释,因为它比表面上看起来要复杂得多。

我能理解您的困惑,因为在运行第一个示例之后,您可能会想到shell显然正在执行以下操作:

  1. 参数扩展
  2. 文件名扩展

所以从您的第一个示例中:

me$ FOO="BAR * BAR"
me$ echo $FOO

参数扩展后等同于:

me$ echo BAR * BAR

文件名扩展后等同于:

me$ echo BAR file1 file2 file3 file4 BAR

如果您在命令行中键入echo BAR * BAR,您会发现它们是等效的。

所以您可能会想:“如果我转义*,就可以防止文件名扩展。”

因此,从您的第二个示例开始:

me$ FOO="BAR \* BAR"
me$ echo $FOO

参数扩展后应该等同于:

me$ echo BAR \* BAR

文件名扩展后应该等同于:

me$ echo BAR \* BAR

如果您直接在命令行中尝试输入“echo BAR \ * BAR”,它确实会打印“BAR * BAR”,因为转义字符阻止了文件名扩展。
那么为什么使用$foo不起作用呢?
这是因为有第三个扩展-引号删除。从bash手册中可以看到,引号删除是:
在前面的扩展之后,所有未引用的出现的字符‘\’、‘'’和‘"’,如果没有来自上述扩展之一,则被删除。
所以当您直接在命令行中输入命令时,转义字符不是先前扩展的结果,因此BASH在发送给echo命令之前将其删除,但在第2个示例中,“*”是先前参数扩展的结果,因此它不会被删除。因此,echo接收“\ *”,并打印它。
请注意第一个示例和第二个示例之间的区别-“*”不包括在引号删除要删除的字符中。
我希望这很明显。最后结论相同-只需使用引号。我只是想解释为什么转义(逻辑上应该有效,只有参数和文件名扩展在起作用)没有起作用。
有关BASH扩展的完整说明,请参阅:

http://www.gnu.org/software/bash/manual/bashref.html#Shell-Expansions


1
很好的答案!现在我不觉得自己问了一个愚蠢的问题了。 :-) - andyuk
1
有一些实用工具可以转义特殊字符吗? - jmj
在将变量用作字符串时,您应始终引用它们以防止出现奇怪的行为。 - Angelo
请参见https://dev59.com/NWkw5IYBdhLWcg3wMHum。 - tripleee
虽然上面@finnw的回答直接回答了问题,但这个回答更好地解释了其中的原因,这有助于更好地推断如何在我们自己的场景中使用。这是我们需要看到更多的答案类型。 - Nicolas Coombs

66
我会添加一些内容到这个旧帖子中。
通常情况下,您会使用以下方法:
$ echo "$FOO"

然而,即使使用这种语法,我也遇到了问题。考虑以下脚本。
#!/bin/bash
curl_opts="-s --noproxy * -O"
curl $curl_opts "$1"
*需要直接传递给curl,但是同样会出现问题。上面的例子无法正常工作(它将被扩展为当前目录中的文件名),\*也不行。你也不能引用$curl_opts,因为它会被识别为curl的一个无效选项。
curl: option -s --noproxy * -O: is unknown
curl: try 'curl --help' or 'curl --manual' for more information

因此,我建议使用变量$GLOBIGNORE来完全防止文件名扩展(如果应用于全局模式),或使用内置标志set -f
#!/bin/bash
GLOBIGNORE="*"
curl_opts="-s --noproxy * -O"
curl $curl_opts "$1"  ## no filename expansion

针对您提供的原始示例进行应用:

me$ FOO="BAR * BAR"

me$ echo $FOO
BAR file1 file2 file3 file4 BAR

me$ set -f
me$ echo $FOO
BAR * BAR

me$ set +f
me$ GLOBIGNORE=*
me$ echo $FOO
BAR * BAR

4
非常清晰的解释,谢谢!我的用例是SELECT * FROM etc.,这是唯一可行的方法。 - knutole
1
感谢您提供 -f 解决方案! - hachre
2
GLOBIGNORE="*" 正是我所需要的,谢谢! - BrettRobi

6
FOO='BAR * BAR'
echo "$FOO"

这个可以工作,但是将第一行的单引号改成双引号并不是必要的。 - finnw
1
不是的,但是当你要包含特殊的shell字符并且不想进行任何替换时,使用单引号的习惯更可取。 - tzot

4
如果您不想被bash的奇怪扩展困扰,可以这样做:
me$ FOO="BAR \x2A BAR"   # 2A is hex code for *
me$ echo -e $FOO
BAR * BAR
me$ 

使用echo命令的-e选项,可以让生活更加轻松。以下是来自man页的相关引用:

SYNOPSIS
   echo [SHORT-OPTION]... [STRING]...
   echo LONG-OPTION

DESCRIPTION
   Echo the STRING(s) to standard output.

   -n     do not output the trailing newline

   -e     enable interpretation of backslash escapes

   -E     disable interpretation of backslash escapes (default)

   --help display this help and exit

   --version
          output version information and exit

   If -e is in effect, the following sequences are recognized:

   \\     backslash

   ...

   \0NNN  byte with octal value NNN (1 to 3 digits)

   \xHH   byte with hexadecimal value HH (1 to 2 digits)

对于十六进制代码,您可以查看man ascii页面(第一行是八进制,第二行是十进制,第三行是十六进制):

   051   41    29    )                           151   105   69    i
   052   42    2A    *                           152   106   6A    j
   053   43    2B    +                           153   107   6B    k

3
echo "$FOO"

3

建议在命令行中习惯使用printf而非echo。虽然在本例中没有很大的优势,但在输出比较复杂时会更有用。

FOO="BAR * BAR"
printf %s "$FOO"

为什么printf是一个单独的进程(至少不是bash内置的),而你展示的printf使用与echo相比没有任何好处。 - ddaa
2
printf是内置的bash命令,我在我的答案中说过:“在这个例子中它并没有太多好处,但在更复杂的输出中可能会更有用。” - David Webb

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