Shell编程之文本处理

211 阅读9分钟

「这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

你必须非常努力,才能看起来毫不费力!

微信搜索公众号[ 漫漫Coding路 ],一起From Zero To Hero !

前言

在日常工作学习中,不免经常要对文本文件(例如日志文件)进行处理工作,包括但不限于分割、查找、替换、删除等操作,Shell 中有没有相应的命令供我们使用呢?那么本篇文章,我们就一起来学习下吧!

cut

grep 命令可以查找文件中符合条件的行,cut 命令则可以根据分隔符,提取行中的列,默认分隔符为 TAB

# cut [选项]  文件名

选项

-f 列号:	提取第几列
-d 分隔符:	按照指定分隔符分割列


# cut默认分隔符是TAB

测试一下这个命令就容易理解了。我们首先新建一个文件,然后使用 TAB 分隔,然后使用 cut 命令提取列。

示例一

[root@VM-0-5-centos ~]# vim student.txt
ID      NAME    Gender  MARK
1       zs      M       98
2       ls      F       99

# 1. 提取第二列
[root@VM-0-5-centos ~]# cut -f 2 student.txt
NAME
zs
ls

# 2. 提取第二列和第四列
[root@VM-0-5-centos ~]# cut -f 2,4 student.txt
NAME	MARK
zs	98
ls	99

# 3. 测试使用分隔符提取列

# 3.1 这是我们需要处理的文本
[root@VM-0-5-centos ~]# cat /etc/passwd | grep /bin/bash | grep -v root
lifelmy:x:1000:1000::/home/lifelmy:/bin/bash
user1:x:1001:1001::/home/user1:/bin/bash
user3:x:1002:1003::/home/user3:/bin/bash

# 3.2 提取第一列
[root@VM-0-5-centos ~]# cat /etc/passwd | grep /bin/bash | grep -v root | cut -f 1 -d :
lifelmy
user1
user3

从上面的例子我们可以看出,cut 命令可以提取行中的列,但是 cut 命令有一个缺点,就是分隔符的长度必须一致。如果列与列之间,有的是一个空格符,有的是两个空格符的话,cut 命令就不能正确划分列了。

示例二

[root@VM-0-5-centos ~]# df  -h
文件系统        容量  已用  可用 已用% 挂载点
devtmpfs        909M     0  909M    0% /dev
tmpfs           919M   24K  919M    1% /dev/shm
tmpfs           919M  540K  919M    1% /run
tmpfs           919M     0  919M    0% /sys/fs/cgroup
/dev/vda1        50G  5.6G   42G   12% /
tmpfs           184M     0  184M    0% /run/user/0

# 使用 空格 作为分隔符,查出第一列
[root@VM-0-5-centos ~]# df  -h | cut -f 1 -d " "
文件系统
devtmpfs
tmpfs
tmpfs
tmpfs
/dev/vda1
tmpfs

# 但是查第二列的时候,得到的都是空格,不是我们想要的"容量"那一列
[root@VM-0-5-centos ~]# df  -h | cut -f 2 -d " "

awk

从上面的示例二可以看出,cut 还是有些缺点的,那么这个问题有没有其他命令可以解决呢,这就轮到我们的 awk 命令出场了。在详细介绍 awk 命令前,我们先来看一下标准输出命令。

在awk命令的输出中支持 printprintf 命令

  • print: 在每个输出之后自动加入一个换行符(Linux默认没有print命令)
  • printf: 标准格式输出命令,并不会自动加入换行符,如果需要换行,需要手动加入换行符

printf

printf "输出类型 输出格式" 输出内容

[输出类型]:

%ns:	输出字符串,n表示输出几个字符
%ni:	输出整数,n表示输出几个数字
%m.nf:	输出浮点数,m、n分别表示输出的位数以及其中的小数位数. %8.2f表示共输出8位数,其中2位是小数,6位整数



[输出格式]:

\a: 输出警告声音
\b:	输出退格键
\f:	清除屏幕
\n:	换行
\r:	回车
\t:	水平输出制表符
\v:	垂直输出制表符

示例

# %-5s 格式为左对齐且宽度为5的字符串代替('-'表示左对齐),不使用则默认右对齐。
# %-4.2f 格式为左对齐宽度为4,保留两位小数。

[root@VM-0-5-centos ~]# printf "%-5s %-10s %-4s\n" NO Name Mark
NO    Name       Mark
[root@VM-0-5-centos ~]# printf "%-5s %-10s %-4.2f\n" 01 Tom 90.3456
01    Tom        90.35
[root@VM-0-5-centos ~]# printf "%-5s %-10s %-4.2f\n" 02 Jack 89.2345
02    Jack       89.23
[root@VM-0-5-centos ~]# printf "%-5s %-10s %-4.2f\n" 03 Jeff 98.4323
03    Jeff       98.43


[root@VM-0-5-centos ~]# printf '%d %d %d\n' 12 34 56
12 34 56

awk 是一种编程语言,用于在linux/unix下对文本和数据进行处理。数据可以来自标准输入(stdin)、一个或多个文件,或其它命令的输出。它支持用户自定义函数和动态正则表达式等先进功能,是linux/unix下的一个强大编程工具。它在命令行中使用,但更多是作为脚本来使用。awk有很多内建的功能,比如数组、函数等,这是它和C语言的相同之处,灵活性是awk最大的优势。

awk 'BEGIN{动作} 条件1{动作1} 条件2{动作2} END{动作}' file


条件(Pattern):
     一般使用关系表达式作为条件
     x>10
     x<=10
 
动作(Action):
 		格式化输出
 		流程控制语句

一个awk脚本由三部分组成:BEGIN语句块、能够使用条件匹配的通用语句块、END语句块3部分组成,这三个部分是可选的。任意一个部分都可以不出现在脚本中。

BEGIN条件,会在读取文件第一行之前执行相应的动作;END条件,读取完文件后执行。

测试

# 1. 在处理第一行文件时先执行BEGIN对应的动作,然后无条件对每一行使用默认的空格分隔后取出第5列,最后执行 END 对应的动作
[root@VM-0-5-centos ~]# df -h | awk 'BEGIN{printf "this is begin \n"} {printf $5 "\n"} END{printf "this is end \n"}'
this is begin
已用%
0%
1%
1%
0%
12%
0%
this is end

# 2. 在处理第一行文件前赋值 i=0,然后每处理一行就将i加一,最后输出i的值
[root@VM-0-5-centos ~]# df -h | awk 'BEGIN{ i=0 } { i++ } END{ print i }'
7

# 3. 将1、2两个例子结合起来
[root@VM-0-5-centos ~]# df -h | awk 'BEGIN{i=0 ;printf "start \n"} {printf $5 "\t"} {i++;  printf i "\n"}  END{printf "end\n"}'
start
已用%	1
0%	2
1%	3
1%	4
0%	5
12%	6
0%	7
end

由于 awk 命令的默认分隔符是空格(没有数量限制),因此对于分隔符不是空格的文件来说,BEGIN 的主要作用就是在处理文件前设置分隔符,其中FS 用来设置分隔符。

# 要处理的数据
[root@VM-0-5-centos ~]# cat /etc/passwd | grep syslog
syslog:x:996:994::/home/syslog:/bin/false

# 使用 BEGIN 设置分隔符为":",然后输出第三列数据
[root@VM-0-5-centos ~]# cat /etc/passwd | grep syslog | awk 'BEGIN {FS=":"} {printf $3 "\n"}'
996

# 使用关系运算符条件
[root@VM-0-5-centos ~]# cat /etc/passwd  | awk 'BEGIN {FS=":"} $3>995{printf $3 "\n"}'
999
998
997
996
1000
1001
1002

下面给出一个具体问题,思考下你会怎么处理。上面我们已经使用过了 df -h 命令,该命令是用于查看Linux的磁盘占用情况,我们想要设置个定时任务,每天去检查某个磁盘的占用情况,如果磁盘占用比例到了80%,就发送邮件报警。这里我们只处理第一步,就是判断磁盘占用情况是否超出了设置的阈值。

脚本文件

#!/bin/bash

read -p 'please input rate: ' r
rate=$(df -h | grep 'vda1'| awk '{printf $5}' | cut -d '%' -f 1)
if [ $rate -gt $r ]
        then
                echo 'alarm'
fi

测试

[root@VM-0-5-centos ~]# chmod 755 test.sh

[root@VM-0-5-centos ~]# ./test.sh
please input rate: 20
[root@VM-0-5-centos ~]# ./test.sh
please input rate: 10
alarm

sed

sed 是一种几乎包括所有unix平台(包括Linux)的轻量级流编辑器,它是文本处理中非常重要的工具,能够完美的配合正则表达式使用,功能不同凡响。

处理时,把当前处理的行存储在临时缓冲区中,称为“模式空间”(pattern space),接着用 sed 命令处理缓冲区中的内容,处理完成后,把缓冲区的内容送往屏幕。接着处理下一行,这样不断重复,直到文件末尾。文件内容并没有改变,除非你使用重定向存储输出。sed 主要用来自动编辑一个或多个文件;简化对文件的反复操作;编写转换程序等。

# sed [选项] '动作' 文件名

选项:

-n  一般sed命令会把所有数据都输出到屏幕,如果加入此选择,则只会把经过sed命令处理的行输出到屏幕
-e	允许对输入数据应用多条sed命令编辑
-i  用sed的修改结果直接修改读取数据的文件,而不是由屏幕输出

动作:

a:  追加,在当前行后添加一行或多行。添加多行时,除最后一行外,每行末尾需要用'\' 代表数据未完结
c:  行替换,用'c' 后面的字符串替换原数据行,替换多行时,除最后一行外,每行末尾用'\' 表示数据未完结
i:  插入,在当前行前插入一行或多行。插入多行时,除最后一行外,每行末尾用'\' 表示数据未完结
d:	 删除,删除指定的行
p:   打印,输出指定的行
s:   字符串替换,用一个字符串替换另一个字符串。格式为 "第几行s/旧字串/新字串/g",使用 '/g'表示替换该行中的所有匹配  (与vim类似)

测试

# 要处理的文件
[root@VM-0-5-centos ~]# cat student.txt
ID	NAME	Gender	MARK
1	zs	M	98
2	ls	F	99

# 打印输出第2行,默认不仅会输出指定的行,还会输出原有文件中的所有行
[root@VM-0-5-centos ~]# sed '2p' student.txt
ID	NAME	Gender	MARK
1	zs	M	98
1	zs	M	98
2	ls	F	99

# -n,只输出sed处理的数据
[root@VM-0-5-centos ~]# sed -n '2p' student.txt
1	zs	M	98

# 删除第2行的数据,但不修改原文件 (在缓冲区修改的)
[root@VM-0-5-centos ~]# sed '2d' student.txt
ID	NAME	Gender	MARK
2	ls	F	99
[root@VM-0-5-centos ~]# cat student.txt
ID	NAME	Gender	MARK
1	zs	M	98
2	ls	F	99

# 在第3行后追加一条数据,不修改原数据 (在缓冲区修改的)
[root@VM-0-5-centos ~]# sed '3a hello world' student.txt
ID	NAME	Gender	MARK
1	zs	M	98
2	ls	F	99
hello world

# 在第3行前插入一条数据,不修改原数据 (在缓冲区修改的)
[root@VM-0-5-centos ~]# sed '3i hello world' student.txt
ID	NAME	Gender	MARK
1	zs	M	98
hello world
2	ls	F	99

# 替换第3行的数据,不修改原数据 (在缓冲区修改的)
[root@VM-0-5-centos ~]# sed '3c hello world' student.txt
ID	NAME	Gender	MARK
1	zs	M	98
hello world

# 替换字符串字段,不修改原数据 (在缓冲区修改的)
[root@VM-0-5-centos ~]# sed  '2s/98/100/g' student.txt
ID	NAME	Gender	MARK
1	zs	M	100
2	ls	F	99

# 字符串替换,直接修改文件
[root@VM-0-5-centos ~]# sed -i '2s/98/100/g' student.txt
[root@VM-0-5-centos ~]# cat student.txt
ID	NAME	Gender	MARK
1	zs	M	100
2	ls	F	99

# 替换多处
[root@VM-0-5-centos ~]# sed -i '2s/M/F/; 2s/zs/ww/' student.txt
[root@VM-0-5-centos ~]# cat student.txt
ID	NAME	Gender	MARK
1	ww	F	100
2	ls	F	99

总结

本篇文章一共学习了三个命令:

  • cut:用于提取一行文本中的列,默认分隔符是 TAB ,同时也可以指定分隔符。如果分隔符是空格,需要保证每列之间的空格数相同;

  • awk:一种编程语言,功能强大,支持数组、函数等,类似 C语言;

  • sed:功能强大的流式文本编辑器,可以很方便的处理文件中的每一行数据。

PS:本文只是简单的介绍了命令的常用部分,如果想要更加深入了解,可以去这里学习。

更多

个人博客: lifelmy.github.io/

微信公众号:漫漫Coding路