【问题标题】:How can I filter STDIN by globbing in bash? [duplicate]如何通过在 bash 中通配来过滤 STDIN? [复制]
【发布时间】:2017-04-11 11:35:56
【问题描述】:

我的 bash 脚本通过管道 (stdin) 获取完整路径,并通过命令行参数获取排除模式。目前这处理正则表达式模式,但我想重写为仅处理 glob 模式。

如何使用 glob 模式(如 fnmatch)过滤 stdin 因为据我所知,我不能使用 grep 进行通配,而且我不会想手动将glob 模式转换为正则表达式。 - 所以我想找到一个快速而不是 hacky 的解决方案。

示例:

echo -e 'apple tree\nbanana tree\norange tree' | ./filter_script.sh '*ban' '*ge*'

预期输出:

# banana tree
# orange tree

完整的示例代码在GitHub Gists

编辑 1:

在现实生活中,这个脚本会得到上千条路径,所以我认为,原生的 bash 实现并不好。

有人知道“grep like”glob 过滤器吗?

【问题讨论】:

  • 给定输入的预期输出是什么?链接中提供的示例没有帮助
  • 而不是要点,为什么不在这里添加预期输出的问题?这要好得多,因为我们可以更快地解决您的问题。
  • ... 就像,看在上帝的份上,*n* 是指apple tree? (如您预期的输出)?!
  • 您是否获得了示例(或)文件中的字符串输入?
  • @jm666,你是对的,对不起,我在 Gists 上修复了示例

标签: bash glob


【解决方案1】:

假设您希望使用原生bash 方式来支持glob 模式,您可以使用Extended Globs 来做到这一点,这不是 默认设置,但可以通过

shopt -s extglob

你有一个按以下方式定义的字符串

myStr=$'apple tree\nbanana tree\norange tree'

您可以按如下方式应用glob 模式。将字符串读入一个数组,用readarray 换行符分割(你需要有bash 4.0 或更大)

readarray -t y <<<"$myStr"

现在是循环,

for i in "${y[@]}"; do 
    [[ $i == @(*n*) ]] && echo "$i" ;
done

根据需要生成结果。

其中,@(list) 代表 Matches one of the given patterns

另一个示例glob 匹配apple 中的*le*(或)orange 中的ge 将使用

for i in "${y[@]}"; do [[ $i == @(*le*|*ge*) ]] && echo "$i" ; done
apple tree
orange tree

对于您有问题的原始输入,

for i in "${y[@]}"; do [[ $i == @(*ban*|*ge*) ]] && echo "$i" ; done
banana tree
orange tree

对于 OP 将输入参数转换为 glob 模式的要求,由 | 分隔,需要额外的一行来解析位置参数,具体取决于计数,

#!/bin/bash

shopt -s extglob

myStr=$'apple tree\nbanana tree\norange tree'
readarray -t y <<<"$myStr"

# if the argument count is more than 1, since the input arguments are 
# separated by ' ', replace them with `|` as required in the glob
# pattern

(($# > 1)) && args=$(printf "%s" "$*" | tr ' ' '|') || args="$*"

for i in "${y[@]}"; do
    [[ $i == @($args) ]] && echo "$i" ;
done

现在你可以运行脚本了

bash script.sh '*ge*' '*le*'
apple tree
orange tree

(或)只有一个论点,

bash script.sh '*ba*'
banana tree

【讨论】:

  • 谢谢。但是,如果您不将整个输入存储在 bash 中,并且一个接一个地处理行,那么您的解决方案可能会更快。 (例如,使用while read -r i 代替readarrayfor)。
  • 另一方面,如何将命令行参数($@ 数组)转换为$(...|...) 表达式?
  • 基于你的解决方案和我之前的第一条评论(w/read)我创建了一个比你丑陋的args hacking 更好的脚本:gist.github.com/andras-tim/909aa427343aede851bc6622cf91d628 - 但是grep 正则表达式快 400 倍比这个和你的。 - 所以我主要是在寻找一个“类似 grep”的 glob 过滤器?
  • @andras.tim:如果没记错的话,您要求使用原生bash 方法,确实bash 慢了几倍
  • 你说得对,我写得太开放了。我想要的解决方案是“glob grep”,但据我所知,grep 没有通配模式。
【解决方案2】:

我认为以下应该可以满足您的需要:

#!/bin/bash
while IFS= read -r line
do
  for pattern in "$@"
  do
    if
      [[ $line = $pattern ]]
    then
      echo "$line"
      break
    fi
  done
done

这会逐行读取标准输入(因此适用于“流式”应用程序或非常大的文件)。在 Bash 条件 ([[ ]]) 中,相等比较(以及 !=)执行 glob 类型匹配,除非右边的字符串被引用。

【讨论】:

  • 这看起来不错,但对于大量路径来说似乎太慢了。我认为本机 bash 实现会太慢。例如。我在我的系统上运行了一个查找,它创建了具有 120 万个路径的输出文件。带正则表达式的 grep 在 0.061 秒内完成,但是您的 globbing 脚本运行了 43 秒。
  • 如果您的 glob 看起来像 *string*,那么只需将 * 替换为 ^.*.*$ 就足够了,这为您提供了 grep 对此类的明显好处应用程序。
  • @andras.tim 请看看我发布的另一个答案,它使用grep,但将模式转换为正则表达式。
【解决方案3】:

grep 的速度中受益的一种方法是将模式转换为正则表达式。

试试这个:

#!/bin/bash
glob_to_regex()
{
  local regex=$1
  regex=${regex//\./\\.}
  regex=${regex/\\/\\\\}
  regex=${regex//\*/.*}
  regex=${regex//\?/.}
  printf %s "^$regex$"
}

regex_from_args()
{
local arg
local not_first=
for arg in "$@"
do
  [[ $not_first ]] && printf %s "|"
  not_first=1
  glob_to_regex "$arg"
done
}

egrep "$(regex_from_args "$@")"

【讨论】:

  • 这个解决方案有点老套......另一方面,当路径包含空格或特殊字符(例如[)时效果不佳 - 您错过了转义正则表达式特殊序列。我认为,如果您想手动转义正则表达式,这种方式永远不会好。
  • 您能解释一下为什么/如何使用带有空格或特殊字符的文件名失败吗?或者你的意思是包含这些的模式?目前尚不清楚您所说的“hacky”是什么意思。您的目标是使用 glob 执行过滤,并且没有工具(据我所知)可以直接快速地执行此操作,并且您不想使用正则表达式(得到更广泛的支持)。这里有很多约束要满足...
  • 为什么我认为这是 hacky:(1) $not_first hack 有点 hacky - 为什么不使用将表达式连接到变量中并与 ${...%|} 剥离? - (2) 我认为glob_to_regex 的大部分行可以替换为sed,例如sed 's|\\|\\\\|g;s|\.|\\.|g;s|\*|.*|g;s|\?|.|g' + 在你的脚本中使用egrep -x
  • 为什么我认为这不能正常工作:检查echo -e 'foo[b]ar' | ./filter_script.sh '*o[b]*'
  • 我的转换例程不处理这种模式匹配,但您并不清楚您希望支持哪种模式匹配语言。你可能想澄清一下。
猜你喜欢
  • 2019-05-14
  • 1970-01-01
  • 2015-11-18
  • 1970-01-01
  • 2021-06-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-09-09
相关资源
最近更新 更多