linux从入门到精通读薄(五)grep/find/sort

第五章  工具和实用程序

5-1 搜索和排序

5-1-1 grep

它是通用规则表达式分析程序(General Regular Expression Parser)的缩写。

grep命令可以在它的输入中搜索指定的字符串模式(Pattern)。输入中所有包含指定字符串模式的行组成grep命令的输出。

例如,要找出carey用户是否在系统中登录,只要在passwd文件中搜索这一用户名称的字符串:

$ grep carey /etc/passwd
carey:……

grep产生的输出表明,在系统中有carey用户。

grep命令用于规则表达式中的基本特殊字符集(B表示基本E表示扩充):

^                    B           在每行的开始进行匹配;
$                   B           在每行的末尾进行匹配;
\<                  B           在字的开始进行匹配;
\>                  B           在字的末尾进行匹配;
.                    B           对任何单个字符进行匹配;
[str]               B           对str中的任何单个字符进行匹配;
[^str]              B           对任何不在str中的单个字符进行匹配;
[ a-b]             B           对a到b之间的任何字符进行匹配;
\                    B           抑止后面的一个字符的特殊含义;
*                    B           对前一项(item)进行0次或多次重复匹配;
+                   E           对前一项进行1次或多次重复匹配;
?                   E           对前一项进行0次或1次重复匹配;
{j}                  E           对前一项进行j次重复匹配;
{j,}                 E           对前一项进行j次或更多次重复匹配;
{,k}                E           对前一项最多进行k次重复匹配;
{j,k}               E           对前一项进行j到k次重复匹配;
slt                 E            匹配s项或t项中的一项;
(exp)             E           将exp作为单项处理

不要忘记,在vi和grep中使用规则表达式的主要差异。在vi编辑程序中,输入的任何字符只被编辑程序本身看到和使用。而grep命令看到的任何参数必须先经过shell。由于在规则表达式中使用的一些特殊字符对shell同样具有特殊含义,要告诉shell:不管这些字符。这样,grep才有机会见到它们。用引用方法实现这一点。回到前面的例子,搜索passwd文件找登录名。现在,形成表示public不是有效登录名的grep命令行

将是一件容易的事:

$ grep '^public' /etc/passwd

前面的^字符强制grep命令只在每行的开关找public。整个搜索模式(pattern)用单引号括起来,使shell不理会它们。shell只将单引号去掉,将搜索模式送给grep命令。

grep命令的有用的开关如下:

-E        用扩充规则表达式进行模式匹配;
-i         不区分大小写;
-n        在每一输出行前显示文件内的行号;
-q        与其他命令一起使用时,抑止输出显示;
-s        抑止文件的出错信息;
-num   在每一匹配行前后各显示num行。

5-1-2 find命令

find命令的主要作用是对树形目录层次结构进行彻底检查。从给定的起点开始能过目录的所有分支一直检查到结点。可以在命令中指定一组操作,对find命令发现的每个文件和子目录进行操作。

find命令最常用来产生给定目录下的所有文件和子目录清单:

$ cd
$ find . -print
./backup
./backup/motd.bak
……

find的两个参数是点和-print,它们告诉find命令从当前目录开始搜索,并显示它发现的所有文件和子目录名称。

find命令输出的文件名清单写到标准输出设备,因而也可以通过管道供其他命令使用:

$ find . -print | grep passwd
./backup/passwd.bak
./text/passwd

find命令的一般格式:

find pathname -expressions

其中pathname可以是任何目录路径名清单,对清单中每个目录进行递归搜索来产生文件名输出。而expressions则是任何表达式的清单,用来定义条件和对每个被发现的文件进行的操作。如果没有指定pathname,默认值是当前目录。如果没有指定-expressions,默认值是-print。

find命令使用三种表达式:选项,条件和操作。每个表达式都送回一个值。这个值是真是假取决于使用的表达式和对它的评价方式。当find命令具有一个以上的表达式时,表达式之间用逻辑操作符来控制它们的执行。

给出表达式e1和e2:

e1 -a e2        仅当e1为真时,对e2求值;
e1 e2           同上;
e1 -o e2        仅当e1为假时,对e2求值;
e1 , e2         对两个表达式都求值,先e1,后e2。

复合表达式的值是最后实际求值的表达式的返回值。由两个以上表达式组成的复合表达式的求值按自左至右的顺序进行,除非括号()改变了求值顺序。如:

e1 -o e2 -a e3等价于(e1 -o e2) -a e3而不同于e1 -o (e2 -a e3)

还可以使用逻辑非操作符:

! e1

当e1为假时,结果为真;反之,当e1为真时,结果为假。


可以和find命令一起使用的一些表达式的类型和它的返回值:

-mount                选项表达式,用来防止搜索范围超出当前文件系统的边界。返回值常为真。
-group grp      条件表达式,检查当前文件是否具有与grp相同的GID或组名。一致为真,否则为假。
-name pattern   条件表达式,检查文件名是否和模式pattern相同。pattern可以用规则表达式给出。必要时使用引号。当文件名与pattern一致时值为真,否则为假。
-type t         条件表达式,检查当前文件的类型是否是t。对目录讲,t值可以是d。对普通文件讲,t值可以是f。对连接讲,t值可以是l等等。如果是,值为真,否则为假。
-user usr       条件表达式,检查当前的文件的所有者或UID是否是usr。如果两者一致,返回真值,否则为假。
-exec cmd;      操作表达式,用来执行cmd命令。如果要将当前的文件名传送给命令,应该加{}标记,分号用来表示cmd的结束,并和后面可能出现的表达式分开。如果成功地执行了cmd,返回真值,否则为假。
-print          操作表达式,将当前的文件名送到标准输出设备显示,返回值常为真。

注意:find命令不允许超越系统对文件和目录的权限设置。因此,如果让find命令从一个目录开始搜索,而这个目录下有不能访问的子目录,find命令每遇到一个这样的目录,将向标准输出设备传送一条错误信息。由于默认的标准出错信息输出设备是屏幕,如果使用了-print操作,可能实际输出许多出错信息,如:

$ find / -name passwd -pint
find:/var/spool/cronermission denied
find:/var/spool/atjobsermission denied
……
/usr/bin/passwd
……

最简单的做法是抑止这些不必要的输出,将它们重定向到一个文件中去。系统专门提供了一个文件,可以将不需要的东西送到那里去:

/dev/null

这是特殊安排的文件,任何时候你将不需要的东西写到那里,相当于将它们抛弃。当试图从那里读取字符时,经常得到的是文件结束的指示。而不会读到任何其他东西。

利用null文件来抑止find命令的出错信息输出,下例就可以写成:

$ find / -name passwd -print 2>/dev/null
/usr/bin/passwd
/etc/passwd

本例中用了两个find命令的表达式,它们之间没有操作符(默认的操作符是-a)。对这一默认的操作符讲,find命令每产生一个文件名,先对第1表达式求值,仅当第一表达式的结果为真时,才用到第2表达式。本例中第1表达式是-name passwd。它从find命令提供的路径名后面找文件名passwd。如果两者相同,它返回真值,否则返回假值。这一返回值决定是否用到第2表达式。如果第1表达式返回值为真,第2表达式(-print)就将find发现的当前路径名送到标准输出设备去显示。总的结果是:将find命令发现的后面带有passwd文件名的所有路径名显示出来。

可以用这种方式单独使用find命令找出路径名清单,不必像前面的例子那样通过管道使用grep命令。但是两者是有差别的,-name表达式只在路径名的末尾找完整的字,而grep命令搜索子字符串(substring):

$ cd
$ find . -name motd -print 2>/dev/null
./text/motd
$ find . -print 2>/dev/null | grep motd
./text/motd
./backup/motd.bak

更复杂一点的例子:如何产生一个目录子树(directory subtree)下的所有包含特定字符串的普通文件的路径名清单呢?

一种可能的解决办法是用find命令产生路径名,然后用grep命令检查这些文件中是否包含特定字符串,并显示所有包含特定字符串的文件的路径名。唯一的问题是grep命令同时在标准输出设备上输出它发现的行和错误信息。克服这一问题的一种办法是将标准输出和出错误信息都重新定向到/dev/null。但grep命令提供了更简单的办法,可以用命令开关抑止这些输出:

$ find /etc -type f -exex grep -q -s mycroft {} \; -print
/etc/HOSTNAME
/etc/hosts
/etc/lilo.conf

这里用find命令产生/etc目录下所有路径名的清单,然后对第1表达式-type f求值。如果当前路径名属于普通文件,送回真值。然后对所有普通文件执行第2表达式(-exec grep -q -s mycroft {}\ ;),在每个普通文件中搜索mycroft字符串。不要忘记,在执行grep命令时,{}已用当前路径名代替,-q和-s开关使grep命令不显示标准输出和出错信息。所以,这一表达式只用来产生真值或假值,用来决定是否执行第3表达式。第2表达式中分号前面的反斜杠用来引用分号,使shell不加改变地将它传送给find命令,如果第2表达式返回真值,将执行-print表达式,它的作用是显示当前的路径名。

5-1-3 sort

sort命令是一个过滤程序。它将输入的正文中的行按命令中指定的顺序进行排序,并将排序结果送给标准输出设备。

可以选择sort使用的字段分隔符,默认为空白字符(如空格,Tab等)。

例如对虚构口令文件pw.test:

root:awmku76tr43d6:0:0::/root/:/bin/sh
pc:bdhd74hs9jh3h:50:50::/usr1/pc:/bin/bash
carey:esJ9ohd8HH89i:501:50::/usr1/carey:/bin/bash
mot:dhjd83kjdJS6D:1500:60::/usr1/mot:/bin/bash
grex:cj8AjoWE8h8fs:1500:60::/usr1/mot:/bin/sh

$ sort pw.test
carey:esJ9ohd8HH89i:501:50::/usr1/carey:/bin/bash
grex:cj8AjoWE8h8fs:1500:60::/usr1/mot:/bin/sh
mot:dhjd83kjdJS6D:1500:60::/usr1/mot:/bin/bash
pc:bdhd74hs9jh3h:50:50::/usr1/pc:/bin/bash
root:awmku76tr43d6:0:0::/root/:/bin/sh

sort命令可以从命令行中指定的任何文件中接受输入。如果没有指定文件,则从标准输入设备接受输入。

sort命令的一些最有用的开关:

-b                不管排序键前面的空格字符;
-f                不区分大小写;
-n                将排序键作为数字而不作为正文;
-r                按从高到低的顺序而不是从低到高的顺序进行排序;
-o file         将输出送到file而不是送到标准输出设备;
-t s            用s代替空白字符作为字段分隔符;
-k s1,s2        用s1字段到(s2-1)字段作为排序键。


-k开关的详解:

-k3                排序键从字段3开始到行的结束;
-k3,6           排序键由行中第3,4,5字段组成;
-k4,5 -k1,3     排序键是第4字段和第1,2字段;
-k3.3,4         排序键是第3字段,但去掉字段中前2个字符;
-k3.2,3.6       排序键是第3字段,从第2字符开始到第5字符共4个字符。

如果想访问文件中的各个字段,首先应该将它的字段分隔符改为需要的类型。下面用第3字段中的UID对pw.test文件排序:

$ sort -t: -k3,4 pw.test
root:awmku76tr43d6:0:0::/root/:/bin/sh
grex:cj8AjoWE8h8fs:1500:60::/usr1/mot:/bin/sh
mot:dhjd83kjdJS6D:1500:60::/usr1/mot:/bin/bash
pc:bdhd74hs9jh3h:50:50::/usr1/pc:/bin/bash
carey:esJ9ohd8HH89i:501:50::/usr1/carey:/bin/bash

再复杂一点儿,当第3字段不能确定行的顺序时,接着用第7字段进行排序:

$ sort -t: -k3,4 -k7 pw.test
root:awmku76tr43d6:0:0::/root/:/bin/sh
mot:dhjd83kjdJS6D:1500:60::/usr1/mot:/bin/bash
grex:cj8AjoWE8h8fs:1500:60::/usr1/mot:/bin/sh
pc:bdhd74hs9jh3h:50:50::/usr1/pc:/bin/bash
carey:esJ9ohd8HH89i:501:50::/usr1/carey:/bin/bash

上面的例子似乎没有正确按UID的值进行排序。关键字的顺序是0,1500,1500,500,501。这是因为sort将UID的内容作为字而不是作为数来对待。解决的办法是使用-n开关:

$ sort -t: -n -k3,4 -k7 pw.test
root:awmku76tr43d6:0:0::/root/:/bin/sh
pc:bdhd74hs9jh3h:50:50::/usr1/pc:/bin/bash
carey:esJ9ohd8HH89i:501:50::/usr1/carey:/bin/bash
grex:cj8AjoWE8h8fs:1500:60::/usr1/mot:/bin/sh
mot:dhjd83kjdJS6D:1500:60::/usr1/mot:/bin/bash

用口令字段的第1,2字符作为排序键:

$ sort -t: -r -k2.1,2.3 pw.test
carey:esJ9ohd8HH89i:501:50::/usr1/carey:/bin/bash
mot:dhjd83kjdJS6D:1500:60::/usr1/mot:/bin/bash
grex:cj8AjoWE8h8fs:1500:60::/usr1/mot:/bin/sh
pc:bdhd74hs9jh3h:50:50::/usr1/pc:/bin/bash
root:awmku76tr43d6:0:0::/root/:/bin/sh

可以将sort输出重新定向到管道和文件,但是如下的形式是不被允许的:

$ sort somefile >somefile

因为shell在做重新定向的准备工作时会删去你的输入文件。如果你一定要将输出送回同一个文件,就在sort命令中加上-o开关;

$ sort somefile -o somefile



(待续)