# 文件的格式化与相关处理
不需要通过 vim 去编辑,而是通过数据流重导向配置 printf 功能以及 awk 指令,可以对文字信息进行排版显示
# 格式化打印:printf
比如将考试分数输出,姓名与科目及分数之间,稍微做个比较漂亮的版面,比如输出下面这样的表格
Name Chinese Enlish Math Average
DmTsai 80 60 92 77.33
VBird 75 55 80 70.00
Ken 60 90 70 73.33
2
3
4
上表数据主要分成 5 个字段,每个字段之间可以使用 tab 或空格进行分割。将上表存储到 printf.txt 文件中,后续会使用到这个文件进行练习。
由于每个字段的长度并不一样,所以要达到上表效果,就需要打印格式管理员 printf 来帮忙了
printf '打印格式' 实际类容
选项与参数:
关于格式方面的几个特殊样式:
\a 警告剩余输出
\b 退格键(backspace)
\f 清楚屏幕(form feed)
\n 输出新的一行
\r Enter 按键,换行
\t 水平的 tab 按键
\v 垂直的 tab 按键
\xNN NN 为两位数的数字,可以转换数字称为字符
关于 C 程序语言内,常见的变量格式:
%ns n 数字,s 表示 string,也就是多少个字符
%ni n 数字,i 表示 integer,多少整数数字
%N.nf n 与 N 都是数字,f 表示 floating(浮点),如果有小数,比如共 10 个位数,小数点 2 位数,则写成 %10.2f
2
3
4
5
6
7
8
9
10
11
12
13
14
15
下面进行练习
# 范例 1:将上面存储的 printf.txt 内容仅列出姓名与成绩,并且用 tab 分割
# 文件存储时,字段之间全部用 tab 隔开的,复制进去就变成下面展示这样了
[mrcode@study tmp]$ cat printf.txt
Name Chinese Enlish Math Average
DmTsai 80 60 92 77.33
VBird 75 55 80 70.00
Ken 60 90 70 73.33
# 由于 printf 不是管线命令,需要通过 cat 先提取出来内容
# %s 表示不固定长度的字符串,后面跟了一个空格,并使用横向制表符 \t 来格式化
[mrcode@study tmp]$ printf '%s \t %s \t %s \t %s \t %s \t \n ' $(cat printf.txt)
Name Chinese Enlish Math Average
DmTsai 80 60 92 77.33
VBird 75 55 80 70.00
Ken 60 90 70 73.33
2
3
4
5
6
7
8
9
10
11
12
13
14
可以看到上述的效果虽然好多了,但是还是没有对齐。可能是由于 Chinese 比其他的长度要长,导致对不齐,那么下面来固定长度
# 范例 2:将上述第二行以后,分别以字符串、整数、小数点来显示
# grep -v Name 排除包含 Name 字符的行
[mrcode@study tmp]$ printf '%10s %5i %5i %5i %8.2f \n' $(cat printf.txt | grep -v Name)
DmTsai 80 60 92 77.33
VBird 75 55 80 70.00
Ken 60 90 70 73.33
# 由于这里是格式化数字,所以第一行无法使用这里的表达式,如果使用将得到数字 0 的展示
# 展示效果好了很多
%10s:这一个字段永远显示 10 个字符宽度,不足的用空格补位
%8.2f:表示 00000.00
2
3
4
5
6
7
8
9
10
printf 除了可以格式化处理之外,还可以根据 ASCII 的数字与图形对应来显示数据,如下
# 范例 3: 列出 16 进制 45 代表的字符是什么
[mrcode@study tmp]$ printf '\x45\n'
E
# 可以将数值转换为字符,如果你会写 script 的话
# 可以测试下,20~80 之间的数值表示的字符是什么
2
3
4
5
printf 使用相当广泛,包括后面提到的 awk 以及在 c 程序语言中使用的屏幕输出,都是利用 printf。
printf 使用场景就是格式化输出,如果你要写自己的软件,把信息漂亮的输出到屏幕的话,可是很有用的
# awk:好用的数据处理工具
- sed:常常用于一整行的处理
- awk:倾向于将一行分成数个字段来处理
因此,awk 适合处理小型的数据处理。
awk '条件类型1{动作1} 条件类型2{动作2} ...' filename
awk 后可以跟文件,也可以接受前个指令的 standard output
awk 主要处理每一行的字段内的数据,他默认的分隔符为「空格键」或「tab 键」
2
3
4
# 范例:使用 last 将登录者数据取出来
[mrcode@study tmp]$ last -n 5 # 取出前 5 行
mrcode pts/1 192.168.0.105 Wed Jan 15 22:20 still logged in
mrcode pts/0 192.168.0.105 Wed Jan 15 22:20 still logged in
reboot system boot 3.10.0-1062.el7. Wed Jan 15 22:19 - 23:05 (00:45)
mrcode pts/1 192.168.0.105 Mon Jan 13 22:51 - 23:13 (00:22)
mrcode pts/0 192.168.0.105 Mon Jan 13 22:51 - 23:13 (00:22)
# 若要取出账户与登录 IP ,且账户与 IP 之间以 tab 隔开,可以这样写
[mrcode@study tmp]$ last -n 5 | awk '{print $1 "\t" $3}'
mrcode 192.168.0.105
mrcode 192.168.0.105
reboot boot
mrcode 192.168.0.105
mrcode 192.168.0.105
wtmp Fri
# 由于每一行数据都需要处理,所以不需要有条件类型
# 通过 print 功能将数据列出来
# 第 3 行数据被误判了,第二个字段中包含了空格
# 那么 $1 开始的变量表示哪一个字段,要注意的是:$0 表示整行数据
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
对于上面示例,awk 的处理流程是:
- 读入第一行,并将第一行的内容填入
$0、$1...
变量中 - 依据 条件类型 的限制,判断是否需要进行后面的 动作
- 做完所有的动作与条件类型
- 若还有后续的「行」数据,则重复上面 1~3 步骤,直到所有数据都处理完为止
awk 是「以行为一次处理的单位」而「以字段为最小的处理单位」,那么 awk 中还提供了以下变量信息
变量名称 | 含义 |
---|---|
NF | 每一行($0 )拥有的字段总数 |
NR | 目前 awk 所处理的是「第几行」数据 |
FS | 目前的分割字符,默认是空格 |
继续上面 last -n 5 的例子来做说明
# 想要列出每一行的账户:就是 $1
# 列出目前处理的行数:NR 变量
# 该行有多少字段:NF 变量
# 注意:在 awk 的格式内使用 print 打印时,非变量部分需要用双引号引用起来,因为 awk 动作是以单引号的
[mrcode@study ~]$ last -n 5 | awk '{print $1 "\t lines:" NR "\t columns:" NF}'
mrcode lines:1 columns:10
mrcode lines:2 columns:10
reboot lines:3 columns:11
mrcode lines:4 columns:10
mrcode lines:5 columns:10
lines:6 columns:0
wtmp lines:7 columns:7
# 注意 NF 等变量不需要有 $ 并且需要大写
2
3
4
5
6
7
8
9
10
11
12
13
# awk 的逻辑运算字符
既然有「条件」,那么就有逻辑运算符号
运算单元 | 代表意义 |
---|---|
> | 大于 |
< | 小于 |
>= | 大于或等于 |
>= | 小于或等于 |
== | 等于 |
!= | 不等于 |
范例:在 /etc/passwd 中是以冒号「:」来分割字段的,第一个字段为账户,第三字段则是 UID.
# 查阅 第三栏小于 10 以下的数据,并且仅列出账户与第三栏
# FS 是字段分隔符
[mrcode@study ~]$ 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
2
3
4
5
6
7
8
9
10
11
12
第一行,没有生效是为啥呢?在 awk 中,在上述定义中,FS 仅能在第二行开始,
# 需要使用关键字 BEGIN,对应的还有 END
[mrcode@study ~]$ cat /etc/passwd | awk 'BEGIN {FS=":"} $3 < 10 {print $1 "\t" $3}'
root 0
bin 1
daemon 2
2
3
4
5
使用 awk 的计算功能,比如有如下的数据 pay.txt
Name 1st 2nd 3th
Mrcode 2300 2400 2500
DMTsai 2100 2000 2300
Mrcode2 4300 4200 4100
2
3
4
# 计算每个人的总额,而且还要格式化输出
- 第一行是说明,不需要计算,所以需要使用条件 NR=1 时再处理
- 第二行才开始计算,NR >=2 才处理
[mrcode@study tmp]$ cat pay.txt |
> awk 'NR==1 {printf "%10s %10s %10s %10s %10s\n",$1,$2,$3,$4,"Total" }
> NR>=2 {total = $2 + $3 + $4 ; printf "%10s %10d %10d %10d %10.2f\n",$1,$2,$3,$4,total}'
Name 1st 2nd 3th Total
Mrcode 2300 2400 2500 7200.00
DMTsai 2100 2000 2300 6400.00
Mrcode2 4300 4200 4100 12600.00
为了方便复制,这里粘贴上完整的一行命令:cat pay.txt | awk 'NR==1 {printf "%10s %10s %10s %10s %10s\n",$1,$2,$3,$4,"Total" } NR>=2 {total = $2 + $3 + $4 ; printf "%10s %10d %10d %10d %10.2f\n",$1,$2,$3,$4,total}'
# 现在来分解上面指令
# 1. 在 awk 中,非变量需要使用双引号引用起来
# 2. 使用 printf 时,需要加上 \n 才能换行
# 下面的含义是,当是第一行的时候,执行打印个格式化,前面是格式化表达式
# 后面用逗号分割,给出对应内容,这里给出了 1~4 个字段,并新增了一个 total 字段
[mrcode@study tmp]$ cat pay.txt | awk 'NR==1 {printf "%10s %10s %10s %10s %10s\n",$1,$2,$3,$4,"total"}'
Name 1st 2nd 3th total
# 对于计算的讲解
# 1. 在{} 动作中可以设置变量,进行运算;这里设置了一个 total 变量,并把 1~3 个字段相加
# 2. 由于这里有多个指令,所以需要使用冒号 「;」 进行分割
# 3. 使用 printf 常规打印,第 5 个字段引用了动作内设置的变量 total,记住 awk 中引用变量不需要使用 % 符号
[mrcode@study tmp]$ cat pay.txt | awk 'NR>=2 {total=$1+$2+$3 ; printf "%10s %10d %10d %10d %10.2f\n",$1,$2,$3,$4,total}'
Mrcode 2300 2400 2500 4700.00
DMTsai 2100 2000 2300 4100.00
Mrcode2 4300 4200 4100 8500.00
# 那么上面两条是针对各自条件进行处理的,相当于 if 语句;多个条件动作之间使用空格分割;链接起来就完成了
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
利用 awk 可以帮助我们处理很多日常工作了,在 awk 的输出格式中,常常会以 printf 来辅助。另外在 {} 动作内,也支持 if(条件) 语句。那么上面的指令可以使用 if 来做,如下
cat pay.txt | awk '{if(NR==1) printf "%10s %10s %10s %10s %10s\n",$1,$2,$3,$4,"Total" } NR>=2 {total = $2 + $3 + $4 ; printf "%10s %10d %10d %10d %10.2f\n",$1,$2,$3,$4,total}'
笔者没有感觉这个 if 有多方便啊?
另外,awk 还可以进行循环计算,不过这个属于比较进阶的单独课程了
# 文件比对工具
通常会在同一个软件包的不同版本之间,比较配置文件与原始文件的差异的时候,就会用到文件对比。
很多时候所谓的对比,通常是用在 ASCII 纯文本的比对。常见的指令有 diff,还可以使用 cmp 来对比非纯文本。同时也可以使用 diff 建立分析文档,以处理补丁 patch 功能的文件
# diff
diff 用在比对两个文件之间的差异,以行为单位来比对的。一般是用在 ASCII 纯文本文件的比对上。
比如:将 /etc/passwd 删除第 4 行,第 6 行则替换为「no six line」,新文件放置到 /tmp/test 里,该如何做?
# 创建测试目录
[mrcode@study tmp]$ mkdir -p /tmp/testpw
[mrcode@study tmp]$ cd /tmp/testpw/
[mrcode@study testpw]$ cp /etc/passwd passwd.old
# sed -e 直接在命令行模式上修改;d 是删除,c是替换;前面 sed 中有讲到过的
# 这里把修改后的内容存到了 passwd.new 文件中
# sed 中有超过两个以上的动作时需要加 -e
[mrcode@study testpw]$ cat /etc/passwd | sed -e '4d' -e '6c no six line' > passwd.new
2
3
4
5
6
7
8
9
diff [-bBi] from-file to-file
选项与参数:
from-file:文件名,原始对比文件
to-file:文件名,目的比较文件
注意:两个文件,都可以使用 - 表示,- 代表 standard input
-b:忽略一行当中,仅有多个空白的差异;例如:“about me“ 与 “about me” 视为相同
-B:忽略空白行的差异
-i:忽略大小写的不同
2
3
4
5
6
7
8
9
10
# 范例 1:比对 passwd.old passwd.new 文件
[mrcode@study testpw]$ diff passwd.old passwd.new
4d3 # 左边第 4 行被删除(d)掉了,基准是右边第 3 行
< adm:x:3:4:adm:/var/adm:/sbin/nologin # 列出了左边被删除的那一行内容
6c5 # 左边第 6 行,被替换(c)成右边文件的第 5 行
< sync:x:5:0:sync:/sbin:/bin/sync # 左边文件第 6 行内容
---
> no six line # 右边文件第 5 行内容
# 注意这里的,左边第 4 行被删除意思是:左边文件是完整的,右边是修改之后的,右边与左边对比,原来的第 4 行被删除了
2
3
4
5
6
7
8
9
如果用 diff 去对比两个完全不相干的文件,是对比不出来什么的;另外 diff 还可以对比整个目录下的差异
# 范例:了解一下不同的开机执行等级(runlevel)内容有啥不同?假设你已经知道执行等级 0 与 5的启动脚本分别放置到 /etc/rc0.d 及 /etc/rc5.d 则可以对比下
[mrcode@study testpw]$ diff /etc/rc0.d/ /etc/rc5.d/
只在 /etc/rc0.d/ 存在:K90network
只在 /etc/rc5.d/ 存在:S10network
2
3
4
# cmp
cmp 主要也是对比两个文件,主要利用字节单位去对比
cmp [-l] file1 file2
-i:将所有的不同点的字节处都列出来。因为 cmp 预设仅会输出第一个发现的不同点
2
# 范例 1:用 cmp 比较 passwd.old 与 passwd.new
[mrcode@study testpw]$ cmp passwd.old passwd.new
passwd.old passwd.new 不同:第 106 字节,第 4 行
2
3
# patch
patch 与 diff 可配合使用,diff 比较出不同,而 patch 则可以将「旧文件升级为新的文件」。
- 先比较新旧版本的差异
- 将差异制作成补丁文件
- 再由补丁文件更新旧文件
# 范例 1:以 /tmp/testpw 内的 passwd.old 与 passwd.new 制作补丁文件
[mrcode@study testpw]$ diff -Naur passwd.old passwd.new > passwd.patch
[mrcode@study testpw]$ cat passwd.patch
--- passwd.old 2020-01-17 15:58:55.405462402 +0800 # 新旧文件的信息
+++ passwd.new 2020-01-17 16:01:03.115462402 +0800
@@ -1,9 +1,8 @@ # 新旧文件要修改数据的界定范围,旧文件在 1-0 行,新文件在 1-8 行
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
-adm:x:3:4:adm:/var/adm:/sbin/nologin # 左侧文件删除
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
-sync:x:5:0:sync:/sbin:/bin/sync # 左侧文件删除
+no six line # 右侧新加入
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
# 这里怎么理解? 可以理解为 old 文件是基准文件
# 根据这里的基准文件,看到 - 就剪掉,看到 + 就增加;执行完成后,则会得到 new 这个文件;
# 并且补丁中限制了行数。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
将 passwd.old 同步为 passwd.new 相同的内容,
# 由于系统未预装 patch 软件,需要将之前的 iso 镜像文件挂载
# 在虚拟机上找到顺序为 0 的控制器位置,选择 iso 文件,设备就能被 linux 找到了
[root@study ~]# mount /dev/sr0 /mnt/
mount: /dev/sr0 写保护,将以只读方式挂载
[root@study ~]# rpm -ivh /mnt/Packages/patch-2.*
警告:/mnt/Packages/patch-2.7.1-11.el7.x86_64.rpm: 头V3 RSA/SHA256 Signature, 密钥 ID f4a80eb5: NOKEY
准备中... ################################# [100%]
正在升级/安装...
1:patch-2.7.1-11.el7 ################################# [100%]
[root@study ~]# umount /mnt/
[root@study ~]# exit
# 透过上述方式安装所需软件
2
3
4
5
6
7
8
9
10
11
12
语法
patch -pN < patch_file # 更新
patch -R -pN < patch_file # 还原
选项与参数:
-p:后面可以接 取消几层目录 的意思
-R:代表还原,将新的文件还原成原来的旧文件
2
3
4
5
6
# 范例 2:将刚刚制作出来的 patch file 用来更新旧版本数据
[mrcode@study testpw]$ patch -p0 < passwd.patch
patching file passwd.old
[mrcode@study testpw]$ ll passwd.*
-rw-rw-r--. 1 mrcode mrcode 2266 1月 17 16:01 passwd.new
-rw-r--r--. 1 mrcode mrcode 2266 1月 17 16:50 passwd.old # 文件大小和new文件一样了
-rw-rw-r--. 1 mrcode mrcode 480 1月 17 16:38 passwd.patch
# 范例 3:恢复旧文件内容
[mrcode@study testpw]$ patch -R -p0 < passwd.patch
patching file passwd.old
[mrcode@study testpw]$ ll passwd.*
-rw-rw-r--. 1 mrcode mrcode 2266 1月 17 16:01 passwd.new
-rw-r--r--. 1 mrcode mrcode 2323 1月 17 16:52 passwd.old
2
3
4
5
6
7
8
9
10
11
12
13
14
这里为什么会使用 -p0
?因为两个文件在同一个目录下,因此不需要减去目录。如果是整体目录比对(diff 旧目录 新目录)时,就要依据建立 patch 文件所在目录来进行目录删减
更详细的 patch 用法在后续的第二十章「原始码编译」
# 文件打印准备:pr
在图形界面中的文字处理软件,打印时可以选择每一页的标头和页码,在文字界面下,可以使用 pr 来实现,由于 pr 参数实在太多了,这里使用最简单的方式来处理
# 打印 /etc/man_db.conf
[mrcode@study testpw]$ pr /etc/man_db.conf
2018-10-31 04:26 /etc/man_db.conf 第 1 页
#
#
# This file is used by the man-db package to configure the man and cat paths.
# It is also used to provide a manpath for those without one by examining
# their PATH environment variable. For details see the manpath(5) man page.
2
3
4
5
6
7
8
9
10
11
12
最上面的一行就是 pr 处理之后的效果。依次是:文件时间、文件名、页码