从Grep RegEx捕获组

时间:2009-12-12 00:55:14

标签: bash shell grep

我在sh(Mac OSX 10.6)中有这个小脚本来查看一系列文件。谷歌此时已停止提供帮助:

files="*.jpg"
for f in $files
    do
        echo $f | grep -oEi '[0-9]+_([a-z]+)_[0-9a-z]*'
        name=$?
        echo $name
    done

到目前为止(显然,对于你的shell大师)$name只包含0,1或2,具体取决于grep是否发现文件名与提供的内容相匹配。 我想要的是捕获parens ([a-z]+)中的内容并将其存储到变量

我希望仅使用grep,如果可能的话。如果没有,请不要使用Python或Perl等sed或类似的东西 - 我是shell的新手,并希望从* nix纯粹的角度来攻击它。

另外,作为一个超酷的bonu ,我很好奇我如何在shell中连接字符串?我捕获的组是$ name中存储的字符串“somename”,我想在其末尾添加字符串“.jpg”,我可以cat $name '.jpg'吗?

如果你有时间的话,请解释一下发生了什么。

10 个答案:

答案 0 :(得分:439)

如果您使用的是Bash,则甚至不必使用grep

files="*.jpg"
regex="[0-9]+_([a-z]+)_[0-9a-z]*"
for f in $files    # unquoted in order to allow the glob to expand
do
    if [[ $f =~ $regex ]]
    then
        name="${BASH_REMATCH[1]}"
        echo "${name}.jpg"    # concatenate strings
        name="${name}.jpg"    # same thing stored in a variable
    else
        echo "$f doesn't match" >&2 # this could get noisy if there are a lot of non-matching files
    fi
done

最好将正则表达式放在一个变量中。如果按字面意思包含,某些模式将无效。

这使用=~,这是Bash的正则表达式匹配运算符。匹配结果将保存到名为$BASH_REMATCH的数组中。第一个捕获组存储在索引1中,第二个(如果有)存储在索引2中,等等。索引零是完全匹配。

你应该知道,如果没有锚点,这个正则表达式(以及使用grep的正则表达式)将匹配以下任何示例以及更多,这可能不是您想要的:

123_abc_d4e5
xyz123_abc_d4e5
123_abc_d4e5.xyz
xyz123_abc_d4e5.xyz

要消除第二个和第四个例子,请使用这样的正则表达式:

^[0-9]+_([a-z]+)_[0-9a-z]*

表示字符串必须以一个或多个数字开始。克拉代表字符串的开头。如果你在正则表达式的末尾添加一个美元符号,如下所示:

^[0-9]+_([a-z]+)_[0-9a-z]*$

然后第三个例子也将被删除,因为点不在正则表达式中的字符之间,而美元符号表示字符串的结尾。请注意,第四个示例也失败了。

如果您有GNU grep(大约2.5或更高版本,我认为,当添加\K运算符时):

name=$(echo "$f" | grep -Po '(?i)[0-9]+_\K[a-z]+(?=_[0-9a-z]*)').jpg

\K运算符(可变长度后视)导致前面的模式匹配,但不包括结果中的匹配。固定长度的等价物是(?<=) - 模式将包括在右括号之前。如果量词可以匹配不同长度的字符串(例如\K+*),则必须使用{2,4}

(?=)运算符匹配固定或可变长度模式,称为“预见”。它也不包括结果中匹配的字符串。

为了使匹配不区分大小写,使用(?i)运算符。它会影响其后的模式,因此它的位置非常重要。

可能需要根据文件名中是否还有其他字符来调整正则表达式。您将注意到,在这种情况下,我展示了在捕获子字符串的同时连接字符串的示例。

答案 1 :(得分:129)

对于纯grep,这实际上是不可能的,至少在一般情况下是这样。

但是如果您的模式合适,您可以在管道中多次使用grep来首先将您的线路缩减为已知格式,然后只提取您想要的位。 (尽管像cutsed这样的工具在这方面要好得多。)

假设为了论证你的模式更简单:[0-9]+_([a-z]+)_你可以像这样提取它:

echo $name | grep -Ei '[0-9]+_[a-z]+_' | grep -oEi '[a-z]+'

第一个grep将删除与您的整体模式不匹配的所有行,第二个grep(指定了--only-matching)将显示名称的alpha部分。这只能起作用,因为这种模式是合适的:“alpha部分”具体到足以拉出你想要的东西。

(旁白:我个人使用grep + cut来实现您的目标:echo $name | grep {pattern} | cut -d _ -f 2。这会让cut通过拆分将行解析为字段在分隔符_上,只返回字段2(字段编号从1开始)。)

Unix哲学是拥有做一件事,做得好的工具,并将它们结合起来实现非平凡的任务,所以我认为grep + sed等更多Unixy的做事方式: - )

答案 2 :(得分:84)

我意识到答案已经被接受了,但是从“严格的* nix纯粹主义角度”看来,这项工作的正确工具似乎是 pcregrep ,似乎还没有提到过。尝试更改行:

    echo $f | grep -oEi '[0-9]+_([a-z]+)_[0-9a-z]*'
    name=$?

以下内容:

    name=$(echo $f | pcregrep -o1 -Ei '[0-9]+_([a-z]+)_[0-9a-z]*')

仅获取捕获组1的内容。

pcregrep 工具使用了与 grep 一起使用的所有相同语法,但实现了您需要的功能。

参数 -o 的工作方式与grep版本相同,如果它是裸的,但它也接受pcregrep中的数字参数,表示哪个捕获你想要表演的小组。

使用此解决方案,脚本中只需要进行最少的更改。您只需将一个模块化实用程序替换为另一个,并调整参数。

有趣的注意事项:您可以使用多个-o参数按照它们在该行上显示的顺序返回多个捕获组。

答案 3 :(得分:24)

我不相信grep不可能

表示sed:

name=`echo $f | sed -E 's/([0-9]+_([a-z]+)_[0-9a-z]*)|.*/\2/'`

我会抓住奖金:

echo "$name.jpg"

答案 4 :(得分:15)

这是一个使用gawk的解决方案。这是我发现我需要经常使用的东西所以我为它创建了一个函数

function regex1 { gawk 'match($0,/'$1'/, ary) {print ary['${2:-'1'}']}'; }

只使用

$ echo 'hello world' | regex1 'hello\s(.*)'
world

答案 5 :(得分:3)

给你一个建议 - 你可以使用参数扩展从最后一个下划线开始删除名称的一部分,同样在开头:

f=001_abc_0za.jpg
work=${f%_*}
name=${work#*_}

然后name的值为abc

请参阅Apple developer docs,向前搜索“参数扩展”。

答案 6 :(得分:3)

forloop.parentloop.counter/counter0

输出: 1 --- 2 ---- 1

答案 7 :(得分:2)

如果你有bash,你可以使用扩展的globbing

shopt -s extglob
shopt -s nullglob
shopt -s nocaseglob
for file in +([0-9])_+([a-z])_+([a-z0-9]).jpg
do
   IFS="_"
   set -- $file
   echo "This is your captured output : $2"
done

ls +([0-9])_+([a-z])_+([a-z0-9]).jpg | while read file
do
   IFS="_"
   set -- $file
   echo "This is your captured output : $2"
done

答案 8 :(得分:2)

我更喜欢单行pythonperl命令,它们通常都包含在主要的Linux发行版中

echo $'
<a href="http://stackoverflow.com">
</a>
<a href="http://google.com">
</a>
' |  python -c $'
import re
import sys
for i in sys.stdin:
  g=re.match(r\'.*href="(.*)"\',i);
  if g is not None:
    print g.group(1)
'

并处理文件:

ls *.txt | python -c $'
import sys
import re
for i in sys.stdin:
  i=i.strip()
  f=open(i,"r")
  for j in f:
    g=re.match(r\'.*href="(.*)"\',j);
    if g is not None:
      print g.group(1)
  f.close()
'

答案 9 :(得分:1)

我在使用带有 perl 的捕获组的正则表达式方面取得了巨大成功,例如

for f in 123_abc_123.jpg 123_xyz_432.jpg
do
    echo $f
    echo $f | perl -ne 'if (/[0-9]+_([[a-z]+)_[0-9a-z]*/) { print $1 . "\n" }'
done

输出:

123_abc_123.jpg
abc
123_xyz_432.jpg
xyz

因此 perl 中的 if-regex 条件将同时过滤掉所有不匹配的行,对于那些匹配的行,它将应用您可以使用 { 访问的捕获组{1}}, $1, ... 分别,