Bash 正则表达式非贪婪匹配

问题描述 投票:0回答:4

我有一个正则表达式模式,应该在字符串中的多个位置匹配。我想将所有匹配组放入一个数组中,然后打印每个元素。

所以,我一直在尝试这个:

#!/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 正则表达式来实现这一点。但每个人似乎都建议将 grep 与 perl 正则表达式一起使用。

但肯定还有其他方法。我在想也许像

[^\\s]+
..但输出是:

New match: 
    Share1   Disk
New match: Share1

... 有什么想法吗?

regex bash regex-greedy
4个回答
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"

6
投票

正如已接受的答案已经指出的那样,这里的解决方案并不是真正使用非贪婪的正则表达式,因为 Bash 不支持符号

.*?
(它是在 Perl 5 中引入的,并且可以在正则表达式实现派生的语言中使用)从那开始,但 Bash 不是其中之一)。但对于在 Google 中找到此问题的访问者来说,标题中实际问题的答案是 sometimes,只需使用比
.*
更有限的正则表达式来实现您正在寻找的非贪婪匹配。

例如,

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[@]}"

2
投票

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

#!/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}

0
投票

我正在寻找一种通用的解决方案来解决匹配/替换字符串中间的第一个和最长的实例的问题,而不依赖于否定。 否定会增加不必要的复杂性,并且由于 ERE 的限制而并不总是有效。 我希望模式

y
能够在
(x)(y)(z)
中匹配,但是
x
却无法匹配。 我发现除了正则表达式匹配之外,还可以通过使用子字符串来实现。

最简单的情况是模式的

x
部分不需要特别匹配任何内容,例如
(.*?)(baz)(.*)
。 删除表达式的
x
部分,然后从目标字符串构建隐式匹配:

text='Foo bar, baz qux. Wiz huz baz dux.'
re='(baz)(.*)'
if [[ "$text" =~ $re ]]; then
    before_end=$(( ${#text} - ${#BASH_REMATCH[0]} ))
    # obviously no need to put $text back into the result
    # there only to demo emulation of $BASH_REMATCH for (.*?)(baz)(.*)
    ungreedy_rematch=( "$text" "${text:0:before_end}" "${BASH_REMATCH[@]:1}" )
    # inspect
    (IFS='|'; echo "$IFS${ungreedy_rematch[*]}$IFS")
    # produces: |Foo bar, baz qux. Wiz huz baz dux.|Foo bar, |baz| qux. Wiz huz baz dux.|
    # replacement
    text="${ungreedy_rematch[1]}boz${ungreedy_rematch[3]}"
    echo "|$text|"
    # produces: |Foo bar, boz qux. Wiz huz baz dux.|
fi

如果

x
部分确实需要匹配某些内容,就像提问者的情况一样,请重复这个技巧:

f=$'\n\tShare1   Disk\n\tShare2  Disk\n\tPrnt1  Printer'
the_rest="$f"
regex_before=$'\n\t(.*)'
regex_after=$'\\s+Disk(\n.*|$)' # desired match is implied before this one
while [[ "$the_rest" =~ $regex_before ]]; do
    # ignore this implied match
    the_rest="${BASH_REMATCH[1]}"
    if [[ "$the_rest" =~ $regex_after ]]; then
        # get this implied match
        before_end=$(( ${#the_rest} - ${#BASH_REMATCH[0]} ))
        match="${the_rest:0:before_end}"
        the_rest="${BASH_REMATCH[1]}"
        echo "New match: $match"
    else
        break
    fi
done

在每种情况下,模式必须消耗整个字符串的其余部分才能计算隐含匹配的偏移量。 如果您需要更多的可重用性,请将逻辑包装在 shell 函数中。

© www.soinside.com 2019 - 2024. All rights reserved.