从Grep RegEx中捕获组

我在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纯粹的角度攻击这个。

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

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

560938 次浏览

我相信在grep是不可能的

对话:

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

不过,我想尝试一下额外的奖励:

echo "$name.jpg"

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

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

那么name的值将是abc

查看Apple 开发人员文档,向前搜索“参数扩展”。

这在纯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等是一种更Unix的做事方式:-)

如果你正在使用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中,等等。索引0是完全匹配。

你应该意识到,如果没有锚,这个正则表达式(以及使用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操作符(变长向后查找)使前面的模式匹配,但不包括结果中的匹配。等量齐观的固定长度是(?<=) -模式将包含在右括号之前。如果量词可以匹配不同长度的字符串(例如+*{2,4}),则必须使用\K

(?=)操作符匹配固定或可变长度模式,称为“超前查找”。它也不包括结果中匹配的字符串。

为了使匹配不区分大小写,使用(?i)操作符。它影响着后面的图案,所以它的位置很重要。

正则表达式可能需要根据文件名中是否有其他字符进行调整。您将注意到,在本例中,我展示了一个在捕获子字符串的同时连接字符串的示例。

如果您有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

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

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

使用just do

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

我意识到这个问题的答案已经被接受了,但从“严格*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参数来返回多个捕获组,按照它们在一行中出现的顺序。

我更喜欢一行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()
'
str="1w 2d 1h"
regex="([0-9])w ([0-9])d ([0-9])h"
if [[ $str =~ $regex ]]
then
week="${BASH_REMATCH[1]}"
day="${BASH_REMATCH[2]}"
hour="${BASH_REMATCH[3]}"
echo $week --- $day ---- $hour
fi
< p >输出: 1—2 ---- 1

下面的例子展示了如何使用正则表达式捕获组从文件名中提取3个字符序列:

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

输出:

f:     123_abc_123.jpg
name:  abc
f:     123_xyz_432.jpg
name:  xyz

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