awk 编程
使用 awk 编写 shell 脚本
- awk 是一个强大的 Linux 命令,有强大的文本格式化的能力
- 三剑客:
- grep:擅长单纯的查找或匹配文本内容
- awk:更适合编辑、处理匹配到的文本内容
- sed:更适合格式化文本内容,对文本进行复杂处理
1. awk 基础
awk 语法:
1
2awk [option] 'pattern[action]' file ...
awk 参数 '条件动作' 文件Action 指的是动作,awk 擅长文本格式化,且输出格式化后的结果,因此最常用的动作就是 print
- 条件动作举例:
1
2
3
4{print $0} # 把每行都打印出来, 等同于 {print}
{print $1} # 把每行的第一列打印出来(默认以空格为列的分隔符)
{print $NF} # 每行的最后一列[倒数第二列可以写成$(NF-1)]
{print $1,$4,$5} # 打印多列(中间加上逗号是为了空格分割)awk 是按行处理文件,一行处理完毕,处理下一行,根据用户指定的分隔符去工作,没有指定则默认空格
awk 分隔符有两种:
- 输入分隔符,awk 默认是空格,空白字符,英文是 file separator,变量名是 FS
- 输出分隔符,output field separator,简称 OFS,默认也是空格
2. 文本格式化
2.1 awk 内置变量
内置变量 | 解释 |
---|---|
$n | 指定分隔符后,当前记录的第n个字段 |
$0 | 完整的输入记录 |
FS | 字段分隔符,默认是空格 |
NF(Number of fields) | 分割后,当前行一共有多少个字段(几列?) |
NR(Number of records) | 当前记录数,行数 |
FILENAME | 当前文件名 |
可用 man 手册查看 | man awk |
2.2 自动定义输出内容
- awk,必须外层单引号,内存双引号
- 内置变量
$1、$2
都不得添加双引号,否则会识别为文本,尽量别加引号- 如:
awk '{print "第一列: "$1, "第二列: "$3}' hello.txt
- 如(将 ipconfig 里的 eth0 段的ip输出):
ipconfig eth0 | awk 'NR==2{print $0}'
- 如:
2.3 awk 参数
参数 | 解释 |
---|---|
-F | 指定分割字段符 |
-v | 定义或修改一个 awk 内部的变量 |
-f | 从脚本文件中读取 awk 命令 |
- 举例(指定冒号作为分隔符):
awk -F ":" '{print $1}' hello.txt
- 也可以用 -v 修改 FS 参数:
awk -v FS=":" '{print $1,$NF}' hello.txt
- 还可以修改输出分隔符:
awk -v OFS="\t" '{print $1,$NF}' hello.txt
2.4 显示文件第5行
NR
在 awk 中表示行号,NR==5
表示行号是 5 的那一行- 举例:找到第五行和第六行的内容并打印:
awk 'NR==5, NR==6{print $0}' hello.txt
- 看行号2-5的内容:
awk 'NR==2, NR==5' hello.txt
- 打印行号37-40的内容同时显示行号:
awk 'NR==37, NR==40{print NR,$0}' hello.txt
- 举例:找到第五行和第六行的内容并打印:
3. awk 模式 pattern
- 特殊的 pattern:
BEGIN
和END
BEGIN
模式是处理文本之前需要执行的操作END
模式是处理完所有行之后执行的操作
- 比如:
awk BEGIN{print "hhh"} hello.txt
awk 'BEGIN{print "处理文本之前"}{print $0}END{print "所有文本处理完毕"}' hello.txt
4. awk 与正则表达式
- 主要与 pattern模式(条件)结合使用
- 不指定模式,awk每一行都会执行对应的动作
- 指定了模式,只有被模式匹配到的、符合条件的行才会执行动作
- awk 使用正则语法:
awk '/正则表达式/动作' /etc/passwd
- awk 命令使用正则表达式,必须把正则放入
//
双斜杠中,匹配到结果后执行动作{print $0}
,打印整行信息
- 比如:
- 输出以
games
开头的行:awk '/^games/{print $0}' hello.txt
- 输出符合上面条件的行的第一列和最后一列(分隔符为冒号):
awk -F ":" '/^games/{print $0, $NF}' hello.txt
- 输出以
5. awk 案例
5.1 插入新字段和格式化空白
- 在
a b c d
的b
后面插入 3 个字段e f g
1 | echo a b c d | awk '{$2=$2" e f g";print}' |
- 移除每行的前缀、后缀空白,并将各部分左对齐
1 | awk 'BEGIN{OFS="\t"}{$1=$1;print}' a.txt |
5.2 筛选 IPV4 地址
- 从 ifconfig 命令的结果中筛选出除了 lo 网卡外的所有 IPv4 地址
1 | 法一:正则匹配开头为inet 且第二列不以 127 开头 |
5.3 读取 .ini 配置文件中的某段
读取 mysql 段
1 | 匹配包含 [mysql] 的,但是可读性较差 |
5.4 根据某字段去重
对出现的次数进行判断?
1 | 以问号为分隔符 |
5.5 次数统计
需要用到数组
- 统计单词出现次数
1 | awk '{arr[$0]++}END{for(i in arr){print arr[i], i}' 1.txt |
- 统计 TCP 链接状态数量
1 | netstat -tnap 2>/dev/null | awk '/^tcp/{arr[$6]++}END{for(i in arr){print arr[i], i}}' |
- 统计日志中各IP访问非200状态码的次数,结合 sort 和 head 命令显示前十个最多的
1 | awk '$8!=200{arr[$1]++}END{for(i in arr){print arr[i], i}' access.log | sort -k1nr | head -n 10 |
- 统计每个 URL 的独立访问 IP 有多少个 (去重),并且要为每个 URL 保存一个对应的文件:
1 | BEGIN { |
6. 课后作业
6.1 需求
使用 awk 编程编写 shell 脚本,需要有函数式、管道式、流水线编程的思想,具体要求如下:
现有
auth.log
日志文件,日志文件格式如下,要求统计登录出错的记录1
2
3
4
5
6
7
8
9
10
11
12
13
14
15Sep 20 06:25:01 iZbp1amwnz2vqj9ehdtgjvZ CRON[6905]: pam_unix(cron:session): session closed for user root
Sep 20 06:35:01 iZbp1amwnz2vqj9ehdtgjvZ CRON[7029]: pam_unix(cron:session): session opened for user root by (uid=0)
Sep 20 06:35:01 iZbp1amwnz2vqj9ehdtgjvZ CRON[7029]: pam_unix(cron:session): session closed for user root
Sep 20 06:45:01 iZbp1amwnz2vqj9ehdtgjvZ CRON[7034]: pam_unix(cron:session): session opened for user root by (uid=0)
...
...
Sep 26 19:07:15 iZbp1amwnz2vqj9ehdtgjvZ sshd[18760]: error: Could not load host key: /etc/ssh/ssh_host_ed25519_key
Sep 26 19:07:28 iZbp1amwnz2vqj9ehdtgjvZ sshd[18760]: Invalid user administrator from 95.10.179.8 port 28508
Sep 26 19:07:29 iZbp1amwnz2vqj9ehdtgjvZ sshd[18760]: pam_unix(sshd:auth): check pass; user unknown
Sep 26 19:07:29 iZbp1amwnz2vqj9ehdtgjvZ sshd[18760]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=95.10.179.8
Sep 26 19:07:31 iZbp1amwnz2vqj9ehdtgjvZ sshd[18760]: Failed password for invalid user administrator from 95.10.179.8 port 28508 ssh2
Sep 26 19:07:32 iZbp1amwnz2vqj9ehdtgjvZ sshd[18760]: Connection closed by invalid user administrator 95.10.179.8 port 28508 [preauth]
Sep 26 19:15:02 iZbp1amwnz2vqj9ehdtgjvZ CRON[18764]: pam_unix(cron:session): session opened for user root by (uid=0)
Sep 26 19:15:02 iZbp1amwnz2vqj9ehdtgjvZ CRON[18764]: pam_unix(cron:session): session closed for user root
...对上述 log 文件进行分析,然后编写 shell 脚本,使用 awk 编程,先读取日志文件,然后将出错的记录以下面的格式写入
/usr/local/log/error.md
文件中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19...
### 2021-9-26 19:07:15 [18760]
#### sshd
**error:** Could not load host key: /etc/ssh/ssh_host_ed25519_key
**pam_unix(sshd:auth):** check pass; user user unknown
**pam_unix(sshd:auth):** authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=95.10.179.8 port 28508
Invalid user administrator from 95.10.1
Failed password for invalid user administrator from 95.10.179.8 port 28508 ssh2
Connection closed by invalid user administrator 95.10.179.8 port 28508 [preauth]
### 2021-9-26 19:15:02 [18764]
#### CRON
**pam_unix(cron:session):** session opened for user root by (uid=0)
**pam_unix(cron:session):** session closed for user root
6.2 实现思路
- 准备工作:
- 定义日志文件路径,确保日志目录存在
- 处理日志文件:
- 使用
awk
处理auth.log
文件,按进程 ID 分类记录错误信息 - 在
BEGIN
块中,初始化全局变量、获取当前年份、以及初始化一些变量 - 在每一行的处理中,解析进程信息、过滤不需要的行、按照进程 ID 分类记录错误信息
- 在
END
块中,处理最后一个进程 ID 的错误信息
- 使用
- 输出错误信息:
- 错误信息会按照进程 ID 分类记录在
/usr/local/log/error.md
文件中
- 错误信息会按照进程 ID 分类记录在
6.3 代码实现
count.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126!/bin/bash
LOG_FILE="/usr/local/log/error.md" # 日志文件名作为常量
# 检查日志目录是否存在,不存在则创建
mkdir -p "$(dirname "$LOG_FILE")"
# 检查日志文件是否存在,存在则清空,不存在则创建
if [ -e "$LOG_FILE" ]; then
true > "$LOG_FILE"
else
touch "$LOG_FILE"
fi
LOG_FILE="/usr/local/log/error.md"
确保日志目录存在,不存在则创建
mkdir -p "$(dirname "$LOG_FILE")"
清空日志文件,如果不存在则创建
"$LOG_FILE"
使用 awk 处理 auth.log 文件
awk -v log_file="$LOG_FILE" '
BEGIN {
# 全局变量的定义
prev_process_id = ""
has_colon = 0 # 初始化 has_colon
# 获取系统当前年份
cmd = "date +%Y"
cmd | getline year
close(cmd)
# 初始化 error_data 为空字符串
error_data = ""
}
{
# 获取当前行的进程id号
parseProcessInfo($5)
process_name = process_info[1]
process_id = process_info[2]
# 过滤掉不是错误信息的行
if (process_name ~ /(vsftpd|systemd-logind)/) {
next
}
# 如果当前行的进程id号不等于上一行的进程id号或上一行的进程号为空
if (process_id != prev_process_id || prev_process_id == "") {
# 打印上一进程号的错误信息
printErrorData(prev_process_id)
# 清空数组
delete error_array
if (prev_process_id != "") {
print "\n\n" >> log_file
}
# 记录当前的进程id号为 prev_process_id
prev_process_id = process_id
# 获取时间并打印, 同时打印进程id, 格式为 ### 2021-9-26 19:07:15 [process_id]
formatted_datetime = getFormattedDateTime($1, $2, $3)
print "###", formatted_datetime, "[" process_id "]" >> log_file
print "####", process_name >> log_file
}
# 解析当前行的错误信息并输出到缓存
error_data = parseErrorInfo($6)
for (i = 7; i <= NF; i++) {
error_data = error_data " " $i
}
# 存储到数组
error_array[has_colon] = error_array[has_colon] error_data "\n"
}
END {
# 打印最后一个进程号的错误信息
printErrorData(prev_process_id)
}
获取格式化的日期和时间
function getFormattedDateTime(month, day, time) {
month_number = (index("JanFebMarAprMayJunJulAugSepOctNovDec", month) + 2) / 3
return year"-"month_number"-"day" "time
}
获取并解析进程名和进程id
function parseProcessInfo(process_data, arr) {
split(process_data, arr, "[")
if (length(arr) > 1) {
gsub("]:", "", arr[2])
process_info[1] = arr[1]
process_info[2] = arr[2]
} else {
process_info[1] = process_data
process_info[2] = ""
}
}
解析错误信息
function parseErrorInfo(error_column) {
has_colon = (substr(error_column, length(error_column), 1) == ":")
return has_colon ? "**" error_column "**" : error_column
}
打印错误信息
function printErrorData(process_id) {
if (process_id != "") {
# 输出标识为1的错误信息
print error_array[1] >> log_file
# 检查是否有标识为0的错误信息
if (error_array[0] != "") {
# 输出空行
print "" >> log_file
# 输出标识为0的错误信息
print error_array[0] >> log_file
}
}
}
' auth.log运行命令如下:
1 | 给脚本赋予执行权限 |
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 thr's blog!