Linux三剑客-grep/awk/sed

Linux下文本处理的三剑客–grep/awk/sed!

概述

Grep/awk/Sed是Linux下处理文本信息的最强的三个命令.使用它们几乎可以完成所有的文本处理, 具体说来是:

1
2
3
grep : 过滤.查找
awk : 文本处理工具
sed : 行编辑器

使用它们不可避免的要使用正则.

因此回顾一下正则的基础:

简单的说, 我们把基础正则分为这样三个部分.

单个字符–表示内容

特定的字符(直接写), 字符范围(使用中括号[]), 任意字符(用一个点.来表示).
使用|来进行逻辑或运算.

重复字符–表示数量

表示从零到任意:使用星号*
表示要么0个要么1个:使用问号?
表示从1到任意个:使用加号+
表示特定的范围:使用大括号{}

边界字符

首字符使用尖角号^
尾字符使用美元符$

元字符

任何字类字符使用\w
任何非字类字符使用\W
划定单词边界使用\b(boundary)

另外,在使用一些字符时, 千万不要忘记使用转义字符.

基础的正则就是这样了.

grep

这个使用率应该是最高的了, 因为他是三个命令中最简单的一个.

grep的基本命令格式是:

1
grep [选项] 匹配字符串 文件

我们经常使用管道符流动的数据来替代文件, grep常用的选项是:

1
2
3
-i (ignore-case) 忽略大小写
-v (invert-match) 反向匹配
-n (line-number) 输出行号

这个没什么好说的了..因为大家都用的很666666. :)

sed

sed相当于是一个自动的vim,常用来分析日志,修改配置,自动处理文件.

简单先说一下Sed的原理, Sed接受一个输入, 接着(A)将读到的一行数据写入到模式空间(其实就是一个临时缓冲区), 接着进行命令处理(B),最后输出到屏幕, (A-B)循环执行直到读取结束.

Sed实际上是行处理! 并且, Sed不改变源文件, 除非指定选项(这个后面再说),

命令格式:

1
sed [选项] '[命令]' 文件名

现将常用的选项参数列出来, 到了下面再慢慢说:

1
2
3
-e 对输入数据进行多条sed命令(;号分割)
-n 仅仅将处理的行输出到屏幕(默认全文件)
-i 直接修改源文件, 而不是由屏幕输出.

那我们从文本处理开始!

Sed的命令有很多和Vim很像, 列举一些常用的:

1
2
3
4
5
6
- a : (append) 追加
- c : (change) 替换
- d : (delete) 删除
- i : (insert) 插入
- p : (print) 打印
- s : (string) 字串替换 -- 行范围s/旧字串/新字串/g (后面说)

下面来动动手吧~

1
2
3
4
[root@WWW ~]$ sed 'p' passwd
root:x:0:0:root:/root:/bin/bash
root:x:0:0:root:/root:/bin/bash
.....

挨!!??输出了两次?想想看这是为什么?

之前说过, sed的原理是读取一行, 而在没有加上-n参数的时候, 又会将全文件输出, 所以就造成了行行重复的情况.

同Vim一样,也可以直接使用行号或者使用斜杠进行搜索.

比如:

1
2
3
4
5
6
7
[root@WWW ~]$ sed -n '10p' passwd
operator:x:11:0:operator:/root:/sbin/nologin
[root@WWW ~]$ sed -n '/user/p' passwd
tss:x:59:59:Account used by the trousers package to sandbox the tcsd daemon:/dev/null:/sbin/nologin
tinyproxy:x:993:991:tinyproxy user:/var/run/tinyproxy:/bin/false
user1:x:1001:1001::/home/user1:/bin/bash
user2:x:1002:1002::/home/user2:/bin/bash

也可以选择一段距离, 比如:

1
2
[root@WWW ~]$ sed -n '10,20p' passwd
[root@WWW ~]$ sed -n '/operator/, /ftp/p' passwd

将数字中的逗号变为波浪线即可进行步进式的跳跃.

也可以进行取反:

1
2
[root@WWW ~]$ sed -n '10!p' passwd
# 表示除了第十行都进行p操作

接着来看一下-i-a两个插入命令:

1
2
3
4
5
6
7
8
9
10
[root@WWW ~]$ sed '2a =========' passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
=========
......
[root@WWW ~]$ sed '2i =========' passwd
root:x:0:0:root:/root:/bin/bash
=========
bin:x:1:1:bin:/bin:/sbin/nologin
......

也就是说, i是向前插入, a是向后插入.

很简单对吧? 删除命令也是很简单的:

1
2
3
4
[root@WWW ~]$ nl passwd | sed '2d'
1 root:x:0:0:root:/root:/bin/bash
3 daemon:x:2:2:daemon:/sbin:/sbin/nologin
....

删除同样也支持跳行和段选择.

最后就是替换命令了, 替换支持行替换和字串替换.

相对来说, 行替换要更简单一点.

1
2
3
4
5
[root@WWW ~]$ sed '2c Hello' passwd | nl
1 root:x:0:0:root:/root:/bin/bash
2 Hello
3 daemon:x:2:2:daemon:/sbin:/sbin/nologin
....

如果在替换命令中使用了段选择, 不会使每一行都做替换, 而是把他们作为一个整体.

1
2
3
4
5
6
[root@WWW ~]$ sed '1,34c Hello' passwd | nl
1 Hello
[root@WWW ~]$ sed 's/root/ROOT/g' passwd
ROOT:x:0:0:ROOT:/ROOT:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
....

来看一个操作实例吧, 现在我们从ifconfig中读取ip地址:

1
2
[root@WWW ~]$ ifconfig | grep inet | sed -e 's/inet //;s/net.*//'
xx.xxx.xxx.xxx

其中我们使用了多重操作, 也就是添加-e参数, 并使用分号进行多个sed命令.

接下来来看一下Sed稍微高级一点的用法:

  • 使用&来复制前一个字符串
  • 大小写转换

使用&的一个实例像这样:

1
[root@WWW ~]$ sed 's/^[a-z]\+/&   /' passwd

接着来看一下这几个大小写转换符:

  • u 转大写(以首字母为单位) (U 以词为单位)
  • l 转小写(以首字母为单位) (L 以词为单位)

比如:

1
[root@WWW ~]$ sed 's/^[a-z-]\+/\U&/' passwd

会将所有的用户名变成大写.

接着,如同我们在Python小技巧中说过的捕获组, 正则拼成的组, 也是可以有编号进行引用的.就像这样:

1
[root@WWW ~]$ sed 's/\(^[0-9A-Za-z-]\+\):x:\([0-9]\+\):\([0-9]\+\).*$/USER:\1   UID:\2   GID:\3/' passwd

这样会输出类似:

1
2
3
User:root   UID: 0 GID:0 
User:bin UID: 1 GID:1
User:daemon UID: 2 GID:2

这样的语句.

sed也可以进行多文件间的操作, 比如这两个命令:(注意:其中的w命令是会进行真实写入的!会直接对文件进行改变并且指针在文件头.)

比方说我们先写入一些数据:(和下面的演示有出入,只要分清字母和数字就好)

1
2
[root@WWW ~]$ echo -e "13243\n545343\n112324" > 123.txt
[root@WWW ~]$ echo -e "ghreqwre\ndwegfew\ndwqcsa" > abc.txt

接着演示文件间的读取和写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@WWW ~]$ sed '1r abc.txt' 123.txt # 写入缓存区中,不会生效
3413254
aDSWvsdfv
fwqwqfwqfwq
fqefqq
14354354
2132421
[root@WWW ~]$ sed '$r abc.txt' 123.txt # 写到尾部
[root@WWW ~]$ sed '1w abc.txt' 123.txt
3413254
14354354
2132421
[root@WWW ~]$ cat abc.txt # 文件被改变
3413254

sed的执行单位是行, 直到读取完整个文件.

但我们可以通过q命令使其提前退出.

比如:

1
2
3
4
5
6
[root@WWW ~]$ nl passwd | sed '3q'
1 root:x:0:0:root:/root:/bin/bash
2 bin:x:1:1:bin:/bin:/sbin/nologin
[root@WWW ~]$ nl passwd | sed '/nologin/q'
1 root:x:0:0:root:/root:/bin/bash
2 bin:x:1:1:bin:/bin:/sbin/nologin

至此, sed的命令就差不多了.

awk

awk是一个与Sed类似的文本与数据处理工具, 同样也是一次处理一行内容.

不同的是, awk对每行可进行切片, 并且awk是可以进行编程的, 因此他十分灵活.

awk的格式为:

1
awk '条件1{动作1}条件2{动作2}...' 文件名

awk的内置变量(1)同shell参数很像, 使用$0表示整行, $1表示第一个字段, $2表示第二个字段….以此类推..

awk的分割符用 -F '' 来进行声明

比如:

1
2
3
4
5
[root@WWW ~]$ cat passwd | awk -F ':' '{print $1 "----" $7}'
root----/bin/bash
bin----/sbin/nologin
daemon----/sbin/nologin
adm----/sbin/nologin

awk的内置变量(2)是NR,NF,FILENAME,分别表示每行的记录号, 字段的数量, 和正在处理的文件名.

awk是支持逻辑运算的, 比如下面这个:

1
2
3
4
5
6
[root@WWW ~]$ awk -F ":" '{if ($3>100) print "Deny:  ",$1} {if ($3<100) print "Access:   ",$1}' passwd
Access: root
Access: bin
Deny: libstoragemgmt
Access: tcpdump
Deny: nginx

awk有两种输出函数, 一个是printf, 一个是print, 其中printf更加古老, 需要手动加上最后的换行符, print会自动加上.

printf基本上和编程语言的同名函数用法相同,都是格式化输出.

作为awk的重要功能, 我们来看一下他的逻辑判断式:

  • , ! : 匹配正则表达式
  • ==, !=, <, > : 判断逻辑表达式

匹配正则以~开头, 后面加上有斜线规定的正则, 后面的指令就只对匹配到的内容有效果了.

比如这样:

1
2
3
$ awk -F ':' '$1~/^r.*$/{print $1}' passwd
root
rpc

当然也可以反选择, 只要把~进行取反就可以啦, 也就是!~.

以上是awk的基础格式.

什么?还有扩展的? 是的, awk的command2拓展就像是这样:

1
[root@WWW ~]$ awk [options] 'BEGIN{print 'start'} pattern{commands} END{ print "end"}'

使用awk扩展格式可以手动制表, 仍然用passwd来举例:

1
2
3
4
5
6
7
[root@WWW ~]$ awk -F ':' 'BEGIN{print "Line\tCol\tUser"}{print NR"\t"NF"\t"$1}END{print "-------"FILENAME"-------"}' passwd 
Line Col User
1 7 root
....
31 7 tomcat
32 7 tinyproxy
-------passwd-------

现在我们利用awk的逻辑控制运算功能, 做一个小实例:(请使用du取代这个愚蠢的实例.)

1
[root@WWW ~]$ ls -la | awk 'BEGIN{size=0}{size+=$5}END{print "The total size of the directory is "size/1024/1024"M"}'

在BEGIN段中, 我们初始化了size,接着让awk一行一行读入大小的值并累加到size上,最后输出.

同样, 我们也可以统计passwd文件的行数, 从而得知有多少用户(请使用wc命令,取代这个愚蠢的实例)

1
[root@WWW ~]$ awk -F ":" 'BEGIN{count=0}$1!~/^$/{count++}END{print "There are "count" users"}' passwd

awk也可以使用for循环和数组, 没想到吧?

1
2
3
4
[root@WWW ~]$ netstat -tanp | awk '$6~/LISTEN|ESTABLISHED/{sum[$6]++}END{for (i in sum)print i, sum[i]}'

LISTEN 9
ESTABLISHED 16

到这里awk的使用就算是差不多了, 简单小结一下吧:

awk的内置参数要知道, $n(数字) 代表那一列的值, NR, NF 分别代表Record每行记录数, Field字段数.

awk支持逻辑符, 判断, 数组, 循环等等, 还有BEGINEND.

其他管道命令

接着记录一下Linux的管道命令, 这些在进行文本处理的时候也是超级常用的.

cut

这个命令其实也是很常用的,听名字就知道是干什么的了–做字符串的裁剪以剪出我们需要的信息.

事实上,这个命令的历史很久的.简单的使用方法就像是这样:

1
2
[root@WWW ~]$ cut -d "分割字符" -f fields # d其实就是delimiter嘛  fields就是要裁剪的数据域
[root@WWW ~]$ cut -c 字符范围

举个例子就明白了:

1
2
3
4
5
6
[root@WWW ~]$ echo $PATH
/usr/lib64/qt-3.3/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
[root@WWW ~]$ # 现在我们可以把这之间的:当成分隔符,这样$PATH的构成就是
[root@WWW ~]$ # |1|2|3|4|5|6|
[root@WWW ~]$ echo $PATH | cut -d ":" -f 1
/usr/lib64/qt-3.3/bin

简单吧,另外cut还能精确的定位到一节字符串的某个位置:

1
2
3
4
[root@WWW ~]$ echo $PATH | cut -c 1,5
// # 这个是第一个usr两边的斜线
[root@WWW ~]$ echo $PATH | cut -c 1-5
/usr/

上面就是在使用cut命令的-c进行的精准截取, ,表示就选择这两个位置的字符, -表示选择这两个字符以及他们中间的所有字符, 其实就类似Python的切片, cut也可以不写两端来进行直到头尾的操作.

当然你也发现了, cut在处理多个空格的情况下是比较吃力的, 这个时候就要选择awk了.

sort

sort是个很有趣的指令,他可以根据不同的数据类型来进行排序.这里要注意一下,排序的依据还取决于当前系统的语系.使用LANG=C来使得语系统一,优化排序

常用的选项和参数如下:

1
2
3
4
5
6
[root@WWW ~]$ sort [options] [file or stdin]
# options
# -f 忽略大小写(很好奇为什么不是i,难道是familiar?) 哦,-i的意思是忽略不可打印的字符,不过基本用不到吧....
# -r 反向排序(reverse)
# -k 排序依据(用什么东西来进行排序)
# -t 分割符号(默认是TAB, 这个选项经常和-k一起使用)

使用sort,我们可以很轻易的得到按照用户名排序的passwd文件:

1
[root@WWW ~]$ cat /etc/passwd | sort

假如说系统的passwd文件不是按照uid排的序的话, 使用sort同样可以达到同样的目的, 这个时候就要使用到-k-t了.

1
[root@WWW ~]$ cat /etc/passwd | sort -t ":" -k 3

稍微解释一下,-t ":"的意思是说以:当做分割符, -k 3的意思是说以第三个的数据域(也就是uid了)当做排序依据.

uniq

这个命令经常用于进行处理重复的行, 一个经典的使用例子就是统计曾经登录的用户的登录次数.

1
[root@WWW ~]$ last | cut -d ' ' -f 1 | sort | uniq -c

我的机器上的输出结果是:

1
2
3
4
  1 
1 reboot
121 root
1 wtmp

一开始我省略了sort命令,结果发现输出的结果是这样的:

1
2
3
4
5
107 root
1 reboot
14 root
1
1 wtmp

root的结果被分开了, 这样就导致uniq的统计结果出现了错误. 这样我们就能发现了uniq的处理逻辑: 从上到下的搜索,如果是一样的就不再增加新条目,一旦遇到不同的,则直接将之前的的条目归档.

wc

这个是一个很好用的统计工具哦, wc可以很方便的统计行数,字数,字符数.

尤其对于一些成行的文件,这个命令可以很灵活的获取一些你想要的值.( 比如/etc/passwd这样的文件,行数即为用户数 )

1
2
[root@WWW ~]$ echo `cat /etc/passwd | wc -l`"个用户"
36个用户

直接使用wc,会按照次序输出行数,字数,字节数.

wc对于单词的界定是被空格分割的非零长度的字符序列.

使用一下选项来限制输出的信息:

1
[root@WWW ~]$ wc -l (lines) -w (words) -m (chars)

tee

tee 的工作流程示意图

像上面的图那样, tee会将输出流复制一份到文件,另外一份到标准输出流(也就是屏幕了)

例如,当你想要将处理的信息输出到屏幕外,还想存一份日志或者什么.那么tee就是一个十分适合的工具

1
[root@WWW ~]$ last | tee last.list | cut -d " " -f 1

这样不仅会新建一个叫last.list的文件,还会将信息同步输出到屏幕上.

如果文件是已经存在的,那么文件指针将会指向文件头.

如果是要进行追加,就要添加-a参数. 如果文件的参数是-,那么将会输出标准输出 ( 也就是说会在屏幕上输出两倍的信息 )

tr

tr的用法还是很灵活的, 因为你可以使用正则来进行各种替换和删除.

直接上实例吧:

1
[root@WWW ~] cat /etc/passwd | tr [:lower:] [:upper:] 

这样便将所有的字母变成了大写.像这样的变量还有:

  • [:alnum:]所有的字母和数字
  • [:alpha:]所有的字母
  • [:blank:]所有的水平空格
  • [:digit:]所有的数字
  • [:upper:]大写字母
  • [:lower:]小写字母

另外tr可以加上参数来说明模式: -d是匹配删除, -t是用来将SET1映射成SET2.

在使用正则的情况下:

1
[root@WWW ~]$ cat /etc/passwd | tr [a-z] [A-Z]

和上面的效果是一样的.

删除功能的示例如下:

1
[root@WWW ~]$ cat /etc/passwd | tr -d ":"

然后你就会看到一团子东西, 所有的分隔符已经被删去了.

tr还有一个有趣的参数-c 意思就是取补集, 换言之就是除了给定的SET之外的. 如果和-d连续使用就是说仅保留给定的字符集. 这样子的一个应用就是生成随机字符串:

1
2
3
4
5
6
7
8
[root@WWW mage_shell_edu]# tr -cd a-zA-Z0-9 < /dev/urandom | head -c 20 | xargs
hd5t7kDdpOwBoiKhYP9K
[root@WWW mage_shell_edu]# tr -cd a-zA-Z0-9 < /dev/urandom | head -c 20 | xargs
PDR1Eb6ZJkFc5851TeUc
[root@WWW mage_shell_edu]# tr -cd a-zA-Z0-9 < /dev/urandom | head -c 20 | xargs
NdH7qFJY7Xo3wWo9Vub8
[root@WWW mage_shell_edu]# tr -cd a-zA-Z0-9 < /dev/urandom | head -c 20 | xargs
jgcXXMGVNoZJPUtY9Kp6

最后一个xargs其实就是为了换行而已, 关于xargs的用法, 下面会有.

split

NT下, 你应该使用过一些txt裁剪工具对吧.这个split就是类似的一个外部命令.

split可以将大文档分割成一个个小的档案.并且支持单位.

split支持按行数分割, 按大小两种方式

1
2
3
4
[root@WWW ~]$ split [OPTION] [INPUT [PREFIX]]
# OPTION 常用的就是 -b 按字节分割 -l 按行数分割
# INPUT 就是输入
# PREFIX 是指生成的文件名前缀

现在来试一下:

1
2
[root@WWW ~]$ split -l 4 /root/test mytest_
[root@WWW ~]$ split -b 100 /root/test mytest_

然后我的机器上就多出来了几个文件, 命名形式是: mytest_a{a,b,c,d...}

如果我是想将屏幕上的信息进行裁剪之后再输出文件怎么办呢?

还记得之前的那个短横线 - 这个横线就可以来解决问题.

1
[root@WWW ~]$ ls -al /| split -l 4 - lsroot_

那如果我要进行文件的合并怎么办呢, 其实这个并不需要什么特定的工具了,直接使用数据流重定向就可以了.

1
[root@WWW ~]$ cat mytest_* >> mytest

这样就可以了.

最后再来看一个参数代换的命令:

xargs

这个命令是个很神奇的命令,参数代换并不能理解吧, 其实说白了就是将前面的输出当做命令的参数.所以这个命令的用处是很大的.

比如说

1
[root@WWW ~]$ cat /etc/passwd \ cut -d ':' -f 1 | xargs -n 1 id

这样就可以打印出所有用户的UID,GID和所属组名.

这样看来xargs就像是find命令后面的那个-exec COMMAND {} \;一样的感觉.

解释一下上面的参数是什么意思: 如果参数是很多个就一定要指定-n参数, 这个告诉xargs一次处理的命令参数数量

另外,如果xargs后面没有指定命令,那么默认就是echo. 另外一个可能会用到的参数是-e(EOF)的意思, 在后面可以跟上一个字符串, 当xargs扫描到了这个字符串就会直接停止工作.