grep可以说是Linux系统当中的一个文本检索工具。在Linux系统当中,其中有两个核心的理念:一切皆文件、配置文件保存为纯文本的格式。既然是纯文本的格式,可以想象,有一个好的文本检索工具是多么必要的。grep就是其中之一,他可以去匹配某种模式(PATTERN)来找出相应的行 (line),并把匹配的行打印出来,并不影响原文件。那么说到这里,就不得会问,什么是模式?模式又是怎么来实现的呢?实际上正则表达式 (regular expression)就是一种模式,在Linux当中,基本上都是通过匹配正则表达式的模式来实现的,所以在说grep使用时就不得不说正则表达式,下面会把grep与正则表达式一块说下它的用法。

  一个完整的正则表达式是由两部分组成的,元字符(metacharacters)和文字(literal),或者说是普通的文本字符。可以把正则表达式理解为一个语言,普通的文本就对应于语言当中的普通文字,而元字符就特指语法。根据语言的规则,按照语法把单词组合起来,就表达了一个完整的思想。正则表达式也是这样,我们通过一些简单的文本和元字符,去表达一种意义,然后告诉计算机我们想要的是什么?比如说我要查询成千上万行中,帮我把有任意个s重复的文本显示出来该怎么办呢?可以简单的用正则表达式写为: s* 这里的s是文本,指定要匹配文本s,而*是元字符,表示把前面的字符匹配任意次。是不是很简单,简简单单的两个字符就完成了很长一句话要表达的东西。当然元字符不仅仅能表示重复的次数,还有位置锚定,字符锚定等,我们下面一点点的细说。

1. grep的基本使用格式   

       grep [options] 'PATTERN' FILE

                   选项          模式          文件

    选项部分可以省略,在写模式时可以用单引,双引或者不加都可以,单引与双引之间的区别就是强弱引用的区别,双引号为弱引用。

2. 字符匹配

    例1. 列出在/etc/passwd中包含root的行,很简单吧,直接表示为:

grep --color "root" /etc/passwd

  这里简单解释下这个命令,grep是命令,--color是选项部分,是指把匹配上的用颜色显示出来,便于初学者理解正则表示式,双引号里面的是匹配的模式,也就是正则表达式部分,最后/etc/passwd是参数,这个命令执行的对象。

执行结果如下显示:

wKioL1P4VheQ-SnrAABYq1rW540026.jpg

  从红色显示的结果可以看出来,都匹配上了。很简单

   这里强调一下:在匹配的时候一定要有一个以字符为思路的匹配而不是整个单词,上面应该理解为先匹配r如果匹配上了,然后是紧着匹配o,依次类推,最后匹配上 t,这样理解的结果跟整个匹配root是一样的,以字符为理解的好处在于新手可以很方便的去看懂一个不懂的正则表达式。       

   例2. 要求找出在/etc/passwd中以l开头,b结尾,中间跟任意字符的那一行怎么匹配呢?那么任意字符该怎么表示呢?元字符点(.)就表示匹配一个任意字符,那么可以写为

grep --color "l.b" /etc/passwd

执行结果如下:

wKioL1P4VnzQ8H0EAACXLC68esU142.jpg

从上面结果可以看出来匹配到了lib和lab,这里的lab是手动创建的一个用户,如果你的etc/passwd文件中没有创建些用户的话,可以通过useradd lab来创建一个lab用户,否则不会匹配到。

  例3. 要求在/etc/passwd当中匹配出所有以r开关,以t结尾,中间有两个任意字符的行,我们知道点就表示一个任意字符,那写两个不就可以表示两个任意字符了吗?这样是不是就可以写为:

grep --color "r..t" /etc/passwd

执行结果如下:

wKioL1P4WACDgdrmAABwG4Ysotw607.jpg

    是不是匹配了出来了呢?确实是的,但是这可以看到一个特殊的结果,看最后一行,r/ft也匹配上了,很奇怪的一个结果,假设如果我们不想匹配这种特殊字符怎么办?有没有一种方法只是匹配字母呢?确实是有的。另外一种特殊的匹配叫做范围匹配,用[]括起来,比如说[a-z]就表示,字母a到字母Z中的任意一个字符,我们再重新去匹配一下,用[]的方式,结果如下

wKioL1P4WX6Q5l7UAABcg0v6toc249.jpg

看到区别了吧,这次我们并没有匹配上r/ft这个字符。

     在正则表达式中,除了用[a-z]这种表示方法外,同样内部也定义了一些表示方式来达到同样的效果,比如说用[[:lower:]]来表示所有的小写字母。那么刚刚上面的写法是不是以可以写为:

grep --color "r[[:lower]][[:lower:]]t" /etc/passwd

    常用的有以下的一些相应的表示

  • [0-9], [[:digit:]], \d: 表示所有数字

  • [a-z], [[:lower:]]:表示所有的小写字母

  • [A-Z], [[:upper:]]:表示所有的大写字母

  • [[:alpha:]]:表示所有的大小写字母

  • [[:alnum:]]:表示大小写字母及数字

  • [[:space:]]:表示空格

  • [[:punct:]]:表示特殊字符

        最后在字符匹配中说一下取反,^(拖字符),比如说要取出来不包含数字的行,可以写为

grep --color "[^0-9]" /etc/passwd

    小结:在grep中经常用到的字符匹配

.:表示匹配任意字符

[]: 指定范围内的任意单个字符

[^]: 指定范围外的任意单个字符

      3.  次数匹配

      再回过头看下例3,要求匹配中间有两个任意字符的要求,当时我们是以两个点来表示的,当然这样写是没有任何错误的,那有没有更简单的方法来表示呢?再想下,这仅仅是两个字符,如果有时候要求任意次的时候该怎么匹配呢?写任意个点?不大现实吧,如果我们可以指定重复的次数是不是就能解决这个问题了。在grep中确实指定了重复的次数,比如说*就表示把*前字符重复任意次数,任意次数当然也包含了2次,所以我们可以把例3中的结果改下成如下,看下效果。

grep  --color "r.*t" /etc/passwd

执行结果如下:

image_thumb

很有趣的结果,看到不仅仅是匹配上root这个字符,还有更长的东西,这里就不得不说一个概念,贪婪模式,默认grep就是工作在这种模式下,他会尽量长的去匹配。看第一行中把两个root之间的所有字符都匹配上了,因为*表示任意次数的任意字符,所以是满足情况的。这里重点关注下用绿色圈出的那一行,"rt”也匹配上了,为什么?那么0算不算任意次呢?当然算,所以匹配上了"rt"。继续看,这样写虽然可以匹配上了两次,但是范围也太广,给出的结果已经完全超出期望,当然这不是我们希望看到的。那能不能指定匹配的次数呢?当然可以,使用\{m,n\}来指定匹配特写的次数,这里表示的是一个范围,m表示最小m次,n表示最大n次。如果只有一个数字,表示精确匹配数字指定的次数。

既然题目要求匹配两次,那就可以这么写:

grep --color "r.\{2\}t" /etc/passwd

执行结果如下:

image_thumb1

比较下两次的执行结果,是一样的。

  小结:匹配次数常用的表达方式:

  • *:表示任意次

  • \?:  表示0次或者1次

  • \{m\}: 精确匹配m次

  • \{m,n\}: 最少m次,最大n次

  • \{m,\}: 至少m次

  • \{0,n\}: 到多n次

4.  锚定

还是说例3, 从上面可以看出来执行结果当中有r/ft这个字段,有时候我们并不想用这种字段,因为我们要匹配的是一个单词(word), 这种中间有特殊字符的结果,并不是我们想看到的。这个时候就需要告诉grep我们要求的是一个完整的word,那怎么告诉它呢?假设如果我们可以指定词首,指定词尾是不是就可以解决了呢?或者我有一个选项直接告诉它,我就是要匹配word不就可以了吗?

这两种方式都是可以的,在grep中可以指定词首,指定词尾,也就是锚定,"\<" 或者"\b”表示锚定词首,"\>"或者"\b”表示锚定词尾,那么我们之前的写法又可以改为:

grep --color "\<r.\{2\}t\>" /etc/passwd

执行结果如下:

image_thumb2

那么r/ft这个字符就不会匹配上了。其实还有一个grep也提供了一个选项,"-w"来指定去匹配word,我们把行尾,行尾的锚定符去掉,然后加上"-w"选项,看下结果

image_thumb3

对比下两种方式执行的结果,是一样的。

既然词首词尾可以锚定,行首行尾当然也可以锚定,我们应该知道/etc/passwd是用来保存用户信息的,那么现在要求找出root用户相关信息的那一行,该怎么匹配呢?直接写grep  --color “root” /etc/passwd能匹配出来吗?先尝试下,

image_thumb4

除了匹配到root之外,我们也看到匹配上了operator这个用户,那么这个时候如果我们可以指定以root开头的是不是就能解决这个问题了呢,这就需要用到行首锚定,"^"用来锚定行首,表达式可以改写为:

grep --color "^root" /etc/passwd

结果如下:

image_thumb5

完美解决了问题。

细心的读者看到这可能会有个小小的疑问,我们之间提到过^可以表示取反,这里^又表示行首锚定,这样不是有冲突吗?其实是没有的,在取反的时候^是在[]内,如果没有[]的话就表示行尾锚定,试着理解下下面这个表达式的意思,

grep --color "^[^r]" /etc/passwd

这里有两个^,第一个^表示以[]内字符为行首,再看[]内,^表示以非r开关的,所以整个的意思就是以非字符r为行首的所有行。从这个例子中我们可以看出来,在[]内与外的意思是不同的,之前讲到过点表示任意字符,那这个点在[]内与外有什么不同呢?答案在这小节结果时说明。

我们继续回到行锚定上来,既然行首可以锚定,那么行尾一样也可以了。"$"就表示行尾锚定,为了明确说明这个有无行尾锚定之间的区别我们可以先添加一个用户bash并指定其shell为nologin,执行如下命令:

useradd -s /bin/nologin bash

添加完用户以后,我们可以去查找下shell为bash的用户的行。/etc/passwd行尾的bash就表示用户的shell类型,如果不把行尾锚定会有什么结果呢?眼见为实,执行命令如下:

grep --color "bash" /etc/passwd

image_thumb6

从结果中可以看出来,行首的bash也匹配上了,但是它的shell是bash吗?不是,它的shell为nologin,所以这个就需要把行尾锚定加上,执行如下命:

grep --color “bash$” /etc/passwd

image_thumb7

比较下两次命令执行的区别,最后一行用户bash并没有匹配上。

我们说过了行首锚定,行尾锚定,那么不妨想下下面这三个表达式都表示什么意思

  • "^$"

  • "^root”

  • "^"

答案:

上文中提到过点在[]内外之间的区别,点如果是在[]外表示匹配任意字符,这个在字符匹配中已经说过,当然也说过[]内表示匹配指定的字符范围,既然是指定字符的范围,那么这个点在[]就仅仅表示字符点。说白了其实在[]内表示是一个字符,而在[]外是元字符,元字符的意思表示任意字符。

特殊的表达式含意

  • "^$" : 行首与行尾都锚定,中间什么都没有表示空白行

  • "^root” : 表示仅仅有root的那一行

  • "^":所有的行都会有行首,只匹配行首的话,表示所有行

5. 分组

    前面说过匹配次数,在匹配次数的时候说到是匹配前面的字符,如果要求匹配以一串字符为单位的重复该怎么办呢?这个时候就需要用到分组了,把几个字符分成一组,后面跟表示重复次数的元字符就可以解决这个问题,分组可以用"\(\)"括起来表示一个组。举个例子来说,我们先在/etc/passwd当中添加两个用户,labb和lablab,然后去匹配用户lablab。

    添加用户执行如下命令:

useradd labb 
useradd lablab

然后我们来比较下加不加分组之间的结果区别:

image_thumb8

从结果中可以看出来,如果不加分组的话,就是b重复两次,加了以后就是lab重复两次。

小结:

  • \(\): 对字符串分组

6. 引用

    说完分组,就可以去说另外一个比较有趣的例子,要求是以a开头以b结尾,中间跟任意一个数字的单词为开头,并且以同样的单词结尾的那一行?这个例子什么意思呢?举个例子来说,如果一行是以a6b开头,那么这一行就必须以a6b结尾,我想题目应该是很清楚了,我们先来分析一下。以a开头,以b结尾并且中间跟任意一个字符,可以写为"a[0-9]b“,以此单词开头那么又需要一个行首以及词尾锚定,因为行首已经锚定了,所以这个时候词首锚定也就不需要了,这样就成为了"^a[0-9]b\>”. 行首的问题解决了,中间跟任意字符可以表示为".*", 到这问题已经解决了一大半,那么行尾怎么办?"\<a[0-9]b$”这样写可以吗?验证下,我们先建立一个测试文档,执行如下命令:

cat > test.txt << EOF 
a6b is a6b a7b is a6b
EOF

通过上面的分析, 那么表达式可以写为:

grep --color "^a[0-9]b\>.*\<a[0-9]b$" test.txt

如果这个表达式可以完成的话,第二行应该匹配不到,对吧?看下结果

image_thumb9

从结果可以看出来,第二行也匹配到了,因为a6b与a7b都满足a[0-9]b的表达式。要解决这个问题,需要用到grep中的引用,引用可以引用前面括号中匹配的结果,而不是表达式。表示方式为\n. n是数字,按照从左向右数,1就表示引用第一个括号之内的内容。那么根据引用,就很容易解决这个问题了。表达式改写为:

grep --color "^\(a[0-9]b\)\>.*\1$" test.txt

执行结果如下:

image_thumb10

7. grep的常用选项及总结

在前文当中应该说过两个选项,一个是--color还有一个-w选项,这里重复下这两个选项的意思,然后再说下其它一些常用的选项。

  • -v: 取反,显示不能被模式所匹配到的行;

  • -o: 仅显示被模式匹配到的字串,而非整行;

  • -i: 不区分字符大小写, ignore-case

  • -n: 显示原文档的行数

  • -w: 以word匹配,相当于词锚定

  • -E: 支持扩展的正则表达式,相当于egrep

  • -A #:#表示数字,同时显示匹配到行的下#行

  • -B #:同时显示匹配到的上#行

  • -C #:同时显示匹配到上下#行

  • 字符匹配:

    • .: 表示匹配任意字符

    • []: 表示匹配指定范围内的字符

  • 次数匹配

    • *:表示任意次数

    • \?:0次或1次

    • \{m,n\}: 最少m次,最大n次

    • \{m\}: 匹配m次

  • 位置锚定

    • ^: 锚定行首

    • $:锚定行尾

    • \<或者\b:锚定词首

    • \>或者\b:锚定词尾

  • 分组与引用

    • \(\): 分组

    • \n: 引用第n个

8. 扩展的正则表达式:egrep

扩展的正则表达式使用方法基于类似于grep,同时grep -E也是支持扩展的正则表达式,常用元字符如下:


    • ():   分组

    • |:表示或的意思

    • \n: 引用第n个

    • ^:    锚定行首

    • $:锚定行尾

    • \<或者\b:锚定词首

    • \>或者\b:锚定词尾

    • *:表示任意次数

    • ?:0次或1次

    • +:表示1次或多次

    • {m,n}: 最少m次,最大n次

    • {m}: 匹配m次

    • .: 表示匹配任意字符

    • []: 表示匹配指定范围内的字符

    • 字符匹配:

    • 次数匹配

    • 位置锚定

    • 分组与引用

9. 快速grep: fgrep

不匹配模式,也就是不做元字符的计算,直接快速匹配字符,相当于grep –F

 

练习题(鼠标划取题目正文空白部分可以看到参考答案):

1、显示/proc/meminfo文件中以大小写s开头的行;
# grep "^[sS]" /proc/meminfo      
# grep -i "^s" /proc/meminfo      
2、取出默认shell为非bash的用户;
# grep -v "bash$" /etc/passwd | cut -d: -f1      
3、取出默认shell为bash的且其ID号最大的用户;
# grep "bash$" /etc/passwd | sort -n -t: -k3 | tail -1 | cut -d: -f1      
4、显示/etc/rc.d/rc.sysinit文件中,以#开头,后面跟至少一个空白字符,而后又有至少一个非空白字符的行;
# grep "^#[[:space:]]\{1,\}[^[:space:]]\{1,\}" /etc/rc.d/rc.sysinit      
5、显示/boot/grub/grub.conf中以至少一个空白字符开头的行;
# grep "^[[:space:]]\{1,\}[^[:space:]]\{1,\}" /boot/grub/grub.conf      
6、找出/etc/passwd文件中一位数或两位数;
# grep --color=auto "\<[0-9]\{1,2\}\>" /etc/passwd