Bash 将变量内的引号读取为文本,而不是引号?Bash 中有“隐式引用”吗?

Cri*_*gie 5 bash shell-script quoting

我有一个 bash 脚本可以定期清理邮件队列。出于某些原因,我们选择删除发送至 @mms.att.net 和其他 email2SMS 网关的所有在队列中超过 9 小时但仍未送达的电子邮件。

简单来说,该脚本执行以下操作:

domains=`cat /etc/mail/email2textdomains.txt`
egrep $domains /var/log/maillog | .... other tasks
Run Code Online (Sandbox Code Playgroud)

的内容/etc/mail/email2textdomains.txt正是

"mms.att.net|vtxt.com|vtext.com|vzwpix.com"
Run Code Online (Sandbox Code Playgroud)

因此,egrep 行应该是这样的,这正是我在命令行中输入的内容。

egrep "mms.att.net|vtxt.com|vtext.com|vzwpix.com" file | ...
Run Code Online (Sandbox Code Playgroud)

如果我像这样运行它,那么它是一个 5 个以上阶段的命令管道,每个命令从前一个标准输出读取标准输入。这显然不是我想做的搜索。

egrep  mms.att.net|vtxt.com|vtext.com|vzwpix.com  file | ...
Run Code Online (Sandbox Code Playgroud)

然而,在运行时,两个双引号的处理方式不同 - 它们成为字符串的一部分,所以我们本质上是在搜索

  • “mms.att.net
  • vtxt.com
  • vtext.com
  • vzwpix.com”

显然,我误解了引用的工作原理 - 解决方案是更改包含的行以删除双引号,导致一行不应该工作,但可以。

我尝试通过管道进行测试,od -a不显示任何非打印字符。

为什么它有效,使得内容/etc/mail/email2textdomains.txt正是

mms.att.net|vtxt.com|vtext.com|vzwpix.com
Run Code Online (Sandbox Code Playgroud)

什么时候应该像所写的那样是一个很长的失败管道?

ter*_*don 8

尝试调试此类事情时,一个很棒的工具是set -x. 使用它,我们可以准确地看到您的命令正在做什么:

$ set -x
$ domains=$(cat domains.txt)
++ cat domains.txt
+ domains='"mms.att.net|vtxt.com|vtext.com|vzwpix.com"'
Run Code Online (Sandbox Code Playgroud)

如您所见,$domains包括引号。因此,当您将其与 一起使用时grep,您会得到:

$ grep -E -- "$domains" file
+ grep --color -E -- '"mms.att.net|vtxt.com|vtext.com|vzwpix.com"' file
Run Code Online (Sandbox Code Playgroud)

您想要做的是在将数据传递给命令之前grep在 shell 级别使用引号,但由于引号是变量数据的一部分,因此它们会像任何其他字符一样被处理。最简单的解决方案是从文件中删除引号,然后只引用变量,这无论如何都是最佳实践:

domains=$(tr -d \" < domains.txt) &&
grep -E -- "$domains" file
Run Code Online (Sandbox Code Playgroud)

顺便说一句, usingvar=$(command)比 using 更受欢迎var=`command`,因为前者更清晰并且允许更多嵌套,并且egrep不推荐使用grep -E

另请注意,这.是一个匹配任何单个字符的正则表达式运算符,因此grep mms.att.net实际上会找到包含mms后跟任何单个字符、后跟att任何单个字符、后跟 的行net。例如,它也会匹配包含.hammstattinet.com

因此,要构建一个E与包含任何这些域的行相匹配的扩展正则表达式,您不仅需要删除 s ",还要转义域名中恰好也是正则表达式运算符的所有字符。对于有效域名,应限制为..

另请注意,对于空正则表达式,不同实现的行为有所不同grep,但其中许多会报告所有行,因此您可能需要对其进行特殊处理。

所以:

regex=$(
  sed 's/"//g; # remove all "s like with tr
       s/\./\\./g; # substitute .s with \.s
      ' domains.txt
) && 
  [ -n "$regex" ] && # check it's not empty 
  grep -E -- "$regex" file
Run Code Online (Sandbox Code Playgroud)

或者,您可以将|s 替换为换行符,并使用(以前的)-F选项来查找固定字符串:grepfgrepF

domains=$(<domains.txt tr -d '"' | tr '|' '\n') &&
  [ -n "$domains" ] &&
  grep -F -- "$domains" file
Run Code Online (Sandbox Code Playgroud)