上篇文章中我们较为详细的介绍了一下 sehll 解释命令行的规则和顺序,本篇我们将详细描述 sehll 中经常用到的一些程序(也称命令、工具)。当然,其中会参插很多前面讲过的。
Shell 脚本中经常用到的工具有 cut tr grep sort uniq re sed awk 等。 Re 不是一个程序,我们认为他是 shell 中一个重量级的工具。对这些工具的使用越精通,编写 shell 脚本解决问题就越容易! re/sed/awk shell 中最难的一部分内容(至少笔者这么认为),在我的其他文章中专门对他们进行过讨论,所以这里将不做介绍。

 

一、 cut

       如果要从数据文件或者命令的输出中截取(也就是剪出)各种各样的数据域, cut 命令都会派上用场,命令的一般格式为
cut –cchars file
其中, chars 指定想从文件的每一行中截取哪些文字。这可以是一个数字,如 -c5 就是把第 5 个字符截取出来;用逗号分隔的数值列表,如 -c1,13,50 把第 1 13 50 个字符截取出来;或用破折号分隔的数值范围,如 -c20-50 截取出第 20 50 之间的字符,包括他们自己。如果要把到行尾的字符全部截取出来,可以用数值范围,缺掉第 2 个参数,因此
cut –c5- data
data 文件每一行中从第 5 个字符到行尾的内容全部截取出来了,并把结果写入标准输出(屏幕)。
如果不指定 file 参数, cut 从标准输入(通常是键盘)读取输入,这以为着可以把 cut 命令用作管道线中的过滤器。我们再看 who 命令的输出:
$who
root     tty2         2009-08-17 16:59
root     pts/0        2009-09-17 23:50 (192.168.1.130)
stu1     pts/2        2009-09-27 15:26 (192.168.1.130)
$who | cut –c1-8
root         2009-08-17 16:59
root         2009-09-17 23:50 (192.168.1.130)
stu1         2009-09-27 15:26 (192.168.1.130)
$
选项 -c1-8,18- 意思是截取行中的第 1 8 个字符(用户名)和第 18 到行尾的字符(登陆时间)。
-d -f 选项

当数据文件或命令的输出不像 who 那样给出整齐的格式,那么 -c 选项就不那么好用了。例如,看一下文件 /etc/passwd:
$cat /etc/passwd
mysql:x:501:501::/home/mysql:/bin/bash
www:x:48:48::/home/www:/bin/bash
ldap:x:55:55:LDAP User:/var/lib/ldap:/bin/false
test:x:502:502::/home/test:/bin/bash
stu1:x:503:503::/home/stu1:/bin/bash
$
该文件的第一个冒号之前的字符都是系统用户名,我们想要截取这些用户名应该怎么办呢?因为每个名字的长度都可能不一样,所以 -c 在这里不能很好得完成任务了。但是这个文件有一个特点,它的各个字段都用冒号分隔。尽管不同行之间的每个字段长度都不一样,但可以数冒号来得到各行中的同一字段。这时候我们需要 -d -f ,命令格式变为:
cut –ddchar –ffields file
其中, dchar 是数据中分隔各字段的分隔符, fields 表示要从文件 file 中截取出来的字段。字段编号从 1 开始,而且格式和 -c 指定字符是一样的 ( -f1-3,8-)
所以要从 /etc/passwd 中截取出系统中的用户名,可以键入以下命令:
$cut –d: -f1 /etc/passwd
mysq
www
ldap
test
stu1
$

 

二、tr

过滤器 tr 用来转换来自标准输入的字符。 Tr 命令的一般格式为
tr from-chars to-chars
其中 from-chars to-chars 是一个或多个字符。所输入的任何字符,如果在 from-chars 中,它就被转换成 to-chars 中对应的字符。转换的结果写入标准输出。在 tr 最简单的格式中,它可以把一个字符转换成另一个字符。
$tr m M < /etc/passwd
Mysql:x:501:501::/hoMe/Mysql:/bin/bash
www:x:48:48::/hoMe/www:/bin/bash
ldap:x:55:55:LDAP User:/var/lib/ldap:/bin/false
test:x:502:502::/hoMe/test:/bin/bash
stu1:x:503:503::/hoMe/stu1:/bin/bash
$
/etc/passwd 文件中的小写 m 都被转换成大写 M 了。
$cut –d: -f1,6 /etc/passwd | tr : ‘ ‘
mysql /home/mysql
www /home/www
ldap /var/lib/ldap
test /home/test
stu1 /home/stu1
$
cut 先截取第1和6字段,然后 tr 将冒号替换成空格。
$cut –d: -f1,6 /etc/passwd | tr ‘[a-z]’ ‘[A-Z] ‘
MYSQL:/HOME/MYSQL
WWW:/HOME/WWW
LDAP:/VAR/LIB/LDAP
TEST:/HOME/TEST
STU1:/HOME/STU1
$
cut 先截取第1和6字段,然后 tr 将所有小写字母转换为大写。
-s 选项

tr 命令的 -s 选项用来压缩 to-chars 中重复的字符。换句话说,如果转换完成后,有 to-chars 中的某个连续字符重复出现,则这些连续的相同字符将被替换为一个字符。
$ cut –d: -f1,6 /etc/passwd | tr –s mw MW
Mysql:/hoMe/Mysql
W:/hoMe/W
ldap:/var/lib/ldap
test:/hoMe/test
stu1:/hoMe/stu1
$
tr m w 分别替换成 M W ,并且将连续的 M W 替换成一个字符,所以第二行的 WWW 变成了单个 W
$cat data
1      2     3
4  5  6
$tr –s ‘ ‘ ‘ ‘ < data
1 2 3
4 5 6
$
-s 参数的 tr data 文件每行的多个空格压缩为一个,这里 from-chars to-chars 同为单个空格。处理的顺序是先替换后压缩,不管替换有没有产生变化。该命令和 sed ‘s/ */ /g’ 是同样的效果,而且和 sed 一样, tr cut 均不改变源文件(只是对文件的一个拷贝进行处理)
-d 选项

tr 也可以删除掉输入流中的字符,其一般格式为
tr –d from-chars
  任何列在 from-chars 中的字符都会被从标准输入中删除。
$ tr -d ‘ ‘ < data
123
456
$
tr –d 删除 data 文件中的所有空格,结果写入标准输出(屏幕)。该命令和 sed ‘s/ //g’ 等效。 tr 只能对个别字符操作,如果要转换多个字母的组合,就必须用其他程序,如 sed

 

三、grep

grep 命令可以从一个或者多个文件中搜索特定的字符串模式,其一般格式为
grep pattern files
每个文件中符合模式 pattern 的字符串所在的行都将显示在终端上。如果给 grep 指定了多个文件,会在每一行的前面显示文件名,这样可以识别从中找到该模式的文件。还是以前面的  /etc/passwd 文件为例:

$cat /etc/passwd
mysql:x:501:501::/home/mysql:/bin/bash
www:x:48:48::/home/www:/bin/bash
ldap:x:55:55:LDAP User:/var/lib/ldap:/bin/false
test:x:502:502::/home/test:/bin/bash
stu1:x:503:503::/home/stu1:/bin/bash
$grep www /etc/passwd
www:x:48:48::/home/www:/bin/bash
$
该输出表明,文件 /etc/passwd 中有一行包含字符串 www 。如果在给定的文件中没有符合该迷失的字符串, grep 命令就什么也不显示:
$grep tom /etc/passwd
$
如果另一个文件 data 的内容如下:
$cat data
www in data
tom in data
jerry in data
$
在在 /etc/passwd data 两个文件中搜索 www 的结果如下:
$ grep www /etc/passwd data
/etc/passwd:www:x:48:48::/home/www:/bin/bash
data:www in data
$
结果,除了显示包含 www 的行外,还显示了他们所在的文件名。
命令格式中的 pattern 通常是一个正则表达式 re www 也是一个 re ),关于 re 的介绍不在本文给出,请参考其他文章。
-v 选项

有时用户所关心的不是包含指定模式的行,而是不包含的那些行。 grep 处理这个问题很容易:用 -v 选项。下面我们来查找 /etc/passwd 中不包含 www 的行:
$grep –v www /etc/passwd
mysql:x:501:501::/home/mysql:/bin/bash
ldap:x:55:55:LDAP User:/var/lib/ldap:/bin/false
test:x:502:502::/home/test:/bin/bash
stu1:x:503:503::/home/stu1:/bin/bash
结果中除了包含 www 的那行外,其他所有行都被显示了出来。
-l 选项

有时,用户可能不关心包含一个模式的具体行,而只想知道包含该模式的文件名。下面的例子给出了简单的方法:
$ grep –l www /etc/passwd data file
/etc/passwd
data
$
其中,只有 file 中没有包含 www 的行,于是结果显示了包含 www 的文件名。因为 grep 很方便的把每个文件显示为一行,我们可以在命令后面通过管道把输出输送给 wc –l 的输入,以获得包含指定模式的文件的个数。
$ grep –l www /etc/passwd data file | wc –l
2
-n 选项

如果 grep 用了 -n 选项,文件中符合指定模式的每一行都将加上该行在文件中相对行号。下面的例子告诉我们如何查明文件中包含模式的确切行号:
$grep –n www /etc/passwd
2 www:x:48:48::/home/www:/bin/bash
结果表明,包含 www 的行位于 /etc/passwd 的第 2 行。

 

四、sort

sort 用于对指定的文件中的行进行排序,其基本格式为:
sort file(s)
将文件 file(s) 中的行排序,若未指定,则排序标准输入(键盘输入)。
$cat names
Lucy
Tony
Fred
Tony
$
sort 有许多选项,可以让排序操作非常灵活。下面我们只介绍其中一部分选项。
-u 选项

-u 选项告诉 sort 命令,在输出结果中去除重复的行:
$sort names
Fred
Lucy
Tony
Tony
$sort –u names
Fred
Lucy
Tony
$
可以看出,包含 Tony 的重复行从输出中被去掉了。
-r 选项

-r 选项颠倒排序顺序:
$sort –r names
Tony
Tony
Lucy
Fred
$
-o 选项

默认情况下, sort 把输出结果写入标准输出。要把结果写入文件,可以用输出重定向:
$sort names > sorted_names
$
另一种方法是用 -o 选项指定输出文件,只要在 -o 之后接着写上输出文件名:
$sort names -o sorted_names
$
该命令把对文件 names 排序的结果写入 sorted_names 。很多时候都会想把一个文件中的各行排序,然后把排好序的结果写回原来的文件,键入
$sort names > names
$
不能成功,它将以破坏文件而告终。然而,采用 -o 选项时,却可以给输出文件指定和输入文件相同的名字:
$sort names -o names
$cat names
Fred
Lucy
Tony
Tony
原来的文件顺序被排好了,并且不会有任何错误,这个 -o 确实蛮好的。
-n 选项

sort 默认是按照 ASCII 编码的顺序来进行排序的,如果要按照数学方式来排序,该怎么做呢?看下面的例子:
$cat data
1 11 www
-3 13 jerry
2 12 tom
$sort data
1 11 www
2 12 tom
-3 13 jerry
$sort –n data
-3 13 jerry
1 11 www
2 12 tom
可以看到, -n 参数按照数字大小来进行排序了。
跳过字段

接着上面的例子,如果想要以第二列数字来进行排序该怎么办呢?看下面的做法:
$sort +1n data
1 11 www
2 12 tom
-3 13 jerry
$
结果是以第二列数字排序的, +1 代表跳过第一字段。注意,不是所有的 sort 都支持 + 这个选项,这和你的系统或者 sort 版本有关。
$sort +1n data
sort: open failed: +1n: No such file or directory
某些系统运行该命令将得到如上的错误信息。
-t 选项

$cat /etc/passwd
mysql:x:501:501::/home/mysql:/bin/bash
www:x:48:48::/home/www:/bin/bash
ldap:x:55:55:LDAP User:/var/lib/ldap:/bin/false
test:x:502:502::/home/test:/bin/bash
stu1:x:503:503::/home/stu1:/bin/bash
$
如果想要以冒号作为分隔符,按照第三列数字(用户 id )来对 /etc/passwd 进行排序,应该怎么做呢。是用下面这个命令吗?
$sort +2n /etc/passwd
错了,因为跳过字段时, sort 默认是以空格或者制表符( tab )来分隔字段的。这里我们需要用 -t 参数,改变 sort 默认的字段分隔符:
$sort +2n –t: /etc/passwd
www:x:48:48::/home/www:/bin/bash
ldap:x:55:55:LDAP User:/var/lib/ldap:/bin/false
mysql:x:501:501::/home/mysql:/bin/bash
test:x:502:502::/home/test:/bin/bash
stu1:x:503:503::/home/stu1:/bin/bash
$
这里我们把第三字段用黑体标注了起来,方便检验排序的正确性。

 

四、uniq

如果要在文件中查找重复的行, uniq 命令会很管用,该命令一般格式为
uniq in_file out_file
该格式中, uniq in_file 复制到 out_file ,处理过程中,去掉重复的行。 uniq 对重复行的定义是完全相同的连续行。
如果不指定第二个参数 out_file ,结果就写入标准输出;如果 in_file 也没有指定, uniq 就成了一个过滤器,从标准输入读入输入。下面有几个例子示意 uniq 如何工作。
$cat names
Lucy
Tony
Fred
Tony
$
可以看出,文件中 Tony 出现了两次,可以用 uniq 来删除这种重复的行:
$uniq names
Lucy
Tony
Fred
Tony
$
奇怪,输出中 Tony 仍然出现了两次, unqi 没有用吗?对了,前面说了, uniq 对重复行的定义是完全相同的连续行。而这里的两行 Tony Fred 分开了, uniq 自然不会改变他们。再看下面是怎么处理的:
$sort names | uniq
Fred
Lucy
Tony
$
sort 先对文件进行了排序,这样两行 Tony 就连续了,这次 uniq 就派上用场了。(想想带 -u 选项的 sort 命令,完成的就是这个功能)
-d 选项

很多时候,用户所关心的是找出文件中有哪些重复行。 uniq -d 选项就是这个作用:它告诉 uniq 把文件中的重复行写入 out_file (或标准输出),不管他们在文件中连续出现多少次,这样的连续行只写一次。
$sort names | uniq -d
Tony
和前面一样, sort 先对文件排序,然后 uniq –d 找到重复的连续行,然后只显示一行。下面的命令可以用来检查系统中是否有相同的帐户:
$sort /etc/passwd | uniq –d
$
结果将显示属性完全相同的两个 unix 帐户,如果只是想要找出同名的帐户呢,我们需要截取 /etc/passwd 文件的第一字段来进行操作:
$sort /etc/passwd | cut –d: -f1 | uniq –d
harry
tom
$
上面的结果表明,系统中至少有 2 harry 2 tom 帐户。
-c 选项

-c 选项的 uniq 删除重复行的同时,显示每行出现的次数。
$sort names | uniq -c
      1 Fred
      1 Lucy
      2 Tony
$

 

本文详细介绍了工具 cut tr grep sort uniq 的用法。 re sed awk 在笔者以下几篇文章中分别加以讨论:
http://licong.blog.51cto.com/542131/200431 《正则表达式详解》
http://licong.blog.51cto.com/542131/152541 sed 学习笔记》
http://licong.blog.51cto.com/542131/204226 sed 学习笔记二 -- 高级命令》
http://licong.blog.51cto.com/542131/151976 awk 学习笔记》
再重复一次,以上工具掌握得越灵活,编写 shell 脚本就越容易。