Linux操作系统(八):Linux下的正在表达式与文件格式化处理
- shell中的正则表达式一些基本概念
- 正则表达式的基础应用
- 文件的格式化与相关处理
- 文件对比工具
一、关于本文内容的导读
这部分不涉及具体内容的解析,只是作为浏览和查找相关知识点的引导内容,采用【主题 | 命令 | 对应内容小节编号】三个关键信息的组合模式,依照这些信息可以快速查找到相关详细的示例和解析。
关于正则表达式相关的内容,我在几年前就有一篇非常详细的博客,但是基于JavaScript做的解析,由于正则表达式是用于处理字符串,它本身的机制与语言并没有什么关系,所以关于正则匹配的底层机制这里就不详细分析了,如果有兴趣可以参考这篇博客: 正则表达式基于JavaScript的入门详解,这里重点来关注正则表达式在shell中的相关使用。
基于正则表达式实现数据过滤的管道命令 | egrep | grep -E | 3.1
实现替换、删除、新增、选取特定行的管道工具 | sed | 3.2
格式化打印 | printf | 4.1
数据处理工具 | awk | 4.2
文本文件对比工具 | diff | 5.1
二进制文件对比工具 | cmp | 5.2
基于补丁文件实现更新或还原 | patch | 5.3
文件打印工具 | pr | (没有介绍,只是在这里列出来,用作后续检索了解这个命令的用途)
二、什么是正则表达式与基础应用
2.1基本知识点
1.转义字符:关于转义字符很容易理解,除了正则表达式中其他一些字符编辑中也都有涉及,但在shell中需要注意{}用作重复限定符时需要使用转义,因为它与变量取值是冲突的; 2.元字符:简单的来说就是一些有特殊含义的字符,但在shell中与其他语言也有一些差异,后面具体介绍; 3.区间——方括号[]:表示匹配字符的范围,中号内可以是由单个字符组合起来的集合、也可以是区间范围的表达,也可以是一些特殊字符或元字符的组合表达式; 4.重复限定符(量词):表示前一个字符重复匹配多少次,具体的表达方式{n}重复匹配n次、{n,}最小重复匹配n次、{n,m}匹配n次到m次; 5.边界与属性:用来定义一些匹配规则,具体后面在相关内容中解析; 6.分组与子表达式及反向引用:可以用于正则表达式内多个字符的匹配; 7.条件或:在编程语言中逻辑符是不可或缺的,正则表达式中同样也有这样的需求,后面具体介绍以下相关使用;
2.2shell中的正则表达式特殊符号
[:alnaum:]:代表英文大小写字符及数字,即0~9、A~Z、a~z; [:alpha:]:代表英文大小写,即A~Z、a~z; [:blank:]:代表空格键与【tab】按键两者; [:cntrl:]:代表键盘上的控制键,包括CR、LF、Tab、Del等; [:digit:]:代表数字,即0~9; [:graph:]:除空格符(空格键、【tab】键)以外的其他所有按键; [:lower:]:代表小写字符; [:print:]:代表任意可以被打印出来的字符; [:punct:]:代表标点符号,即:"'?!;:#$; [:upper:]:代表大写字符,即A~Z; [:space:]:任何会产生空白的字符,包括空格键、[Tab]、CR等; [:xdigit:]:代表十六进制的数字类型,包括0~9、A~F、a~f的数字与字符;
关于这些特殊字符的应用需要注意的是它需要写在区间中,特殊字符本身的中括号并不表示区间,比如要匹配内容中匹配大写字符要这样写[[:upper:]],然后就是shell的特殊字符类似其他编程语言中的一些元字符的功能,比如在其他编程语言中[:alnaum:]使用\w来表示,具体可以对照我之前的那一篇博客。
2.3shell中正则表达式的区间
在前面的基本概念2.1中解释了区间的概念,比如用正则的区间实现匹配字符,这个字符的取值范围是abc,那区间的表达式就是[abc],如果要匹配全部大小写字母就可以使用特殊字符来实现[[:alpha:]],在区间的表达式中除了逐个字符和特殊字符的表达方式还可以使用[-]的方式来实现匹配字符集中的字符,比如在ascll中大写字母十进制序号是65开始到90,因为字符集这种特定的结构,在日常用语中我们为了比较方便的表达它们会使用第65~90的ascll码字符来表达所有大写字母,还可以使用A~Z的方式来表达所有大写字母,与之相对应的在正则表达式中的表达方式就是[A-Z]。
但需要注意的是,因为[A-Z]的匹配方式是基于字符集对照表的排列结构,所以在不同的字符集中使用[A-Z]匹配的范围会有差异,比如在基于big5的字符编码[A-Z]匹配的结果是AaBbCc...zZ这样的,这就可能会导致与实际匹配到的内容不一致,所以如果建议使用UTF8的字符集来解决问题,因为UTF8是基于ascll扩展而来的,而正则表达式的规则是基于ascll,所以在使用正则表达式时要注意字符编码集这个问题。查看字符编码设置的命令是locale,注意LANG=这个配置信息,相关字符编码集语系设置查阅相关文档吧,这里就不赘述了。
#区间与特殊字符的转换示例
[[:alnaum:]]===[0-9A-z];
[[:alpha:]]===[A-z]===[A-Za-z];
2.4重复限定符
用于指定匹配字符连续重复匹配的次数。
*:表示前一个字符匹配任意次,可以是零次,也可以是多次;
+:表示前一个字符至少匹配一次,也可以是多次;
?:表示前一个字符匹配一次,或零次;
{n}:表示前一个字符匹配n次;
{n,}:表示前一个字符至少匹配n次,也可以是大于n次的更多;
{n,m}:表示前一个字符至少匹配n次,最多匹配m次;
2.5边界限定符
^:在整个匹配字符的最前面表示匹配字符必须是内容的起始位置;
$:在整个匹配字符的最后面表示匹配字符必须是内容的结束位置;
2.6其他正则规则字符的功能解析
[^]:在中括号内部最前面使用^符号表示取该集合的反向,即表示非的意思;
.:通配符,表示匹配一个任意字符;
()、()+:群组,可以在小括号内实现多个字符的匹配表达式,当然单独用于匹配多个字符没有什么意义,可以使用()+实现多个字符匹配多次;
(|):表示在一个群组内可以同时编写多个匹配表达式,比如表示匹配139、151、181三个开头的电话号码可以这样实现^(139|151|181)[0-9]{8}$
三、正则表达式的基础应用
3.1grep -E及egrep命令
关于grep在上一篇shell与Bash博客中的管道命令中就有介绍,当使用grep -E或者egrep命令就是grep使用正则实现指定内容筛选,下面先准备一个测试文件:
cd /tmp; vim re.txt #在tmp目录下创建测试文件re.txt,具体vim的编辑过程就不说了
测试文件的内容:
"Open Source" is a good mechanism to develop programs. apple is my favorite food. Football game is not use feet only. this dress doesn't fit me. However, this dress is about $ 3183 dollars .^M GNU is free air not free beer .^M Her hair is very beauty .^M I can't finish the test .^M Oh! The soup taste good .M motorcycle is cheap than car. This window is clear. the symbol '*' is represented as start. Oh! My god! The gd software is a library for drafting programs .^M You are the best is mean you are the no. 1. The world <Happy> is the same with "glad". I like dog. google is the best tools for search keyword. goooooogle yes! go! go! Let's go. # I am VBird
确保文件内容编辑完成并保存以后,下面进入egrep命令的正则匹配筛选示例测试:
#查找包含特定字符串的整行数据
grep -n 'the' re.txt #这个就是普通的字符串查询筛选
#使用集合[]实现查找包含test、taste这两个字符的整行数据
grep -n 't[ae]st' re.txt #默认情况下grep是基于basic正则匹配规则
egrep -n 't[ae]st' re.txt #这个是基于extended的匹配规则
grep -En 't[ae]st' re.txt #这一行命令实现的效果与上两行的一样,只是通过选项-E将grep的匹配规则指定为extended
#匹配非小写字母开头且后面连续两个“o”的字符串
grep -n '[^a-z]oo' re.txt #使用[-]区间实现
grep -n '[[:upper:]]oo' re.txt #虽然这一行命令实现的效果与上面一行命令一致,但它们所匹配的表达式实际是不一样的
上面^a-z是除小写字母以外的任意字符开头的*oo的字符,但[:upper:]只能匹配大写字母,只是它们的筛选结果在这个示例中是一样的而已。在bash中支持两种正则匹配模式,一种是basic,另一种是extended,basic相对extended的匹配规则要简单一些,有一些比较复杂的匹配在basic中没有实现,比如在2.6中介绍到的群组使用默认的basic就无法实现匹配,就需要使用egrep或者grep -E来实现匹配,所以如果使用的是正则匹配建议就直接使用egrep命令。
然后,再来看egrep实现几个示例:
#下面这个几个示例使用基于basic的正则匹配同样能实现
egrep -n 'g..d' re.txt
egrep -n '\.$' re.txt
egrep -n 'g*g' re.txt
egrep -n 'g.*g' re.txt
egrep -n '[0-9][0-9]*' re.txt
egrep -n '[o\{2\}]' re.txt
egrep -n '[o\{2,5\}]' re.txt
egrep -n '\{2,\}' re.txt
3.2使用sed工具实现替换、删除、新增、选取特定行的管道工具
sed [-nefr] [操作]
由于sed本身还是一个管道工具,所以它默认情况下只会操作stdin的数据,而不会操作实际读取的文件数据,可以使用选项-f将操作后的数据写入到指定的文件,也可以使用选项-i将操作直接作用到读取的文件上,详细的相关内容建议查看man文档,下面了解一些sed命令的常用选项和参数解析:
选项解析 -n:安静模式,默认情况下来自stdin的数据一般都会被列出来,如果使用-n选项则只会将sed处理的那一行列出来; -e:直接在命令行模式上进行sed的编辑操作; -f:直接将sed的操作写在一个文件内; -r:使用扩展正则表达式的语法,也就是+、?、|、()、()+的相关匹配规则; -i:直接修改读取的文件内容,而不是屏幕输出。 操作说明:[n1[,n2]] function n1,n2:用于选取操作的行数,可选 具体的操作方法function有: a:新增,后面可以接字符,字符会被添加到当前操作行的下一行; c:替换,后面可以接字符,这些字符可以替换n1,n2之间的内容; d:删除,删除选取行; i:插入,后面可接字符,字符会被添加到当前操作行的上一行; p:打印,将选取的数据打印出来; s:替换,根据正则表达式匹配实现替换;
sed命令的相关示例:
#删除第2~5行
nl re.txt | sed '2,5d' #这里只会删除标准输入中2~5行的数据,删除而不会直接俄作用到re.txt文件上,可以使用nl再次查看文件内容核对
nl re.txt
#在第三行新增一些字符
nl re.txt | sed '2a drink rea' #原本第三行的数据会向下移,这里的dirink rea会作为独立的一行添加到第三行
#在第二行新增一些字符
nl re.txt | sed '2i Drink rea or ...' #原本第二行数据会向下移,这里的Dirink rea or ...会被作为独立的一行添加到第二行
#用新的内容替换2~5行
nl re.txt | sed '2,5c No 2-5 number' #原数据中的2-5行数据会被替换成No 2-5 number
通过上面的示例大概了解了sed的基本使用,下面来看sed的s操作替换语法,然后基于这个语法与正则匹配实现删除与替换:
sed 's/要替换的字符/新增字符/g‘ #要替换的字符就可以使用正则表达式来匹配,如果不输入新增字符就会将匹配的内容删除,g表示全局匹配
cat re.txt | egrep 'go!' | sed 's/\( \?\)go!\( \?\)//g' #通过egrep 'go!'可以筛选出第二十行的数据,然后通过sed删除正则表达式匹配到的内容
#打印结果:Let's go.
cat re.txt | egrep 'go!' | sed 's/\( \?\)go!\( \?\)//' #对比测试以下不使用g全局匹配的输出结果:go! Let's go.
cat re.txt | egrep 'go!' | sed 's/\( \?\)go!\( \?\)/abcd/g' #对比使用新增字符替换匹配内容:abcdabcdLet's go.,从输出可以看到全局匹配多少个就会替换多少个
通过上面的示例就可以看到基于正则匹配就可以实现非常方便的数据处理,想象以下在一个内容非常多且重复结构的数据中,使用正则的方式查找、删除、替换是一种非常高效的处理方式,但由于这篇博客主要是介绍以下正则表达式在shell中的应用,正则的原理就不做太多解析,如果又不了解的可以查看我之前那篇基于JavaScript的正则博客,那里非常详细的介绍了正则匹配的规则,正则可能在不同的语言和环境下有一些实现差异,但总体的规则是不变的,多做一些测试就了解其中的细微差异了。
四、文件的格式化与相关处理
这里的文件格式化是指将文件内容编排成固定的格式输出,类似于文档排版和超文本中的html的属性和css的文本排版样式的作用。还是跟前面正则测试一样先来创建一个测试文件printf.txt,文件内容如下(下面的内容模拟分数统计信息,数据字符之间使用【tab】和空格间隔,手动编辑的文本不需要要求整齐,因为排版就是要解决文件的整齐性问题的):
Name Chinese English Math Average DmTsai 80 60 90 77.33 VBird 75 55 80 70.00 Ken 60 90 70 73.33
4.1格式化打印:printf
printf '打印格式' 实际内容
printf命令的选项与参数:
关于printf的打印格式特殊样式: \a:警告声输出; \b:退格键; \f:清除屏幕; \n:输出新的一行; \r:回车按键; \t:水平的【tab】按键; \v:垂直的【tab】按键; \xNN:NN为两位数的数字,可以转换数字成为字符; 关于C语言程序内,常见的变量格式: %ns:n是数字,s代表string,即多少个字符; %ni:n是数字,i代表integer,即多少整数位数; %N.nf:N与n是数字,f表示floating(浮点),例如总共有十位数,但小数点有两位表示为%10.2f
使用printf命令对测试文件printf.txt进行格式化排版在终端上打印出来的示例:
printf '%s\t %s\t %s\t %s\t %s\t \n' $(cat printf.txt)
这里没办法将输出结果用文本的方式粘贴过来了,因为在不同的文本解析器下对文件字符的解析排列是不一样的,下面是打印结果的截图:
这里先不讨论它的匹配规则,从这个示例可以看到printf就是将实际输出的内容按照自己的匹配排版规则打印到终端,然后需要注意的是printf不是管道命令,他需要使用$()的方式来将标准输出作为临时的变量值传给printf使用。
本质上printf命令就是C语言中的printf函数,如果了解C语言的话这个就很容易理解了,这里就着示例来简单的解释以下,%\s代表匹配不定长的字符,\t表示在字符串中间使用【tab】作为分隔,\n表示一行结束后使用换行符实现换行,总体的匹配规则就是基于字符之间的空字符将数据分段匹配,然后对每一段字符占用字符长度和字符类型解析格式化输出。在示例中的打印格式\t后面还有一个空格,实际上每个字符串中间就是一个【tab】键和一个空格实现的间隔。由上面的示例可以看到字段的长度不固定导致行与行之间不能对齐,这个就可以通过设置字符的位数长度来解决。
printf '%10s %10s %10s %10s %10s \n' $(cat printf.txt)
这次的输出结果:
然后需要注意的是printf打印的数据它还可以对数据类型做处理,因为不同的类型数据有不同的变量格式,也就是\s、\i、\f这些区别,下面来上上面的示例还可以通过下面的命令实现一样的效果:
printf '%10s %10s %10s %10s %10s \n' $(cat printf.txt | grep 'Name');printf '%10s %10i %10i %10i %10.2f \n' $(cat printf.txt | grep -v 'Name')
printf除了格式化数据还可以根据ASCLL的字符的不同机制编码显示数据,例如下面这个打印十六进制的字符E示例:
printf '\x45\n' #打印结果:E
4.2数据处理工具:awk
还记得前面在介绍正则表达式的时候的sed命令处理数据吗?sed是在一行数据的基础上实现数据处理,而在前面的printf中它会基于空白字符将一行数据拆分来处理,这就给批量处理有排列规则的数据打下了基础,然后结合awk就可以实现对数据的每一行分段进行处理,而且在系统管理中这样有排版规律的数据还非常多,比如文件信息数据、用户登入信息、进程状态数据等,下面先来看一下awk的语法:
awk '条件类型1{操作1} 条件类型2{操作2} ...' filename
在这个简化的命令语法中并没有列出选项,而是只有参数,并不是awk没有选项,而是关于选项在这个命令使用中并不是重点,如果有必要查阅man文档即可。关于awk命令的使用重点在于使用参数内的数据处理机制,其中包括一些特定的符号和逻辑运算,关于这些内容下面通过示例逐步来解析,下面先来看示例:
last -n 5 #查看最近5条用户登入信息
#打印结果:
root pts/1 192.168.1.102 Sat Jul 16 12:59 still logged in
root pts/1 192.168.1.102 Sat Jul 16 11:25 - 12:36 (01:10)
tx pts/0 :0 Sat Jul 16 11:24 still logged in
tx :0 :0 Sat Jul 16 11:24 still logged in
reboot system boot 3.10.0-1160.el7. Sat Jul 16 11:21 - 07:01 (19:39)
基于awk取出用户登录信息中的用户名和登入IP,账号和IP之间使用【tab】隔开:
last -n 5 | awk '{print $1 "\t" $3}' #查看最近登入的5个用户的名称及登入IP
打印结果:
root 192.168.1.102
root 192.168.1.102
tx :0
tx :0
reboot boot
在这个示例中使用到了print操作,这个操作跟printf一样可以将每行数据根据空格符【tab】键和空格键将数据进行切割分段,然后通过$n的方式取出每个分段,注意分段编号是从1开始,$0表示的是整行数据。print除了可以通过$n获取对应的数据,还内置了了一些变量具体如下:
NF:表示每一行有几个字段;
NR:表示当前awk处理的是第几行数据;
FS:表示用来分隔字段的字符,默认情况下使用的是空格符【tab】键和空格键,也可以通过这个变量来自定义分隔符;
测试awk中的print操作的变量:
last -n 5 | awk '{print $1 "\t 当前行:"NR"\t 当前行的字段数"NF}'
测试结果:
root 当前行:1 当前行的字段数10
root 当前行:2 当前行的字段数10
root 当前行:3 当前行的字段数10
tx 当前行:4 当前行的字段数10
tx 当前行:5 当前行的字段数10
通过这个实例可以看到,使用变量不需要使用引号包裹,下面再来了解以下在语法中提到的条件类型,先来来看在awk中可以使用的条件运算符:>、<、>=、<=、==、!=,这些条件运算几乎在所有编程语言中都是一样的,下面直接来看一个awk的条件类型示例:
cat /etc/passwd | awk '{FS=":"} $3 < 10 {print $1 "\t" $3}'
打印结果:
root:x:0:0:root:/root:/bin/bash
bin 1
daemon 2
adm 3
lp 4
sync 5
shutdown 6
halt 7
mail 8
从示例中可以看到一个问题,就是第一行数据没有被切割分段,这是因为在读入第一行的时候还默认是使用空格符切割分段,这个需要使用到BEGIN关键字来解决:
cat /etc/passwd | awk 'BEGIN {FS=":"} $3 < 10 {print $1 "\t" $3}'
在awk中除了BEGIN关键字,还有END关键字,详细参考man文档。
有了上面这些awk示例的练习以后,可以感受到其功能的强大,但也值得思考的是既然可以将数据分段提取出来了,那可不可以使用这些数据做一些更多的事情能,毕竟数据不仅仅只是字符串的概念,比如数学运算,这个还真可以,比如我们再来基于前面的printf.txt做下面这样的测试:
cat printf.txt | \
awk '{if(NR==1) printf "%10s %10s %10s %10s %10s\n",$1,$2,$3,$4,"Acerage"}
NR>=2{Acerage = ($2 + $3 + $4)/3
printf "%10s %10d %10d %10d %10.2f\n",$1,$2,$3,$4,Acerage}'
打印的结果:
关于格式化处理就就介绍到这里把,这里涉及比较多的编程概念,没办法在一篇博客中全部介绍完,想要了解更多建议参考man文档和借鉴更多应用示例练习。
五、文件对比工具
文件对比通常用于同一个软件包的不同版本之间,比较配置文件与原文件的差异,当然对比并不是我们的最终目标,还要基于对比出来的差异生成补丁文件,从而实现使用补丁对相关程序进行升级,但在这里简单的介绍一下文件对比和补丁生成,关于更多的补丁升级在后面的源码编译中再详细介绍。
5.1文件差异对比工具diff
diff [-bBi] from-file to-file
diff命令的选项及参数解析:
from-file:原文件的文件名; to-file:目标文件的文件名; -b:忽略字符之间的空白差异,简单的说就是忽略字符之间的空格符个数的差异; -B:忽略空白行的差异; -i:忽略大小写的差异;
下面的示例就基于之前3.*的re.txt来测试,下面再来基于这个文件模拟创建一个新版文件:
cat re.txt |sed -e '4d' -e '6c no six line' > re.new #使用sed更新一些数据,然后将更新的数据保存为文件re.new
diff re.txt re.new #看下面的打印结果分析
4d3 -->表示原文件相对更新文件,第四行被删除了,基准是跟新文件的第三行数据
< this dress doesn't fit me.
6c5 -->表示原文件相对更新文件,第六行被替换了,基准是更新文件的第五行数据
< GNU is free air not free beer.
---
> no six line -->这里打印的是更新文件第五行数据,表达式相对原文件的第六行更新的数据
关于diff的打印信息参考sed命令的操作方法关键字,非常容易理解,关于diff更多的使用参考man文档。
5.2二进制文件对比工具:cmp
相对于diff的文本文件对比,cmp用作二进制文件对比,因为是二进制文件就不可能做详细的数据分析,diff主要是基于行为单位的对比,而cmp则是以字节为单位对比,这就导致一个问题,如果要对比全部的差异cmp只要一旦检查到了差异,那后面所有的内容对比都会显示差异。
cmp [-l] file1 file2 #-l表示将所有不同点的字符都列举出来
默认情况下只打印出第一个不同点的字符位置和行。
5.3制作补丁文件与更新及还原:patch
制作补丁还是需要用到diff,patch只是基于补丁文件实现文件更新和还原,下面先来看patch的语法介绍,然后再写一个完整的示例:
pathc -pN < patch_file #更新语法
patch -R -pN < patch_file #还原语法
patch的选项和参数:
-p:表示取消基层目录的意思;
-R:表示还原;
使用diff -Naur制作补丁文件:
diff -Naur re.txt re.new > re.patch #生成补丁文件
cat re.patch #查看补丁文件的内容
补丁文件内容分析:
--- re.txt 2022-07-16 11:31:03.690010380 +0800 +++ re.new 2022-07-17 20:27:16.038148057 +0800 #---->这上面两行是文件信息 @@ -1,9 +1,8 @@ #---->这里表示修改数据的界定范围,原文件再1~9行,更新文件再1~8行 "Open Source" is a good mechanism to develop programs. apple is my favorite food. Football game is not use feet only. -this dress doesn't fit me. #---->这里表示的是原文件中这行数据要删除 However, this dress is about $ 3183 dollars. -GNU is free air not free beer. #---->这里表示的是原文件中这行数据要删除 +no six line #---->这里表示的是原文件中要添加这行数据 Her hair is very beauty. I can't finish the test. Oh! The soup taste good.
一般情况下并不需要我们人工阅读补丁文件,只需要将这个补丁文件交给进行更新或还原的工具进行相关操作,下面基于re.patch补丁文件来测试更新re.txt:
patch -p0 < re.patch
cat re.txt #查看更新后的re.txt,你会发现它跟re.new的文件内容一模一样了
diff re.txt re.new #这时候测试它们的差异不会有任何信息输出,表示文件内容一致
patch -R -p0 < re.patch #然后再还原re.txt
diff re.txt re.new #这时候又会打印出之前的内容差异信息