Linux常用命令:文本处理

一、grep

1.用法

根据正则表达式查询内容,按行返回匹配结果

1
2
3
4
# 语法
grep [options] search_string path/to/file # 查询文件中的特定字符串
grep [options] search_string file1 file2 ... # 查询多个文件
<other command> | grep [options] search_string # 查询输出流中的特定内容

2.options

  • 返回内容控制

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
# -c --count
# 只返回匹配数
$ grep -c error ./*.log # 返回所有.log后缀文件匹配到error的数量
./adservice.log:0
./currencyservice.log:0
./frontend.log:11
./shippingservice.log:0

# -l --files-with-matches
# 返回匹配成功的文件名
# -L --files-without-match
# 返回匹配失败的文件名
# -Z --null
# 指定以0值(\0)字节作为grep输出中的终结符文件名, -Z通常和-l结合使用
$ grep -l ShipOrder ./*.log # 返回当前目录下所有.log后缀文件中,成功匹配到ShipOrder的文件名称
./shippingservice.log

# -n
# 在匹配的内容前显示行号
# --line-number
# 仅打印行号
# --line-buffered
# flush output on every line
$ grep -n error ./*.log # 返回所有.log后缀文件内容中,匹配到error的行以及行号
./frontend.log:26:{"error":"failed to get ads...",...}

# -q --quiet或--silent
# 测试命令是否执行成功,执行成功返回0
grep -q "test" filename
  • 匹配规则控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# -i --ignore-case
# 忽略大小写
# --no-ignore-case
# 不忽略大小写(默认模式)
$ echo "hello world" | grep -i "HELLO" # 匹配'hello world'中的hello

# -w --word-regexp
# 只匹配完整单词
# -x --line-regexp
# 只有完整行尝试匹配
$ grep -c -w Ship shippingservice.log
0 # 无单独的完整Ship单词
$ grep -c -x Ship shippingservice.log
0 # 无完整内容为Ship的行
$ grep -c Ship shippingservice.log
157 # 原日志中内容为"message":"[ShipOrder]... , 因此去掉-w和-x才能匹配成功

# -v --invert-match
# 反选
grep -v "info" shippingservice.log # 排除含有info的行
  • 检索模式

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
# -d, --directories=ACTION  
# how to handle directories;
# ACTION is 'read', 'recurse', or 'skip'
$ grep -d skip -c xxx .
# -D, --devices=ACTION
# how to handle devices, FIFOs and sockets;
# ACTION is 'read' or 'skip'
$ grep -D read -c xxx .

# -r --recursive --directories=recurse
# 在子目录内递归搜索
$ grep -r -c main *.go # 在当前目录下,递归查找所有.go后缀文件中,成功匹配main的次数
deployment_details.go:1
handlers.go:1
main.go:2
middleware.go:1
rpc.go:1
$ grep -r -n -C 2 main *.go # 在当前目录下,递归查找所有.go后缀文件中,成功匹配到main的前后2行以及行号
deployment_details.go:1:package main
deployment_details.go-2-
deployment_details.go-3-import (
--
...


  • 匹配结果显示设置

1
2
3
4
5
6
7
8
9
10
11
12
13
# -A num 显示所有匹配行以及其后num行
# -B num 显示匹配行以及其前num行
# -C num 显示匹配行以及其前后各num行
$ grep -C 1 1676941012528 currencyservice.log
{"level":30,"time":1676941012527,...}
{"level":30,"time":1676941012528,...}
{"level":30,"time":1676941012528,...}
{"level":30,"time":1676941012529,...}

# -m num 显示匹配成功的前num行内容
$ grep -C 2 1676941012528 currencyservice.log
{"level":30,"time":1676941012528,...}
{"level":30,"time":1676941012528,...}
  • 正则式设置

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
# 用于匹配的PATTERNS的规则
$ ls | grep PATTERN # PATTERN作为匹配正则式
$ ls | grep 'PATTERN STRING' # PATTERN STRING作为匹配正则式
# 内可包含空格
# 单双引号均可

# -G, --basic-regexp
# PATTERNS are basic regular expressions
# 默认模式,可省略-G
$ grep ^1[34578][0-9] telephone_number.txt
13611778887 # 匹配136
13697156332 # 匹配136
13339987 # 匹配133
1369715633X # 匹配136

# -E, --extended-regexp
# PATTERNS are extended regular expressions
$ grep -E ^1[34578][0-9]{9}$ telephone_number.txt
13611778887 # 匹配13611778887
13697156332 # 匹配13697156332

# -F, --fixed-strings
# PATTERNS are strings
$ grep -c -F ^136 telephone_number.txt
0 # ^136被用作固定字符
$ grep -c ^136 telephone_number.txt
3 # ^136中^作为行首匹配使用

# -P, --perl-regexp
# PATTERNS are Perl regular expressions
#-e, --regexp=PATTERNS
# use PATTERNS for matching
# -f, --file=FILE
# take PATTERNS from FILE

二、find

1.用法

find用于检索文件的属性,如文件名、文件类型、文件大小等(相对的grep是对文件内容进行检索)

1
2
3
4
5
6
7
# 递归查找path路径下符合要求的文件
$ find [path...] [options] [expression]

# 可以查找多个目录
$ find /opt /usr /var -name foo.scala -type f
# 可以添加多个检索条件
$ find . -type f \( -name "*.sh" -or -name "*.txt" \) # 查找.sh .txt的文件

2.option

  • 检索规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# -regex 使用正则表达式检索
$ find . -regex '.*/log/.*log'
./log/productcatalogservice.log
./log/shippingservice.log
./log/checkoutservice.log

# -maxdepth 最大目录深度
# -mindepth 最小目录深度
$ find . -maxdepth 1 -name '*.log' # 仅查找当前目录
$ find . -mindepth 2 -name '*.log' # 仅查找子目录
./log/productcatalogservice.log
./log/shippingservice.log
./log/checkoutservice.log

  • 按文件名检索

1
2
3
4
5
# -name 	按名称查找文件
# -iname 按名称查找文件, 不区分大小写
$ find . -name "*.txt" # 查找txt后缀文件
$ find . -name "2020*.csv" # 查找2020x的.csv文件
$ find . -name "json_*" # 查找json_xxx名的文件
  • 按文件类型检索

1
2
3
4
5
6
7
8
9
10
11
12
# -type 
# d: 目录
# f: 一般文件
$ find . -type d -empty # 列出空目录
$ find . -type f -empty -delete # 删除所有空文件
# c: 字型装置文件
# b: 区块装置文件
# p: 具名贮列
# l: 符号连结
# s: socket


  • 按权限检索

1
2
3
4
5
6
7
8
9
10
11
# -perm 按rwx权限检索
$ find . -type f ! -perm 777 # !表示检索非777文件
$ find . -type f -perm 644 -exec ls -l {} \
# 查找当前目录中普通文件
# user具有读、写权限
# group用户和其他用户具有读权限

# -user 文件所有者用户
# -group 文件所有者所在群组
$ find /home -user wilson
$ find /home -group developer
  • 按大小检索(+/-表示范围)
1
2
3
4
5
6
# -size 按大小检索
# b 512字节块(default)
# c/k/M/G 字节/kb/MB/GB/
# T/P TB(仅BSD)/PB(仅BSD)
find / -type f -size 0 -exec ls -l {} \
# 查找系统中所有文件长度为 0 的普通文件,并列出它们的完整路径
  • 按时间检索(+/-表示范围)
1
2
3
4
5
6
7
# -ctime 访问时间(上次文件打开)
# -mtime 修改时间(上次文件内容被修改)
# -ctime 更改时间(上次文件 inode 已更改)
find . -ctime 20 # 最近20天内更新
find . -ctime -6h30m # 文件状态6小时30分内变化
find . -atime -1 # <24小时前访问过(同-atime 0)
find . -mtime +1w # 修改时间超1周

3.复杂操作示例

1
2
3
4
5
6
7
8
9
10
11
# 查找并删除,删除前需询问
find /var/log -type f -mtime +7 -ok rm {} \
# 更改时间在 7 日以前的普通文件

# 查找并排序
$ find . -printf "%T+\t%p\n" | sort
$ find . -printf "%T+\t%p\n" | sort -r

# 查找并chmod
$ find / -type f -perm 0777 -print -exec chmod 644 {} \; # 查找文件并将权限设置为 644
$ find / -type d -perm 777 -print -exec chmod 755 {} \; # 查找目录并将权限设置为 755

三、awk

1.用法

awk用来处理文本,将文本按照指定的格式输出。其中包含了变量,循环以及数组

1
2
3
4
5
awk [选项] 'awk脚本' [处理文本路径]
# 常用options选项:
# -F :指定描绘一行中数据字段的文件分隔符,默认为空格(即指定行的分隔符)
# -f file:指定读取程序的文件名,就是将awk的命令放到文本中用-f选项调用
# -v var=value:定义awk程序中使用的变量和默认值

2.awk脚本

参考: 18个awk的经典实战案例 | 骏马金龙 (junmajinlong.com)

脚本使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# awk脚本,规则如下
BEGIN {<初始化>}
<pattern 1> {<计划动作>}
<pattern 2> {<计划动作>}
...
# 对每次匹配pattern1成功的行执行的计划动作
# 可以使用多条动作
# 不使用pattern默认对每行使用
END {< 最后的动作 >}

# 使用字符串作为awk脚本
$ awk -F, \
'BEGIN {print "Error logs : "} \
/("severity":"info")/ {print $NF} \
END {print "End"}' ./*.log

# 使用.awk脚本文件作为awk脚本
awk -f <文件名> [待处理文件]
$ awk -f demo.awk /etc/passwd

输入控制

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
# options选项设置行内字段分隔符,默认为逗号,
-F: # 以:作为分隔符
# 逐行打印passwd内第一个字段与最后一个字段
$ awk -F: '{print NR: $1, $NF} \
END{print "end of " FILENAME}' /etc/passwd | tail -n 5
root /bin/bash
daemon /usr/sbin/nologin
...
# -F: 表示用冒号:作为分隔符来识别各列
# '{print NR: $1, $NF}' 为处理程序
# {...} {}内表示逐行处理
# NR 当前行号
# NF 当前行字段数
# $1 表示每行的第一个字段
# $NF 表示每行的最后一个字段
# /etc/passwd 表示待处理的文件

$ awk -F":" '{print NR ":" $1, $NF} \
END{print "end of file : " FILENAME}' /etc/passwd \
| sort | tail -n 5
6:games /usr/sbin/nologin
7:man /usr/sbin/nologin
8:lp /usr/sbin/nologin
9:mail /usr/sbin/nologin
end of/etc/passwd
# -F":" 分割符:
# {print NR ":" $1, $NF} 打印行号NR, 打印字符串 : , 打印首尾字段
# END{print "end of file : " FILENAME}' 结尾行打印文件名
# | sort 打印的各行排序
# | tail -n 5 只显示排序后的末尾5行
1
2
3
# awk脚本内设置分隔符
FS=":" # 行内分隔符设置为冒号":", 默认为逗号","
RS="" # 行间分隔符设置为空行, 默认为换行符"\n"

输出控制

1
2
3
4
5
6
7
8
# 指定位置输出字段
$ awk -F: NR==2'{print $1, $(NF-1), $NF}' /etc/passwd
daemon /usr/sbin /usr/sbin/nologin
# -F: 指定字段分隔符
# NR==2 只对第2行内容执行后续操作
# $0 输出所有字段
# $1 每行第一字段
# $NF, $(NF-1) 每行最后字段与倒数第二字段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 指定字段内容输出范围

$ cat a.txt
16 001agdcdafasd
16 002agdcxxxxxx
23 001adfadfahoh
23 001fsdadggggg

# 指定每行第二个字段只输出前3个字符
$ awk '{print $1,substr($2,1,3)}' #
$ awk 'BEGIN{FIELDWIDTH="2 2:3"}{print $1,$2}' a.txt
16 001
16 002
23 001
23 002
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
# 格式化输出print与printf

# print 在显示多个结果时以OFS分隔行内变量,以ORS作行间分隔
OFS 输出的列间隔符,默认为tab;
ORS 输出的行间隔符,默认为\n
OFS=",\t" # 在输出各字段后加上逗号
ORS=";\n" # 在输出各行末尾加上;再换行

# printf可更灵活控制某字段输出格式
# 格式控制 %s
$ awk 'BEGIN{printf "|%10s|\n", "hello"}'
| hello| # %10s右对齐,定长10字符长度
$ awk 'BEGIN{printf "|%-10s|\n", "hello"}'
|hello | # %-10s左对齐,定长10字符长度
# 浮点数控制 %f %E %e
$ awk 'BEGIN{sum=0} {sum+=$1} \
END{printf(“%f\n”,sum)}' test.txt 214449.110000 # 默认保留六位小数
$ awk 'BEGIN{sum=0} {sum+=$1} \
END{printf("%.2f\n",sum)}' test.txt 57760731.18 # 指定小数位数
$ awk 'BEGIN{sum=0} {sum+=$1} \
END{printf("%12.2f\n",sum)}' test.txt 57760731.18 # 指定位数过多,前面补空格
# ASCII字符 %c
# 十进制整数 %d
# 无符号八进制 %o

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 格式化文本

$ cat a.txt
aaaa bbb ccc
bbb aaa ccc
ddd fff eee gg hh ii

# 移除每行的前缀、后缀空白,并将各部分左对齐
$ awk 'BEGIN{OFS="\t"}{$1=$1;print}' a.txt
aaaa bbb ccc
bbb aaa ccc
ddd fff eee gg hh ii
# BEGIN{OFS="\t"} 输出字段分隔符(OFS)为tab
# {$1=$1;print} 各字段保持不变,并打印

逻辑控制

1
2
3
4
5
# 正则表达式匹配行
/regex/ # 行匹配
!/regex/ # 行不匹配
$1 ~ /regex/ # 字段匹配
$1 !~ /regex/ # 字段不匹配

常用awk函数

1
2
3
4
5
6
7
8
9
10
# 将 $0 设置为当前输入文件中的下一个输入记录
getline # 即读入下一行

# 0 到 1 之间的随机数
rand
# 为 rand 设置种子并返回之前的种子
srand

# 将 x 截断为整数值
int(x)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 字符串 s 的长度(如果没有arg,则为 $0)
length(s)

# 大小写转换
tolower(s) # 字符串 s 转小写
toupper(s) # 字符串 s 转大写

# 字符串 s 中出现字符串 t 的位置,未找到为 0
index(s,t)
# 字符串 s 中出现正则表达式 r 的位置,未找到为 0
match(s,r)
# 返回从index开始的 s 的len长子字符串(1开始计数)
substr(s,index,len)

# 将字符串 s 拆分为数组 a 由 fs 拆分
# 返回 a 的长度
split(s,a,fs)

# 将 t 替换为字符串 s 中第一次出现的正则表达式 r
# (如果未给出 s,则替换为 $0)
sub(r,t,s)
# 用 t 替换字符串 s 中所有出现的正则表达式 r
gsub(r,t,s)

去重与统计功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 去重功能

$ cat a.txt
2019-01-13_12:00_index?uid=123
2019-01-13_13:00_index?uid=123
2019-01-13_14:00_index?uid=333
2019-01-13_15:00_index?uid=9710
2019-01-14_12:00_index?uid=123
2019-01-14_13:00_index?uid=123
2019-01-15_14:00_index?uid=333
2019-01-16_15:00_index?uid=9710

# 按uid值去重
$ awk -F"?" '!arr[$2]++{print}' a.txt
2019-01-13_12:00_index?uid=123
2019-01-13_14:00_index?uid=333
2019-01-13_15:00_index?uid=9710
# -F"?" ?作为字段分割符
# !arr[$2]++ {print} 匹配条件:每行的第二个字段(即uid值)重复时就不打印该行; 其中arr[$2]++用于记录出现次数
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
# 统计功能

# 统计行数
awk 'END{printf NR \n}' a.txt
# NR 记录总数

# 统计各重复项数目
$ awk '{arr[$1]++} \
END {OFS="\t"; \
ORS=";\n";\
for(idx in arr) \
{print arr[idx],idx}}' a.txt
6 portmapper
2 nfs_acl
5 nlockmgr
2 status
4 nfs
6 mountd

# 统计多项数据:awk脚本的子数组实现
{ a[$1][$2]++ }
END{
# 遍历数组,统计每个ip的访问总数
for(ip in a){
for(uri in a[ip]){
b[ip] += a[ip][uri]
}
}
# 再次遍历
for(ip in a){
for(uri in a[ip]){
print ip, b[ip], uri, a[ip][uri]
}
}
}
# 统计多项数据:awk脚本的复合索引实现
{ a[$1]++; b[$1"_"$2]++; }
END{
for(i in b){
split(i,c,"_");
print c[1],a[c[1]],c[2],b[i]
}
}


筛选功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 读取特定段落

# 读取.ini 配置文件中的某段
$ cat a.ini
>[base]
...

[mysql]
name=mysql_repo
vaseurl=http://xxx/...

enable=1

[...]
...
# 读取 [mysql] 段落的awk脚本
BEGIN{RS=""} # 按段落划分行
/\[mysql\]/ # 正则匹配(\[...\]中[]需要用\转译)
{
print;
while( (getline)>0 ) # 利用getline进入后续行继续判断,防止漏掉上文中的enable=1
{ if(/\[.*\]/) { exit } } # 如果遇到其他[...]开头,停止打印
print;
}
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
# 筛选时间范围内日志

# mktime() 转换timestamp值
$ awk 'BEGIN{print mktime("2019 11 10 03 42 40")}'
1573328560

# 时间筛选awk脚本
BEGIN { which_wime = mktime("2019 11 10 03 42 40") }
{
# 匹配时间字符串
match($0, "^.*\\[(.*)\\].*")
# 字符串转timestamp
tmp_time = strptime2(arr[1])
# 通过timestamp筛选
if(tmp_time > which_time){ print }
}

# 构建的时间字符串格式为:"10/11/2019:23:53:44+08:00"
function strptime2(str ,dt_str,arr,Y,M,D,H,m,S) {
dt_str = gensub("[/:+]"," ","g",str)
# dt_sr = "10 11 2019 23 53 44 08 00"
split(dt_str,arr," ")
Y=arr[3]
M=arr[2]
D=arr[1]
H=arr[4]
m=arr[5]
S=arr[6]
return mktime(sprintf("%s %s %s %s %s %s",Y,M,D,H,m,S))
}

行处理与列处理功能

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
# 行列转换awk脚本
{
for(i=1;i<=NF;i++){
if(!(i in arr)){ arr[i]=$i }
else { arr[i]=arr[i]" "$i }
}
}
END{
for(i=1;i<=NF;i++){ print arr[i] }
}

# 第一列相同时,将第二列数据合并至同一行

# awk脚本
{
if($1 in arr){ arr[$1] = arr[$1]" "$2 } # 如果第一列已记录,将第二列续接至已有数据
else { arr[$1] = $2 } # 第一列未记录,直接存入当前第二列数据
}
END{
for(i in arr){ printf "%s %s\n",i,arr[i] }
}
# 处理效果
74683 1001
74683 1002
74683 1011
74684 1000
74684 1001
>>>
74683 1001 1002 1011
74684 1000 1001 1002

四、sed

五、组合使用例

1.查询容器名并用作后续指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash

# 目标命令模板
# kubectl exec
# --namespace example-namespace
# example-pod -c second-container
# -- cat /tmp/example-file > local-file

# 目标查询结果
# wilson@ubuntu:~/workspace/microservices/microservices-demo$ kubectl get pods
# NAME READY STATUS RESTARTS AGE
# adservice-6fb9779796-l7bxm 1/1 Running 0 4m18s
# cartservice-6557d5b47d-c229p 1/1 Running 0 4m18s
# ...
# shippingservice-6975d4b698-rr9nq 1/1 Running 0 4m17s

# POD_NAME每次会有随机生成的后缀,因此需要先查询再在目标命令中使用
POD_NAME=`kubectl get pods | grep adservice | awk '{print $1}'`
kubectl exec --namespace default $POD_NAME -c server -- cat /app/log/adservice/log.txt > ./adservice.log

2.删除没有error级别日志的日志文件

1
2
3
4
$ grep -r "error" . -LZ | xargs -0 rm
# -r 表示在当前目录.下递归搜索
# -LZ 表示返回匹配error失败的文件名,并用0值字节(\0)分隔
# xargs -0 rm 读取输入并用0值字节(\0)终结符分隔文件名,然后删除匹配文件

3.

1
2
3
4
5
6
# 打印passwd中第一列内容,即所有用户名
$ cat /etc/passwd | awk -F: '{print $1}'
root
daemon
...