第 1 章 Linux 进阶命令

作者: Brinnatt 分类: ARM64 Linux 进阶架构 发布时间: 2022-01-21 12:32

在 Primary School 篇,我们详细介绍了很多基础命令,也涉及很多系统层级的基础知识,都可以算作入门篇章。从这里开始,我们继续深入探索应用层面的相关知识,主要从工作中经常应用这个角度出发,总结一些宝贵经验。同样的,在没有特别说明情况下,所有的操作都是在 ARM64 架构服务器下完成。

1.1 高效处理文本

在工作当中,涉及到文本处理,最先想到的就是 grep、sed、awk 三个命令,俗称文本三剑客;这三个工具实际上借用正则表达式对文本进行匹配,然后进行动作处理。

即便是在编程领域,正则表达式用起来也有一定难度,所以下面我会介绍这三个工具的常规用法,如果满足不了需求,不防使用 python 编程,没有必要在一个工具上花费太多时间。

1.1.1、grep 命令

grep 全称是 Global Regular Expression Print,表示全局正则表达式版本,它的使用权限是所有用户。

grep 的工作方式是这样的,它在一个或多个文件中搜索字符串模板。如果模板包括空格,则必须被引用,模板后的所有字符串被看作文件名。搜索的结果被送到标准输出,不影响原文件内容。

grep 可用于 shell 脚本,因为 grep 通过返回一个状态值来说明搜索的状态,如果模板搜索成功,则返回 0,如果搜索不成功,则返回 1,如果搜索的文件不存在,则返回 2。我们利用这些返回值就可进行一些自动化的文本处理工作。

egrep = grep -E:扩展的正则表达式( 除了 \< , \> , \b 使用其他正则表达式都可以去掉转义符 \ )

1.1.1.1、grep 语法格式

命令格式:grep [options] pattern file

1.1.1.2、options 选项

-A[num]:            除了显示符合 pattern 的那一列之外,并显示该行之后的 num 行。
-B[num]:            除了显示符合 pattern 的那一行之外,并显示该行之前的 num 行。
-C[num]:            除了显示符合 pattern 的那一行之外,并显示该行之前后的 num 行。
-c:                 统计匹配的行数
-e:                 实现多个选项间的逻辑 or 关系
-E:                 扩展的正则表达式
-f FILE:            从 FILE 获取 PATTERN 匹配
-F:                 相当于 fgrep
-i --ignore-case:   忽略字符大小写的差别。
-n:                 显示匹配的行号
-o:                 仅显示匹配到的字符串
-q:                 静默模式,不输出任何信息
-s:                 不显示错误信息。
-v:                 显示不被 pattern 匹配到的行,相当于 [^] 反向匹配
-w:                 匹配整个单词
[root@arm64v8 ~]# cat test.txt 
aaaaa
bbbbb
ccccc
AAAbb
DDDDCCCBBB
[root@arm64v8 ~]# grep -A2 "b" test.txt 
bbbbb
ccccc
AAAbb
DDDDCCCBBB
[root@arm64v8 ~]# grep -e "AAA" -e "BBB" test.txt 
AAAbb
DDDDCCCBBB
[root@arm64v8 ~]# grep -in "b" test.txt 
2:bbbbb
4:AAAbb
5:DDDDCCCBBB
[root@arm64v8 ~]# grep -wn "aaaaa" test.txt 
1:aaaaa
[root@arm64v8 ~]#
[root@arm64v8 ~]# cat pattern.txt 
DDDDC
[root@arm64v8 ~]# grep -n -f pattern.txt test.txt 
5:DDDDCCCBBB
[root@arm64v8 ~]#

1.1.1.3、pattern 模式

基本正则表达式

匹配字符

.                           匹配任意单个字符,不能匹配空行
[]                          匹配指定范围内的任意单个字符
[^]                         取反
[:alnum:] or [0-9a-zA-Z]    匹配大小写字母和数字集
[:alpha:] or [a-zA-Z]       匹配大小写字母集
[:upper:] or [A-Z]          匹配大写字母集
[:lower:] or [a-z]          匹配小写字母集
[:blank:]                   匹配空白字符集(空格和制表符)
[:space:]                   匹配空格字符集(空格、制表符、垂直制表符、换行符、回车符和分页符)
[:cntrl:]                   匹配控制字符集(ASCII中,这些字符的八进制代码从000到037,还包括177(DEL))
[:digit:]                   匹配数字字符集(0-9)
[:xdigit:]                  匹配十六进制集(0-9A-Fa-f)
[:graph:]                   匹配绘图集(大小写字母、数字和标点符号)
[:print:]                   匹配打印字符集(大小写字母、数字、标点符号和空格)
[:punct:]                   匹配标点符号集 '! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ' { | } ~'。
[root@arm64v8 ~]# cat test.txt 
aaa
a1a
b//cd

cc44AA
[root@arm64v8 ~]# grep . test.txt 
aaa
a1a
b//cd
cc44AA
[root@arm64v8 ~]# grep "[a/]" test.txt 
aaa
a1a
b//cd
[root@arm64v8 ~]#
[root@arm64v8 ~]# grep [[:punct:]] test.txt 
b//cd
[root@arm64v8 ~]#

匹配次数

*           匹配前面的字符任意次,包括0次,贪婪模式:尽可能长的匹配
.*          匹配任意长度的任意字符,不包括0次
\?          匹配其前面的字符0 或 1次
\+          匹配其前面的字符至少1次
\{n\}       匹配前面的字符n次
\{m,n\}     匹配前面的字符至少m 次,至多n次
\{,n\}      匹配前面的字符至多n次
\{n,\}      匹配前面的字符至少n次
[root@arm64v8 ~]# cat test.txt 
ggle
gogle
google
gooooooooooooogle
gagle
[root@arm64v8 ~]# grep "g[o]*gle" test.txt 
ggle
gogle
google
gooooooooooooogle
[root@arm64v8 ~]# grep "g[o].*gle" test.txt 
gogle
google
gooooooooooooogle
[root@arm64v8 ~]# grep "g[o]\?gle" test.txt 
ggle
gogle
[root@arm64v8 ~]# grep "g[o]\+gle" test.txt 
gogle
google
gooooooooooooogle
[root@arm64v8 ~]# grep "g[o]\{1,2\}gle" test.txt 
gogle
google
[root@arm64v8 ~]# grep -E "g[o]{10,}gle" test.txt 
gooooooooooooogle
[root@arm64v8 ~]# egrep "g[o]{,10}gle" test.txt 
ggle
gogle
google
[root@arm64v8 ~]#

位置锚定:定位出现的位置

^                   行首锚定,用于模式的最左侧
$                   行尾锚定,用于模式的最右侧
^PATTERN$           用于模式匹配整行
^$                  空行
\< or \b             词首锚定,用于单词模式的左侧
\> or \b             词尾锚定;用于单词模式的右侧
\<PATTERN\>           匹配整个PATTERN
[root@arm64v8 ~]# cat test.txt 
aaa
bbb

abcd
[root@arm64v8 ~]# grep ^a test.txt 
aaa
abcd
[root@arm64v8 ~]# grep b$ test.txt 
bbb
[root@arm64v8 ~]# grep ^$ test.txt 

[root@arm64v8 ~]#
[root@arm64v8 ~]# grep "\<a.*d\>" test.txt 
abcd
[root@arm64v8 ~]#

分组和后向引用

1. 分组:\(\) 将一个或多个字符捆绑在一起,当作一个整体进行处理
   分组括号中的模式匹配到的内容会被正则表达式引擎记录于内部的变量中,这些变量的命名方式为: \1, \2, \3, ...

2. 后向引用
   引用前面的分组括号中的模式所匹配字符,而非模式本身
   \1 表示从左侧起第一个左括号以及与之配对右括号之间的模式所匹配到的字符
   \2 表示从左侧起第2个左括号以及与之配对右括号之间的模式所匹配到的字符,以此类推
   \& 表示前面的分组中所有字符
[root@arm64v8 ~]# cat test.txt 
He beautifies his beauty.
She beautifies her pretty.
[root@arm64v8 ~]# 
[root@arm64v8 ~]# grep "\(beau\).*\1" test.txt 
He beautifies his beauty.
[root@arm64v8 ~]#

扩展正则表达式

(1)字符匹配:

 .  任意单个字符
 []  指定范围的字符
 [^] 不在指定范围的字符
   次数匹配:
 * :匹配前面字符任意次
 ?  : 0 或1次
 + :1 次或多次
 {m} :匹配m次 次
 {m,n} :至少m ,至多n次
(2)位置锚定:

 ^ : 行首
 $ : 行尾
 \<, \b : 语首
 \>, \b : 语尾
   分组:()
 后向引用:\1, \2, ...
(3)总结

  除了\<, \b : 语首、\>, \b : 语尾;使用其他正则都可以去掉\;上面有演示案例,不再进行演示

1.1.2、sed 命令

sed 是一个流式编辑器程序,其工作路线大致如下:

  1. 它读取输入流(可以是文件、标准输入)的每一行放进模式空间(pattern space),同时将此行行号通过 sed 行号计数器记录在内存中;
  2. 然后对模式空间中的行进行模式匹配,如果能匹配上则使用 sed 程序内部的命令进行处理,处理结束后,从模式空间中输出(默认)出去,并清空模式空间;
  3. 随后再从输入流中读取下一行到模式空间中进行相同的操作,直到输入流中的所有行都处理完成。由此可见,sed 是一个循环一个循环处理内容的。

1.1.2.1、sed 语法格式

sed OPTIONS SCRIPT INPUT_STREAM
  • 其中 SCRIPT 部分就是所谓的 sed 脚本,它是 sed 内部命令的集合,sed 中的命令有些奇特,它包含行匹配以及要执行的命令。
    • 格式为 ADDR1[,ADDR2]cmd_list
    • 例如,要对第 2 行执行删除命令,其命令为 sed 2d filename,只输出第 4 行到 6 行,其命令为 sed -n 4,6p
  • 既然 SCRIPT 是命令的集合,于是 sed 循环过程可以修改为如下:
    1. 读取输入流的一行到模式空间
    2. 对模式空间中内容执行 SCRIPT(包括上面示例中的 "2d" 和 "4,6p")
    3. 读取输入流的下一行到模式空间
    4. 对模式空间中内容执行 SCRIPT

其中 SCRIPT 部分包含了 sed 命令行中的内部命令,还包括两个特殊动作:自动输出和清空模式空间内容。这两个动作是一定会执行的,只不过有些时候通过某些命令可以使其输出空内容、使其清空不了模式空间。

如果使用编程结构来描述,则大致过程如下:

for ((line=1;line<=last_line_num;++line))
do
    read $line to pattern_space;
    while pattern_space is not null
    do
        execute cmd1 in SCRIPT;
        execute cmd2 in SCRIPT;
        execute cmd3 in SCRIPT;
        ……
        auto_print;
        remove_pattern_space;
    done
done
  • 其中 while 循环执行的正是 SCRIPT 中的所有命令,只不过一般情况下,while 循环只执行一轮就退出并进入外层的 for 循环。

  • 于是,外层的 for 循环称之为 "sed循环",内层的 while 循环称之为 "SCRIPT" 循环。所以,for 循环只包含了两个动作:读取下一行和执行 SCRIPT 循环。

  • 其实 while 循环中是有 continue、break 甚至是 exit 的,分别表示回到 SCRIPT 的顶端(即进入下一个 SCRIPT 循环)、退出当前 SCRIPT 循环回到外层 sed 循环以及退出整个 sed 循环。

  • 最后,说明下 sed 命令行如何书写,其实就是写 SCRIPT 部分,这部分的写法比较灵活,大致有以下几种:

    # 一行式,多个命令使用分号分隔
    sed Address{cmd1;cmd2;cmd3...}
    
    # 多个表达式时,可以使用"-e"选项,也可以不用,但使用分号分隔
    sed Address1{cmd1;cmd2;cmd3};Address2{cmd1;cmd2;cmd3}...
    sed  -e 'Address1{cmd1;cmd2;cmd3}' -e 'Address2{cmd1;cmd2;cmd3}' ...
    
    # 分行写时
    sed Address1{
      cmd1
      cmd2
      cmd3
    }
    Address2{
      cmd1
      cmd2
      cmd3
    }
  • 如果是写在文件中,即 sed 脚本,以文件名为 a.sed 为例。

    #!/usr/bin/sed -f
    #注释行
    Address1{cmd1;cmd2...}
    Address2{cmd1;cmd2...}
    ......
    • 其中 cmd 部分还可以进行模式匹配,也即类似于 "Address{{pattern1}cmd1;{pattern2}cmd2}" 的写法。例如,/^abc/{2d;p}

1.1.2.2、options 选项

-n:             默认情况下,sed将在每轮script循环结束时自动输出模式空间中的内容。
                使用该选项后可以使得这次自动输出动作输出空内容,而不是当前模式空间中的内容。
                注意,"-n"是输出空内容而不是禁用输出动作,有些依赖于输出动作和输出流的地方,它们的区别是很大的。

-e SCRIPT       可以指定多个"-e"选项向SCRIPT中添加命令。
                可以省略"-e"选项,但如果命令行容易产生歧义,则使用"-e"选项可明确说明这部分是SCRIPT中的命令。

-f SCRIPT-FILE  指定包含命令集合的SCRIPT文件,让sed根据SCRIPT文件中的命令集处理输入流。
-i[SUFFIX]      该选项指定要将sed的输出结果保存(覆盖的方式)到当前编辑的文件中。
                sed是通过创建一个临时文件并将处理后的结果写入到该临时文件,然后重命名为源文件来实现的。
                如果还提供了SUFFIX,则在重命名临时文件之前,先使用该SUFFIX修改源文件名,从而生成一个源文件的备份文件。
                如果SUFFIX中包含了一个或多个字符"*",则每个"*"都替换为原文件名(参见实例3)。
                该选项隐含了"-s"选项。

-r              使用扩展正则表达式,而不是使用默认的基础正则表达式。sed所支持的扩展正则表达式和egrep一样。

-s              默认情况下,如果为sed指定了多个输入文件,如sed OPTIONS SCRIPT file1 file2 file3,
                则多个文件会被sed当作一个长的输入流,也就是说所有文件被当成一个大文件。
                指定该选项后,sed将认为命令行中给定的每个文件都是独立的输入流。

制作实验样本文件

[root@arm64v8 ~]# grep -v "^$" /etc/fstab | grep -v "^#$" > fstab.txt
[root@arm64v8 ~]# cat fstab.txt 
# /etc/fstab
# Created by anaconda on Tue Aug 24 08:30:54 2021
# Accessible filesystems, by reference, are maintained under '/dev/disk'
# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info
UUID=7d2ddfd1-f735-4176-b2a8-cf64cc13ffa0 /                       ext4    defaults        1 1
UUID=02d21f87-4742-4e2f-92c1-e8df1001a781 /boot                   ext4    defaults        1 2
UUID=67D1-D2C2          /boot/efi               vfat    umask=0077,shortname=winnt 0 0
UUID=8272fd7c-6bfa-47d2-af97-429aa83ab510 swap                    swap    defaults        0 0
[root@arm64v8 ~]#

实例 1:只输出 fstab.txt 第 4 行。

[root@arm64v8 ~]# sed -n 4p fstab.txt 
# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info
[root@arm64v8 ~]#
  • 这里使用了 "-n" 选项,使得读取到模式空间的每一行都无法被输出,只有明确使用了 "p" 选项才能被 "p" 动作输出。
  • 上面的命令和 sed -n -e 4p fstab.txt 是完全一样的,因为 "4p" 在 sed 解析命令行时不会产生歧义,所以可以省略 "-e" 选项。

实例 2:输出 fstab.txt,并输出每行的行号。

[root@arm64v8 ~]# sed "=" fstab.txt 
1
# /etc/fstab
2
# Created by anaconda on Tue Aug 24 08:30:54 2021
3
# Accessible filesystems, by reference, are maintained under '/dev/disk'
4
# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info
5
UUID=7d2ddfd1-f735-4176-b2a8-cf64cc13ffa0 /                       ext4    defaults        1 1
6
UUID=02d21f87-4742-4e2f-92c1-e8df1001a781 /boot                   ext4    defaults        1 2
7
UUID=67D1-D2C2          /boot/efi               vfat    umask=0077,shortname=winnt 0 0
8
UUID=8272fd7c-6bfa-47d2-af97-429aa83ab510 swap                    swap    defaults        0 0
[root@arm64v8 ~]#
  • 由于要输出 fstab.txt 的内容,所以不使用 "-n" 选项,同时 "=" 命令会输出每行行号。

实例 3:输出 fstab.txt 第 4 行,并以 ".bak" 后缀备份源文件。

[root@arm64v8 ~]# cat fstab.txt 
# /etc/fstab
# Created by anaconda on Tue Aug 24 08:30:54 2021
# Accessible filesystems, by reference, are maintained under '/dev/disk'
# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info
UUID=7d2ddfd1-f735-4176-b2a8-cf64cc13ffa0 /                       ext4    defaults        1 1
UUID=02d21f87-4742-4e2f-92c1-e8df1001a781 /boot                   ext4    defaults        1 2
UUID=67D1-D2C2          /boot/efi               vfat    umask=0077,shortname=winnt 0 0
UUID=8272fd7c-6bfa-47d2-af97-429aa83ab510 swap                    swap    defaults        0 0
[root@arm64v8 ~]# 
[root@arm64v8 ~]# sed -i'.bak' -n '4p' fstab.txt 
[root@arm64v8 ~]# 
[root@arm64v8 ~]# ls
fstab.txt  fstab.txt.bak
[root@arm64v8 ~]# 
[root@arm64v8 ~]# cat fstab.txt
# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info
[root@arm64v8 ~]#
[root@arm64v8 ~]# ls
fstab.txt
[root@arm64v8 ~]#
[root@arm64v8 ~]# sed -i'*-*.bak' -n '4p' fstab.txt 
[root@arm64v8 ~]# ls
fstab.txt  fstab.txt-fstab.txt.bak
[root@arm64v8 ~]# 
[root@arm64v8 ~]# cat fstab.txt
# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info
[root@arm64v8 ~]#
[root@arm64v8 ~]# cat fstab.txt-fstab.txt.bak 
# /etc/fstab
# Created by anaconda on Tue Aug 24 08:30:54 2021
# Accessible filesystems, by reference, are maintained under '/dev/disk'
# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info
UUID=7d2ddfd1-f735-4176-b2a8-cf64cc13ffa0 /                       ext4    defaults        1 1
UUID=02d21f87-4742-4e2f-92c1-e8df1001a781 /boot                   ext4    defaults        1 2
UUID=67D1-D2C2          /boot/efi               vfat    umask=0077,shortname=winnt 0 0
UUID=8272fd7c-6bfa-47d2-af97-429aa83ab510 swap                    swap    defaults        0 0
[root@arm64v8 ~]#

实例 4:使用扩展正则表达式,输出 fstab.txt 包含连续 4 个以上大写字母的行。

[root@arm64v8 ~]# cat fstab.txt 
# /etc/fstab
# Created by anaconda on Tue Aug 24 08:30:54 2021
# Accessible filesystems, by reference, are maintained under '/dev/disk'
# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info
UUID=7d2ddfd1-f735-4176-b2a8-cf64cc13ffa0 /                       ext4    defaults        1 1
UUID=02d21f87-4742-4e2f-92c1-e8df1001a781 /boot                   ext4    defaults        1 2
UUID=67D1-D2C2          /boot/efi               vfat    umask=0077,shortname=winnt 0 0
UUID=8272fd7c-6bfa-47d2-af97-429aa83ab510 swap                    swap    defaults        0 0
[root@arm64v8 ~]# 
[root@arm64v8 ~]# 
[root@arm64v8 ~]# sed -r -n "/[[:upper:]]{4,}/p" fstab.txt 
UUID=7d2ddfd1-f735-4176-b2a8-cf64cc13ffa0 /                       ext4    defaults        1 1
UUID=02d21f87-4742-4e2f-92c1-e8df1001a781 /boot                   ext4    defaults        1 2
UUID=67D1-D2C2          /boot/efi               vfat    umask=0077,shortname=winnt 0 0
UUID=8272fd7c-6bfa-47d2-af97-429aa83ab510 swap                    swap    defaults        0 0
[root@arm64v8 ~]#

1.1.2.3、address 定址表达式

当 sed 将输入流中的行读取到模式空间后,就需要对模式空间中的内容进行匹配,如果能匹配就能执行对应的命令,如果不能匹配就直接输出、清空模式空间并进入下一个 sed 循环读取下一行。

匹配的过程称为定址。定址表达式有多种,但总的来说,其格式为 [ADDR1][,ADDR2]。这可以分为 3 种方式:

  1. ADDR1 和 ADDR2 都省略时,表示所有行都能被匹配上。
  2. 省略 ADDR2 时,表示只有被 ADDR1 表达式匹配上的行才符合条件。
  3. 不省略 ADDR2 时,是范围地址。表示从 ADDR1 匹配成功的行开始,到 ADDR2 匹配成功的行结束。

无论是 ADDR1 还是 ADDR2,都可以使用两种方式进行匹配:行号和正则表达式。如下:

N:              指定一个行号,sed将只匹配该行。("-s"或"-i"选项将对多个文件的行连续计数)
FIRST~STEP:     表示从第FIRST行开始,每隔STEP行就再取一次。也就是取行号满足FIRST+(N*STEP) (其中N>=0)的行。
                因此,要选择所有奇数行,使用"1~2";要从第2行开始每隔3行取一次,使用"2~3";
                要从第10行开始每隔5行取一次,使用"10~5";而"50~0"则表示只取第50行。

$:              默认该符号匹配的是最后一个文件的最后一行。
                如果指定了"-i"或"-s",则匹配的是每个文件的最后一行。总之,"$"匹配的是每个输入流的最后一行。

/REGEXP/        将选择能被正则表达式REGEXP匹配的所有行。如果REGEXP中自身包含了字符"/",则必须使用反斜线转义,即"\/"。
/REGEXP/I       和"/REGEXP/"是一样的,只不过匹配的时候不区分大小写。
\%REGEXP%       这和上一个定址表达式的作用是一样的,只不过是使用符号"%"替换了符号"/"。
                当REGEXP中包含"/"符号时,使用该定址表达式就无需对"/"使用反斜线"\"转义。
                但如果此时REGEXP中包含了"%"符号时,该符号需要使用"\"转义。

ADDR1,+N        匹配ADDR1和其后的N行。
ADDR1,~N        匹配ADDR1和其后的行直到出现N的倍数行。
                倍数可为随意整数倍,只要N的倍数是最接近且大于ADDR1的即可。
                如ADDR1=1,N=3匹配1-3行,ADDR1=5,N=4匹配5-8行。而"1,+3"匹配的是第一行和其后的3行即1-4行。
  • 在定址表达式的后面加"!"符号表示反转匹配的含义。也就是说那些匹配的行将不被选择,而是不匹配的行被选择。

举几个实例:

[root@arm64v8 ~]# cat fstab.txt 
# /etc/fstab
# Created by anaconda on Tue Aug 24 08:30:54 2021
# Accessible filesystems, by reference, are maintained under '/dev/disk'
# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info
UUID=7d2ddfd1-f735-4176-b2a8-cf64cc13ffa0 /                       ext4    defaults        1 1
UUID=02d21f87-4742-4e2f-92c1-e8df1001a781 /boot                   ext4    defaults        1 2
UUID=67D1-D2C2          /boot/efi               vfat    umask=0077,shortname=winnt 0 0
UUID=8272fd7c-6bfa-47d2-af97-429aa83ab510 swap                    swap    defaults        0 0

[root@arm64v8 ~]# sed -n '3p' fstab.txt 
# Accessible filesystems, by reference, are maintained under '/dev/disk'

[root@arm64v8 ~]# sed -n '3,5!p' fstab.txt 
# /etc/fstab
# Created by anaconda on Tue Aug 24 08:30:54 2021
UUID=02d21f87-4742-4e2f-92c1-e8df1001a781 /boot                   ext4    defaults        1 2
UUID=67D1-D2C2          /boot/efi               vfat    umask=0077,shortname=winnt 0 0
UUID=8272fd7c-6bfa-47d2-af97-429aa83ab510 swap                    swap    defaults        0 0

[root@arm64v8 ~]# sed -n '2,/^#.*/p' fstab.txt 
# Created by anaconda on Tue Aug 24 08:30:54 2021
# Accessible filesystems, by reference, are maintained under '/dev/disk'

[root@arm64v8 ~]# sed -n '2,/^#.*/!p' fstab.txt 
# /etc/fstab
# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info
UUID=7d2ddfd1-f735-4176-b2a8-cf64cc13ffa0 /                       ext4    defaults        1 1
UUID=02d21f87-4742-4e2f-92c1-e8df1001a781 /boot                   ext4    defaults        1 2
UUID=67D1-D2C2          /boot/efi               vfat    umask=0077,shortname=winnt 0 0
UUID=8272fd7c-6bfa-47d2-af97-429aa83ab510 swap                    swap    defaults        0 0
[root@arm64v8 ~]#

1.1.2.4、command 功能命令

  1. 强制输出命令 "p"

    该命令能强制输出当前模式空间的内容。即使使用了 "-n" 选项。

    事实上,它们本就不冲突,因为循环过程如下:

    for ((line=1;line<=last_line_num;++line))
    do
       read $line to pattern_space;
       while pattern_space is not null
       do
           execute cmd1 in SCRIPT;
           execute cmd2 in SCRIPT;
           ADDR1,ADDR2{print};        # "p" command
           ……
           auto_print;
           remove_pattern_space;
       done
    done
    • 在 sed 处理的过程中,"p" 和 "auto_print" 是两个输出动作,都是输出当前模式空间的内容,只不过 auto_print 是隐含动作。

    • 使用了 "-n" 选项,其所影响的动作仅是 "auto_print",使其输出空内容。也因此,当没有使用 "-n" 选项时,模式空间的内容会被输出两次。

      [root@arm64v8 ~]# echo -e "abc\nxyz" | sed -n 2p
      xyz
      [root@arm64v8 ~]#
      [root@arm64v8 ~]# echo -e "abc\nxyz" | sed 2p
      abc
      xyz    # 这是p命令输出的结果
      xyz    # 这是自动输出的结果
      [root@arm64v8 ~]#
  2. 删除命令 "d"

    命令 "d" 用于删除整个模式空间中的内容,并立即退出当前 SCRIPT 循环,进入下一个 sed 循环,即读取下一行。

    循环大致格式如下:

    for ((line=1;line<=last_line_num;++line))
    do
       read $line to pattern_space;
       while pattern_space is not null
       do
           execute cmd1 in SCRIPT;
           execute cmd2 in SCRIPT;
           ADDR1,ADDR2{delete;break};     # "d" command
           ……
           auto_print;
           remove_pattern_space;
       done
    done

    唯一需要注意的一点是立即退出当前 SCRIPT 循环,这意味着如果 "d" 命令后面还有其他的命令,则这些命令都不会执行。

    举几个实例:

    # 删除fstab.txt中的第5行,并保存到原文件中,并备份原fstab.txt为fstab.txt.bak
    [root@arm64v8 ~]# cat fstab.txt 
    # /etc/fstab
    # Created by anaconda on Tue Aug 24 08:30:54 2021
    # Accessible filesystems, by reference, are maintained under '/dev/disk'
    # See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info
    UUID=7d2ddfd1-f735-4176-b2a8-cf64cc13ffa0 /                       ext4    defaults        1 1
    UUID=02d21f87-4742-4e2f-92c1-e8df1001a781 /boot                   ext4    defaults        1 2
    UUID=67D1-D2C2          /boot/efi               vfat    umask=0077,shortname=winnt 0 0
    UUID=8272fd7c-6bfa-47d2-af97-429aa83ab510 swap                    swap    defaults        0 0
    [root@arm64v8 ~]# 
    [root@arm64v8 ~]# sed -i'.bak' '5d' fstab.txt 
    [root@arm64v8 ~]# cat fstab.txt
    # /etc/fstab
    # Created by anaconda on Tue Aug 24 08:30:54 2021
    # Accessible filesystems, by reference, are maintained under '/dev/disk'
    # See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info
    UUID=02d21f87-4742-4e2f-92c1-e8df1001a781 /boot                   ext4    defaults        1 2
    UUID=67D1-D2C2          /boot/efi               vfat    umask=0077,shortname=winnt 0 0
    UUID=8272fd7c-6bfa-47d2-af97-429aa83ab510 swap                    swap    defaults        0 0
    [root@arm64v8 ~]#
    # 删除fstab.txt中包含"#"开头的注释行,但第一行的# /etc/fstab不删除。
    [root@arm64v8 ~]# cat fstab.txt
    # /etc/fstab
    # Created by anaconda on Tue Aug 24 08:30:54 2021
    # Accessible filesystems, by reference, are maintained under '/dev/disk'
    # See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info
    UUID=02d21f87-4742-4e2f-92c1-e8df1001a781 /boot                   ext4    defaults        1 2
    UUID=67D1-D2C2          /boot/efi               vfat    umask=0077,shortname=winnt 0 0
    UUID=8272fd7c-6bfa-47d2-af97-429aa83ab510 swap                    swap    defaults        0 0
    [root@arm64v8 ~]# 
    [root@arm64v8 ~]# sed '/^#/{1!d}' fstab.txt
    # /etc/fstab
    UUID=02d21f87-4742-4e2f-92c1-e8df1001a781 /boot                   ext4    defaults        1 2
    UUID=67D1-D2C2          /boot/efi               vfat    umask=0077,shortname=winnt 0 0
    UUID=8272fd7c-6bfa-47d2-af97-429aa83ab510 swap                    swap    defaults        0 0
    [root@arm64v8 ~]#
    # 如果"d"后面还有命令,在删除模式空间后,这些命令不会执行,因为会立即退出当前SCRIPT循环。
    [root@arm64v8 ~]# echo -e "abc\nxyz" | sed '{/abc/d;=}'
    2
    xyz
    [root@arm64v8 ~]#
    • 其中 "=" 这个命令用于输出行号,但是结果并没有输出被 "abc" 匹配的行的行号。
  3. 退出 sed 程序命令 "q" 和 "Q"

    使用 "q" 和 "Q" 命令的作用是立即退出当前 sed 程序,使其不再执行后面的命令,也不再读取后面的行。因此,在处理大文件或大量文件时,使用 "q" 或 "Q" 命令能提高很大效率。

    它们之间的不同之处在于 "q" 命令被执行后还会使用自动输出动作输出模式空间的内容,除非使用了 "-n" 选项。而 "Q" 命令则会立即退出,不会输出模式空间内容。另外,可以为它们指定退出状态码,例如 "q 1"。

    使用了 "q" 和 "Q" 的 sed 循环结构大致如下:

    # "q"命令
    for ((line=1;line<=last_line_num;++line))
    do
       read $line to pattern_space;
       while pattern_space is not null
       do
           execute cmd1 in SCRIPT;
           execute cmd2 in SCRIPT;
           ADDR1,ADDR2{auto_print;exit};     # "q" command
           ……
           auto_print;
           remove_pattern_space;
       done
    done
    
    # "Q"命令
    for ((line=1;line<=last_line_num;++line))
    do
       read $line to pattern_space;
       while pattern_space is not null
       do
           execute cmd1 in SCRIPT;
           execute cmd2 in SCRIPT;
           ADDR1,ADDR2{exit};      # "Q" command
           ……
           auto_print;
           remove_pattern_space;
       done
    done

    举个例子:

    # 搜索脚本source.txt,当搜索到使用了"."或"source"命令加载环境配置脚本时就输出并立即退出。
    [root@arm64v8 ~]# cat source.txt
    source /etc/profile.d/bash_completions
      . /etc/init.d/functions
    ls /etc
    cat ~/.bashrc
    [root@arm64v8 ~]# 
    [root@arm64v8 ~]# sed -n -r "/^[ \t]*(\.|source) /{p;q}" source.txt 
    source /etc/profile.d/bash_completions
    [root@arm64v8 ~]# 
    [root@arm64v8 ~]# sed -n -r "/^[ \t]*(\.|source) /{p}" source.txt 
    source /etc/profile.d/bash_completions
      . /etc/init.d/functions
    [root@arm64v8 ~]#
  4. 输出行号命令 "="

    "=" 命令用于输出最近被读取行的行号。在 sed 内部,使用行号计数器进行行号计数,每读取一行,行号计数器加 1。

    计数器的值存储在内存中,在要求输出行号时,直接插入在输出流中的指定位置。由于值是存在于内存中,而非模式空间中,因此不受 "-n" 选项的影响。

    这是一个依赖于输出流的命令,只要有输出动作就会追加在该输出流的尾部。

    举个例子:

    # 搜索出httpd.conf中"DocumentRoot"开头的行的行号,允许有前导空白字符。
    [root@arm64v8 ~]# sed -n '/^[ \t]*DocumentRoot/{p;=}' httpd.conf 
    DocumentRoot "/var/www/html"
    119
    [root@arm64v8 ~]#
    • 如果 "=" 命令前没有 "p" 输出命令,且没有使用 "-n" 选项,则行号输出在 Document 所在行的前一行,因为 SCRIPT 最后的自动输出动作也有输出流。
  5. 字符一一对应替换命令 "y"

    该命令和 "tr" 命令的映射功能一样,都是将字符进行一一替换。

    举个例子:

    # 仔细观察,什么是一一映射替换,什么是整体替换,这个y命令和后面要讲的s命令要区分开
    [root@arm64v8 ~]# echo "YESYesyEs" | sed 'y/YES/yes/'
    yesyesyes
    [root@arm64v8 ~]# echo "YESYesyEs" | sed 's/YES/yes/'
    yesYesyEs
    [root@arm64v8 ~]#
  6. 替换命令 "s"

    这是 sed 用的最多的命令。两个字就能概括其功能:替换。将匹配到的内容替换成指定的内容。

    "s" 命令的语法格式为:其中 "/" 可以替换成任意其他单个字符。

    s/REGEXP/REPLACEMENT/FLAGS

    它使用 REGEXP 去匹配行,将匹配到的那部分字符替换成 REPLACEMENT。FLAGS 是 "s" 命令的修饰符,常见的有 "g"、"p" 和 "i" 或 "I"。

    • "g":表示替换行中所有能被 REGEXP 匹配的部分。不使用 g 时,默认只替换行中的第一个匹配内容。此外,"g" 还可以替换成一个数值 N,表示只替换行中第 N 个被匹配的内容。
    • "p":输出替换后模式空间中的内容。
    • "i" 或 "I":REGEXP 匹配时不区分大小写。

    REPLACEMENT 中可以使用 "\N"(N 是从 1 到 9 的整数) 进行后向引用,所代表的是 REGEXP 第 N 个括号 (...) 中匹配的内容。

    举几个例子:

    # 删除sysctl.conf中所有"#"开头(可以包括前导空白)的注释符号"#",第一行不处理
    [root@arm64v8 ~]# cat sysctl.conf 
    # sysctl settings are defined through files in
    # /usr/lib/sysctl.d/, /run/sysctl.d/, and /etc/sysctl.d/.
    #
    # Vendors settings live in /usr/lib/sysctl.d/.
    # To override a whole file, create a new file with the same in
    # /etc/sysctl.d/ and put new settings there. To override
    # only specific settings, add a file with a lexically later
    # name in /etc/sysctl.d/ and put new settings there.
    #
    # For more information, see sysctl.conf(5) and sysctl.d(5).
    [root@arm64v8 ~]# 
    [root@arm64v8 ~]# sed '2,$s/^[ \t]*#//' sysctl.conf 
    # sysctl settings are defined through files in
    /usr/lib/sysctl.d/, /run/sysctl.d/, and /etc/sysctl.d/.
    
    Vendors settings live in /usr/lib/sysctl.d/.
    To override a whole file, create a new file with the same in
    /etc/sysctl.d/ and put new settings there. To override
    only specific settings, add a file with a lexically later
    name in /etc/sysctl.d/ and put new settings there.
    
    For more information, see sysctl.conf(5) and sysctl.d(5).
    [root@arm64v8 ~]#
    # 为sysctl.conf文件中的第2行到最后一行的行首加上注释符号"#"。
    [root@arm64v8 ~]# cat sysctl.conf
    # sysctl settings are defined through files in
    /usr/lib/sysctl.d/, /run/sysctl.d/, and /etc/sysctl.d/.
    
    Vendors settings live in /usr/lib/sysctl.d/.
    To override a whole file, create a new file with the same in
    /etc/sysctl.d/ and put new settings there. To override
    only specific settings, add a file with a lexically later
    name in /etc/sysctl.d/ and put new settings there.
    
    For more information, see sysctl.conf(5) and sysctl.d(5).
    [root@arm64v8 ~]# 
    [root@arm64v8 ~]# sed '2,$s/^/#/' sysctl.conf 
    # sysctl settings are defined through files in
    # /usr/lib/sysctl.d/, /run/sysctl.d/, and /etc/sysctl.d/.
    #
    # Vendors settings live in /usr/lib/sysctl.d/.
    # To override a whole file, create a new file with the same in
    # /etc/sysctl.d/ and put new settings there. To override
    # only specific settings, add a file with a lexically later
    # name in /etc/sysctl.d/ and put new settings there.
    #
    # For more information, see sysctl.conf(5) and sysctl.d(5).
    [root@arm64v8 ~]# 
    # 将string.test中的love替换成like,对比思考
    [root@arm64v8 ~]# cat string.test 
    I love China love you love Country love you!
    [root@arm64v8 ~]# 
    [root@arm64v8 ~]# sed 's/\blove\b/like/' string.test 
    I like China love you love Country love you!
    [root@arm64v8 ~]# 
    [root@arm64v8 ~]# sed 's/\blove\b/like/g' string.test 
    I like China like you like Country like you!
    [root@arm64v8 ~]#
    # 偿试将cmd2和cmd3位置对调
    [root@arm64v8 ~]# echo "cmd1 && cmd2 || cmd3" | sed 's%&&\(.*\) || \(.*\)%%'
    cmd1 
    [root@arm64v8 ~]# echo "cmd1 && cmd2 || cmd3" | sed 's%&&\(.*\) || \(.*\)%&%'
    cmd1 && cmd2 || cmd3
    [root@arm64v8 ~]# echo "cmd1 && cmd2 || cmd3" | sed 's%&&\(.*\) || \(.*\)%\&\& \2 ||\1%'
    cmd1 && cmd3 || cmd2
    [root@arm64v8 ~]#
    • 这里使用了 "%" 代替 "/",且第三句在 REPLACEMENT 部分对 "&" 进行了转义,因为该符号在 REPLACEMENT 中表示的是引用 REGEXP 所匹配的所有内容。
  7. 追加、插入和修改命令 "a"、"i"、"c"

    这 3 个命令的格式是 "[a|i|c] TEXT",表示将 TEXT 内容队列化到内存中,当有输出流或者说有输出动作的时候,半路追上输出流,分别追加、插入和替换到该输出流然后输出。

    追加是指追加在输出流的尾部,插入是指插入在输出流的首部,替换是指将整个输出流替换掉。

    "c" 命令和 "a"、"i" 命令有一丝不同,它替换结束后立即退出当前 SCRIPT 循环,并进入下一个 sed 循环,因此 "c" 命令后的命令都不会被执行。

    举几个例子:

    [root@arm64v8 ~]# echo -e "you\nare\nsomething" | sed '/you/a really'
    you
    really
    are
    something
    [root@arm64v8 ~]# 
    • 其实 "a"、"i" 和 "c" 命令的 TEXT 部分写法是比较复杂的,如果TEXT只是几个简单字符,如上即可。
    • 但如果要 TEXT 是分行文本,或者包含了引号,或者这几个命令是写在 "{}" 中的,则上面的写法就无法实现。
    • 需要使用符号 "\" 来转义行尾符号,这表示开启一个新行,此后输入的内容都是 TEXT,直到遇到引号或者 ";" 开头的行时。
    # 在#!/bin/bash后行追加# Script filename: bash.sh和空行
    [root@arm64v8 ~]# cat bash.sh 
    #!/bin/bash
    [root@arm64v8 ~]#
    [root@arm64v8 ~]# sed '\@^#!/bin/bash@a\# Script filename: bash.sh\n' bash.sh 
    #!/bin/bash
    # Script filename: bash.sh
    
    [root@arm64v8 ~]#
    • 使用 @ 替代 / 表述正则表达式,但是 @ 符号需要转义 \@。

    • "a" 命令后的第一个反斜线用于标记 TEXT 的开始,"\n" 用于添加空白行。如果分行写,或者 "a" 命令写在大括号 "{}" 中,则格式如下:

      sed '\@^#!/bin/bash@a\
      # Script filename: bash.sh\n
      ' bash.sh 

    说明:这 3 个命令的 TEXT 是存放在内存中的,不会进入模式空间,因此不受 "-n" 选项或某些命令的影响。此外,这 3 个命令依赖于输出流,只要有输出动作,不管是空输出流还是非空的输出流,只要有输出,这几个命令就会半路 "劫杀"。

1.1.3、awk 命令

awk 是一种编程语言,用于在 linux/unix 下对文本和数据进行处理。数据可以来自标准输入(stdin)、一个或多个文件,或其它命令的输出。

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

1.1.3.1、安装新版本 gawk

awk 有很多种版本,例如 nawk、gawk。gawk 是 GNU awk,它的功能很丰富。

本教程采用的是 gawk 4.2.0 版本,4.2.0 版本的 gawk 是一个比较大的改版,新支持的一些特性非常好用,而在低于 4.2.0 版本时这些语法可能会报错。所以,请先安装 4.2.0 版本或更高版本的 gawk。

查看系统自带 awk 版本

[root@arm64v8 ~]# awk --version | grep "GNU Awk"
GNU Awk 4.0.2
[root@arm64v8 ~]#

这里以安装 gawk 4.2.0 为例

  1. 下载

    [root@arm64v8 ~]# wget --no-check-certificate https://mirrors.tuna.tsinghua.edu.cn/gnu/gawk/gawk-4.2.0.tar.gz
  2. 解压、进入解压后目录

    [root@arm64v8 ~]# tar xf gawk-4.2.0.tar.gz 
    [root@arm64v8 ~]# cd gawk-4.2.0/
    [root@arm64v8 gawk-4.2.0]#
  3. 编译,并执行安装目录为 /usr/local/gawk4.2

    [root@arm64v8 gawk-4.2.0]# ./configure --prefix=/usr/local/gawk4.2 && make && make install
  4. 创建一个软链接:让 awk 指向刚新装的 gawk 版本

    [root@arm64v8 gawk-4.2.0]# ln -fsv /usr/local/gawk4.2/bin/gawk /usr/bin/awk
    ‘/usr/bin/awk’ -> ‘/usr/local/gawk4.2/bin/gawk’
    [root@arm64v8 gawk-4.2.0]#
  5. 此时,调用 awk 将调用新版本的 gawk,调用 gawk 将调用旧版本的 gawk

    [root@arm64v8 ~]# awk --version | grep "GNU Awk"
    GNU Awk 4.2.0, API: 2.0
    [root@arm64v8 ~]# gawk --version | grep "GNU Awk"
    GNU Awk 4.0.2
    [root@arm64v8 ~]#

1.1.3.2、awk 用例文件

整个 awk 的讲解都会用到如下示例文件 a.txt

ID  name    gender  age  email          phone
1   Bob     male    28   abc@qq.com     18023394012
2   Alice   female  24   def@gmail.com  18084925203
3   Tony    male    21   aaa@163.com    17048792503
4   Kevin   male    21   bbb@189.com    17023929033
5   Alex    male    18   ccc@xyz.com    18185904230
6   Andy    female  22   ddd@139.com    18923902352
7   Jerry   female  25   exdsa@189.com  18785234906
8   Peter   male    20   bax@qq.com     17729348758
9   Steven  female  23   bc@sohu.com    15947893212
10  Bruce   female  27   bcbd@139.com   13942943905

1.1.3.3、awk 用法入门

简单来说,awk 的语法格式如下:

awk 'awk_program' a.txt
  • 单引号包围的是 awk 代码,也称为 awk 程序。
    • 最好使用单引号,因为在 awk 中经常使用 $ 符号,$ 既可以被 shell 解析,也可以被 awk 解析,而双引号属于弱引用,单引号属于强引用,这里必须使用强引用的单引号,使得 $ 仅被 awk 解析。
  • a.txt 是 awk 要读取的文件,可以是 0 个文件或一个文件,也可以是多个文件。
    • 不给定文件则可以从标准输入读取
  • awk 程序中,大量使用大括号,大括号表示代码块,多个代码块之间可以连用,代码块内部的多个语句需要使用 ";" 分隔。

awk 示例:

# 输出a.txt中的每一行
[root@arm64v8 ~]# awk '{print $0}' a.txt 
ID  name    gender  age  email          phone
1   Bob     male    28   abc@qq.com     18023394012
2   Alice   female  24   def@gmail.com  18084925203
3   Tony    male    21   aaa@163.com    17048792503
4   Kevin   male    21   bbb@189.com    17023929033
5   Alex    male    18   ccc@xyz.com    18185904230
6   Andy    female  22   ddd@139.com    18923902352
7   Jerry   female  25   exdsa@189.com  18785234906
8   Peter   male    20   bax@qq.com     17729348758
9   Steven  female  23   bc@sohu.com    15947893212
10  Bruce   female  27   bcbd@139.com   13942943905
[root@arm64v8 ~]#

# 多个代码块,代码块中多个语句
# 输出每行之后还输出两行:hello行和world行
[root@arm64v8 ~]# awk '{print $0}{print "hello";print "world"}' a.txt 
ID  name    gender  age  email          phone
hello
world
1   Bob     male    28   abc@qq.com     18023394012
hello
world
2   Alice   female  24   def@gmail.com  18084925203
hello
world
3   Tony    male    21   aaa@163.com    17048792503
hello
world
4   Kevin   male    21   bbb@189.com    17023929033
hello
world
5   Alex    male    18   ccc@xyz.com    18185904230
hello
world
6   Andy    female  22   ddd@139.com    18923902352
hello
world
7   Jerry   female  25   exdsa@189.com  18785234906
hello
world
8   Peter   male    20   bax@qq.com     17729348758
hello
world
9   Steven  female  23   bc@sohu.com    15947893212
hello
world
10  Bruce   female  27   bcbd@139.com   13942943905
hello
world
[root@arm64v8 ~]#
  • 对于 awk '{print $0}' a.txt ,它类似于 shell 的 while 循环 while read line;do echo "$line";done <a.txt

下面再分析该 awk 命令的执行过程:

  1. 读取文件第一行( awk 默认按行读取文件 )。
  2. 将所读取的行赋值给 awk 的变量 $0,于是 $0 中保存的就是本次所读取的行数据。
  3. 进入代码块 print $0 并执行其中代码 print $0,即输出 $0,也即输出当前所读取的行。
  4. 执行完本次代码之后,进入下一轮 awk 循环:继续读取下一行(第二行)
    • 将第二行赋值给变量 $0
    • 进入代码块执行 print $0
    • 执行完代码块后再次进入下一轮 awk 循环,即读取第三行,然后赋值给 $0,再执行代码块
    • …不断循环,直到读完文件所有数据…
  5. 退出 awk

1.1.3.4、BEGIN 和 END 语句块

awk 的所有代码( 目前这么认为 )都是写在语句块中的。

例如:

awk '{print $0}' a.txt
awk '{print $0}{print $0;print $0}' a.txt

每个语句块前面可以有 pattern,所以格式为:

pattern1{statement1}pattern2{statement3;statement4;...}
  • 语句块可分为 3 类:BEGIN 语句块、END 语句块和 main 语句块。
    • 其中 BEGIN 语句块和 END 语句块格式分别为 BEGIN{...} 和 END{...},而 main 语句块是一种统称,它的 pattern 部分没有固定格式,也可以省略,main 代码块是在读取文件的每一行的时候都执行的代码块。

分析下面三个 awk 命令的执行结果:

[root@arm64v8 ~]# awk 'BEGIN{print "我在前面"}{print $0}' a.txt 
我在前面
ID  name    gender  age  email          phone
1   Bob     male    28   abc@qq.com     18023394012
2   Alice   female  24   def@gmail.com  18084925203
3   Tony    male    21   aaa@163.com    17048792503
4   Kevin   male    21   bbb@189.com    17023929033
5   Alex    male    18   ccc@xyz.com    18185904230
6   Andy    female  22   ddd@139.com    18923902352
7   Jerry   female  25   exdsa@189.com  18785234906
8   Peter   male    20   bax@qq.com     17729348758
9   Steven  female  23   bc@sohu.com    15947893212
10  Bruce   female  27   bcbd@139.com   13942943905
[root@arm64v8 ~]#
[root@arm64v8 ~]# awk 'END{print "我在后面"}{print $0}' a.txt
ID  name    gender  age  email          phone
1   Bob     male    28   abc@qq.com     18023394012
2   Alice   female  24   def@gmail.com  18084925203
3   Tony    male    21   aaa@163.com    17048792503
4   Kevin   male    21   bbb@189.com    17023929033
5   Alex    male    18   ccc@xyz.com    18185904230
6   Andy    female  22   ddd@139.com    18923902352
7   Jerry   female  25   exdsa@189.com  18785234906
8   Peter   male    20   bax@qq.com     17729348758
9   Steven  female  23   bc@sohu.com    15947893212
10  Bruce   female  27   bcbd@139.com   13942943905
我在后面
[root@arm64v8 ~]#
[root@arm64v8 ~]# awk 'BEGIN{print "我在前面"}{print $0}END{print "我在后面"}' a.txt 
我在前面
ID  name    gender  age  email          phone
1   Bob     male    28   abc@qq.com     18023394012
2   Alice   female  24   def@gmail.com  18084925203
3   Tony    male    21   aaa@163.com    17048792503
4   Kevin   male    21   bbb@189.com    17023929033
5   Alex    male    18   ccc@xyz.com    18185904230
6   Andy    female  22   ddd@139.com    18923902352
7   Jerry   female  25   exdsa@189.com  18785234906
8   Peter   male    20   bax@qq.com     17729348758
9   Steven  female  23   bc@sohu.com    15947893212
10  Bruce   female  27   bcbd@139.com   13942943905
我在后面
[root@arm64v8 ~]#

根据上面 3 行命令的执行结果,可总结出如下有关于 BEGIN、END 和 main 代码块的特性:

  • BEGIN 代码块:
    • 在读取文件之前执行,且执行一次
    • 在 BEGIN 代码块中,无法使用 $0 或其它一些特殊变量
  • main 代码块:
    • 读取文件时循环执行,(默认情况)每读取一行,就执行一次 main 代码块
    • main 代码块可有多个
  • END 代码块:
    • 在读取文件完成之后执行,且执行一次
    • 有 END 代码块,必须要有读取的数据(可以是标准输入)
    • END 代码块中可以使用 $0 等一些特殊变量,只不过这些特殊变量保存的是最后一轮 awk 循环的数据

1.1.3.5、awk 命令行结构和语法结构

awk 命令行结构

awk [ -- ] program-text file ...        (1)
awk -f program-file [ -- ] file ...     (2)
awk -e program-text [ -- ] file ...     (3)
  • program-text 即 awk 命令行中的 awk 代码部分,一般使用单引号包围
  • -f program-file 表示将 awk 代码部分写在文件中,然后使用 -f 选项去引用这个文件
  • -e program-text 也用于指定 awk 代码,但是既要 -f 指定代码文件,又要用命令行 awk 代码,则必须同时使用 -f 和 -e,即 awk -f file -e 'awk_code'

awk 语法结构

awk 语法结构即 awk 代码部分的结构。

awk 的语法充斥着 pattern{action} 的模式,它们称为 awk rule。

[root@arm64v8 ~]# awk 'BEGIN{n=3} /^[0-9]/&&$1>5{print $1} /Alice/{print "Alice"} END{print "hello"}' a.txt
Alice
6
7
8
9
10
hello
[root@arm64v8 ~]#
  • 上面示例中,有 BEGIN 语句块,有 END 语句块,还有 2 个 main 代码块,两个 main 代码块都使用了正则表达式作为 pattern。

关于 awk 的语法:

  • 多个 pattern{action} 可以直接连用
  • action 中多个语句如果写在同一行,则需使用分号分隔
  • pattern 部分用于筛选行,action 表示在筛选通过后执行的操作
  • pattern 和 action 都可以省略
    • 省略 pattern,等价于对每一行数据都执行 action
    • 例如:awk '{print $0}' a.txt
    • 省略代码块 action,等价于 print 即输出所有行
    • 例如:awk '/Alice/' a.txt 等价于 awk '/Alice/{print $0}' a.txt
    • 省略代码块中的 action,表示对筛选的行什么都不做
    • 例如:awk '/Alice/{}' a.txt
    • pattern{action} 任何一部分都可以省略
    • 例如:awk '' a.txt

pattern 和 action

对于 pattern{action} 语句结构(都称之为语句块),其中的 pattern 部分可以使用下面列出的模式:

# 特殊pattern
BEGIN
END

# 布尔pattern
/regular expression/            # 正则匹配成功与否 /a.*ef/{action}
relational expression           # 即等值比较、大小比较 3>2{action}
pattern && pattern              # 逻辑与 3>2 && 3>1 {action}
pattern || pattern              # 逻辑或 3>2 || 3<1 {action}
! pattern                       # 逻辑取反 !/a.*ef/{action}
(pattern)                       # 改变优先级
pattern ? pattern : pattern     # 三目运算符决定的布尔值

# 范围pattern,非布尔代码块
pattern1, pattern2              # 范围,pat1打开、pat2关闭,即flip,flop模式

action 部分,可以是任何语句,例如 print。

1.1.3.6、awk 读取行的细节

awk 读取输入文件时,每次读取一条记录(record)(默认情况下按行读取,所以此时记录就是行)。每读取一条记录,将其保存到 $0 中,然后执行一次 main 代码段。

awk '{print $0}' a.txt

如果是空文件,则因为无法读取到任何一条记录,将导致直接关闭文件,而不会进入 main 代码段。

[root@arm64v8 ~]# touch x.log
[root@arm64v8 ~]# awk '{print "hello world"}' x.log 
[root@arm64v8 ~]#

可设置表示输入记录分隔符的预定义变量 RS(Record Separator) 来改变每次读取的记录模式。

# RS="\n" 、 RS="m"
awk 'BEGIN{RS="\n"}{print $0}' a.txt
awk 'BEGIN{RS="m"}{print $0}' a.txt

RS 通常设置在 BEGIN 代码块中,因为要先于读取文件确定好 RS 分隔符。

RS 指定输入记录分隔符时,所读取的记录中是不包含分隔符字符的。例如 RS="a",则 $0 中一定不可能出现字符 a。

RS 两种可能情况:

  • RS 为单个字符:直接使用该字符来分割记录。
  • RS 为多个字符:将其当作正则表达式,只要匹配正则表达式的符号,都用来分割记录。
    • 设置预定义变量 IGNORECASE 为非零值,正则表达式匹配时表示忽略大小写。
    • 兼容模式下,只有首字符才生效,不会使用正则模式去分割记录。

特殊的 RS 值用来解决特殊读取需求:

  • RS="":按段落读取
  • RS="\0":一次性读取所有数据,但有些特殊文件中包含了空字符 \0
  • RS="^$":真正的一次性读取所有数据,因为非空文件不可能匹配成功
  • RS="\n+":按行读取,但忽略所有空行

示例:

# 按段落读取:RS=''
[root@arm64v8 ~]# awk 'BEGIN{RS=""}{print $0"------"}' a.txt
ID  name    gender  age  email          phone
1   Bob     male    28   abc@qq.com     18023394012
2   Alice   female  24   def@gmail.com  18084925203
3   Tony    male    21   aaa@163.com    17048792503
4   Kevin   male    21   bbb@189.com    17023929033
5   Alex    male    18   ccc@xyz.com    18185904230
6   Andy    female  22   ddd@139.com    18923902352
7   Jerry   female  25   exdsa@189.com  18785234906
8   Peter   male    20   bax@qq.com     17729348758
9   Steven  female  23   bc@sohu.com    15947893212
10  Bruce   female  27   bcbd@139.com   13942943905------
[root@arm64v8 ~]#

# 一次性读取所有数据:RS='\0' RS="^$"
[root@arm64v8 ~]# awk 'BEGIN{RS='\0'}{print $0"------"}' a.txt
[root@arm64v8 ~]# awk 'BEGIN{RS="^$"}{print $0"------"}' a.txt

# 忽略空行:RS='\n+'
[root@arm64v8 ~]# awk 'BEGIN{RS="\n+"}{print $0"-------"}' a.txt 

# 忽略大小写:预定义变量IGNORECASE设置为非0值
[root@arm64v8 ~]# awk 'BEGIN{IGNORECASE=1}{print $0"-------"}' RS='[ab]' a.txt

预定义变量 RT:

  • 在 awk 每次读完一条记录时,会设置一个称为 RT 的预定义变量,表示 Record Termination。

  • 当 RS 为单个字符时,RT 的值和 RS 的值是相同的。

  • 当 RS 为多个字符(正则表达式)时,则 RT 设置为正则匹配到记录分隔符之后,真正用于划分记录时的字符。

  • 当无法匹配到记录分隔符时,RT 设置为控制空字符串(即默认的初始值)。

    [root@arm64v8 ~]# awk 'BEGIN{RS="(fe)?male"}{print RT}' a.txt
    male
    female
    male
    male
    male
    female
    female
    male
    female
    female
    
    [root@arm64v8 ~]#

两种行号:NR 和 FNR

在读取每条记录之后,将其赋值给 $0,同时还会设置 NR、FNR、RT。

  • NR:所有文件的行号计数器
  • FNR:是各个文件的行号计数器
[root@arm64v8 ~]# awk '{print NR}' a.txt a.txt 
[root@arm64v8 ~]# awk '{print FNR}' a.txt a.txt 

1.1.3.7、awk 划分字段

awk 读取每一条记录之后,会将其赋值给 $0,同时还会对这条记录按照 预定义变量 FS 划分字段,将划分好的各个字段分别赋值给 $1 $2 $3 $4...$N,同时将划分的字段数量赋值给 预定义变量NF

1.1.3.7.1、引用字段的方式

$N 引用字段:

  • N=0:即 $0,引用记录本身
  • 0<N<=NF:引用对应字段
  • N>NF:表示引用不存在的字段,返回空字符串
  • N<0:报错

可使用变量或计算的方式指定要获取的字段序号。

[root@arm64v8 ~]# awk '{n=5;print $5}' a.txt 
email
abc@qq.com
def@gmail.com
aaa@163.com
bbb@189.com
ccc@xyz.com
ddd@139.com
exdsa@189.com
bax@qq.com
bc@sohu.com
bcbd@139.com
[root@arm64v8 ~]# awk '{print $(2+2)}' a.txt  # 括号必不可少,用于改变优先级
age
28
24
21
21
18
22
25
20
23
27
[root@arm64v8 ~]# awk '{print $(NF-3)}' a.txt 
gender
male
female
male
male
male
female
female
male
female
female
[root@arm64v8 ~]#
1.1.3.7.2、分割字段的方式

读取 record 之后,将使用预定义变量 FS、FIELDWIDTHS 或 FPAT 中的一种来分割字段。分割完成之后,再进入 main 代码段(所以,在 main 中设置 FS 对本次已经读取的 record 是没有影响的,但会影响下次读取)。

划分字段方式(一):FS 或 -F

FS 或者 -F:字段分隔符

  • FS 为单个字符时,该字符即为字段分隔符
  • FS 为多个字符时,则采用正则表达式模式作为字段分隔符
  • 特殊的,也是 FS 默认的情况,FS 为单个空格时,将以连续的空白(空格、制表符、换行符)作为字段分隔符
  • 特殊的,FS 为空字符串 ”” 时,将对每个字符都进行分隔,即每个字符都作为一个字段
  • 设置预定义变量 IGNORECASE 为非零值,正则匹配时表示忽略大小写(只影响正则,所以 FS 为单字时无影响)
  • 如果 record 中无法找到 FS 指定的分隔符(例如将 FS 设置为 "\n"),则整个记录作为一个字段,即 $1$0 相等
# 字段分隔符指定为单个字符
[root@arm64v8 ~]# awk -F":" '{print $1}' /etc/passwd
[root@arm64v8 ~]# awk 'BEGIN{FS=":"}{print $1}' /etc/passwd

# 字段分隔符指定为正则表达式
[root@arm64v8 ~]# awk 'BEGIN{FS=" +|@"}{print $1,$2,$3,$4,$5,$6}' a.txt 
ID name gender age email phone
1 Bob male 28 abc qq.com
2 Alice female 24 def gmail.com
3 Tony male 21 aaa 163.com
4 Kevin male 21 bbb 189.com
5 Alex male 18 ccc xyz.com
6 Andy female 22 ddd 139.com
7 Jerry female 25 exdsa 189.com
8 Peter male 20 bax qq.com
9 Steven female 23 bc sohu.com
10 Bruce female 27 bcbd 139.com
[root@arm64v8 ~]#

划分字段方式(二):FIELDWIDTHS

指定预定义变量 FIELDWIDTHS 按字符宽度分割字段,这是 gawk 提供的高级功能。在处理某字段缺失时非常好用。

  • FIELDWIDTHS="3 5 6 9" 表示第一个字段读 3 个字符,第二个字段 5 字符...
  • FIELDWIDTHS="8 1:5 6 2:33" 表示:
    • 第一个字段读 8 个字符
    • 然后跳过 1 个字符再读 5 个字符作为第二个字段
    • 然后读 6 个字符作为第三个字段
    • 然后跳过 2 个字符,再读 33 个字符作为第四个字段(不足 33 个字符,读到结尾)
  • FIELDWIDTHS="2 3 *":
    • 第一个字段 2 个字符
    • 第二个字段 3 个字符
    • 剩余所有字符作为第三个字段
    • 星号只能放在最后,且只能单独使用,表示剩余所有

实例 1:

# 没取完的字符串DDD被丢弃,且NF=3
[root@arm64v8 ~]# awk 'BEGIN{FIELDWIDTHS="2 3 2"}{print $1,$2,$3,$4}' <<< "AABBBCCDDDD"
AA BBB CC 
[root@arm64v8 ~]#

# 字符串不够长度时无视
[root@arm64v8 ~]# awk 'BEGIN{FIELDWIDTHS="2 3 2 100"}{print $1,$2,$3,$4"-"}' <<<"AABBBCCDDDD"
AA BBB CC DDDD-
[root@arm64v8 ~]#

# *号取剩余所有,NF=3
[root@arm64v8 ~]# awk 'BEGIN{FIELDWIDTHS="2 3 *"}{print $1,$2,$3}' <<<"AABBBCCDDDD"
AA BBB CCDDDD
[root@arm64v8 ~]#

# 字段数多了,则取完字符串即可,NF=2
[root@arm64v8 ~]# awk 'BEGIN{FIELDWIDTHS="2 30 *"}{print $1,$2,NF}' <<<"AABBBCCDDDD"
AA BBBCCDDDD 2
[root@arm64v8 ~]#

实例 2:处理某些字段缺失的数据

如果按照常规的 FS 进行字段分割,则对于缺失字段的行和没有缺失字段的行很难统一处理,但使用 FIELDWIDTHS 则非常方便。

假设 a.txt 文本内容如下:

ID  name    gender  age  email          phone
1   Bob     male    28   abc@qq.com     18023394012
2   Alice   female  24   def@gmail.com  18084925203
3   Tony    male    21   aaa@163.com    17048792503
4   Kevin   male    21   bbb@189.com    17023929033
5   Alex    male    18                  18185904230
6   Andy    female  22   ddd@139.com    18923902352
7   Jerry   female  25   exdsa@189.com  18785234906
8   Peter   male    20   bax@qq.com     17729348758
9   Steven  female  23   bc@sohu.com    15947893212
10  Bruce   female  27   bcbd@139.com   13942943905

因为 email 字段有的是空字段,所以直接用 FS 划分字段不便处理。可使用 FIELDWIDTHS。

# 字段1:4字符
# 字段2:8字符
# 字段3:8字符
# 字段4:2字符
# 字段5:先跳过3字符,再读13字符,该字段13字符
# 字段6:先跳过2字符,再读11字符,该字段11字符
[root@arm64v8 ~]# awk '
BEGIN{FIELDWIDTHS="4 8 8 2 3:13 2:11"}
NR>1{
    print "<"$1">","<"$2">","<"$3">","<"$4">","<"$5">","<"$6">"
}' a.txt
<1   > <Bob     > <male    > <28> <abc@qq.com   > <18023394012>
<2   > <Alice   > <female  > <24> <def@gmail.com> <18084925203>
<3   > <Tony    > <male    > <21> <aaa@163.com  > <17048792503>
<4   > <Kevin   > <male    > <21> <bbb@189.com  > <17023929033>
<5   > <Alex    > <male    > <18> <             > <18185904230>
<6   > <Andy    > <female  > <22> <ddd@139.com  > <18923902352>
<7   > <Jerry   > <female  > <25> <exdsa@189.com> <18785234906>
<8   > <Peter   > <male    > <20> <bax@qq.com   > <17729348758>
<9   > <Steven  > <female  > <23> <bc@sohu.com  > <15947893212>
<10  > <Bruce   > <female  > <27> <bcbd@139.com > <13942943905>
[root@arm64v8 ~]#

# 如果email为空,则输出它
[root@arm64v8 ~]# awk '
> BEGIN{FIELDWIDTHS="4 8 8 2 3:13 2:11"}
> NR>1{
>     if($5 ~ /^ +$/){print $0}
> }' a.txt
5   Alex    male    18                  18185904230
[root@arm64v8 ~]#

划分字段方式(三):FPAT

FS 是指定字段分隔符,来取得除分隔符外的部分作为字段。

FPAT 是取得匹配的字符部分作为字段。它是 gawk 提供的一个高级功能。

FPAT 根据指定的正则来全局匹配 record,然后将所有匹配成功的部分组成 $1、$2...,不会修改 $0

  • awk 'BEGIN{FPAT="[0-9]+"}{print $3"-"}' a.txt
  • 之后再设置 FS 或 FPAT,该变量将失效

FPAT 常用于字段中包含了字段分隔符的场景。例如,CSV 文件中的一行数据如下:

Robbins,Arnold,"1234 A Pretty Street, NE",MyTown,MyState,12345-6789,USA

其中逗号分隔每个字段,但双引号包围的是一个字段整体,即使其中有逗号。

这时使用 FPAT 来划分各字段比使用 FS 要方便的多。

[root@arm64v8 ~]# echo 'Robbins,Arnold,"1234 A Pretty Street, NE",MyTown,MyState,12345-6789,USA' | awk '
BEGIN{FPAT="[^,]*|(\"[^\"]*\")"}
{
        for (i=1;i<NF+1;i++){
            print "<"$i">"
        }
}
'
<Robbins>
<Arnold>
<"1234 A Pretty Street, NE">
<MyTown>
<MyState>
<12345-6789>
<USA>
[root@arm64v8 ~]#

最后,patsplit() 函数和 FPAT 的功能一样。

1.1.3.7.3、检查字段划分的方式

有 FS、FIELDWIDTHS、FPAT 三种获取字段的方式,可使用 PROCINFO 数组来确定本次使用何种方式获得字段。

PROCINFO 是一个数组,记录了 awk 进程工作时的状态信息。

  • PROCINFO["FS"]=="FS",表示使用 FS 分割获取字段
  • PROCINFO["FPAT"]=="FPAT",表示使用FPAT匹配获取字段
  • PROCINFO["FIELDWIDTHS"]=="FIELDWIDTHS",表示使用 FIELDWIDTHS 分割获取字段
if(PROCINFO["FS"]=="FS"){
    ...FS spliting...
} else if(PROCINFO["FPAT"]=="FPAT"){
    ...FPAT spliting...
} else if(PROCINFO["FIELDWIDTHS"]=="FIELDWIDTHS"){
    ...FIELDWIDTHS spliting...
}

1.1.3.8、$0 重新计算

修改字段或 NF 值的联动效应

注意下面的分割和计算两词:分割表示使用 FS(field Separator),计算表示使用预定义变量 OFS(Output Field Separator)。

  • 修改 $0,将使用 FS 重新分割字段,所以会影响 $1、$2...

  • 修改 $1、$2,将根据 $1$NF 等各字段来重新计算 $0

    • 即使是 $1 = $1 这样的原值不变的修改,也一样会重新计算 $0
  • 为不存在的字段赋值,将新增字段按需使用空字符串填充中间的字段,并使用 OFS 重新计算 $0

    [root@arm64v8 ~]# awk 'BEGIN{OFS="-"}{$(NF+2)=5;print $0}' a.txt
    ID-name-gender-age-email-phone--5
    1-Bob-male-28-abc@qq.com-18023394012--5
    2-Alice-female-24-def@gmail.com-18084925203--5
    3-Tony-male-21-aaa@163.com-17048792503--5
    4-Kevin-male-21-bbb@189.com-17023929033--5
    5-Alex-male-18-ccc@xyz.com-18185904230--5
    6-Andy-female-22-ddd@139.com-18923902352--5
    7-Jerry-female-25-exdsa@189.com-18785234906--5
    8-Peter-male-20-bax@qq.com-17729348758--5
    9-Steven-female-23-bc@sohu.com-15947893212--5
    10-Bruce-female-27-bcbd@139.com-13942943905--5
    [root@arm64v8 ~]#
    • 注意后面的 --5,原本 a.txt 文件每行只有 6 个字段,通过 $(NF+2)=5 重新计算后,共 8 个字段,第 7 个字段为空。
  • 增加 NF 值,将使用空字符串新增字段,并使用 OFS 重新计算 $0

    [root@arm64v8 ~]# awk 'BEGIN{OFS="-"}{NF+=3;print $0}' a.txt
    ID-name-gender-age-email-phone---
    1-Bob-male-28-abc@qq.com-18023394012---
    2-Alice-female-24-def@gmail.com-18084925203---
    3-Tony-male-21-aaa@163.com-17048792503---
    4-Kevin-male-21-bbb@189.com-17023929033---
    5-Alex-male-18-ccc@xyz.com-18185904230---
    6-Andy-female-22-ddd@139.com-18923902352---
    7-Jerry-female-25-exdsa@189.com-18785234906---
    8-Peter-male-20-bax@qq.com-17729348758---
    9-Steven-female-23-bc@sohu.com-15947893212---
    10-Bruce-female-27-bcbd@139.com-13942943905---
    [root@arm64v8 ~]#
    • 通过 NF+=3,重新计算后最终共 9 个字段,最后三个字段为空。
  • 减小 NF 值,将丢弃一定数量的尾部字段,并使用 OFS 重新计算 $0

    [root@arm64v8 ~]# awk 'BEGIN{OFS="-"}{NF-=3;print $0}' a.txt
    ID-name-gender
    1-Bob-male
    2-Alice-female
    3-Tony-male
    4-Kevin-male
    5-Alex-male
    6-Andy-female
    7-Jerry-female
    8-Peter-male
    9-Steven-female
    10-Bruce-female
    [root@arm64v8 ~]#
    • 通过 NF -=3,重新计算后最终共 3 个字段。

关于$0

当读取一条 record 之后,将原原本本地被保存到 $0 当中。但是,只要出现了上面所说的任何一种导致 $0 重新计算的操作,都会立即使用 OFS 去重建 $0

换句话说,没有导致 $0 重建,$0 就一直是原原本本的数据,所以指定 OFS 也无效。

awk 'BEGIN{OFS="-"}{print $0}' a.txt  # OFS此处无效

$0 重建后,将自动使用 OFS 重建,所以即使没有指定 OFS,它也会采用默认值(空格)进行重建。

[root@arm64v8 ~]# awk '{$1=$1; print $0}' a.txt 
ID name gender age email phone
1 Bob male 28 abc@qq.com 18023394012
2 Alice female 24 def@gmail.com 18084925203
3 Tony male 21 aaa@163.com 17048792503
4 Kevin male 21 bbb@189.com 17023929033
5 Alex male 18 ccc@xyz.com 18185904230
6 Andy female 22 ddd@139.com 18923902352
7 Jerry female 25 exdsa@189.com 18785234906
8 Peter male 20 bax@qq.com 17729348758
9 Steven female 23 bc@sohu.com 15947893212
10 Bruce female 27 bcbd@139.com 13942943905
[root@arm64v8 ~]# 
[root@arm64v8 ~]# awk '{print $0; $1=$1; print $0}' OFS="-" a.txt
ID  name    gender  age  email          phone
ID-name-gender-age-email-phone
1   Bob     male    28   abc@qq.com     18023394012
1-Bob-male-28-abc@qq.com-18023394012
2   Alice   female  24   def@gmail.com  18084925203
2-Alice-female-24-def@gmail.com-18084925203
3   Tony    male    21   aaa@163.com    17048792503
3-Tony-male-21-aaa@163.com-17048792503
4   Kevin   male    21   bbb@189.com    17023929033
4-Kevin-male-21-bbb@189.com-17023929033
5   Alex    male    18   ccc@xyz.com    18185904230
5-Alex-male-18-ccc@xyz.com-18185904230
6   Andy    female  22   ddd@139.com    18923902352
6-Andy-female-22-ddd@139.com-18923902352
7   Jerry   female  25   exdsa@189.com  18785234906
7-Jerry-female-25-exdsa@189.com-18785234906
8   Peter   male    20   bax@qq.com     17729348758
8-Peter-male-20-bax@qq.com-17729348758
9   Steven  female  23   bc@sohu.com    15947893212
9-Steven-female-23-bc@sohu.com-15947893212
10  Bruce   female  27   bcbd@139.com   13942943905
10-Bruce-female-27-bcbd@139.com-13942943905
[root@arm64v8 ~]#

如果重建 $0 之后,再去修改 OFS,将对当前行无效,但对之后的行有效。所以如果也要对当前行生效,需要再次重建。

# OFS对第一行无效
[root@arm64v8 ~]# awk '{$4+=10;OFS="-";print $0}' a.txt
ID name gender 10 email phone
1-Bob-male-38-abc@qq.com-18023394012
2-Alice-female-34-def@gmail.com-18084925203
3-Tony-male-31-aaa@163.com-17048792503
4-Kevin-male-31-bbb@189.com-17023929033
5-Alex-male-28-ccc@xyz.com-18185904230
6-Andy-female-32-ddd@139.com-18923902352
7-Jerry-female-35-exdsa@189.com-18785234906
8-Peter-male-30-bax@qq.com-17729348758
9-Steven-female-33-bc@sohu.com-15947893212
10-Bruce-female-37-bcbd@139.com-13942943905
[root@arm64v8 ~]#

# 对所有行有效
[root@arm64v8 ~]# awk '{$4+=10;OFS="-";$1=$1;print $0}' a.txt
ID-name-gender-10-email-phone
1-Bob-male-38-abc@qq.com-18023394012
2-Alice-female-34-def@gmail.com-18084925203
3-Tony-male-31-aaa@163.com-17048792503
4-Kevin-male-31-bbb@189.com-17023929033
5-Alex-male-28-ccc@xyz.com-18185904230
6-Andy-female-32-ddd@139.com-18923902352
7-Jerry-female-35-exdsa@189.com-18785234906
8-Peter-male-30-bax@qq.com-17729348758
9-Steven-female-33-bc@sohu.com-15947893212
10-Bruce-female-37-bcbd@139.com-13942943905
[root@arm64v8 ~]#

关注 $0 重建是一个非常有用的技巧。

例如,下面通过重建 $0 的技巧来实现去除行首行尾空格并压缩中间空格:

[root@arm64v8 ~]# echo "   a b    c   d" | awk '{$1=$1;print}'
a b c d
[root@arm64v8 ~]#
[root@arm64v8 ~]# echo "    a   b c   d" | awk '{$1=$1;print}' OFS="-"
a-b-c-d
[root@arm64v8 ~]#

1.1.3.9、awk 数据筛选示例

筛选行

  1. 根据行号筛选

    [root@arm64v8 ~]# awk 'NR==2' a.txt 
    1   Bob     male    28   abc@qq.com     18023394012
    [root@arm64v8 ~]# 
    [root@arm64v8 ~]# awk 'NR>=2' a.txt 
    1   Bob     male    28   abc@qq.com     18023394012
    2   Alice   female  24   def@gmail.com  18084925203
    3   Tony    male    21   aaa@163.com    17048792503
    4   Kevin   male    21   bbb@189.com    17023929033
    5   Alex    male    18   ccc@xyz.com    18185904230
    6   Andy    female  22   ddd@139.com    18923902352
    7   Jerry   female  25   exdsa@189.com  18785234906
    8   Peter   male    20   bax@qq.com     17729348758
    9   Steven  female  23   bc@sohu.com    15947893212
    10  Bruce   female  27   bcbd@139.com   13942943905
    [root@arm64v8 ~]#
  2. 根据正则表达式筛选整行

    [root@arm64v8 ~]# awk '/qq.com/' a.txt           # 输出带有qq.com的行
    1   Bob     male    28   abc@qq.com     18023394012
    8   Peter   male    20   bax@qq.com     17729348758
    [root@arm64v8 ~]# 
    [root@arm64v8 ~]# awk '$0 ~ /qq.com/' a.txt      # 等价于上面命令
    1   Bob     male    28   abc@qq.com     18023394012
    8   Peter   male    20   bax@qq.com     17729348758
    [root@arm64v8 ~]#
    
    [root@arm64v8 ~]# awk '/^[^@]+$/' a.txt      # 输出不包含@符号的行
    ID  name    gender  age  email          phone
    [root@arm64v8 ~]# 
    [root@arm64v8 ~]# awk '!/@/' a.txt               # 输出不包含@符号的行
    ID  name    gender  age  email          phone
    [root@arm64v8 ~]#
  3. 根据字段来筛选行

    # 输出第4字段大于24的行
    [root@arm64v8 ~]# awk '($4+0) > 24{print $0}' a.txt  
    1   Bob     male    28   abc@qq.com     18023394012
    7   Jerry   female  25   exdsa@189.com  18785234906
    10  Bruce   female  27   bcbd@139.com   13942943905
    [root@arm64v8 ~]#
    
    # 输出第5字段包含qq.com的行
    [root@arm64v8 ~]# awk '$5 ~ /qq.com/' a.txt
    1   Bob     male    28   abc@qq.com     18023394012
    8   Peter   male    20   bax@qq.com     17729348758
    [root@arm64v8 ~]#
  4. 将多个筛选条件结合起来进行筛选

    [root@arm64v8 ~]# awk 'NR>=2 && NR<=7' a.txt 
    1   Bob     male    28   abc@qq.com     18023394012
    2   Alice   female  24   def@gmail.com  18084925203
    3   Tony    male    21   aaa@163.com    17048792503
    4   Kevin   male    21   bbb@189.com    17023929033
    5   Alex    male    18   ccc@xyz.com    18185904230
    6   Andy    female  22   ddd@139.com    18923902352
    [root@arm64v8 ~]# 
    [root@arm64v8 ~]# awk '$3=="male" && $6 ~ /^170/' a.txt 
    3   Tony    male    21   aaa@163.com    17048792503
    4   Kevin   male    21   bbb@189.com    17023929033
    [root@arm64v8 ~]# 
    [root@arm64v8 ~]# awk '$3=="male" || $6 ~ /^170/' a.txt 
    1   Bob     male    28   abc@qq.com     18023394012
    3   Tony    male    21   aaa@163.com    17048792503
    4   Kevin   male    21   bbb@189.com    17023929033
    5   Alex    male    18   ccc@xyz.com    18185904230
    8   Peter   male    20   bax@qq.com     17729348758
    [root@arm64v8 ~]#
  5. 按照范围进行筛选 flip flop

    # pattern1,pattern2{action}
    [root@arm64v8 ~]# awk 'NR==2, NR==7' a.txt 
    1   Bob     male    28   abc@qq.com     18023394012
    2   Alice   female  24   def@gmail.com  18084925203
    3   Tony    male    21   aaa@163.com    17048792503
    4   Kevin   male    21   bbb@189.com    17023929033
    5   Alex    male    18   ccc@xyz.com    18185904230
    6   Andy    female  22   ddd@139.com    18923902352
    [root@arm64v8 ~]# 
    [root@arm64v8 ~]# awk 'NR==2, $6 ~ /^170/' a.txt 
    1   Bob     male    28   abc@qq.com     18023394012
    2   Alice   female  24   def@gmail.com  18084925203
    3   Tony    male    21   aaa@163.com    17048792503
    [root@arm64v8 ~]#

处理字段

修改字段时,一定要注意,可能带来的联动效应:即使用 OFS 重建 $0

[root@arm64v8 ~]# awk 'NR>1{$4=$4+5;print $0}' a.txt
1 Bob male 33 abc@qq.com 18023394012
2 Alice female 29 def@gmail.com 18084925203
3 Tony male 26 aaa@163.com 17048792503
4 Kevin male 26 bbb@189.com 17023929033
5 Alex male 23 ccc@xyz.com 18185904230
6 Andy female 27 ddd@139.com 18923902352
7 Jerry female 30 exdsa@189.com 18785234906
8 Peter male 25 bax@qq.com 17729348758
9 Steven female 28 bc@sohu.com 15947893212
10 Bruce female 32 bcbd@139.com 13942943905
[root@arm64v8 ~]#
[root@arm64v8 ~]# awk 'BEGIN{OFS="-"}NR>1{$4=$4+5;print $0}' a.txt
1-Bob-male-33-abc@qq.com-18023394012
2-Alice-female-29-def@gmail.com-18084925203
3-Tony-male-26-aaa@163.com-17048792503
4-Kevin-male-26-bbb@189.com-17023929033
5-Alex-male-23-ccc@xyz.com-18185904230
6-Andy-female-27-ddd@139.com-18923902352
7-Jerry-female-30-exdsa@189.com-18785234906
8-Peter-male-25-bax@qq.com-17729348758
9-Steven-female-28-bc@sohu.com-15947893212
10-Bruce-female-32-bcbd@139.com-13942943905
[root@arm64v8 ~]#
[root@arm64v8 ~]# awk 'NR>1{$6=$6"*";print $0}' a.txt
1 Bob male 28 abc@qq.com 18023394012*
2 Alice female 24 def@gmail.com 18084925203*
3 Tony male 21 aaa@163.com 17048792503*
4 Kevin male 21 bbb@189.com 17023929033*
5 Alex male 18 ccc@xyz.com 18185904230*
6 Andy female 22 ddd@139.com 18923902352*
7 Jerry female 25 exdsa@189.com 18785234906*
8 Peter male 20 bax@qq.com 17729348758*
9 Steven female 23 bc@sohu.com 15947893212*
10 Bruce female 27 bcbd@139.com 13942943905*
[root@arm64v8 ~]#

综合实例

从 ifconfig 命令的结果中筛选出除了 lo 网卡外的所有 IPv4 地址。

[root@arm64v8 ~]# ifconfig | awk '/\<inet\>/ && !($2 ~ /^127/){print $2}'
192.168.0.181
[root@arm64v8 ~]#

1.1.3.10、awk 工作流程

[root@arm64v8 ~]# man --pager='less -p ^"AWK PROGRAM EXECUTION"' awk
  1. 解析 -v var=val... 选项中的变量赋值。
  2. 编译 awk 源代码为 awk 可解释的内部格式,包括 -v 的变量。
  3. 执行 BEGIN 代码段。
  4. 根据输入记录分隔符 RS 读取文件(根据 ARGV 数组的元素决定要读取的文件),如果没有指定文件,则从标准输入中读取文件,同时执行 main 代码段。
    • 如果文件名部分指定为 var=val 格式,则声明并创建变量,此阶段的变量在 BEGIN 之后声明,所以 BEGIN 中不可用, main 代码段可用。
    • 每读取一条记录:
      • 都将设置 NR、FNR、RT、$0 等变量。
      • 默认根据输入字段分隔符 FS 切割字段,将各字段保存到 $1, $2,... 中。
      • 测试 main 代码段的 pattern 部分,如果测试成功则执行 action 部分。
  5. 执行 END 代码段。

1.1.3.11、getline 用法详解

除了可以从标准输入或非选项型参数所指定的文件中读取数据,还可以使用 getline 从其它各种渠道获取需要处理的数据,它的用法有很多种。

getline 的返回值:

  • 如果可以读取到数据,返回 1
  • 如果遇到了 EOF,返回 0
  • 如果遇到了错误,返回负数。如 -1 表示文件无法打开,-2 表示 IO 操作需要重试(retry)。在遇到错误的同时,还会设置 ERRNO 变量来描述错误

为了健壮性,getline 时强烈建议进行判断。例如:

if ( (getline) <= 0 ){...}
if ( (getline) < 0 ){...}
if ( (getline) > 0 ){...}

上面的 getline 的括号尽量加上,因为 getline < 0 表示的是输入重定向,而不是和数值 0 进行小于号的比较。

1.1.3.11.1、无参数的 getline

getline 无参数时,表示从当前正在处理的文件中立即读取下一条记录保存到 $0 中,并进行字段分割,然后继续执行后续代码逻辑。

此时的 getline 会设置 NF、RT、NR、FNR、$0 和 $N。

next 也可以读取下一行。

  • getline:读取下一行之后,继续执行 getline 后面的代码
  • next:读取下一行,立即回头 awk 循环的头部,不会再执行 next 后面的代码

例如,匹配到某行之后,再读一行就退出:

[root@arm64v8 ~]# awk '/^1/{print;getline;print;exit}' a.txt 
1   Bob     male    28   abc@qq.com     18023394012
2   Alice   female  24   def@gmail.com  18084925203
[root@arm64v8 ~]#
[root@arm64v8 ~]# awk '/^1/{print;next;print;exit}' a.txt 
1   Bob     male    28   abc@qq.com     18023394012
10  Bruce   female  27   bcbd@139.com   13942943905
[root@arm64v8 ~]#

为了更健壮,应当对 getline 的返回值进行判断。

[root@arm64v8 ~]# awk '/^1/{print;if((getline)<=0){exit};print}' a.txt 
1   Bob     male    28   abc@qq.com     18023394012
2   Alice   female  24   def@gmail.com  18084925203
10  Bruce   female  27   bcbd@139.com   13942943905
[root@arm64v8 ~]#
1.1.3.11.2、一个参数的 getline

没有参数的 getline 是读取下一条记录之后将记录保存到 $0 中,并对该记录进行字段的分割。

一个参数的 getline 是将读取的记录保存到指定的变量当中,并且不会对其进行分割。

[root@arm64v8 ~]# awk '/^1/{print;getline var;print;exit}' a.txt 
1   Bob     male    28   abc@qq.com     18023394012
1   Bob     male    28   abc@qq.com     18023394012
[root@arm64v8 ~]# 
[root@arm64v8 ~]# awk '/^1/{print;getline var;print var;exit}' a.txt 
1   Bob     male    28   abc@qq.com     18023394012
2   Alice   female  24   def@gmail.com  18084925203
[root@arm64v8 ~]#

此时的 getline 只会设置 RT、NR、FNR 变量和指定的变量 var。因此 $0$N 以及 NF 保持不变。

[root@arm64v8 ~]# awk '
> /^1/{
>   if((getline var)<=0){exit}
>   print var
>   print $0"--"$2
> }' a.txt
2   Alice   female  24   def@gmail.com  18084925203
1   Bob     male    28   abc@qq.com     18023394012--Bob
[root@arm64v8 ~]#
1.1.3.11.3、从指定文件中读取数据
  1. getline < filename:从指定文件 filename 中读取一条记录并保存到 $0 中
    • 会进行字段的划分,会设置变量 $0 $N NF,不会设置变量 NR FNR
  2. getline var < filename:从指定文件 filename 中读取一条记录并保存到指定变量 var 中
    • 不会划分字段,不会设置变量 NR FNR NF $0 $N

filename 需使用双引号包围表示文件名字符串,否则会当作变量解析 getline < "a.txt"。此外,如果路径是使用变量构建的,则应该使用括号包围路径部分。

例如 getline < dir "/" filename 中使用了两个变量构建路径,这会产生歧义,应当写成 getline <(dir "/" filename)

注意,每次从 filename 读取之后都会做好位置偏移标记,下次再从该文件读取时将根据这个位置标记继续向后读取。

例如,每次行首以 1 开头时就读取 a.txt 文件的所有行。

[root@arm64v8 ~]# awk '/^1/{
> print;
> while((getline < "a.txt")>0){print};
> close("a.txt")
> }' a.txt
1   Bob     male    28   abc@qq.com     18023394012
ID  name    gender  age  email          phone
1   Bob     male    28   abc@qq.com     18023394012
2   Alice   female  24   def@gmail.com  18084925203
3   Tony    male    21   aaa@163.com    17048792503
4   Kevin   male    21   bbb@189.com    17023929033
5   Alex    male    18   ccc@xyz.com    18185904230
6   Andy    female  22   ddd@139.com    18923902352
7   Jerry   female  25   exdsa@189.com  18785234906
8   Peter   male    20   bax@qq.com     17729348758
9   Steven  female  23   bc@sohu.com    15947893212
10  Bruce   female  27   bcbd@139.com   13942943905
10  Bruce   female  27   bcbd@139.com   13942943905
ID  name    gender  age  email          phone
1   Bob     male    28   abc@qq.com     18023394012
2   Alice   female  24   def@gmail.com  18084925203
3   Tony    male    21   aaa@163.com    17048792503
4   Kevin   male    21   bbb@189.com    17023929033
5   Alex    male    18   ccc@xyz.com    18185904230
6   Andy    female  22   ddd@139.com    18923902352
7   Jerry   female  25   exdsa@189.com  18785234906
8   Peter   male    20   bax@qq.com     17729348758
9   Steven  female  23   bc@sohu.com    15947893212
10  Bruce   female  27   bcbd@139.com   13942943905
[root@arm64v8 ~]#

上面的 close("a.txt") 表示在 while(getline) 读取完文件之后关掉,以便后面再次读取,如果不关掉,则文件偏移指针将一直在文件结尾处,使得下次读取时直接遇到 EOF。

1.1.3.11.4、从 Shell 命令输出结果中读取数据
  1. cmd | getline:从 Shell 命令 cmd 的输出结果中读取一条记录保存到 $0
    • 会进行字段划分,设置变量 $0 NF $N RT,不会修改变量 NR FNR
  2. cmd | getline var:从 Shell 命令 cmd 的输出结果中读取数据保存到 var 中
    • 除了 var 和 RT,其它变量都不会设置

如果要再次执行 cmd 并读取其输出数据,则需要 close 关闭该命令。例如 close("seq 1 5"),参见下面的示例。

例如:每次遇到以 1 开头的行都输出 seq 命令产生的 1 2 3 4 5

[root@arm64v8 ~]# awk '/^1/{print;while(("seq 1 5"|getline)>0){print};close("seq 1 5")}' a.txt 
1   Bob     male    28   abc@qq.com     18023394012
1
2
3
4
5
10  Bruce   female  27   bcbd@139.com   13942943905
1
2
3
4
5
[root@arm64v8 ~]#

再例如,调用 Shell 的 date 命令生成时间,然后保存到 awk 变量 cur_date 中:

[root@arm64v8 ~]# awk '/^1/{
> print
> "date +\"%F %T\""|getline cur_date
> print cur_date
> close("date +\"%F %T\"")
> }' a.txt
1   Bob     male    28   abc@qq.com     18023394012
2021-09-12 19:02:24
10  Bruce   female  27   bcbd@139.com   13942943905
2021-09-12 19:02:24
[root@arm64v8 ~]#
  • 可以将 cmd 保存成一个字符串变量。

    [root@arm64v8 ~]# awk 'BEGIN{get_date="date +\"%F %T\""}
    > /^1/{
    > print
    > get_date | getline cur_date
    > print cur_date
    > close(get_date)
    > }' a.txt
    1   Bob     male    28   abc@qq.com     18023394012
    2021-09-12 19:06:12
    10  Bruce   female  27   bcbd@139.com   13942943905
    2021-09-12 19:06:12
    [root@arm64v8 ~]#

更为复杂一点的,cmd 中可以包含 Shell 的其它特殊字符,例如管道、重定向符号等:

[root@arm64v8 ~]# awk '
>   /^1/{
>     print
>     if(("seq 1 5 | xargs -i echo x{}y 2>/dev/null"|getline) > 0){
>       print
>     }
>     close("seq 1 5 | xargs -i echo x{}y 2>/dev/null")
> }' a.txt
1   Bob     male    28   abc@qq.com     18023394012
x1y
10  Bruce   female  27   bcbd@139.com   13942943905
x1y
[root@arm64v8 ~]#
1.1.3.11.5、awk 中的 coprocess

awk 虽然强大,但是有些数据仍然不方便处理,这时可将数据交给 Shell 命令去帮助处理,然后再从 Shell 命令的执行结果中取回处理后的数据继续 awk 处理。

awk 通过 |& 符号来支持 coproc。

awk_print[f] "something" |& Shell_Cmd
Shell_Cmd |& getline [var]

这表示 awk 通过 print 输出的数据将传递给 Shell 的命令 Shell_Cmd 去执行,然后 awk 再从 Shell_Cmd 的执行结果中取回 Shell_Cmd 产生的数据。

例如,不想使用 awk 的 substr() 来取子串,而是使用 sed 命令来替换。

[root@arm64v8 ~]# awk '
>     BEGIN{
>       CMD="sed -nr \"s/.*@(.*)$/\\1/p\"";
>     }
> 
>     NR>1{
>         print $5;
>         print $5 |& CMD;
>         close(CMD,"to");
>         CMD |& getline email_domain;
>         close(CMD);
>         print email_domain;
> }' a.txt
abc@qq.com
qq.com
def@gmail.com
gmail.com
aaa@163.com
163.com
bbb@189.com
189.com
ccc@xyz.com
xyz.com
ddd@139.com
139.com
exdsa@189.com
189.com
bax@qq.com
qq.com
bc@sohu.com
sohu.com
bcbd@139.com
139.com
[root@arm64v8 ~]#

对于 awk_print |& cmd; cmd |& getline 的使用,须注意的是:

  • awk_print |& cmd 会直接将数据写进管道,cmd 可以从中获取数据
  • 强烈建议在 awk_print 写完数据之后加上 close(cmd,"to"),这样表示向管道中写入一个 EOF 标记,避免某些要求读完所有数据再执行的 cmd 命令被永久阻塞
  • 如果 cmd 是按块缓冲的,则 getline 可能会陷入阻塞。这时可将 cmd 部分改写成 stdbuf -oL cmd 以强制其按行缓冲输出数据
    • CMD="stdbuf -oL cmdline";awk_print |& CMD;close(CMD,"to");CMD |& getline

对于那些要求读完所有数据再执行的命令,例如 sort 命令,它们有可能需要等待数据已经完成后(遇到 EOF 标记)才开始执行任务,对于这些命令,可以多次向 coprocess 中写入数据,最后 close(CMD,"to") 让 coprocess 运行起来。

例如,对 age 字段(即 $4)使用 sort 命令按数值大小进行排序:

[root@arm64v8 ~]# awk '
>     BEGIN{
>       CMD="sort -k4n";
>     }
> 
>     # 将所有行都写进管道
>     NR>1{
>       print $0 |& CMD;
>     }
> 
>     END{
>       close(CMD,"to");  # 关闭管道通知sort开始排序
>       while((CMD |& getline)>0){
>         print;
>       }
>       close(CMD);
> } ' a.txt
5   Alex    male    18   ccc@xyz.com    18185904230
8   Peter   male    20   bax@qq.com     17729348758
3   Tony    male    21   aaa@163.com    17048792503
4   Kevin   male    21   bbb@189.com    17023929033
6   Andy    female  22   ddd@139.com    18923902352
9   Steven  female  23   bc@sohu.com    15947893212
2   Alice   female  24   def@gmail.com  18084925203
7   Jerry   female  25   exdsa@189.com  18785234906
10  Bruce   female  27   bcbd@139.com   13942943905
1   Bob     male    28   abc@qq.com     18023394012
[root@arm64v8 ~]#
1.1.3.11.6、close()
close(filename)
close(cmd,[from | to])  # to参数只用于coprocess的第一个阶段

如果 close() 关闭的对象不存在,awk 不会报错,仅仅只是让其返回一个负数返回值。

close() 有两个基本作用:

  1. 关闭文件,丢弃已有的文件偏移指针
    • 下次再读取文件,将只能重新打开文件,重新打开文件会从文件最开头开始读取
  2. 发送 EOF 标记

awk 中任何文件都只会在第一次使用时打开,之后都不会再重新打开。只有关闭之后,再使用才会重新打开。

[root@arm64v8 ~]# cat x.log 
I am an interpreter
[root@arm64v8 ~]# 
[root@arm64v8 ~]# awk '/^1/{
>     print;
>     while((getline var < "x.log") > 0){
>         print var
>     }
>     close("x.log")
> }' a.txt
1   Bob     male    28   abc@qq.com     18023394012
I am an interpreter
10  Bruce   female  27   bcbd@139.com   13942943905
I am an interpreter
[root@arm64v8 ~]#
  • 本例需求是只要在 a.txt 中匹配到 1 开头的行就输出另一个文件 x.log 的所有内容;
  • 那么在第一次输出 x.log 文件内容之后,文件偏移指针将在 x.log 文件的结尾处,如果不关闭该文件,则后续所有读取 x.log 的文件操作都从结尾处继续读取,但是显然总是得到 EOF 异常,所以 getline 返回值为 0,而且也读取不到任何数据。
  • 所以,必须关闭它才能在下次匹配成功时再次从头读取该文件。

在处理 Coprocess 的时候,close() 可以指定第二个参数 "from" 或 "to" ,它们都针对于 coproc 而言,from 时表示关闭 coproc |& getline 的管道,使用 to 时,表示关闭 print something |& coproc 的管道。

awk '
BEGIN{
  CMD="sed -nr \"s/.*@(.*)$/\\1/p\"";
}
NR>1{
    print $5;
    print $5 |& CMD;
    close(CMD,"to");   # 本次close()是必须的
    CMD |& getline email_domain;
    close(CMD);
    print email_domain;
}' a.txt
  • 上面的第一个 close 是必须的,否则 sed 会一直阻塞。因为 sed 一直认为还有数据可读,只有关闭管道发送一个 EOF,sed 才会开始处理。

1.1.3.12、awk 变量

awk 的变量是动态变量,在使用时声明。

所以 awk 变量有 3 种状态:

  • 未声明状态:称为 untyped 类型
  • 引用过但未赋值状态:unassigned 类型
  • 已赋值状态

引用未赋值的变量,其默认初始值为空字符串或数值 0。

在 awk 中未声明的变量称为 untyped,声明了但未赋值(只要引用了就声明了)的变量其类型为 unassigned。

gawk 4.2版提供了 typeof() 函数,可以测试变量的数据类型,包括测试变量是否声明。

[root@arm64v8 ~]# awk 'BEGIN{
>   print(typeof(a))            # untyped
>   if(b==0){print(typeof(b))}  # unassigned
> }'
untyped
unassigned
[root@arm64v8 ~]#

除了 typeof(),还可以使用下面的技巧进行检测:

[root@arm64v8 ~]# awk 'BEGIN{
>   if(a=="" && a==0){    # 未赋值时,两个都true
>     print "untyped or unassigned"
>   } else {
>       print "assigned"
>   }
> }'
untyped or unassigned
[root@arm64v8 ~]#
1.1.3.12.1、变量赋值

awk 中的变量赋值语句也可以看作是一个有返回值的表达式。

例如,a=3 赋值完成后返回 3,同时变量 a 也被设置为 3。

基于这个特点,有两点用法:

  • 可以 x=y=z=5,等价于 z=5 y=5 x=5
  • 可以将赋值语句放在任意允许使用表达式的地方
    • x != (y = 1)
    • awk 'BEGIN{print (a=4);print a}'

问题:a=1;arr[a+=2] = (a=a+6) 是怎么赋值的,对应元素结果等于?arr[3]=7。但不要这么做,因为不同 awk 的赋值语句左右两边的评估顺序有可能不同。

1.1.3.12.2、awk 中声明变量的位置
  1. 在 BEGIN 或 main 或 END 代码段中直接引用或赋值
  2. 使用 -v var=val 选项,可定义多个,必须放在 awk 代码的前面
    • 它的变量声明早于 BEGIN 块
    • 普通变量:awk -v age=123 'BEGIN{print age}'
    • 使用 shell 变量赋值:awk -v age=$age 'BEGIN{print age}'
  3. 在 awk 代码后面使用 var=val 参数
    • 它的变量声明在 BEGIN 之后
    • awk '{print n}' n=3 a.txt n=4 b.txt
    • awk '{print $1}' FS=' ' a.txt FS=":" /etc/passwd
    • 使用 Shell 变量赋值:awk '{print age}' age=$age a.txt
1.1.3.12.3、awk 中使用 Shell 变量

要在 awk 中使用 Shell 变量,有三种方式:

  1. 在 -v 选项中将 Shell 变量赋值给 awk 变量

    [root@arm64v8 ~]# num=$(cat a.txt | wc -l)
    [root@arm64v8 ~]# awk -v n=$num 'BEGIN{print n}'
    11
    [root@arm64v8 ~]#
    • -v 选项是在 awk 工作流程的第一阶段解析的,所以 -v 选项声明的变量在 BEGIN{}、END{} 和 main 代码段中都能直接使用。
  2. 在非选项型参数位置处使用 var=value 格式将 Shell 变量赋值给 awk 变量

    [root@arm64v8 ~]# num=$(cat a.txt | wc -l)
    [root@arm64v8 ~]# awk '{print n}' n=$num a.txt 
    11
    11
    11
    11
    11
    11
    11
    11
    11
    11
    11
    [root@arm64v8 ~]#
    • 非选项型参数设置的变量不能在 BEGIN 代码段中使用。
  3. 直接在 awk 代码部分暴露 Shell 变量,交给 Shell 解析进行 Shell 的变量替换

    [root@arm64v8 ~]# num=$(cat a.txt | wc -l)
    [root@arm64v8 ~]# awk 'BEGIN{print '"$num"'}'
    11
    [root@arm64v8 ~]#
    • 这种方式最灵活,但可读性最差,可能会出现大量的引号。

1.1.3.13、数据类型和字面量

1.1.3.13.1、数据类型

gawk 有两种基本的数据类型:数值和字符串。在 gawk 4.2.0 版本中,还支持第三种基本的数据类型:正则表达式类型。

数据是什么类型在使用它的上下文中决定:在字符串操作环境下将转换为字符串,在数值操作环境下将转换为数值。这和自然语言中的一个词语、一个单词在不同句子内的不同语义是一样的。

隐式转换:

  • 算术加 0 操作可转换为数值类型
    • "123" + 0 返回数值 123
    • " 123abc" + 0 转换为数值时为 123
    • 无效字符串将转换成 0,例如 "abc"+3 返回 3
  • 连接空字符串可转换为字符串类型
    • 123"" 转换为字符串 ”123”
[root@arm64v8 ~]# awk 'BEGIN{a="123";print typeof(a+0)}' # number
number
[root@arm64v8 ~]# awk 'BEGIN{a=123;print typeof(a"")}'   # string
string
[root@arm64v8 ~]# 
[root@arm64v8 ~]# awk 'BEGIN{a=2;b=3;print(a b)+4}' # 27
27
[root@arm64v8 ~]#

显式转换:

  • 数值->字符串:

    • CONVFMT 或 sprintf():功能等价。都是指定数值转换为字符串时的格式
    [root@arm64v8 ~]# awk 'BEGIN{a=123.4567;CONVFMT="%.2f";print a""}' #123.46
    123.46
    [root@arm64v8 ~]# awk 'BEGIN{a=123.4567;print sprintf("%.2f", a)}' #123.46
    123.46
    [root@arm64v8 ~]# awk 'BEGIN{a=123.4567;printf("%.2f",a)}'
    123.46[root@arm64v8 ~]# 
    [root@arm64v8 ~]#
  • 字符串->数值:strtonum()

    [root@arm64v8 ~]# gawk 'BEGIN{a="123.4567";print strtonum(a)}' # 123.457
    123.457
    [root@arm64v8 ~]#
1.1.3.13.2、awk 字面量

awk 中有 3 种字面量:字符串字面量、数值字面量和正则表达式字面量。

数值字面量

整数、浮点数、科学计数

  • 105、105.0、1.05e+2、1050e-1

awk 内部总是使用浮点数方式保存所有数值,但用户在使用可以转换成整数的数值时总会去掉小数点

  • 数值 12.0 面向用户的值为 12,12 面向 awk 内部的值是 12.00000...0
# 结果是123而非123.0
[root@arm64v8 ~]# awk 'BEGIN{a=123.0;print a}'
123
[root@arm64v8 ~]#

算术运算

++ --    自增、自减,支持i++和++i或--i或i--  
^        幂运算(**也用于幂运算)
+ -      一元运算符(正负数符号)
* / %    乘除取模运算
+ -      加减法运算

# 注:
# 1.
#     
# 2.
  • ++ 和 -- 既可以当作独立语句,也可以作为表达式,如:

    [root@arm64v8 ~]# awk 'BEGIN{a=3;a++;a=++a;print a}'
    5
    [root@arm64v8 ~]#
  • **^ 幂运算是从右向左计算的:print 2**1**3 得到 2 而不是 8

赋值操作(优先级最低):

= += -= *= /= %= ^= **=
  • 疑惑:b = 6;print b += b++ 输出结果?可能是 12 或 13。不同的 awk 的实现在评估顺序上不同,所以不要用这种可能产生歧义的语句。

字符串字面量

awk 中的字符串都以双引号包围,不能以单引号包围。

  • "abc"
  • ""
  • "\0""\n"

字符串连接(串联):awk 没有为字符串的串联操作提供运算符,可以直接连接或使用空格连接。

[root@arm64v8 ~]# awk 'BEGIN{print ("one" "two")}'  # "onetwo"
onetwo
[root@arm64v8 ~]# awk 'BEGIN{print ("one""two")}'
onetwo
[root@arm64v8 ~]# awk 'BEGIN{a="one";b="two";print (a b)}'
onetwo
[root@arm64v8 ~]#

注意:字符串串联虽然方便,但是要考虑串联的优先级。例如下面的:

# 下面第一个串联成功,第二个串联失败,
# 因为串联优先级低于加减运算,等价于`12 (" " -23)`
# 即:先转为数值0-23,再转为字符串12-23
[root@arm64v8 ~]# awk 'BEGIN{a="one";b="two";print (12 " " 23)}'
12 23
[root@arm64v8 ~]# awk 'BEGIN{a="one";b="two";print (12 " " -23)}'
12-23
[root@arm64v8 ~]#

正则表达式字面量

普通正则:

  • /[0-9]+/
  • 匹配方式:"str" ~ /pattern/"str" !~ /pattern/
  • 匹配结果返回值为 0(匹配失败) 或 1(匹配成功)
  • 任何单独出现的 /pattern/ 都等价于 $0 ~ /pattern/
    • if(/pattern/) 等价于 if($0 ~ /pattern/)

强类型的正则字面量(gawk 4.2.0 才支持)

  • @/pattern/ 作为独立的一种数据类型:正则表达式类型
  • 在使用正则字面量变量进行匹配的时候,不能简写 a=@/Alice/;a{print},只能写完整的匹配 a=@/Alice/;$0 ~ a{print}
  • 可使用 typeof()(也是 4.2 才支持的)检查类型,得到的结果将是 regexp
    • awk 'BEGIN{re=@/abc/;print typeof(re)}'

1.1.3.14、gawk 支持的正则表达式

.       # 匹配任意字符,包括换行符
^
$
[...]
[^...]
|
+
*
?
()
{m}
{m,}
{m,n}
{,n}

[:lower:]
[:upper:]
[:alpha:]
[:digit:]
[:alnum:]
[:xdigit:]
[:blank:]
[:space:]
[:punct:]
[:graph:]
[:print:]
[:cntrl:]

以下是gawk支持的:
\y    匹配单词左右边界部分的空字符位置 "hello world"
\B    和\y相反,匹配单词内部的空字符位置,例如"crate" ~ `/c\Brat\Be/`成功
\<    匹配单词左边界
\>    匹配单词右边界
\s    匹配空白字符
\S    匹配非空白字符
\w    匹配单词组成字符(大小写字母、数字、下划线)
\W    匹配非单词组成字符
\`    匹配字符串的绝对行首  "abc\ndef"
\'    匹配字符串的绝对行尾

gawk 不支持正则修饰符,所以无法直接指定忽略大小写的匹配。

如果想要实现忽略大小写匹配,则可以将字符串先转换为大写、小写再进行匹配。或者设置预定义变量 IGNORECASE 为非 0 值。

# 转换为小写
[root@arm64v8 ~]# awk 'tolower($0) ~ /bob/{print $0}' a.txt
1   Bob     male    28   abc@qq.com     18023394012
[root@arm64v8 ~]#

# 设置IGNORECASE
[root@arm64v8 ~]# awk '/BOB/{print $0}' IGNORECASE=1 a.txt
1   Bob     male    28   abc@qq.com     18023394012
[root@arm64v8 ~]#

1.1.3.15、awk 布尔值、比较和逻辑运算

1.1.3.15.1、awk 布尔值

在 awk 中,没有像其它语言一样专门提供 true、false 这样的关键字。

但它的布尔值逻辑非常简单:

  • 数值 0 表示布尔假
  • 空字符串表示布尔假
  • 其余所有均为布尔真
    • 字符串 ”0” 也是真,因为它是字符串
  • awk 中,正则匹配也有返回值,匹配成功则返回 1,匹配失败则返回 0
  • awk 中,所有的布尔运算也有返回值,布尔真返回值 1,布尔假返回值为 0
[root@arm64v8 ~]# awk '
> BEGIN{
>     if(1){print "haha"}
>     if("0"){print "hehe"}
>     if(a=3){print "hoho"}  # if(3){print "hoho"}
>     if(a==3){print "aoao"}
>     if(/root/){print "heihei"}  # $0 ~ /root/
> }'
haha
hehe
hoho
aoao
[root@arm64v8 ~]#
1.1.3.15.2、awk 中比较操作

strnum 类型

awk 最基本的数据类型只有 string 和 number(gawk 4.2.0版本之后支持正则表达式类型)。但是,对于用户输入数据(例如从文件中读取的各个字段值),它们理应属于 string 类型,但有时候它们看上去可能像是数值(例如 $2=37),而有时候有需要这些值是数值类型。

awk 的数据来源:

  1. awk 内部产生的,包括变量的赋值、表达式或函数的返回值。
  2. 从其它来源获取到的数据,都是外部数据,也是用户输入数据,这些数据理应全部都是 string 类型的数据。

所以 POSIX 定义了一个名为 ”numeric string” 的 ”墙头草” 类型,gawk 中则称为 strnum 类型。当获取到的用户数据看上去是数字时,那么它就是 strnum 类型。strnum 类型在被使用时会被当作数值类型。

注意,strnum类型只针对于awk中除数值常量、字符串常量、表达式计算结果外的数据。例如从文件中读取的字段 $1、$2、ARGV 数组中的元素等等。

[root@arm64v8 ~]# echo "30" | awk '{print typeof($0) " " typeof($1)}'
strnum strnum
[root@arm64v8 ~]# echo "+30" | awk '{print typeof($1)}'
strnum
[root@arm64v8 ~]# echo "30a" | awk '{print typeof($1)}'
string
[root@arm64v8 ~]# echo "30 a" | awk '{print typeof($0) " " typeof($1)}'
string strnum
[root@arm64v8 ~]# echo " +30 " | awk '{print typeof($0) " " typeof($1)}'
strnum strnum
[root@arm64v8 ~]#

大小比较操作

比较操作符:

< > <= >= != ==  大小、等值比较
in     数组成员测试

比较规则:

       |STRING NUMERIC STRNUM
-------|-----------------------
STRING |string string  string
NUMERIC|string numeric numeric
STRNUM |string numeric numeric

简单来说,string 优先级最高,只要 string 类型参与比较,就都按照 string 的比较方式,所以可能会进行隐式的类型转换。

其它时候都采用 num 类型比较。

[root@arm64v8 ~]# echo ' +3.14' | awk '{print typeof($0) " " typeof($1)}'
strnum strnum
[root@arm64v8 ~]#
[root@arm64v8 ~]# echo ' +3.14' | awk '{print($0 == " +3.14")}'
1
[root@arm64v8 ~]#
[root@arm64v8 ~]# echo ' +3.14' | awk '{print($0 == "+3.14")}'
0
[root@arm64v8 ~]#
[root@arm64v8 ~]# echo ' +3.14' | awk '{print($0 == "3.14")}'
0
[root@arm64v8 ~]#
[root@arm64v8 ~]# echo ' +3.14' | awk '{print($0 == 3.14)}'
1
[root@arm64v8 ~]#
[root@arm64v8 ~]# echo ' +3.14' | awk '{print($1 == 3.14)}'
1
[root@arm64v8 ~]# 
[root@arm64v8 ~]# echo ' +3.14' | awk '{print($1 == " +3.14")}'
0
[root@arm64v8 ~]#
[root@arm64v8 ~]# echo ' +3.14' | awk '{print($1 == "+3.14")}'
1
[root@arm64v8 ~]# 
[root@arm64v8 ~]# echo ' +3.14' | awk '{print($1 == "3.14")}'
0
[root@arm64v8 ~]#
[root@arm64v8 ~]# echo 1e2 3|awk '{print ($1<$2)?"true":"false"}'
false
[root@arm64v8 ~]#

采用字符串比较时需注意,它是逐字符逐字符比较的。

"11" < "9"  # true
"ab" < 99   # false
1.1.3.15.3、逻辑运算
[root@arm64v8 ~]# awk 'BEGIN{print(!99)}'
0
[root@arm64v8 ~]# awk 'BEGIN{print(!"ab")}'
0
[root@arm64v8 ~]# awk 'BEGIN{print(!0)}'
1
[root@arm64v8 ~]# awk 'BEGIN{print(!ab)}'
1
[root@arm64v8 ~]# awk 'BEGIN{print(!!99)}'
1
[root@arm64v8 ~]# awk 'BEGIN{print(!!"ab")}'
1
[root@arm64v8 ~]# awk 'BEGIN{print(!!0)}'
0
[root@arm64v8 ~]# awk 'BEGIN{print(!!ab)}'
0
[root@arm64v8 ~]# 

由于 awk 中的变量未赋值时默认初始化为空字符串或数值 0,也就是布尔值为假。那么可以直接对一个未赋值的变量执行 ! 操作。

1.1.3.15.4、运算符优先级

优先级从高到低:

()
$                       # $(2+2)
++ --
^ **
+ - !                   # 一元运算符
* / %
+ -
space                   # 这是字符连接操作 `12 " " 23`  `12 " " -23`
| |&
< > <= >= != ==         # 注意>即是大于号,也是print/printf的重定向符号
~ !~
in
&&
||
?:
= += -= *= /= %= ^=

对于相同优先级的运算符,通常都是从左开始运算,但下面 2 种例外,它们都从右向左运算:

  • 赋值运算:如 = += -= *=
  • 幂运算
a - b + c  =>  (a - b) + c
a = b = c  =>  a =(b = c)
2**2**3    =>  2**(2**3)

再者,注意 print 和 printf 中出现的 > 符号,这时候它表示的是重定向符号,不能再出现优先级比它低的运算符,这时可以使用括号改变优先级。例如:

awk 'BEGIN{print "foo" > a < 3 ? 2 : 1)'   # 语法错误
awk 'BEGIN{print "foo" > (a < 3 ? 2 : 1)}' # 正确

1.1.3.16、awk 流程控制

if…else

# 单独的if
if(cond){
    statements
}

# if...else
if(cond1){
    statements1
} else {
    statements2
}

# if...else if...else
if(cond1){
    statements1
} else if(cond2){
    statements2
} else if(cond3){
    statements3
} else{
    statements4
}
[root@arm64v8 ~]# awk '
>   BEGIN{
>     mark = 999
>     if (mark >=0 && mark < 60) {
>       print "学渣"
>     } else if (mark >= 60 && mark < 90) {
>       print "还不错"
>     } else if (mark >= 90 && mark <= 100) {
>       print "学霸"
>     } else {
>       print "错误分数"
>     }
>   }
> '
错误分数
[root@arm64v8 ~]#

三目运算符?:

expr1 ? expr2 : expr3

if(expr1){
    expr2
} else {
    expr3
}
[root@arm64v8 ~]# awk 'BEGIN{a=50;b=(a>60) ? "及格" : "不及格";print(b)}'
不及格
[root@arm64v8 ~]# awk 'BEGIN{a=50; a>60 ? b="及格" : b="不及格";print(b)}'
不及格
[root@arm64v8 ~]#

switch…case

switch (expression) {
    case value1|regex1 : statements1
    case value2|regex2 : statements2
    case value3|regex3 : statements3
    ...
    [ default: statement ]
}

awk 中的 switch 分支语句功能较弱,只能进行等值比较或正则匹配。

各分支结尾需使用 break 来终止。

{
    switch($1){
        case 1:
            print("Monday")
            break
        case 2:
            print("Tuesday")
            break
        case 3:
            print("Wednesday")
            break
        case 4:
            print("Thursday")
            break
        case 5:
            print("Friday")
            break
        case 6:
            print("Saturday")
            break
        case 7:
            print("Sunday")
            break
        default:
            print("What day?")
            break
    }
}

分支穿透:

{
    switch($1){
        case 1:
        case 2:
        case 3:
        case 4:
        case 5:
            print("Weekday")
            break
        case 6:
        case 7:
            print("Weekend")
            break
        default:
            print("What day?")
            break
    }
}

while 和 do…while

while(condition){
    statements
}

do {
    statements
} while(condition)

while 先判断条件再决定是否执行 statements,do…while 先执行 statements 再判断条件决定下次是否再执行 statements。

[root@arm64v8 ~]# awk 'BEGIN{i=0;while(i<5){print i;i++}}'
0
1
2
3
4
[root@arm64v8 ~]# awk 'BEGIN{i=0;do {print i;i++} while(i<5)}'
0
1
2
3
4
[root@arm64v8 ~]#

多数时候,while 和 do…while 是等价的,但如果第一次条件判断失败,则 do…while 和 while 不同。

[root@arm64v8 ~]# awk 'BEGIN{i=0;while(i == 2){print i;i++}}'
[root@arm64v8 ~]# awk 'BEGIN{i=0;do {print i;i++} while(i ==2 )}'
0
[root@arm64v8 ~]#

所以,while 可能一次也不会执行,do…while 至少会执行一次。一般用 while,do…while 相比 while 来说,用的频率非常低。

for 循环

for (expr1; expr2; expr3) {
    statement
}

for (idx in array) {
    statement
}

break 和 continue

break 可退出 for、while、do…while、switch 语句。continue 可让 for、while、do…while 进入下一轮循环。

awk '
BEGIN{
  for(i=0;i<10;i++){
    if(i==5){
      break
    }
    print(i)
  }

  # continue
  for(i=0;i<10;i++){
    if(i==5)continue
    print(i)
  }
}'

next 和 nextfile

next 会在当前语句处立即停止后续操作,并读取下一行,进入循环顶部。

例如,输出除第 3 行外的所有行。

[root@arm64v8 ~]# awk 'NR==3{next}{print}' a.txt
ID  name    gender  age  email          phone
1   Bob     male    28   abc@qq.com     18023394012
3   Tony    male    21   aaa@163.com    17048792503
4   Kevin   male    21   bbb@189.com    17023929033
5   Alex    male    18   ccc@xyz.com    18185904230
6   Andy    female  22   ddd@139.com    18923902352
7   Jerry   female  25   exdsa@189.com  18785234906
8   Peter   male    20   bax@qq.com     17729348758
9   Steven  female  23   bc@sohu.com    15947893212
10  Bruce   female  27   bcbd@139.com   13942943905
[root@arm64v8 ~]# awk 'NR==3{getline}{print}' a.txt
ID  name    gender  age  email          phone
1   Bob     male    28   abc@qq.com     18023394012
3   Tony    male    21   aaa@163.com    17048792503
4   Kevin   male    21   bbb@189.com    17023929033
5   Alex    male    18   ccc@xyz.com    18185904230
6   Andy    female  22   ddd@139.com    18923902352
7   Jerry   female  25   exdsa@189.com  18785234906
8   Peter   male    20   bax@qq.com     17729348758
9   Steven  female  23   bc@sohu.com    15947893212
10  Bruce   female  27   bcbd@139.com   13942943905
[root@arm64v8 ~]#

nextfile 会在当前语句处立即停止后续操作,并直接读取下一个文件,并进入循环顶部。

例如,每个文件只输出前 2 行:

[root@arm64v8 ~]# awk 'FNR==3{nextfile}{print}' a.txt a.txt
ID  name    gender  age  email          phone
1   Bob     male    28   abc@qq.com     18023394012
ID  name    gender  age  email          phone
1   Bob     male    28   abc@qq.com     18023394012
[root@arm64v8 ~]#

exit

exit [exit_code]

直接退出 awk 程序。注意,END 语句块也是 exit 操作的一部分,所以在 BEGIN 或 main 段中执行 exit 操作,也会执行 END 语句块。如果 exit 在 END 语句块中执行,则立即退出。

所以,如果真的想直接退出整个 awk,则可以先设置一个 flag 变量,然后在 END 语句块的开头检查这个变量再 exit。

BEGIN{
    ...code...
    if(cond){
        flag=1
        exit
    }
}
{}
END{
    if(flag){
        exit
    }
    ...code...
}

awk '
    BEGIN{print "begin";flag=1;exit}
    {}
    END{if(flag){exit};print "end2"}
'

exit 可以指定退出状态码,如果触发了两次 exit 操作,即 BEGIN 或 main 中的 exit 触发了 END 中的 exit,且 END 中的 exit 没有指定退出状态码时,则采取前一个退出状态码。

[root@arm64v8 ~]# awk 'BEGIN{flag=1;exit 2}{}END{if(flag){exit 1}}' 
[root@arm64v8 ~]# echo $?
1
[root@arm64v8 ~]# awk 'BEGIN{flag=1;exit 2}{}END{if(flag){exit}}'
[root@arm64v8 ~]# echo $?
2
[root@arm64v8 ~]#

1.1.3.17、awk 数组

awk 数组特性:

  • awk 的数组是关联数组(即 key/value 方式的 hash 数据结构),索引下标可为数值(甚至是负数、小数等),也可为字符串
    • 在内部,awk 数组的索引全都是字符串,即使是数值索引在使用时内部也会转换成字符串
    • awk 的数组元素的顺序和元素插入时的顺序很可能是不相同的
  • awk 数组支持数组的数组

awk 访问、赋值数组元素

arr[idx]
arr[idx] = value

索引可以是整数、负数、0、小数、字符串。如果是数值索引,会按照 CONVFMT 变量指定的格式先转换成字符串。

[root@arm64v8 ~]# awk '
>   BEGIN{
>     arr[1]   = 11
>     arr["1"] = 111
>     arr["a"] = "aa"
>     arr[-1]  = -11
>     arr[4.3] = 4.33
>     print arr[1]
>     print arr["1"]
>     print arr["a"]
>     print arr[-1]
>     print arr[4.3]
>   }
> '
111
111
aa
-11
4.33
[root@arm64v8 ~]#

通过索引的方式访问数组中不存在的元素时,会返回空字符串,同时会创建这个元素并将其值设置为空字符串。

[root@arm64v8 ~]# awk '
>   BEGIN{
>     arr[-1]=3;
>     print length(arr);  # 1
>     print arr[1];
>     print length(arr)   # 2
>   }'
1

2
[root@arm64v8 ~]# 

awk 数组长度

awk 提供了 length() 函数来获取数组的元素个数,它也可以用于获取字符串的字符数量。还可以获取数值转换成字符串后的字符数量。

[root@arm64v8 ~]# awk 'BEGIN{arr[1]=1;arr[2]=2;print length(arr);print length("hello")}'
2
5
[root@arm64v8 ~]#

awk 删除数组元素

  • delete arr[idx]:删除数组 arr[idx] 元素
    • 删除不存在的元素不会报错
  • delete arr:删除数组所有元素
[root@arm64v8 ~]# awk 'BEGIN{arr[1]=1;arr[2]=2;arr[3]=3;delete arr[2];print length(arr)}'
2
[root@arm64v8 ~]# 

awk 检测是否是数组

isarray(arr) 可用于检测 arr 是否是数组,如果是数组则返回 1,否则返回 0。

typeof(arr) 可返回数据类型,如果 arr 是数组,则其返回 "array"。

[root@arm64v8 ~]# awk 'BEGIN{
>     arr[1]=1;
>     print isarray(arr);
>     print (typeof(arr) == "array")
> }'
1
1
[root@arm64v8 ~]#

awk 测试元素是否在数组中

不要使用下面的方式来测试元素是否在数组中:

if(arr["x"] != ""){...}
  • 如果不存在 arr[“x”],则会立即创建该元素,并将其值设置为空字符串
  • 有些元素的值本身就是空字符串

应当使用数组成员测试操作符 in 来测试:

# 注意,idx不要使用index,它是一个内置函数
if (idx in arr){...}

它会测试索引 idx 是否在数组中,如果存在则返回 1,不存在则返回 0。

[root@arm64v8 ~]# awk '
>     BEGIN{
>         arr[1]=1;
>         arr[2]=2;
>         arr[3]=3;
> 
>         arr[1]="";
>         delete arr[2];
> 
>         print (1 in arr);  # 1
>         print (2 in arr);  # 0
>     }'
1
0
[root@arm64v8 ~]#

awk 遍历数组

awk 提供了一种 for 变体来遍历数组:

for(idx in arr){print arr[idx]}

因为 awk 数组是关联数组,元素是不连续的,也就是说没有顺序。遍历 awk 数组时,顺序是不可预测的。

[root@arm64v8 ~]# awk '
>     BEGIN{
>         arr["one"] = 1
>         arr["two"] = 2
>         arr["three"] = 3
>         arr["four"] = 4
>         arr["five"] = 5
> 
>         for(i in arr){
>             print i " -> " arr[i]
>         }
>     }
> '
three -> 3
two -> 2
five -> 5
four -> 4
one -> 1
[root@arm64v8 ~]#

此外,不要随意使用 for(i=0;i<length(arr);i++) 来遍历数组,因为 awk 数组是关联数组。但如果已经明确知道数组的所有元素索引都位于某个数值范围内,则可以使用该方式进行遍历。

[root@arm64v8 ~]# awk '
>     BEGIN{
>         arr[1] = "one"
>         arr[2] = "two"
>         arr[3] = "three"
>         arr[4] = "four"
>         arr[5] = "five"
>         arr[10]= "ten"
> 
>         for(i=0;i<=10;i++){
>             if(i in arr){
>                 print arr[i]
>             }
>         }
>     }
> '
one
two
three
four
five
ten
[root@arm64v8 ~]#

awk 子数组

子数组是指数组中的元素也是一个数组,即 Array of Array,它也称为子数组(subarray)。

awk 也支持子数组,在效果上即是嵌套数组或多维数组。

a[1][1] = 11
a[1][2] = 12
a[1][3] = 13
a[2][1] = 21
a[2][2] = 22
a[2][3] = 23
a[2][4][1] = 241
a[2][4][2] = 242
a[2][4][1] = 241
a[2][4][3] = 243

通过如下方式遍历二维数组:

[root@arm64v8 ~]# awk '
> BEGIN{
> a[1][1] = 11
> a[1][2] = 12
> a[1][3] = 13
> a[2][1] = 21
> a[2][2] = 22
> a[2][3] = 23
> for(i in a){
>     for (j in a[i]){
>         if(isarray(a[i][j])){
>             continue
>         }
>         print a[i][j]
>     }
> }}'
11
12
13
21
22
23
[root@arm64v8 ~]#

awk 指定数组遍历顺序

由于 awk 数组是关联数组,默认情况下,for(idx in arr) 遍历数组时顺序是不可预测的。

但是 gawk 提供了 PROCINFO["sorted_in"] 来指定遍历的元素顺序。它可以设置为两种类型的值:

  • 设置为用户自定义函数
  • 设置为下面这些 awk 预定义好的值:
    • @unsorted:默认值,遍历时无序
    • @ind_str_asc:索引按字符串比较方式升序遍历
    • @ind_str_desc:索引按字符串比较方式降序遍历
    • @ind_num_asc:索引强制按照数值比较方式升序遍历。所以无法转换为数值的字符串索引将当作数值 0 进行比较
    • @ind_num_desc:索引强制按照数值比较方式降序遍历。所以无法转换为数值的字符串索引将当作数值 0 进行比较
    • @val_type_asc:按值升序比较,此外数值类型出现在前面,接着是字符串类型,最后是数组类(即认为 num<str<arr)
    • @val_type_desc:按值降序比较,此外数组类型出现在前面,接着是字符串类型,最后是数值型(即认为 num<str<arr)
    • @val_str_asc:按值升序比较,数值转换成字符串再比较,而数组出现在尾部(即认 str<arr)
    • @val_str_desc:按值降序比较,数值转换成字符串再比较,而数组出现在头部(即认 str<arr)
    • @val_num_asc:按值升序比较,字符串转换成数值再比较,而数组出现在尾部(即认 num<arr)
    • @val_num_desc:按值降序比较,字符串转换成数值再比较,而数组出现在头部(即认为 num<arr)
[root@arm64v8 ~]# awk '
>   BEGIN{
>     arr[1] = "one"
>     arr[2] = "two"
>     arr[3] = "three"
>     arr["a"] ="aa"
>     arr["b"] ="bb"
>     arr[10]= "ten"
> 
>     #PROCINFO["sorted_in"] = "@ind_num_asc"
>     #PROCINFO["sorted_in"] = "@ind_str_asc"
>     PROCINFO["sorted_in"] = "@val_str_asc"
>     for(idx in arr){
>       print idx " -> " arr[idx]
>     }
> }'
a -> aa
b -> bb
1 -> one
10 -> ten
3 -> three
2 -> two
[root@arm64v8 ~]#

1.1.3.18、awk ARGC 和 ARGV

预定义变量 ARGV 是一个数组,包含了所有的命令行参数。该数组使用从 0 开始的数值作为索引。

预定义变量 ARGC 初始时是 ARGV 数组的长度,即命令行参数的数量。

ARGV 数组的数量和 ARGC 的值只有在 awk 刚开始运行的时候是保证相等的。

[root@arm64v8 ~]# awk -va=1 -F: '
>   BEGIN{
>     print ARGC;
>     for(i in ARGV){
>       print "ARGV[" i "]= " ARGV[i]
>     }
> }' b=3 a.txt b.txt
4
ARGV[0]= awk
ARGV[1]= b=3
ARGV[2]= a.txt
ARGV[3]= b.txt
[root@arm64v8 ~]#

awk 读取文件是根据 ARGC 的值来进行的,有点类似于如下伪代码形式:

while(i=1;i<ARGC;i++){
    read from ARGV[i]
}

默认情况下,awk 在读完 ARGV 中的一个文件时,会自动从它的下一个元素开始读取,直到读完所有文件。

直接减小 ARGC 的值,会导致 awk 不会读取尾部的一些文件。此外,增减 ARGC 的值,都不会影响 ARGV 数组,仅仅只是影响 awk 读取文件的数量。

# 不会读取b.txt
[root@arm64v8 ~]# awk 'BEGIN{ARGC=2}{print}' a.txt b.txt

# 读完b.txt后自动退出
[root@arm64v8 ~]# awk 'BEGIN{ARGC=5}{print}' a.txt b.txt

可以将 ARGV 中某个元素赋值为空字符串 "",awk 在选择下一个要读取的文件时,会自动忽略 ARGV 中的空字符串元素。也可以 delete ARGV[i] 的方式来删除 ARGV 中的某元素。

用户手动增、删 ARGV 元素时,不会自动修改 ARGC,而 awk 读取文件时是根据 ARGC 值来确定的。所以,在增加 ARGV 元素之后,要手动的去增加 ARGC 的值。

# 不会读取b.txt文件
[root@arm64v8 ~]# awk 'BEGIN{ARGV[2]="b.txt"}{print}' a.txt

# 会读取b.txt文件
[root@arm64v8 ~]# awk 'BEGIN{ARGV[2]="b.txt";ARGC++}{print}' a.txt

awk 判断命令行中给定文件是否可读

awk 命令行中可能会给出一些不存在或无权限或其它原因而无法被 awk 读取的文件名,这时可以判断并从中剔除掉不可读取的文件。

  • 排除命令行尾部(非选项型参数)的 var=val-、和 /dev/stdin 这 3 种特殊情况
  • 如果不可读,则从 ARGV 中删除该参数
  • 剩下的都是可在 main 代码段正常读取的文件
BEGIN{
  for(i=1;i<ARGC;i++){
    if(ARGV[i] ~ /[a-zA-Z_][a-zA-Z0-9_]*=.*/ \
    || ARGV[i]=="-" || ARGV[i]=="/dev/stdin"){
      continue
    } else if((getline var < ARGV[i]) < 0){
      delete ARGV[i]
    } else{
      close(ARGV[i])
    }
  }
}

1.1.3.19、awk 自定义函数

可以定义一个函数将多个操作整合在一起。函数定义之后,可以到处多次调用,从而方便复用。

使用 function 关键字来定义函数:

function func_name([parameters]){
    function_body
}

对于 gawk 来说,也支持 func 关键字来定义函数。

func func_name(){}

函数可以定义在下面使用下划线的地方:

awk '_ BEGIN{} _ MAIN{} _ END{} _'

无论函数定义在哪里,都能在任何地方调用,因为 awk 在 BEGIN 之前,会先编译 awk 代码为内部格式,在这个阶段会将所有函数都预定义好。

awk '
    BEGIN{
        f()
        f()
        f()
    }
    function f(){
        print "星期一"
        print "星期二"
        print "星期三"
        print "星期四"
        print "星期五"
        print "星期六"
        print "星期日"
    }
'

awk 函数的 return 语句

如果想要让函数有返回值,那么需要在函数中使用 return 语句。return 语句也可以用来立即结束函数的执行。

[root@arm64v8 ~]# awk '
>     function add(){
>         return 40
>     }
>     BEGIN{
>         print add()
>         res = add() 
>         print res
>     }
> '
40
40
[root@arm64v8 ~]#

如果不使用 return 或 return 没有参数,则返回值为空,即空字符串。

[root@arm64v8 ~]# awk '
>   function f1(){        }
>   function f2(){return  }
>   function f3(){return 3}
>   BEGIN{
>     print "-"f1()"-"
>     print "-"f2()"-"
>     print "-"f3()"-"
>   }
> '
--
--
-3-
[root@arm64v8 ~]#

awk 函数参数

为了让函数和调用者能够进行数据的交互,可以使用参数。

[root@arm64v8 ~]# awk '
>   function f(a,b){
>     print a
>     print b
>     return a+b
>   }
>   BEGIN{
>     x=10
>     y=20
>     res = f(x,y)
>     print res
>     print f(x,y)
>   }
> '
10
20
30
10
20
30
[root@arm64v8 ~]#

例如,实现一个重复某字符串指定次数的函数:

[root@arm64v8 ~]# awk '
>     function repeat(str,cnt  ,res_str){
>         for(i=0;i<cnt;i++){
>             res_str = res_str""str
>         }
>         return res_str
>     }
>     BEGIN{
>         print repeat("abc",3)
>         print repeat("-",30)
>     }
> '
abcabcabc
------------------------------
[root@arm64v8 ~]#

调用函数时,实参数量可以比形参数量少,也可以比形参数量多。但是,在多于形参数量时会给出警告信息。

awk '
  function f(a,b){
    print a
    print b
    return a+b
  }
  BEGIN{
    x=10
    y=20

    print "---1----"
    print "-"f()"-"          # 不传递参数

    print "---2----"
    print "-"f(30)"-"        # 传递1个参数

    print "---3----"
    print "-"f(10,20,30)"-"  # 传递多个参数
  }
'

awk 函数参数数据类型冲突问题

如果函数内部使用参数的类型和函数外部变量的类型不一致,会出现数据类型不同而导致报错。

awk '
    function f(a){
        a[1]=30
    }
    BEGIN{
        a="hello world"
        f(a)   # 报错

        f(x)
        x=10   # 报错
    }
'

函数内部参数对应的是数组,那么外面对应的也必须是数组类型。

awk 参数按值传递还是按引用传递

在调用函数时,将数据作为函数参数传递给函数时,有两种传递方式:

  • 传递普通变量时,是按值拷贝传递
    • 直接拷贝普通变量的值到函数中
    • 函数内部修改不会影响到外部
  • 传递数组时,是按引用传递
    • 函数内部修改会影响到外部
# 传递普通变量:按值拷贝
awk '
  function modify(a){
    a=30
    print a
  }
  BEGIN{
    a=40
    modify(a)
    print a
  }
'

# 传递数组:按引用拷贝
awk '
  function modify(a){
    a[1]=20
  }

  BEGIN{
    a[1]=10
    modify(a)
    print a[1]
  }
'

awk 作用域问题

awk 只有在函数参数中才是局部变量,其它地方定义的变量均为全局变量。

函数内部新增的变量是全局变量,会影响到全局,所以在函数退出后仍然能访问。例如上面的 e 变量。

awk '
  function f(){
    a=30  # 新增的变量,是全局变量
    print "in f: " a
  }
  BEGIN{
    a=40
    f()
    print a  # 30
  }
'

函数参数会遮掩全局同名变量,所以在函数执行时,无法访问到或操作与参数同名的全局变量,函数退出时会自动撤掉遮掩,这时才能访问全局变量。所以,参数具有局部效果。

awk '
  function f(a){
    print a    # 50,按值拷贝,和全局a已经没有关系
    a=40
    print a    # 40
  }
  BEGIN{
    a=50
    f(a)
    print a     # 50,函数退出,重新访问全局变量
  }
'

由于函数内部新增变量均为全局变量,awk 也没有提供关键字来修饰一个变量使其成为局部变量。所以,awk 只能将本该出现在函数体内的局部变量放在参数列表中,只要调用函数时不要为这些参数传递数据即可,从而实现局部变量的效果。

awk '
  function f(a,b       ,c,d){

    # a,b是参数,调用时需传递两个参数
    # c,d是局部变量,调用时不要给c和d传递数据
    a=30
    b=40
    c=50
    d=60
    e=70  # 全局变量

    print a,b,c,d,e  # 30 40 50 60 70
  }
  BEGIN{
    a=31
    b=41
    c=51
    d=61
    f(a,b)  # 调用函数时值传递两个参数
    print a,b,c,d,e  # 31 41 51 61 70
  }
'

所以,awk 对函数参数列表做了两类区分:

  • arguments:调用函数时传递的参数
  • local variables:调用函数时省略的参数

为了区分 arguments 和 local variables,约定俗成的,将 local variables 放在一大堆空格后面来提示用户。例如 function name(a,b, c,d) 表示调用函数时,应当传递两个参数,c 和 d 是本函数内部使用的局部变量,不要传递对应的参数。

区分参数和局部变量:

  • 参数提供了函数和它调用者进行数据交互的方式
  • 局部变量是临时存放数据的地方

arguments 部分体现的是函数调用时传递的参数,这些参数在函数内部会遮掩全局同名变量。例如上面示例中,函数内部访问不了全局的 a 和 b,所有对 a 和 b 的操作都是函数内部的,函数退出后才能重新访问全局 a 和 b。因此,arguments 也有局部特性。

local variables 是 awk 实现真正局部变量的技巧,只是因为函数内部新增的变量都是全局变量,所以退而求其次将其放在参数列表上来实现局部变量。

1.1.3.20、awk 选项、内置变量

选项

-e program-text
--source program-text
指定awk程序表达式,可结合-f选项同时使用
在使用了-f选项后,如果不使用-e,awk program是不会执行的,它会被当作ARGV的一个参数

-f program-file
--file program-file
从文件中读取awk源代码来执行,可指定多个-f选项

-F fs
--field-separator fs
指定输入字段分隔符(FS预定义变量也可设置)

-n
--non-decimal-data
识别文件输入中的8进制数(0开头)和16进制数(0x开头)
echo '030' | awk -n '{print $1+0}'

-o [filename]
格式化awk代码。
不指定filename时,则默认保存到awkprof.out
指定为`-`时,表示输出到标准输出

-v var=val
--assign var=val
在BEGIN之前,声明并赋值变量var,变量可在BEGIN中使用

预定义变量

预定义变量分为两类:控制 awk 工作的变量和携带信息的变量。

第一类:控制 AWK 工作的预定义变量

  • RS:输入记录分隔符,默认为换行符 \n
  • IGNORECASE:默认值为 0,表示所有的正则匹配不忽略大小写。设置为非 0 值(例如 1),之后的匹配将忽略大小写。例如在 BEGIN 块中将其设置为 1,将使 FS、RS 都以忽略大小写的方式分隔字段或分隔 record
  • FS:读取记录后,划分为字段的字段分隔符。
  • FIELDWIDTHS:以指定宽度切割字段而非按照 FS。
  • FPAT:以正则匹配匹配到的结果作为字段,而非按照 FS 划分。
  • OFS:print 命令输出各字段列表时的输出字段分隔符,默认为空格 " "
  • ORS:print 命令输出数据时在尾部自动添加的记录分隔符,默认为换行符 \n
  • CONVFMT:在 awk 中数值隐式转换为字符串时,将根据 CONVFMT 的格式按照 sprintf() 的方式自动转换为字符串。
  • OFMT:在 print 中,数值会根据 OFMT 的格式按照 sprintf() 的方式自动转换为字符串。

第二类:携带信息的预定义变量

  • ARGCARGV:awk 命令行参数的数量、命令参数的数组。
  • ARGIND:awk 当前正在处理的文件在 ARGV 中的索引位置。所以,如果 awk 正在处理命令行参数中的某文件,则 ARGV[ARGIND] == FILENAME 为真
  • FILENAME:awk 当前正在处理的文件(命令行中指定的文件),所以在 BEGIN 中该变量值为空
  • ENVIRON:保存了 Shell 的环境变量的数组。例如 ENVIRON["HOME"] 将返回当前用户的家目录
  • NR:当前已读总记录数,多个文件从不会重置为 0,所以它是一直叠加的
    • 可以直接修改 NR,下次读取记录时将在此修改值上自增
  • FNR:当前正在读取文件的第几条记录,每次打开新文件会重置为 0
    • 可以直接修改 FNR,下次读取记录时将在此修改值上自增
  • NF:当前记录的字段数
  • RT:在读取记录时真正的记录分隔符
  • RLENGTH:match() 函数正则匹配成功时,所匹配到的字符串长度,如果匹配失败,该变量值为 -1
  • RSTART:match() 函数匹配成功时,其首字符的索引位置,如果匹配失败,该变量值为 0
  • SUBSEParr[x,y] 中下标分隔符构建成索引时对应的字符,默认值为 \034,是一个不太可能出现在字符串中的不可打印字符。

1.1.3.21、综合案例

请参见 Awk Examples

1.2、网络抓包和扫描

1.2.1、tcpdump 命令

tcpdump 采用命令行方式对接口的数据包进行筛选抓取,其丰富特性表现在灵活的表达式上。不带任何选项的 tcpdump,默认会抓取第一个网络接口,且只有将 tcpdump 进程终止才会停止抓包。

tcpdump 选项

tcpdump [ -DenNqvX ] [ -c count ] [ -F file ] [ -i interface ] [ -r file ]
        [ -s snaplen ] [ -w file ] [ expression ]

抓包选项:
-c:             指定要抓取的包数量。注意,是最终要获取这么多个包。
                例如,指定"-c 10"将获取10个包,但可能已经处理了100个包,只不过只有10个包是满足条件的包。

-i interface:   指定tcpdump需要监听的接口。若未指定该选项,将从系统接口列表中搜寻编号最小的已配置好的接口,
                不包括loopback接口,要抓取loopback接口使用tcpdump -i lo
                一旦找到第一个符合条件的接口,搜寻马上结束。可以使用'any'关键字表示所有网络接口。

-n:             对地址以数字方式显式,否则显式为主机名,也就是说-n选项不做主机名解析。
-nn:            除了-n的作用外,还把端口显示为数值,否则显示端口服务名。
-N:             不打印出host的域名部分。例如tcpdump将会打印'nic'而不是'nic.ddn.mil'。
-P:             指定要抓取的包是流入还是流出的包。可以给定的值为"in"、"out"和"inout",默认为"inout"。

-s len:         设置tcpdump的数据包抓取长度为len,如果不设置默认将会是65535字节。对于要抓取的数据包较大时,
                长度设置不够可能会产生包截断,若出现包截断,输出行中会出现"[|proto]"的标志(proto实际会显示为协议名)。
                但是抓取len越长,包的处理时间越长,并且会减少tcpdump可缓存的数据包的数量,从而会导致数据包的丢失,
                所以在能抓取我们想要的包的前提下,抓取长度越小越好。

输出选项:
-e:             输出的每行中都将包括数据链路层头部信息,例如源MAC和目标MAC。
-q:             快速打印输出。即打印很少的协议相关信息,从而输出行都比较简短。
-X:             输出包的头部数据,会以16进制和ASCII两种方式同时输出。
-XX:            输出包的头部数据,会以16进制和ASCII两种方式同时输出,更详细。
-v:             当分析和打印的时候,产生详细的输出。
-vv:            产生比-v更详细的输出。
-vvv:           产生比-vv更详细的输出。

其他功能性选项:
-D:             列出可用于抓包的接口。将会列出接口的数值编号和接口名,它们都可以用于"-i"后。
-F:             从文件中读取抓包的表达式。若使用该选项,则命令行中给定的其他表达式都将失效。
-w:             将抓包数据输出到文件中而不是标准输出。可以同时配合"-G time"选项使得输出文件每time秒就自动切换到另一个文件。
                可通过"-r"选项载入这些文件以进行分析和打印。

-r:             从给定的数据包文件中读取数据。使用"-"表示从标准输入中读取。

所以常用的选项也就这几个:

  • tcpdump -D
  • tcpdump -c num -i int -nn -XX -vvv

tcpdump 表达式

表达式用于筛选输出哪些类型的数据包,如果没有给定表达式,所有的数据包都将输出,否则只输出表达式为 true 的包。在表达式中出现的 shell 元字符建议使用单引号包围。

tcpdump 的表达式由一个或多个 "单元" 组成,每个单元一般包含 ID 的修饰符和一个 ID(数字或名称)。有三种修饰符:

  1. type:指定 ID 的类型。
    • 可以给定的值有 host/net/port/portrange。
    • 例如 "host foo","net 128.3","port 20","portrange 6000-6008"。默认的 type 为 host。
  2. dir:指定 ID 的方向。
    • 可以给定的值包括 src/dst/src or dst/src and dst,默认为 src or dst。
    • 例如,"src foo" 表示源主机为 foo 的数据包,"dst net 128.3" 表示目标网络为 128.3 的数据包,"src or dst port 22" 表示源或目的端口为 22 的数据包。
  3. proto:通过给定协议限定匹配的数据包类型。
    • 常用的协议有 tcp/udp/arp/ip/ether/icmp 等,若未给定协议类型,则匹配所有可能的类型。
    • 例如 "tcp port 21","udp portrange 7000-7009"。

所以,一个基本的表达式单元格式为 "proto dir type ID"

tcpdump

除了使用修饰符和 ID 组成的表达式单元,还有关键字表达式单元:gateway,broadcast,less,greater 以及算术表达式。表达式单元之间可以使用操作符 " and / && / or / || / not / ! " 进行连接,从而组成复杂的条件表达式。

tcpdump 示例

  1. 默认启动

    [root@arm64v8 ~]# tcpdump 
    • 注意,tcpdump 只能抓取流经本机的数据包。
    • 默认情况下,直接启动 tcpdump 将监视第一个网络接口(非 lo 口)上所有流通的数据包。这样抓取的结果会非常多,滚动非常快。
  2. 监视指定网络接口的数据包

    [root@arm64v8 ~]# tcpdump -i eth0 
    • 如果不指定网卡,默认 tcpdump 只会监视第一个网络接口,如 eth0。
  3. 监视指定主机的数据包,例如所有进入或离开 arm64v8 的数据包

    [root@arm64v8 ~]# tcpdump host arm64v8
  4. 打印 arm64v8<-->brinnatt 或 arm64v8<-->printer 之间通信的数据包

    [root@arm64v8 ~]# tcpdump host arm64v8 and \( brinnatt or printer \)
  5. 打印 arm64v8 与任何其他主机之间通信的 IP 数据包,但不包括与 brinnatt 之间的数据包

    [root@arm64v8 ~]# tcpdump ip host arm64v8 and not brinnatt
  6. 截获主机 arm64v8 发送的所有数据

    [root@arm64v8 ~]# tcpdump src host arm64v8
  7. 监视指定主机和端口的数据包

    [root@arm64v8 ~]# tcpdump tcp port 22 and host arm64v8
  8. 监视指定网络的数据包,如本机与 10.51.111 网段通信的数据包,"-c 10" 表示只抓取 10 个包

    [root@arm64v8 ~]# tcpdump -c 10 net 10.51.111
  9. 抓取 ping 包

    [root@arm64v8 ~]# tcpdump -c 5 -nn -i eth0 icmp
    tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
    listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
    02:43:47.202288 IP 10.51.111.142 > 10.51.111.1: ICMP echo request, id 5327, seq 7, length 64
    02:43:47.203538 IP 10.51.111.1 > 10.51.111.142: ICMP echo reply, id 5327, seq 7, length 64
    02:43:48.203592 IP 10.51.111.142 > 10.51.111.1: ICMP echo request, id 5327, seq 8, length 64
    02:43:48.204751 IP 10.51.111.1 > 10.51.111.142: ICMP echo reply, id 5327, seq 8, length 64
    02:43:49.204790 IP 10.51.111.142 > 10.51.111.1: ICMP echo request, id 5327, seq 9, length 64
    5 packets captured
    5 packets received by filter
    0 packets dropped by kernel
    [root@arm64v8 ~]#

    如果明确要抓取主机为 10.51.111.52 对本机的 ping,则使用 and 操作符。

    [root@arm64v8 ~]# tcpdump -c 5 -nn -i eth0 icmp and src 10.51.111.52
    tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
    listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
    02:45:59.686057 IP 10.51.111.52 > 10.51.111.142: ICMP echo request, id 1, seq 92, length 40
    02:46:00.692499 IP 10.51.111.52 > 10.51.111.142: ICMP echo request, id 1, seq 93, length 40
    02:46:01.727462 IP 10.51.111.52 > 10.51.111.142: ICMP echo request, id 1, seq 94, length 40
    02:46:03.000676 IP 10.51.111.52 > 10.51.111.142: ICMP echo request, id 1, seq 95, length 40
    02:46:03.748285 IP 10.51.111.52 > 10.51.111.142: ICMP echo request, id 1, seq 96, length 40
    5 packets captured
    5 packets received by filter
    0 packets dropped by kernel
    [root@arm64v8 ~]#
    • 注意不能直接写 icmp src 10.51.111.52,因为 icmp 协议不支持直接应用 host 这个 type。
  10. 抓取到本机 22 端口包

    [root@arm64v8 ~]# tcpdump -c 5 -nn -i eth0 tcp dst port 22
    tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
    listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
    02:48:51.781168 IP 10.51.111.52.5679 > 10.51.111.142.22: Flags [.], ack 3276621926, win 510, length 0
    02:48:51.781182 IP 10.51.111.52.2414 > 10.51.111.142.22: Flags [.], ack 319842498, win 509, length 0
    02:48:51.783513 IP 10.51.111.52.2414 > 10.51.111.142.22: Flags [.], ack 329, win 508, length 0
    02:48:51.851711 IP 10.51.111.52.2414 > 10.51.111.142.22: Flags [.], ack 477, win 507, length 0
    02:48:52.332314 IP 10.51.111.52.2414 > 10.51.111.142.22: Flags [.], ack 625, win 513, length 0
    5 packets captured
    6 packets received by filter
    0 packets dropped by kernel
    [root@arm64v8 ~]#
  11. 解析包数据

    [root@arm64v8 ~]# tcpdump -c 2 -q -XX -vvv -nn -i eth0 tcp dst port 22
    tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
    02:49:36.623578 IP (tos 0x0, ttl 64, id 14045, offset 0, flags [DF], proto TCP (6), length 40)
        10.51.111.52.2414 > 10.51.111.142.22: tcp 0
        0x0000:  0007 3e9b 155a e4a4 71bf b661 0800 4500  ..>..Z..q..a..E.
        0x0010:  0028 36dd 4000 4006 10cb 0a33 6f34 0a33  .(6.@.@....3o4.3
        0x0020:  6f8e 096e 0016 fbf5 a621 1310 815e 5010  o..n.....!...^P.
        0x0030:  0200 7aa2 0000 0000 0000 0000            ..z.........
    02:49:36.628549 IP (tos 0x0, ttl 64, id 14046, offset 0, flags [DF], proto TCP (6), length 40)
        10.51.111.52.2414 > 10.51.111.142.22: tcp 0
        0x0000:  0007 3e9b 155a e4a4 71bf b661 0800 4500  ..>..Z..q..a..E.
        0x0010:  0028 36de 4000 4006 10ca 0a33 6f34 0a33  .(6.@.@....3o4.3
        0x0020:  6f8e 096e 0016 fbf5 a621 1310 83de 5010  o..n.....!....P.
        0x0030:  01fe 7824 0000 0000 0000 0000            ..x$........
    2 packets captured
    2 packets received by filter
    0 packets dropped by kernel
    [root@arm64v8 ~]#

1.2.2、nmap 命令

安装

[root@arm64v8 ~]# yum install nmap -y
Loaded plugins: fastestmirror, langpacks
Loading mirror speeds from cached hostfile
Package 2:nmap-6.40-19.axs7.aarch64 already installed and latest version
Nothing to do
[root@arm64v8 ~]#

nmap 命令语法

nmap [Scan Type...] [Options] {target specification}

-iL <inputfilename>:  从输入文件中读取主机或者IP列表作为探测目标
-sn:                    PING扫描,但是禁止端口扫描。默认总是会扫描端口。禁用端口扫描可以加速扫描主机
-n/-R:                  永远不要/总是进行DNS解析,默认情况下有时会解析
-PE/PP/PM:              分别是基于echo/timestamp/netmask的ICMP探测报文方式。使用echo最快
-sS/sT/sA/sW:           TCP SYN/Connect()/ACK/Window,其中sT扫描表示TCP扫描
-sU:                    UDP扫描
-sO:                    IP扫描
-p <port ranges>:     指定扫描端口
-v:                     显示详细信息,使用-vv或者更多的v显示更详细的信息
-oN/-oX/ <file>:      输出扫描结果到普通文件或XML文件中。输入到XML文件中的结果是格式化的结果
--min-hostgroup/max-hostgroup <size>:             对目标主机进行分组然后组之间并行扫描
--min-parallelism/max-parallelism <numprobes>:        设置并行扫描的探针数量

尝试一次扫描

nmap 扫描一般会比较慢,特别是扫描非本机的时候。

[root@arm64v8 ~]# nmap 127.0.0.1

Starting Nmap 6.40 ( http://nmap.org ) at 2021-09-24 03:05 CST
Nmap scan report for localhost (127.0.0.1)
Host is up (0.0000090s latency).
Not shown: 996 closed ports
PORT    STATE SERVICE
22/tcp  open  ssh
25/tcp  open  smtp
111/tcp open  rpcbind
631/tcp open  ipp

Nmap done: 1 IP address (1 host up) scanned in 1.74 seconds
[root@arm64v8 ~]#
  • 只扫描出了 4 个端口,但是不代表真的只开了 4 个端口,这样不加任何参数的 nmap 将自动决定扫描 1000 个高危端口,但哪些是高危端口由 nmap 决定。
  • 从结果中也能看出来,"NOT shown:996 closed ports" 表示 996 个关闭的端口未显示出来,随后又显示了 4 个 open 端口,正好 1000 个。虽说默认只扫描 1000 个,但常见的端口都能扫描出来。

从 arm64 台式机扫描 x86 windows 看看。可以感受到,扫描速度明显降低了。

[root@arm64v8 ~]# nmap 10.51.111.52

Starting Nmap 6.40 ( http://nmap.org ) at 2021-09-24 03:08 CST
Nmap scan report for 10.51.111.52
Host is up (0.014s latency).
Not shown: 992 filtered ports
PORT     STATE SERVICE
135/tcp  open  msrpc
139/tcp  open  netbios-ssn
443/tcp  open  https
445/tcp  open  microsoft-ds
902/tcp  open  iss-realsecure
912/tcp  open  apex-mesh
1031/tcp open  iad2
5357/tcp open  wsdapi
MAC Address: E4:A4:71:BF:B6:61 (Unknown)

Nmap done: 1 IP address (1 host up) scanned in 33.28 seconds
[root@arm64v8 ~]#
  • 可以指定 "-p [1-65535]" 来扫描所有端口,或者使用 "-p-" 选项也是全面扫描。

    [root@arm64v8 ~]# nmap -p- 127.0.0.1
  • nmap 默认总是会扫描端口,可以使用 -sn 选项禁止扫描端口,以加速扫描主机是否存活。

扫描目标说明

Nmap 支持 CIDR 风格的地址,Nmap 将会扫描所有和该参考 IP 地址具有相同 cidr 位数的所有 IP 地址或主机。

CIDR 标志位很简洁但有时候不够灵活。可以用逗号分开的数字或范围列表为 IP 地址指定它的范围。例如 "192.168.0-255.1-254" 将略过该范围内以 ".0" 和 ".255" 结束的地址。

Nmap 命令行接受多个主机说明,它们不必是相同类型。如:

nmap www.hostname.com 192.168.0.0/8 10.0.0,1,3-7.0-255

虽然目标通常在命令行指定,下列选项也可用来控制目标的选择:

  • -iL <inputfilename> :从列表中输入
  • --exclude <host1[,host2][,host3],...>:排除主机/网络
  • --excludefile <excludefile>:排除文件中的列表,这和 --exclude 的功能一样,只是所排除的目标是用 <excludefile> 提供的。

范围扫描

[root@arm64v8 ~]# nmap 10.51.111.0/24
Nmap scan report for 10.51.111.1
Host is up (0.0018s latency).
Not shown: 998 closed ports
PORT   STATE    SERVICE
21/tcp open     ftp
22/tcp filtered ssh
MAC Address: 00:00:5E:00:01:6F (USC Information Sciences Inst)

Nmap scan report for 10.51.111.10
Host is up (0.0022s latency).
Not shown: 999 closed ports
PORT   STATE SERVICE
23/tcp open  telnet
MAC Address: CC:D8:1F:1C:A0:F1 (Unknown)
......
Nmap done: 256 IP addresses (57 hosts up) scanned in 6898.00 seconds
  • 总共花了 6898s,时间很漫长。
  • 该局域网下面,有很多种设备,计算机、路由器、交换机和虚拟网卡等,所以可能有的设备只有一个 IP 地址和 MAC 地址,没有打开的端口号。

端口状态说明

Nmap 功能越来越多,但它赖以成名的是它的核心功能——端口扫描。

Nmap 把端口分成六个状态:open(开放的),closed(关闭的),filtered(被过滤的),unfiltered(未被过滤的),open|filtered(开放或者被过滤的),或者 closed|filtered(关闭或者被过滤的)。

这些状态并非端口本身的性质,而是描述 Nmap 怎样看待它们。例如,对于同样的目标机器的 135/tcp 端口,从同网络扫描显示它是开放的,而跨网络做完全相同的扫描则可能显示它是 filtered(被过滤的)。

  1. open:(开放的)有程序正在该端口接收 TCP 或者 UDP 报文。它常常是端口扫描的主要目标。
  2. closed:(关闭的)关闭的端口对于 Nmap 也是可访问的(它接受 Nmap 的探测报文并作出响应),但没有应用程序在其上监听。
  3. filtered:(被过滤的)由于目标上设置了包过滤(如防火墙设备),使得探测报文被阻止到达端口,Nmap 无法确定该端口是否开放。过滤可能来自专业的防火墙设备,路由器规则或者主机上的软件防火墙。
  4. unfiltered:(未被过滤的)未被过滤状态意味着端口可访问,但 Nmap 不能确定它是开放还是关闭。用其它类型的扫描如窗口扫描、SYN 扫描、FIN 扫描来扫描这些未被过滤的端口可以帮助确定端口是否开放。
  5. open|filtered:(开放或被过滤的)当无法确定端口是开放还是被过滤的,Nmap 就把该端口划分成这种状态。开放的端口不响应就是一个例子。没有响应也可能意味着目标主机上报文过滤器丢弃了探测报文或者它引发的任何响应。因此 Nmap 无法确定该端口是开放的还是被过滤的。
  6. closed|filtered:(关闭或被过滤的)该状态用于 Nmap 不能确定端口是关闭的还是被过滤的。它只可能出现在 IPID Idle 扫描中。

时间参数优化

改善扫描时间的技术有:忽略非关键的检测、升级最新版本的 Nmap(文档中说 nmap 版本越高性能越好)等。

  1. -T<0-5>:这表示直接使用 namp 提供的扫描模板,不同的模板适用于不同的环境下,默认的模板为 "-T 3",具体的看 man 文档,其实用的很少。

  2. --min-hostgroup \<milliseconds>; --max-hostgroup \<milliseconds>:调整并行扫描组的大小。

    • Nmap 具有并行扫描多主机端口的能力,实现方法是将所有给定的目标 IP 按空间分成组,然后一次扫描一个组。
    • 通常组分的越大效率越高,但分组的缺点是只有当整个组扫描结束后才会返回该组中主机扫描结果。例如,组的大小定义为 50,则只有前 50 个主机扫描结束后才能得到这 50 个 IP 内的结果。
    • 默认方式下,Nmap 采取折衷的方法。开始扫描时的组较小,默认值为 5,这样便于尽快产生结果,随后增长组的大小,默认最大为 1024。但最小和最大确切的值则依赖于所给定的选项。
    • --max-hostgroup 选项用于说明使用最大的组,Nmap 不会超出这个大小。
    • --min-hostgroup 选项说明最小的组,Nmap 会保持组大于这个值。如果在指定的接口上没有足够的目标主机来满足所指定的最小值,Nmap 可能会采用比所指定的值小的组。
  3. --min-parallelism \<milliseconds>; --max-parallelism \<milliseconds>:调整探测报文的并行度,即探针数。

    • 这些选项用于控制主机组的探测报文数量,可用于端口扫描和主机发现
    • 默认状态下,Nmap 基于网络性能计算一个理想的并行度,这个值经常改变。如果报文被丢弃,Nmap 降低速度,探测报文数量减少。随着网络性能的改善,理想的探测报文数量会缓慢增加。默认状态下,当网络不可靠时,理想的并行度值可能为 1,在好的条件下,可能会增长至几百。
    • 最常见的应用是 --min-parallelism 值大于 1,以加快性能不佳的主机或网络的扫描。这个选项具有风险,如果过高则影响准确度,同时也会降低 Nmap 基于网络条件动态控制并行度的能力。一般说来,这个值要设置的和 --min-hostgroup 的值相等或大于它性能才会提升。

扫描操作系统类型

扫描操作系统。操作系统的扫描有可能会出现误报。

[root@arm64v8 ~]# nmap -O 127.0.0.1

快速扫描存活的主机

要快速扫描存活的主机,需要使用的几个重要选项

  • -n:永远不要 DNS 解析。这个不管是给定地址扫描还是给定网址扫描,加上它速度都会极速提升

  • -sn:禁止端口扫描

  • -PE:只根据 echo 回显判断主机在线,这种类型的选项使用越多,速度越慢,如 -PM -PP 选项都是类似的,但他们速度要慢的多的多,PE 有个缺点,不能穿透防火墙

  • --min-hostgroup N:当 IP 太多时,nmap 需要分组,然后并扫描,使用该选项可以指定多少个 IP 一组

  • --min-parallelism N:这个参数非常关键,为了充分利用系统和网络资源,设置好合理的探针数。一般来说,设置的越大速度越快,且和 min-hostgroup 的值相等或大于它性能才会提升

示例一:扫描 10.51.111.0/24 网段存活的机器

[root@arm64v8 ~]# nmap -sn -n -PE --min-hostgroup 1024 --min-parallelism 1024 10.51.111.0/24
......
Nmap done: 256 IP addresses (50 hosts up) scanned in 0.74 seconds
  • 256 个 IP 地址,0.74s 完成,非常快

示例二:再测试扫描下以 www.baidu.com 作为参考地址的地址空间。

[root@arm64v8 ~]# nmap -sn -PE -n --min-hostgroup 1024 --min-parallelism 1024 -oX nmap_output.xml www.baidu.com/16
......
Nmap done: 65536 IP addresses (13160 hosts up) scanned in 28.28 seconds
  • 65536 个 IP 地址,28.28s 完成,非常快

快速扫描端口

既然是扫描端口,就不能使用 -sn 选项,也不能使用 -PE,否则不会返回端口状态,只会返回哪些主机。

[root@arm64v8 ~]# nmap -n -p 20-2000 --min-hostgroup 1024 --min-parallelism 1024 10.51.111.0/24
标签云