Linux:管道命令与文本处理三剑客(grep、sed、awk)
工作中经常用到管道技术,这里整体做一个学习和摘要,转载了此博客
1 管道命令(pipe)介绍
众所周知,bash命令执行的时候会输出信息,但有时这些信息必须要经过几次处理之后才能得到我们想要的格式,此时应该如何处置?这就牵涉到**管道命令(pipe)**了。管道命令使用的是 | 这个界定符号。另外,管道命令与连续执行命令是不一样的,这点下面我们会说明。
我们先来看一个管道命令的例子。假设我们需要看 /etc 目录下有多少文件,那么可以利用 ls /etc 来查看,不过由于文件数量太多,导致一口气就将屏幕塞满了,而不知道前面输出的内容是啥:
1 | |
此时,我们可以使用 less 命令的协助:
1 | |
如此一来,使用 ls 命令输出的内容就能够被 less 读取,并且利用 less 的功能,我们就能够前后翻动相关的信息了。其中的关键就是这个管道命令 |。
管道命令 | 仅能处理前一个命令传来的标准输出信息,而对于标准错误信息并没有直接处理能力。那么整体的管道命令可以使用下图表示:
1 | |
在每个管道后面接的第一个数据必定是命令,而且这个命令必须要能够接受标准输出的数据才行,这样的命令才可为管道命令。例如 less、grep、sed、awk 等都是可以接受标准输入的管道命令,而 ls、cp、mv 就不是管道命令,因为它们并不会接受来自stdin的数据。
总结一下,管道命令主要有两个需要注意的地方:
- 管道命令仅会处理标准输出,对于标准错误会予以忽略;
- 管道命令必须要能够接受来自前一个命令的数据成为标准输入继续处理才行(这也是其与由
;隔开的连续执行命令之不同)。
如果我们强行让标准错误为管道命令所用,那么可以使用 2>&1 将标准错误 2> 重定向到标准输出 1>。
接下来我们选取 grep、sed、awk 这三个用于文本处理的管道命令来进行介绍。这三个命令可谓是Linux下操作文本的三大利器,合称 Linux文本处理三剑客。
2 行选取命令 grep
grep 命令可以一行一行地分析信息,若某行含有我们所需要的信息,则就将该行拿出来。简单的语法如下:
1 | |
它的选项与参数如下:
-a:将二进制文件以文本文件的方式查找数据。-c:计算找到’查找字符’的次数。-i:忽略大小写的不同,所以大小写视为相同。-n:顺便输出行号。-v:反向选择,亦即显示出没有’查找字符’内容的那些行。
下面展示几个例子。
范例一:将 last 当中,有出现 root 的那一行就显示出来。
1 | |
这里前3行是我们校内的局域网IP(以 10.249 打头),172.17.0.1 是Docker中默认网桥 docker0 的IP地址,127.0.0.1 为本地回环地址。
范例二:与范例一相反,只要没有 root 的就取出。
1 | |
范例三:在 last 的输出信息中,只要有 root 就取出,并且仅取第一栏:
1 | |
这里用到了我们后面要讲的 awk 命令,这一命令用于将一行分为多个字段来处理,我们后面将会详细介绍。
范例四:取出 /etc/adduser.conf 内含 UID 的那几行,且将找到的关键字部分用特殊颜色显示出来:
1 | |
可以看到找到的关键字部分用红色显示(当然这里的代码块看不出来效果,需要在终端进行渲染)。注意,在我的Ubuntu 18.04系统中默认的 grep 已经主动使用 --color=auto 选项在 alias 中了,因此不用手动加 --color=auto 也会标红(事实上,在我本地的Mac系统中也是如此)。
3 行操作命令 sed
前面我们说过,grep 命令可以解析一行文字,若该行含有某关键词就会将其整行列出来。接下来我们要讲的 sed 命令也是一个管道命令(可以分析标准输入),它还可以对特定行进行新增、删除、替换等。
sed 的用法如下:
1 | |
它的选项与参数如下:
-n:使用安静(silent)模式。在一般的sed用法中,所有来自 stdin 的数据一般都会被列出到屏幕上,但如果加上-n参数后,则只有经过sed选择的那些行才会被列出来。-e:使sed的操作结果由屏幕输出,而改变原有文件(默认已选该参数,与-i的直接修改文件相反)。-f:从一个文件内读取将要执行的sed操作,-f filename可以执行filename中写好的sed操作。-r:sed的操作使用的是扩展型正则表达式的语法(默认是基础正则表达式语法)。-i:直接修改读取的文件内容,而不是由屏幕输出。
关于其中的 [操作] 部分,其格式如下:
1 | |
n1, n2:不一定会存在,一般代表选择进行操作的行数,比如我的操作需要在10到20行之间进行,则写为 10, 20[操作名称]。
具体地,对行的操作函数 function 包括下面这些东西:
a:新增,a的后面可以接字符,这些字符将被添加在n1/n2的下一行;c:替换,c的后面可以接字符,这些字符可以替换n1,n2之间的行;d:删除,因为是删除,所以d后面通常不需要接任何东西;i:插入,i的后面可以接字符,这些字符将被添加在n1/n2的上一行;p:打印,亦即将某些选择的行打印出来。通常p会与参数sed -n一起运行。s:替换,可以直接进行替换的工作,通常这个s的操作可以搭配正则表达式。
下面我们来举几个例子进行说明。
以行为单位的新增/删除功能
范例一:查看 /etc/passwd 文件的内容并且在每一行前面加上行号,同时将 2-5 行删除。
1 | |
可以看到 sed 的操作为 2,5d,也即删除25行,所以显示的数据就没有25行。此外,请注意原本应该是要执行 sed -e 才对,不过这里没有 -e 也行,因为已经默认选了。同时也要注意 sed 后面接的操作务必以两个单引号 '' 括住。
我们将范例变一下,如果要删除第2行,那么可以使用 cat -n /etc/passwd | sed '2d';如果是要删除第3到最后一行,则是 cat -n /etc/passwd | sed '3,$d',这里美元符号 $ 代表最后一行。
范例二:承接上题,在第2行后(亦即是第3行)加上 drink tea 字样。
1 | |
如果想要加在第2行前面,将新增操作改为插入操作,即 cat -n /etc/passwd | sed '2i Drink tea?' 就行了。
范例三:继续承接上题,现在我们想要在第2行后面加上两行字,例如 Drink tea or... 与 Drink beer?
1 | |
这里的重点在于我们可以不只增加一行,可以增加很多行,但每一行之间必须以反斜杠 \ 来进行新行的增加。
以行为单位的替换与显示功能
刚刚是介绍如何新增与删除行。接下来我们看看如何进行整行的替换。
范例四:我想将2~5行的内容替换为 No 2-5 number。
1 | |
除此之外,sed 还有很有趣的功能,以前我们想要列出第11~25行,得用 head -n 20 或 tail -n 10 之类的命令来处理,很麻烦。而 sed 则可以直接取出你想要的那几行,这是通过行号来识别的。例如下面这个范例:
范例五:仅列出 /etc/passwd 文件内的第5-7行。
1 | |
注意,这里必须要加 -n 表示安静模式。如果不加 -n 改为 sed 5,7p,那么第5-7行会重复输出:
1 | |
部分数据的查找并替换的功能
除了整行的处理模式之外,sed 还可以对某行进行部分数据的查找和替换。基本上 sed 的查找与替换与 vi 相当的类似,它的格式如下所示:
1 | |
接下来我们来看一个取得IP数据的范例,我们将该任务拆解为多步,一段一段地处理。
步骤一:先观察原始信息,利用 /sbin/ifconfig 查询IP是什么?
1 | |
我们希望用关键词识别出 172.17.0.12,这是我所用学校服务器的内网私有IP地址。
步骤二:利用关键字配合 grep 选取出 inet 所在的那关键的一行。
1 | |
好了,现在只剩下一行了,但我们想只留下IP地址 addr:172.17.0.12 而将其它部分统统删除。
步骤三:先将IP前面的部分予以删除(用到正则表达式)。
1 | |
这里正则表达式中的 ^ 表示待查找的字符串在行首;. 表示一定有一个任意字符,* 表示重复前一个字符0到无穷多次,连在一起的 .* 就表示任意字符重复任意次;// 之间为空,也就表示删除的意思。
步骤四:将IP后面的部分也予以删除。
1 | |
这里 * 表示空格重复任意次;.* 表示任意字符重复任意次;$ 表示待查找的字符串在行尾。
我们再来继续研究 sed 与正则表达式的配合练习。假设我们只要 /etc/adduser.conf 文件中 UID 存在的那几行数据,但是有 # 在内的注释我们不要,而且空白行我们也不要,那么应该如何处理?可以通过这几个步骤来实践看看:
步骤一:先用 grep 将关键字 UID 所在行取出来。
1 | |
步骤二:删除掉注释之后的内容:
1 | |
这样原本注释的内容都变成了空白行,接下来我们删除空白行:
1 | |
注意这里的 ^$ 表示行首 ^ 和行尾 $ 之间没有字符,也即空白行。
直接修改文件内容(危险操作)
sed 的能耐可不止于我们上面所说的,它甚至可以直接修改文件的内容,而不必使用管道命令或数据流重定向。不过这个操作会修改到原始的文件,所以请你千万不要随便拿系统配置文件来测试。我们下面使用 regular_express.txt 文件来测试(文件可以去鸟哥的官网下载:regular_express.txt)。
1 | |
范例六:利用 sed 将 regular_express.txt 内每一行结尾若为 . 则换成 !。
1 | |
上面的 -i 选项可以让你的 sed 直接去修改后面所接的文件内容而不是由屏幕输出。
范例七:利用 sed 直接在 regular_express.txt 最后一行加入 # This is a test。
1 | |
由于 $ 代表的是最后一行,而 a 的操作是新增,因此是该文件最后新增。
sed 的 -i 选项可以直接修改文件内容,因此这功能非常有帮助。比如如果你有一个100万行的文件,你要在第100行加某些文字,此时使用vim可能会疯掉,因为文件太大了。此时就可以利用 sed 来直接修改与替换,而不需要使用 vim 去修改了。
4 字段操作命令 awk
相较于 sed 常常对一整行进行操作,awk 则倾向于将一行分为多个字段来处理。因此,awk 相当适合处理小型的文本数据,其运行模式通常是这样的:
1 | |
awk 后接两个单引号并加上大括号 {} 来设置想要对数据进行的处理操作。awk 可以处理后续接的文件,也可以读取来自前一个命令的标准输出。如前面所说,awk 主要是将每一行分为多个字段来处理,而默认的字段分隔符为空格键或 [Tab]键。举例来说,我们用 last 将登陆者的数据取出来(仅取出前3行):
1 | |
若我想取出账号与登陆者的IP,且账号与IP之间以 [Tab] 隔开,则会变成这样:
1 | |
注意,awk 的所有后续操作都是以单引号括住的,而 awk 的格式内容如果想要以 print 打印时,记得将非变量的文字部分使用双引号括起来,因为单引号已经是 awk 命令的固定用法了。此外,因为这里无论哪一行我们都要处理,因此就不需要有条件类型的限制。
另外,由上面的例子我们看到,在 awk 的括号内,每一行的每个字段都有变量名称($1,$2 等)。在上面的例子中,root 位于第1栏,故其变量名称为 $1;而 10.249.45.37 是第3栏,故它是 $3,后面以此类推。还有个变量比较特殊,那就是 $0,它表示一整行数据。由此可知,刚刚上面5行当中,整个 awk 的处理流程就是:
- 一次性读入第1行整行的数据并存入
$0,然后将其拆分为多个字段并写入$1、$2、$3等变量当中。 - 根据条件类型的限制,判断是否需要进行后面的操作(在上面这个例子中没有条件类型)。
- 完成所有操作与条件类型。
- 若还有后续行的数据,则重复上面1~3的步骤,直到所有的数据都读完为止。
经过这样的步骤,我们看到了 awk 是以行为一次处理的单位,而以字段为最小的处理单位。好了,那么如何快速地获得我们的数据有几行几列呢?这就需要 awk 的内置变量的帮忙。
| 变量名称 | 代表意义 |
|---|---|
NF |
每一行(也即 $0)所拥有的字段总数 |
NR |
目前 awk 所处理的是第几行数据 |
FS |
目前的分割字符,默认是空格键 |
我们继续以上面 last -n 3 的例子来做说明,如果我想要:
- 列出每一行的账号(也就是
$1); - 列出目前处理的行数(就是
awk内的NR变量); - 并且说明该行有多少字段(也就是
awk内的NF字段);
则可以这样:
1 | |
注意,在 awk 内的 NR、NF 等变量要用大写,且不需要有美元符号 $。
awk 的逻辑运算字符
既然要用到“条件”的类别,那么自然就需要一些逻辑运算,如下所示:
| 运算单元 | 代表意义 |
|---|---|
> |
大于 |
< |
小于 |
>= |
大于或等于 |
<= |
小于或等于 |
== |
等于 |
!= |
不等于 |
注意,逻辑运算即所谓的大于、小于等于等判断式上面,习惯上用==而不是=来表示,=符号在awk操作这里留给了变量赋值用。
我们来看下面一个例子。比如在/etc/passwd中是以冒号:来作为字段的分隔,该文件中第一字段为账号,第三字段为UID。如下所示:
1 | |
那假设我要查看第三栏小于10的数据,并且仅列出账号与第三列,那么可以这样做:
1 | |
诶,不过怎么第一行没有正确地显示出来?这是因为我们在读入第一行的时候,那些变量$1、$2等等默认还是以空格键做为分割,所以虽然我们定义了FS=":",但却仅能在第二行后才开始生效。那怎么办呢?我们可以预先设置awk的变量,利用BEGIN这个关键词,这样做:
1 | |
接下来我们来看如何用awk来完成计算功能。
假设我们有一个薪资数据表文件为pay.txt,内容如下:
1 | |
如何来计算每个人1st、2nd、3th的总额呢?而且我们还需要格式化输出。我们可以这样考虑:
第一行只是表头,所以第一行不进行求和而仅需要对表头进行打印(也即NR==1时处理)。
第二行以后进行求和(NR>=2以后处理)。
1 | |
上面的例子有几个重要事项应该要先说明:
awk的命令间隔:所有awk的操作,亦即在{}里的操作,如果有需要多个命令辅助时,可利用分号;间隔。- 逻辑运算中,如果是“等于”的情况,请务必使用
==; - 格式化输出时,在printf的格式设置中,务必加上
\n,才能分行(这里注意可以和Python的print函数和shell的echo函数做对比,此二者自带换行); - 与bash shell中的变量不同,awk中的变量可以直接使用,不需要加上$符号。
另外,awk的操作内{}也是支持if( 条件 )的,比如上面的命令也可以写为:
1 | |
第一种写法相较于第二种写法更好,因为比较有统一性。
除此之外,awk还可以帮我们进行循环计算,不过那属于比较高级的单独课程了,这里就不再多加以介绍。
5 习题
情景模拟题一
通过grep配合子命令$(command)来从大量文件中查找含有星号*的文件与内容。
- 我们先来看如何在
/etc下面找出含有星号*的文件与内容。
1 | |
注意,这里单引号''内的型号是正则表达式的字符,但由于我们要找的是星号,因此需要加上转义符\;而/etc/*的那个*是bash通配符中的“万用字符”,在这里代表拥有任意多个字符的文件名。
不过在上述的这个例子中,我们仅能找到/etc下第一层子目录的数据,无法找到次目录的数据。如果想要连同完整的/etc此目录数据,就得要这样做:
1 | |
如果只想列出文件名而不想列出内容的话,可以加个-l参数:
1 | |
- 又是文件数量会太多,比如如果我们要找的是全系统
/的话:
1 | |
芜湖,一运行这个命令,由于要打印的东西太多,终端直接卡死。这下该如何是好呢?此时我们可以通过管道命令以及xargs来处理。比如,让grep每次仅能处理10个文件名,我们可以:
a. 先用find去找出文件;
b. 用xargs将这些文件每次丢10个给grep来作为参数处理;
c. grep实际开始查找文件内容;
所以整个做法会变成这样:
1 | |
然而,从输出的结果看,数据量实在非常庞大,如果我们只想知道文件名的话也可以给grep加上-l参数:
1 | |
情景模拟题二
使用管道命令配合正则表达式建立新命令与新变量。我们想要建立一个名为myip的新命令,这个命令能够将我系统的IP识别出来并显示。而且我们想要有个新变量MYIP来记录我们的IP。
处理的方式如下所示:
首先根据我们前面所讲的ifconfig、sed与awk来取得我们的IP:
1 | |
接着,我们可以将此命令利用alias指定为myip,如下所示:
1 | |
最终,我们可以通过变量设置来处理MYIP。
1 | |
如果每次登陆都要生效,可以将alias与MYIP设置的那两行写入你的~/.bashrc即可。