# 文件的格式化与相关处理

不需要通过 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
1
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
1
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 
1
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
1
2
3
4
5
6
7
8
9
10

printf 除了可以格式化处理之外,还可以根据 ASCII 的数字与图形对应来显示数据,如下

# 范例 3: 列出 16 进制 45 代表的字符是什么
[mrcode@study tmp]$ printf '\x45\n'
E
# 可以将数值转换为字符,如果你会写 script 的话
# 可以测试下,20~80 之间的数值表示的字符是什么
1
2
3
4
5

printf 使用相当广泛,包括后面提到的 awk 以及在 c 程序语言中使用的屏幕输出,都是利用 printf。

printf 使用场景就是格式化输出,如果你要写自己的软件,把信息漂亮的输出到屏幕的话,可是很有用的

# awk:好用的数据处理工具

  • sed:常常用于一整行的处理
  • awk:倾向于将一行分成数个字段来处理

因此,awk 适合处理小型的数据处理。

awk '条件类型1{动作1} 条件类型2{动作2} ...' filename

awk 后可以跟文件,也可以接受前个指令的 standard output
awk 主要处理每一行的字段内的数据,他默认的分隔符为「空格键」或「tab 键」
1
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 表示整行数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

对于上面示例,awk 的处理流程是:

  1. 读入第一行,并将第一行的内容填入 $0、$1... 变量中
  2. 依据 条件类型 的限制,判断是否需要进行后面的 动作
  3. 做完所有的动作与条件类型
  4. 若还有后续的「行」数据,则重复上面 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 等变量不需要有 $ 并且需要大写
1
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
1
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
1
2
3
4
5

使用 awk 的计算功能,比如有如下的数据 pay.txt

Name		1st		2nd		3th
Mrcode	2300	2400	2500
DMTsai	2100	2000	2300
Mrcode2	4300	4200	4100
1
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 语句;多个条件动作之间使用空格分割;链接起来就完成了
1
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}'
1

笔者没有感觉这个 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

1
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:忽略大小写的不同
1
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 行被删除了
1
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
1
2
3
4

# cmp

cmp 主要也是对比两个文件,主要利用字节单位去对比

cmp [-l] file1 file2
-i:将所有的不同点的字节处都列出来。因为 cmp 预设仅会输出第一个发现的不同点
1
2
# 范例 1:用 cmp 比较 passwd.old 与 passwd.new
[mrcode@study testpw]$ cmp passwd.old passwd.new 
passwd.old passwd.new 不同:第 106 字节,第 4
1
2
3

# patch

patch 与 diff 可配合使用,diff 比较出不同,而 patch 则可以将「旧文件升级为新的文件」。

  1. 先比较新旧版本的差异
  2. 将差异制作成补丁文件
  3. 再由补丁文件更新旧文件
# 范例 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 这个文件;
 # 并且补丁中限制了行数。
1
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
# 透过上述方式安装所需软件
1
2
3
4
5
6
7
8
9
10
11
12

语法

patch -pN < patch_file  # 更新
patch -R -pN < patch_file  # 还原

选项与参数:
-p:后面可以接 取消几层目录 的意思
-R:代表还原,将新的文件还原成原来的旧文件
1
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 117 16:01 passwd.new
-rw-r--r--. 1 mrcode mrcode 2266 117 16:50 passwd.old	# 文件大小和new文件一样了
-rw-rw-r--. 1 mrcode mrcode  480 117 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 117 16:01 passwd.new
-rw-r--r--. 1 mrcode mrcode 2323 117 16:52 passwd.old
1
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.
1
2
3
4
5
6
7
8
9
10
11
12

最上面的一行就是 pr 处理之后的效果。依次是:文件时间、文件名、页码