我正在尝试构建一个命令,将一个 grep 命令的结果通过管道传输到 bash 函数中的另一个 grep 命令。最终,我希望执行的命令如下所示:
grep -I -r FooBar /code/internal/dev/ /code/public/dev/ | grep .c:\|.h:
我正在编写的函数将命令的第一部分存储在字符串中,然后附加第二部分:
grep_cmd="grep -I -r $pattern $@"
if (( ${#file_types[@]} > 0 )); then
file_types="${file_types[@]}"
file_types=.${file_types// /':\|.'}:
grep_cmd="$grep_cmd | grep $file_types"
fi
echo "$grep_cmd"
${grep_cmd}
这会在第一部分的输出之后引发错误:
grep: |: No such file or directory
grep: grep: No such file or directory
grep: .c:\|.h:: No such file or directory
将最后一行从 更改${grep_cmd}
为 仅"$grep_cmd"
显示第一部分的任何输出并引发不同的错误:
bash: grep -I -r FooBar /code/internal/dev/ /code/public/dev/ | grep .c:\|.h:: No such file or directory
每这个答案,我尝试将最后一行更改为$(grep_cmd)
。这会引发另一个错误:
bash: grep_cmd: command not found
这个答案建议使用eval $grep_cmd
.这会抑制错误,但也会抑制输出。
这个建议使用eval ${grep_cmd}
.这具有相同的结果(抑制错误和输出)。我尝试在 bash 中启用调试(使用set -x
),这给了我这个:
+ eval grep -I -r FooBar /code/internal/dev/ /code/public/dev/ '|' grep '.c:\|.h:'
++ grep -I -r FooBar /code/internal/dev/ /code/public/dev/
++ grep '.c:|.h:'
看起来管道正在被转义,因此 shell 将该命令解释为两个命令。如何正确转义管道字符,以便将其解释为一个命令?
答案1
正如评论中提到的,您遇到的很多困难是因为您尝试将命令存储在变量中,然后稍后运行该命令。
如果您立即运行该命令而不是尝试保存它,您的运气会好得多。
例如,这应该可以完成您想要完成的任务:
if (( ${#file_types[@]} > 0 )); then
regex="${file_types[*]}"
regex="\.\(${regex// /\|}\):"
grep -I -r "$pattern" "$@" | grep "$regex"
else
grep -I -r "$pattern" "$@"
fi
答案2
关于 shell 编程需要记住的一件事是,有两种类型的数据,教程中通常没有清楚地解释这一点:字符串和字符串列表。字符串列表与带有换行符或空格分隔符的字符串不同,它有自己的东西。
另一件要记住的事情是,大多数扩展仅在 shell 解析文件时应用。执行命令不涉及任何扩展。
变量的值确实会发生一些扩展:$foo
意味着“获取变量的值foo
,使用空格作为分隔符将其拆分为字符串列表,并将列表中的每个元素解释为通配符模式,然后进行扩展”。仅当变量在调用列表的上下文中使用时才会发生这种扩展。在需要字符串的上下文中,$foo
意味着“获取变量的值foo
”。双引号强加了字符串上下文,因此建议:始终在双引号中使用变量替换和命令替换:"$foo"
,"$(somecommand)"
². (与变量一样,未受保护的命令替换也会发生相同的扩展。)
解析和执行之间的区别的结果是您不能简单地将命令填充到字符串中并执行它。当您编写 时${grep_cmd}
,只会发生拆分和通配符,而不发生解析,因此像这样的字符|
没有特殊含义。
如果你绝对需要将 shell 命令填充到字符串中,你可以eval
这样做:
eval "$grep_cmd"
请注意双引号 - 变量的值包含 shell 命令,因此我们需要它的确切字符串值。然而,这种方法往往很复杂:您需要真正拥有 shell 源语法中的某些内容。例如,如果您需要文件名,则必须正确引用该文件名。因此,您不能只是将$pattern
and$@
放在那里,您需要构建一个字符串,该字符串在解析时会生成包含模式的单个单词以及包含参数的单词列表。
总结一下:不要将 shell 命令填充到变量中。反而,使用功能。如果您需要带有参数的简单命令,而不是更复杂的命令(例如管道),则可以使用数组(数组变量存储字符串列表)。
这是一种可能的方法。run_grep
您所显示的代码实际上并不需要该函数;我将其包含在这里是假设这是一个较大脚本的一小部分,并且还有更多的中间代码。如果这确实是整个脚本,只需在您知道将其通过管道传输到的位置运行 grep 即可。我还修复了构建过滤器的代码,这看起来不太正确(例如,.
在正则表达式中表示“任何字符”,但我认为您需要一个文字点)。
grep_cmd=(grep -I -r "$pattern" "$@")
if (( ${#file_types[@]} > 0 )); then
regexp='\.\('
for file_type in "${file_types[@]}"; do
regexp="$regexp$file_type\\|"
done
regexp="${regexp%?}):"
run_grep () {
"${grep_cmd[@]}" | grep "$file_types"
}
else
run_grep () {
"${grep_cmd[@]}"
}
fi
run_grep
1更一般地,使用 的值IFS
。
²仅适用于专家:始终在变量和命令替换周围使用双引号,除非您了解为什么不使用双引号会产生正确的效果。
仅限专家:如果您需要将 shell 命令填充到变量中,请务必小心引用。
请注意,您正在做的事情似乎过于复杂且不可靠 - 如果您有一个包含的文件怎么办foo.c: 42
? GNU grep 有一个--include
选项,可以在递归遍历中仅查找某些文件 - 只需使用它即可。
grep_cmd=(grep -I -r)
for file_type in "${file_types[@]}"; do
grep_cmd+=(--include "*.$file_type")
done
"${grep_cmd[@]}" "$pattern" "$@"
答案3
command="grep $regex1 filelist | grep $regex2"
echo $command | bash