Bash正则表达式非贪婪匹配

12
我有一个正则表达式模式,应该在字符串的多个位置进行匹配。我想将所有匹配组合成一个数组,然后打印每个元素。
所以,我一直在尝试这样做:
#!/bin/bash

f=$'\n\tShare1   Disk\n\tShare2  Disk\n\tPrnt1  Printer'
regex=$'\n\t(.+?)\\s+Disk'
if [[ $f =~ $regex ]]
then
    for match in "${BASH_REMATCH[@]}"
    do
        echo "New match: $match"
    done
else
    echo "No matches"
fi

结果:

New match: 
    Share1   Disk
    Share2  Disk
New match: Share1   Disk
    Share2 

预期的结果应该是:

New match: Share1
New match: Share2

我认为它不起作用是因为我的.+?是贪婪匹配。所以我查找了如何使用bash正则表达式完成这个任务。但每个人似乎都建议使用具有perl正则表达式的grep。

但肯定还有其他方法。我想也许可以使用[^\\s]+之类的东西。但结果是:

New match: 
    Share1   Disk
New match: Share1

... 有什么想法吗?


一个想法是使用[^\\s]+?代替.+?。这将匹配字符,直到找到空格为止。 - Rahul
2
根据这个答案POSIX正则表达式(在=~运算符中使用)没有非贪婪量词。 - NarūnasK
@Forivin:你应该使用第一个捕获组。类似 $match[1](不太懂bash)。 - Rahul
@Rahul 嗯,是的,我成功地避免了它,但正如我所说,没有产生期望的结果。 - Forivin
2
你应该首先使用换行符拆分字符串,然后迭代每个块并使用你的正则表达式检查每个块,并使用${BASH_REMATCH[1]}获取值。 - Wiktor Stribiżew
显示剩余2条评论
3个回答

6

这里有一些问题。首先,BASH_REMATCH的第一个元素是与模式匹配的整个字符串,而不是捕获组,所以您需要使用${BASH_REMATCH[@]:1}来获取在捕获组中的内容。

然而,Bash正则表达式不支持多次在字符串中重复匹配,因此Bash可能不是执行此操作的正确工具。但是,由于每行都是单独的,您可以尝试利用这一点将其拆分并将模式应用于每行,例如:

f=$'\n\tShare1   Disk\n\tShare2  Disk\n\tPrnt1  Printer'
regex=$'\t(\S+?)\\s+Disk'
while IFS=$'\n' read -r line; do
    if [[ $line =~ $regex ]]
    then
        printf 'New match: %s\n' "${BASH_REMATCH[@]:1}"
    else
        echo "No matches"
    fi
done <<<"$f"

我简直被震惊了,原来Bash可以进行数组切片操作(是的,我去查了一下${arr[@]:1}到底是什么鬼)。 - André Chalella

6

正如已经被接受的回答所述,这里的解决方案并不是使用非贪婪正则表达式,因为Bash不支持符号.*?(它是在Perl 5中引入的,在其派生自此的语言中可用,但Bash不是其中之一)。但对于从谷歌搜索中找到这个问题的访问者,实际标题问题的答案有时是只需使用比.*更有限的正则表达式来实现您正在寻找的非贪婪匹配。

例如,

re='(Disk.*)'
if [[ $f =~ $re ]]; then
 ... # ${BASH_REMATCH[0]} contains everything after (the first occurrence of) Disk

这只是一个构建块;你需要使用额外的正则匹配或循环来进一步操作。下面列出了一个不使用正则表达式的变体,基本上可以实现相同的功能。

如果你不想匹配的东西是一个特定的字符,使用否定字符类是简单、优雅、方便且与 Ken Thompson 原始正则表达式库的黑暗开端兼容的。在 OP 的示例中,看起来你想跳过一个换行符和一个制表符,然后匹配任何不是字面空格的字符。

re=$'\n\t([^ ]+)'

但在这种情况下,更好的解决方法可能是在循环中实际使用参数扩展

f=$'\n\tShare1   Disk\n\tShare2  Disk\n\tPrnt1  Printer'
result=()
f=${f#$'\n\t'}      # trim any newline + tab prefix
while true; do
  case $f in
    *\ Disk*)
        d=${f%% *}           # capture up to just before first space
        result+=("$d")
        f=${f#*$'\n\t'}     # trim up to next newline + tab
        ;;
    *)
        break ;;
  esac
done
echo "${result[@]}"

请参阅 https://dev59.com/n2Ml5IYBdhLWcg3wTlcl,了解如何解决Bash(以及更一般的POSIX风格正则表达式)中缺少某些PCRE正则表达式功能的更广泛讨论。 - tripleee

1

我遇到了一个非常类似的问题,并以以下方式解决它。

#!/bin/bash

# Captures all %{...} patterns and stops greedy matching by not matching 
# the } inside using [^}] yet capturing it once outside. 
# It also matches all remaining characters.
 
regex="^[^}]*(%{[^}]+})(.*)"

URL="http://%{host}/%{path1}/%{path2}"

value=$URL
matches=()

while true 
do
  if [[ $value =~ $regex ]]
  then 
    matches+=( ${BASH_REMATCH[1]} )
    value=${BASH_REMATCH[2]};
    echo "Yes: ${BASH_REMATCH[1]}  ${BASH_REMATCH[2]}";
  else 
    break; 
  fi
done

echo ${matches[@]}

以上的输出将如下所示,最后一行是匹配项的数组:
$ . loop-match.sh
Yes: %{host}  /%{path1}/%{path2}
Yes: %{path1}  /%{path2}
Yes: %{path2}

%{host} %{path1} %{path2}

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