查看原文
其他

Linux命令拾遗-文本处理篇

扣钉日记 扣钉日记 2022-11-27

原创:打码日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处。

简介

这是Linux命令拾遗系列的第二篇,本篇主要介绍Linux中与文本处理相关的命令,如xargs、grep、sed、awk等。

本系列文章索引
Linux命令拾遗-入门篇

常用文本相关命令

cat、tac、less

# 打印输入内容到标准输出
$ seq 3 | cat
# -n 带行号输出
$ seq 3 | cat -n
# -A 可以用来查询特殊字符
$ seq 3 | cat -A
# tac可以倒序输出
$ seq 3 | tac
3
2
1

less用于查看文件内容,如下:

$ less app.log

同时,less还是一个可交互的命令,交互方式类似于vim,如下:

操作描述
Ctrl + f向后翻页(forward)
Ctrl + b向前翻页(backward)
g跳转到首行
G跳转到尾行,同时按Shift+g
63G跳转到63行
j 或向下滚动一行
k 或向上滚动一行
q退出less程序
/abc向后搜索abc
再按n继续搜索下一个abc,再按N搜索上一个abc
?abc向前搜索abc
再按n继续搜索上一个abc,再按N搜索下一个abc
F不断显示文件新内容,同时按Shift + f
v在编辑器中打开当前文件
-N显示行号
(先按-,再按Shift + n,再按)
-I忽略大小写搜索
(先按-,再按Shift + i,再按)
-S不换行查看
(先按-,再按Shift + s,再按)
-R保留颜色
(先按-,再按Shift + r,再按)
-F一屏可展示,则直接输出
(先按-,再按Shift + f,再按)

另外,less也经常用来查看命令输出的大量内容,比如ps -ef一般会显示大量内容,这会将之前命令的执行结果从屏幕上往前推很远,使用ps -ef | less就不会有这种烦恼了。

head、tail

# 显示前10行
$ seq 20 | head -n10
# 显示后10行
$ seq 20 | tail -n10
# 显示从第10行开始到末尾的行
$ seq 20 | tail -n+10
# 一直查看文件新追加的内容
$ tail -f temp.txt
# 生成16个字节的随机hex
$ cat /dev/urandom | head -c 16 | xxd -ps

wc、sort、uniq

# 统计行数,单词数,字节数,之所以有10字节,是因为把换行符也算进去了
$ seq 5 | wc 
      5       5      10

# 只统计行数
$ seq 5 | wc -l
5

# 排序,-n表示数值排序,-r表示倒序,-k1表示使用第一列排序
$ seq 5 |sort -nrk1
5
4
3
2
1

# uniq做分组计数,使用uniq前数据必须排好序,故前面要加sort
$ (seq 6;seq 3 8) |sort|uniq -c
      1 1
      1 2
      2 3
      2 4
      2 5
      2 6
      1 7
      1 8

# 并集
cat a b | sort | uniq > c 
# 交集
cat a b | sort | uniq -d > c 
# 差集
cat a b b | sort | uniq -u > c 

grep

根据正则搜索内容,它会一行一行的拿出数据中的内容,然后看这一行是否匹配正则,匹配则输出这一行的内容。

# 使用正则搜索,默认BRE,不支持+?,不支持\d
$ seq 12|grep '11*'
1
10
11
12
# -F纯字符串搜索,而不是当成正则搜索
$ seq 12|grep -F '11*'

# -w单词搜索,所以11这种搜不到
$ seq 12|grep -w '1'
1
# -E使用ERE正则搜索,支持+?,不支持\d
$ seq 12|grep -E '1+'
1
10
11
12
# -P使用PCRE正则搜索,支持+?,支持\d
seq 12|grep -P '\d\d+'
10
11
12

# -v反向搜索,显示不包含1的行
seq 12|grep -v 1
2
3
4
5
6
7
8
9
# -o只输出匹配到的数据,而不是整行
echo hello,java|grep -oP '\w+'
hello
java
# -c显示搜索行数
$ seq 12|grep -P '\d\d+' -c
3
# -m限制搜索行数最多2行
$ seq 12|grep -P '\d\d+' -m 2
10
11

# 搜索10并也显示之后的2行(-A2)
$ seq 12|grep -A2 -w 10
10
11
12
# 搜索10并也显示之前的2行(-B2)
$ seq 12|grep -B2 -w 10
8
9
10
# 搜索10并也显示之前以及之后的2行(-C2)
$ seq 12|grep -C2 -w 10
8
9
10
11
12

# -r在当前目录递归的找文件,并在文件中找8080这个词,-n显示8080在文件中的行号
$ grep -rn -w 8080 .

find与ls

ls一般用来在当前目录的找文件

# 列出当前目录的文件名
ls
# -l列出当前目录的文件,以及文件属性,如创建用户、时间、大小等
ls -l
# 在当前目录找txt后缀的文件
ls *.txt
# 列出当前目录的文件,按时间倒序显示
ls -lt
# 列出当前目录的文件,按大小倒序显示
ls -lS

find一般用来递归的找文件

# 当前目录递归查找txt后缀的文件,会深入到子目录中,-type f表示查找文件,不然输出结果可能会有目录
find -name '*.txt' -type f
# 查找大于800M的文件
find . -type f -size +800M 
# 1分钟内修改过的文件 
find . -type f -mmin -1 
# 7天内修改过的文件 
find . -type f -mtime -7 

xargs

作用:将标准输入流中的数据,转换为命令参数,并执行命令。
引入这个命令的原因是,有些命令不支持处理标准输入的数据,而只支持命令参数,如杀死进程的kill命令。
当我们想要杀死所有java进程时,可以这样做:

# 使用pgrep找出java进程
pgrep java
856
857

# 再使用kill杀死这两个java进程
kill 856 857

# 写成一行命令,如下,利用了bash的命令替换语法
kill `pgrep java`
kill $(pgrep java)

# 假如kill每次只支持一次传一个参数的话,可以用bash的for与while循环语法
for pid in `pgrep java`;do kill $piddone
pgrep java | while read pid;do kill $pid;done

可以看到,对于上面的场景,命令越写越复杂,而xargs就可以很好的解决这个问题,如下:

# 使用xargs,将输入流按空白分拆成参数,传给kill命令,等效于上面的 kill `pgrep java`
pgrep java | xargs kill
# 使用-n选项,将输入流按空白分拆成参数,每次传一个参数给kill命令,类似上面for与while循环实现
pgrep java | xargs -n1 kill

下面体会一下xargs中常用的选项,如下:

拆分参数

# xargs默认以空白分隔参数,不指定命令时,默认执行echo,并默认将尽可能多的参数传递给命令
$ seq 20|xargs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
# 使用-d指定分隔符
$ seq -s, 20|xargs -d,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

注意:没有指定-d时,xargs默认使用空白分隔,这里的空白指的是空格TAB换行符,且多个空白符理解为一个空白,Linux中大多数需要分拆文本为列的命令,基本都遵从这个原则,如上面介绍过的sort,以及后面将要介绍的awk。

体会一下有无-d选项上的不同,如下:

$ seq 20|xargs printf '"%s"\n'|xargs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

$ seq 20|xargs printf '"%s"\n'|xargs -d'\n' 
"1" "2" "3" "4" "5" "6" "7" "8" "9" "10" "11" "12" "13" "14" "15" "16" "17" "18" "19" "20"

在没有指定-d选项时,xargs默认会忽略掉',",\,而有-d时则不会忽略。

参数分批

# 使用-n或-L指定每次传参的数量
$ seq 20|xargs -n4
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
17 18 19 20

$ seq 20|xargs -L4
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
17 18 19 20

体会一下 -n-L 的细节上的不同,如下:

$ seq 20|xargs -n2
1 2 
3 4 
5 6 
7 8 
9 10 
11 12 
13 14 
15 16 
17 18 
19 20

$ seq 20|xargs -n2|xargs -n4
1 2 3 4 
5 6 7 8 
9 10 11 12 
13 14 15 16 
17 18 19 20

$ seq 20|xargs -n2|xargs -L4
1 2 3 4 5 6 7 8 
9 10 11 12 13 14 15 16 
17 18 19 20

看起来好像是,在没指定-d时,-n默认使用空白符(包含空格、TAB与换行符)来分拆参数,而-L,默认使用换行符来分拆参数。

参数占位

# 使用-i后,可使用{}来作为参数的占位符
seq 20|xargs -i echo 'id={}'

体会一下-i的细节,如下:

$ seq 20|xargs -n2|xargs -i echo 'id={}'
id=1 2 
id=3 4 
id=5 6 
id=7 8 
id=9 10 
id=11 12 
id=13 14 
id=15 16 
id=17 18 
id=19 20

看起来好像是,在没指定-d时,使用-i后,默认使用换行符来分拆参数。

调试与并发

# 使用-p来调试xargs传参细节
$ seq 20|xargs -i -p echo 'id={}'
echo 'id=1' ?...y
id=1
echo 'id=2' ?...

# 使用-P来并发运行命令,没有-P4时需要执行10s,有-P4只需要4s
$ seq 4|xargs -n1 -P4 sleep

有些时候,利用xargs的-P选项,还可以做一些简单的压力测试哩!

常用场景

xargs常与ls、find、grep配合,用来在指定文件中搜索内容,其中ls、find用来找文件,xargs将找到的文件名变成grep的参数,如下:

# 在当前目录的所有xml文件中,搜索8080端口配置
ls *.xml |xargs grep -w 8080
# 在当前目录及子目录的xml文件中,搜索8080端口配置
find -name '*.xml'|xargs grep -w 8080

sed

一般用于替换修改文本数据,有流文本编辑器(Stream editor)之称,实际上,你也可以将其看做一个极其简化的脚本语言。

语法

基本语法形如pattern action,sed会读取每一行到模式空间,看是否匹配pattern,如果匹配则执行action。

注:模式空间pattern space,后面会详细解释,现在理解为存储当前行数据的变量即可。

sed '3,5 s/a/c/g'将第3到5行中的a替换为c,其中3,5为pattern部分,s/a/c/g为action部分,只有满足pattern条件的行,action才会执行,pattern部分可以省略,这样每一行都会执行action。

# yes可以用来不断的重复生成字符串,以此作为我们的测试数据
$ yes abcde|head -n5
abcde 
abcde 
abcde 
abcde 
abcde
# 第3到5行中的a替换为c,其中的g表示替换所有
$ yes abcde|head -n5|sed '3,5 s/a/c/g'
abcde 
abcde 
cbcde 
cbcde 
cbcde

另外pattern action还可以是如下的形式:

  1. 可以组合写多个,如sed '3,5 s/a/c/g; 2,4 s/b/d/g'表示第3行到第5行的a替换为c,第2行到第4行的b替换为d。

  2. 还可以嵌套,如sed '3,5{3,4 s/a/c/g; 4,5 s/b/d/g}'表示第3到5行中,其中3到4行执行a替换为c,第4到5行执行b替换为d。其实你可以认为在action只有一个时,大括号被省略了。

  3. 还可以求反,如sed '3,5! s/a/c/g'表示非第3到5行的行,将a替换为c。

# 第3行到第5的a替换为c,第2行到第4行的b替换为d
$ yes abcde|head -n5|sed '3,5 s/a/c/g; 2,4 s/b/d/g'
abcde 
adcde 
cdcde 
cdcde 
cbcde
# 第3到5行中,其中3到4行执行a替换为c,第4到5行执行b替换为d
$ yes abcde|head -n5|sed '3,5{3,4 s/a/c/g; 4,5 s/b/d/g}'
abcde 
abcde 
cbcde 
cdcde 
adcde
# 非第3到5行的行,将a替换为c
$ yes abcde|head -n5|sed '3,5! s/a/c/g'
cbcde 
cbcde 
abcde 
abcde 
abcde

常见pattern

sed默认会把action处理后的每一行打印出来,加上-n选项可以关掉默认打印,如下:

# 显示1到3行,这里action为p,表示打印,-n用来关闭默认打印,不然1到3行会打印2遍,p一般都和-n配合使用
$ seq 5|sed -n '1,3 p'
1
2
3
# pattern部分可以使用正则表达式,注意sed中的正则也不能使用\d,且记得时常搭配-E选项
# 注意pattern部分和action部分是可以随意组合的,也就是说正则形式的pattern也可以和s搭配使用
$ seq 5|sed -n '/[2-4]/ p'
2
3
4
# pattern部分也可以是逗号分隔的两个正则表达式,匹配从找到第一个正则表达式开始的行,到找到第二个正则表达式的行结束
$ seq 5|sed -n '/[2]/,/[4]/ p'
2
3
4
# 打印第1行,以及之后第间隔2行的行
$ seq 5|sed -n '1~2 p'
1
3
5
# 打印匹配行与之后的2行
$ seq 5|sed -n '/^1$/,+2 p'
1
2
3

常见action

除了s(替换)与p(打印)外,还有d(删除)、i(插入)、a(追加)、c(修改)、q(退出)、l(打印特殊字符),如下:

# 删除包含1和2的行
$ seq 3|sed '/[1-2]/ d'
3
# 向前插入一行,常用于设置csv标题
$ seq 3|sed '1 i\id'
id 


3
# 在最后一行之后追加一行,其中$表示最后一行
$ seq 3|sed '$ a\id'



id
# 将第一行整行直接修改为id
$ seq 3|sed '1 c\id'
id 

3
# 打印前5行,因为sed执行到第5行,q命令让其退出了
$ seq 9|sed '5q'
1
2
3
4
5
# 显示特殊字符
echo -ne '\r\n'|sed -n 'l0'
\r$

另外,s(替换)还有一些细节,这些细节实际上非常有用,体会一下:

# 替换可以使用正则的捕获组功能
echo 'id=1,name=zs'|sed -E 's/id=(\w+),name=(\w+)/\1 \2/'
1 zs
# g表示将所有的a替换为c
echo 'a,a,a,a'|sed 's/a/c/g'
c,c,c,c
# 3g表示将第3次匹配到的a以及后面匹配到的a,都替换为c
echo 'a,a,a,a'|sed 's/a/c/3g'
a,a,c,c
# 没有g只能替换第1次匹配
echo 'a,a,a,a'|sed 's/a/c/'
c,a,a,a
# 3表示只替换第3次匹配到的a为c
echo 'a,a,a,a'|sed 's/a/c/3'
a,a,c,a
# &表示之前匹配到的内容
echo 'a,a,a,a'|sed 's/.,./[&]/g' 
[a,a],[a,a]
# 大小写转换
echo 'hello'|sed -E 's/.+/\U&/g' 
HELLO
echo 'hello'|sed -E 's/.+/\u&/g' 
Hello
echo 'HELLO'|sed -E 's/.+/\L&/g' 
hello
echo 'HELLO'|sed -E 's/.+/\l&/g' 
hELLO

模式空间与保留空间

sed中有2个概念,模式空间pattern space与保留空间hold space,简单来说,可以将其看成2个变量,其中模式空间是局部变量,sed读到的当前行数据,会被保存其中,而保留空间是全局变量。

sed运行过程可描述为如下代码:

hold_space="";
while read pattern_space; do
    # sed script here 
done 

理解了这个概念,就可以介绍下面这些action了,如下:

action描述
n加载下一行文本到模式空间,覆盖模式空间原数据
N追加下一行文本到模式空间
P打印模式空间数据的第一行
D删除模式空间数据的第一行
: label标记待跳转位置
b label跳转到: label标记的位置,可用于实现分支判断与循环
# 打印偶数行
$ seq 9|sed -n 'n;p'
2
4
6
8

$ seq -s, 9
1,2,3,4,5,6,7,8,9
# 每3个一行,使用s切出新行,P打印第一行,D再删掉,如此往复直到D将模式空间数据全删掉
$ seq -s, 9|sed 's/,/\n/3;P;D'
1,2,3
4,5,6
7,8,9

$ seq 9
1
2
3
4
5
6
7
8
9
# 每3个一行,用ba跳转实现一个循环,配合N追加3行到模式空间,再将\n替换为,即可
$ seq 9|sed ':a;N;0~3!{$!ba};s/\n/,/g'
1,2,3
4,5,6
7,8,9


分段
用sed来获取指定段中的内容,所谓段就是用空行分隔的多个行,如下:

比如需要获取eth0网卡的ip地址,如下:

$ ifconfig|sed -nE '/\S/{:a;N;/\n$/!{$!ba}}; /eth0/s/.*inet (\S*).*/\1/gp'
172.21.117.1

运行过程:

  1. 使用/\S/从非空白行开始,并用:a标记。
  2. 不断使用N读取下一行追加到模式空间。
  3. 然后使用/\n$/!{$!ba}},若之前读到的不是空行,也不是最后一行,会命中/\n$/!$!,然后使用ba跳转到开头,继续加载下一行进来,直到遇到空行,这样就读到了一个完整的段。
  4. 使用/eth0/判断段中是否包含eth0,如果是,则提取ip地址并打印。

保留空间action

action描述
h将模式空间数据覆盖到保留空间
H将模式空间数据追加到保留空间
g将保留空间数据覆盖到模式空间
G将保留空间数据追加到模式空间
x交换模式空间与保留空间的数据
# 倒序输出,运行过程如下:
# sed处理第1行时会把1保存到保留空间
# 处理第2行时,会先把保留空间的1追加到模式空间,追加后模式空间就是2 1了,然后又将其保存到保留空间
# 接下来第3行就是3 2 1,如此往复,到最后一行时再输出内容即可
$ seq 5|sed -n '1!G; $p; h;'
5
4
3
2
1

# 过程与上面类似,不过每3行会打印一次并清空模式空间与保留空间罢了
$ seq 9|sed -n 'G; 0~3{p;s/.*//g};h'
3
2
1

6
5
4

9
8
7

# 打印匹配行,以及其前面4行,类似seq 9|grep -B4 7
$ seq 9|sed -n 'H; x; 4,$s/^[^\n]*\n//; x; /^7$/{g;p}'
4
5
6
7

sed里面掺杂了操作保留空间的命令后,执行过程就变得非常烧脑了,你的大脑需要飞速的运转才行。

例子:实现urldecode

echo hello%E7%BC%96%E7%A8%8B|sed 's/%/\\x/g'
hello\xE7\xBC\x96\xE7\xA8\x8B

echo hello%E7%BC%96%E7%A8%8B|sed 's/%/\\x/g'|xargs -d"\n" echo -e
hello编程

awk

awk是一个强大的文本处理工具,本质上可以看做是一门脚本语言了,可以用来对文本进行过滤、替换等操作,还能实现简单的统计以及类似SQL的join功能等。

语法

awk基本语法如下:

awk 'BEGIN{
    //your code

pattern1{
    //your code

pattern2{
    //your code

END{
    //your code
}'

  1. BEGIN部分的代码,最先执行。

  2. 然后循环从标准输入中读取的每行文本,如果匹配pattern1,则执行pattern1的代码,匹配pattern2,则执行pattern2中的代码。

  3. 最后,执行END部分的代码。

运行过程

如下所示,分别求奇数行与偶数行的和:

$ seq 1 5




5

$ seq 1 5|awk 'BEGIN{print "odd","even"} NR%2==1{odd+=$0} NR%2==0{even+=$0} END{print odd,even}'
odd even 
9 6
  1. seq 1 5用来生成1到5的数字。

  2. awk先执行BEGIN块,打印标题。

  3. 然后第1行尝试匹配NR%2==1这个pattern,其中NR为awk的内置变量,表示行号,从1开始,$0为awk读到的当前行数据,显然匹配NR%2==1,则执行里面的代码。

  4. 然后第1行尝试匹配NR%2==0这个pattern,显然不匹配。

  5. 然后第2行、第3行...,一直到最后一行,都执行上面两步。

  6. 最后执行END块,将前面求和的变量打印出来,其中9=1+3+56=2+4

这个程序还可以如下这样写:

seq 1 5|awk 'BEGIN{print "odd","even"} {if(NR%2==1){odd+=$0}else{even+=$0}} END{print odd,even}'

这里使用了if语句,实际上awk的程序语法与C是非常类似的,所以awk也有else,while,for,break,continue,exit等,常见语法如下:

if (condition) statement [ else statement ]
while (condition) statement
do statement while (condition)
for (expr1; expr2; expr3) statement
for (var in array) statement
i++; i--;
i > 0 ? 1 : 0

分列

可以看到,awk程序在处理时,默认是一行一行处理的,注意我这里说的是默认,并不代表awk只能一行一行处理数据,接下来看看awk的分列功能,可通过-F选项提供,如下:

$ cat temp.txt 
1,6 
2,7 
3,8 
4,9 
5,10

$ cat temp.txt |awk -F, '{printf "%s\t%s\n",$1,$2}'
1       6 
2       7 
3       8 
4       9 
5       10

这个例子用-F指定了,,这样awk会自动将读取到的每行,使用,分列,拆分后的结果保存在$1,$2...中,另外,你也可以使用$NF, $(NF-1)来引用最后两列的值,不指定-F时,awk默认使用空白字符分列。

注意这里面的printf "%s\t%s\n",$1,$2,printf是一个格式化打印函数,其实也可以写成printf("%s\t%s\n", $1, $2),只不过awk中函数调用可以省略括号。

另外,对于字符串拼接,awk不需要任何连接符号,只需要将两个字符串挨在一起即可,这和C语言中一样,不同于Java中使用+拼接字符串,如下:

$ awk 'BEGIN{print "a""b"}'
ab
# 当然在中间加上空格,也是一样的,awk会忽略它
$ awk 'BEGIN{print "a" "b"}'
ab
# 但如果你用,分隔起来,就不一样了,它相当于传给print两个参数,类似print("a","b")
$ awk 'BEGIN{print "a","b"}'
a b

关联数组

awk支持一维数组,使用数组实现上面计算奇偶数和,如下:

$ seq 1 5|awk 'BEGIN{print "odd","even"} {S[NR%2]+=$0} END{print S[1],S[0]}'
odd even
9 6

注意,awk中的数组叫关联数组,即数组key可以是任意值,不一定是数字,概念上类似于java中的Map,如下:

# 统计各进程的数量,显示数量最多的前4个
$ ps h -eo comm|awk '{S[$0]++}END{for(k in S){print S[k],k}}'|sort -nr|head -n4
9 sshd
6 httpd
3 systemd
3 bash

如果要删除数组中的元素,使用delete S[k]即可。

内置变量

上面已经提到了NR这个内置变量,awk还有如下内置变量

内置变量作用
$0当前记录(这个变量中存放着整个行的内容)
$i~$n当前记录的第n个字段
NF当前记录中的字段个数,就是有多少列
NR已经读出的记录数,就是行号,从1开始,如果有多个文件话,这个值也是不断累加中。
FNR当前记录数,与NR不同的是,这个值会是各个文件自己的行号
FS与-F功能类似,用来分列的,不过FS可以是正则表达式,默认是空白字符。
注:如果FS的值是空,代表每个字母拆分为一个
OFS与FS对应,指定print函数输出时的列分隔符,默认空格
RS记录分隔符,默认记录分隔符是\n
注:如果RS的值是空,代表按段划分记录
ORS与RS对应,指定print函数输出时的记录分隔符,默认\n
FILENAME当前输入文件的名字

用2个例子体会一下:

echo -n '1,2,3|4,5,6|7,8,9'|awk 'BEGIN{RS="|";FS=","} {print $1,$2,$3}'
1 2 3 
4 5 6 
7 8 9
echo -n '1,2,3|4,5,6|7,8,9'|awk 'BEGIN{RS="|";FS=",";ORS=",";OFS="|"} {print $1,$2,$3}'
1|2|3,4|5|6,7|8|9,

总结:awk数据读取模式,总是以RS为记录分隔符,一条一条的读取记录,然后每条记录按FS拆分为字段。

再看看这个例子:

$ seq 1 5|awk '/^[1-4]/ && !/^[3-4]/'
1
2
$ seq 1 5|awk '$0 ~ /^[1-4]/ && $0 !~ /^[3-4]/{print}'
1
2
$ seq 1 5|awk '$0 ~ /^[1-4]/ && $0 !~ /^[3-4]/{print $0}'
1
2

可以看到:

  1. awk中pattern部分可以直接使用正则表达式,而且可以使用&&,||,!这样的逻辑运算符.

  2. 如果正则表达式没有指定匹配变量,默认对$0执行匹配,所以awk '/regex/'直接就可以等效于grep -E 'regex'.

  3. 另外pattern后面的代码部分如果省略的话,默认打印$0.

  4. print函数如果没有指定参数,也默认打印$0.

  5. 另外,一定注意awk中的正则表达式不支持\d,匹配数字请使用[0-9],因为Linux中正则语法分BRE,ERE,PCRE,而awk支持的是ERE.

分段

ifconfig

如下,看看用awk如何获取eht0网卡的ip地址:

ifconfig|awk -v RS= '/eth0/{print $6}'
172.21.117.1 

常用函数

函数名说明示例
sub替换一次sub(/,/,"|",$0)
gsub替换所有,传入字符串被替换,返回替换次数gsub(/,/,"|",$0)
gensub替换,返回替换后的字符串$0=gensub(/,/,"|","g",$0)
match匹配,捕获内容在a数组中match($0,/id=(\w+)/,a)
split拆分,拆分内容在a数组中split($0,a,/,/)
index查找字符串,返回查找到的位置,从1开始i=index($0,"hi")
substr截取子串substr($0,1,i)substr($0,i)
tolower转小写tolower($0)
toupper转大写toupper($0)
srand,rand生成随机数BEGIN{srand();printf "%d",rand()*10}

查找与提取

示例数据如下,也是用awk生成的:

$ seq 1 10|awk '{printf "id=%s,name=person%s,age=%d,sex=%d\n",$0,$0,$0/3+15,$0/6}'|tee person.txt
id=1,name=person1,age=15,sex=0
id=2,name=person2,age=15,sex=0
id=3,name=person3,age=16,sex=0
id=4,name=person4,age=16,sex=0
id=5,name=person5,age=16,sex=0
id=6,name=person6,age=17,sex=1
id=7,name=person7,age=17,sex=1
id=8,name=person8,age=17,sex=1
id=9,name=person9,age=18,sex=1
id=10,name=person10,age=18,sex=1

然后用awk模拟select id,name,age from person where age > 15 and age < 18 limit 4这样SQL的逻辑,如下:

$ cat person.txt |awk 'match($0, /^id=(\w+),name=(\w+),age=(\w+)/, a) && a[3]>15 && a[3]<18 { print a[1],a[2],a[3]; if(++limit >= 4) exit 0}'
3 person3 16
4 person4 16
5 person5 16
6 person6 17
  1. 首先使用match函数以及正则表达式的捕获组功能,将id,name,age的值提取到a[1],a[2],a[3]中.
  2. 然后a[3]>15 && a[3]<18即类似SQL中age > 15 and age < 18的功能.
  3. 然后打印a[1],a[2],a[3],类似SQL中select id,name,age的功能.
  4. 最后,如果打印条数到达4条,退出程序,即类似limit 4的功能.

简单统计分析

awk可以做一些简单的统计分析任务,还是以SQL为例。
select age,sex,count(*) num, group_concat(id) ids from person where age > 15 and age < 18 group by age,sex这样的统计SQL,用awk实现如下:

$ cat person.txt |awk '
    BEGIN{
        printf "age\tsex\tnum\tids\n"
    }
    match($0, /^id=(\w+),name=(\w+),age=(\w+),sex=(\w+)/, a) && a[3]>15 && a[3]<18 { 
        s[a[3],a[4]]["num"]++; 
        s[a[3],a[4]]["ids"] = (s[a[3],a[4]]["ids"] ? s[a[3],a[4]]["ids"] "," a[1] : a[1])
    } 
    END{
        for(key in s){
            split(key, k, SUBSEP);
            age=k[1];
            sex=k[2];
            printf "%s\t%s\t%s\t%s\n",age,sex,s[age,sex]["num"],s[age,sex]["ids"]
        }
    }'

age     sex     num     ids
17      1       3       6,7,8
16      0       3       3,4,5

awk代码稍微有点长了,但逻辑还是很清晰的。

  1. BEGIN中打印标题行.
  2. match获取出id,name,age,sex,并过滤age>15且age<18的数据,然后将统计结果累计到s这个关联数组中。你可以把s这个关联数组想象中map,然后只是有两级key而已。(注意在awk中,拼接字符串使用空格即可,并不像java中使用+号).
  3. 最后END块中,遍历s这个关联数组,注意,类似s[a[3],a[4]]这样,在awk中是一个key,awk会使用SUBSEP这个变量将a[3],a[4]拼接起来,需要split(key, k, SUBSEP),将key按SUBSEP拆分到k中,SUBSEP默认是\034文件分隔符.

多文件join处理

awk还可以实现类似SQL中的join处理,求交集或差集,如下:

$ cat user.txt
1 zhangsan
2 lisi
3 wangwu
4 pangliu

$ cat score.txt
1 86
2 57
3 92

# 类似 select a.id,a.name,b.score from user a left join score b on a.id=b.id
# 这里FNR是当前文件中的行号,而NR一直是递增的,所以对于第一个score.txt,NR==FNR成立,第二个user.txt,NR!=FNR成立
$ awk 'NR==FNR{s[$1]=$2} NR!=FNR{print $1,$2,s[$1]}' score.txt user.txt
1 zhangsan 86
2 lisi 57
3 wangwu 92
4 pangliu

# 当然,也可以直接使用FILENAME内置变量,如下
$ awk 'FILENAME=="score.txt"{s[$1]=$2} FILENAME=="user.txt"{print $1,$2,s[$1]}' score.txt user.txt

# 求差集,打印user.txt不在score.txt中的行
$ awk 'FILENAME=="score.txt"{s[$1]=$2} FILENAME=="user.txt" && !($1 in s){print $0}' score.txt user.txt
4 pangliu

例子:ip转数字

# ip地址转数字
echo 192.168.0.101|awk -F. '{print strtonum("0x"sprintf("%02X",$1)sprintf("%02X",$2)sprintf("%02X",$3)sprintf("%02X",$4))}' 
3232235621

# 数字转ip地址
echo 3232235621|awk -v ORS=. '{match(sprintf("%08X",$0),/(..)(..)(..)(..)/,a);for(i=1;i<=4;i++){print strtonum("0X"a[i])}}' 
192.168.0.101.

例子:实现urlencode

echo -n hello编程|od -An -t u1|xargs -n1|awk -v ORS= '{c=sprintf("%c",$1);print c~/[0-9a-zA-Z.-_]/ ? c : sprintf("%%%02X",$1)}'
hello%E7%BC%96%E7%A8%8B

理解grep、sed、awk的能力

文本处理中,最常用的就是grep、sed、awk了,因此,这哥仨也常被人合称为Linux三剑客,可见它们的重要性了,下面介绍下它们在处理能力上的异同点。

从处理文本的能力上来看,grep < sed < awk
从命令学习难度上来看,grep < sed < awk
SQL来类比,如下:
grep实现了是行级别的where正则过滤功能。
sed实现了是行级别的where过滤、行号过滤与update、insert、delete等更新功能。
awk实现了是列级别的where过滤、行号过滤与update、insert、delete等更新功能,以及group by统计功能。

功能基础命令grep实现sed实现awk实现
过滤前10行seq 20 | head -n10seq 20 | grep -m10 '.*'seq 20 | sed -n '1,10p'seq 20 | awk 'NR<=10'
过滤出包含1的行
seq 20 | grep 1seq 20 | sed -n '/1/p'seq 20 | awk '/1/'
过滤出不包含1的行
seq 20 | grep -v 1seq 20 | sed -n '/1/!p'seq 20 | awk '!/1/'
过滤出大于等于8的行
seq 20 | grep -E '^([89]|[1-2][0-9])$'seq 20 | sed -nE '/^([89]|[1-2][0-9])$/p'seq 20 | awk '$1 >= 8'
过滤出10个包含1的行
seq 20 | grep -m10 1
seq 20 | awk '/1/ && ++n <= 10'
多条件过滤,过滤出既包含1又包含2,或不包含1与2的行
seq 50 | grep -P '^(?=.*1)(?=.*2).+|^(?!.*1)(?!.*2).+'seq 50 | sed -n '/1/{/2/p;d};/1/!{/2/!p;d};/2/!{/1/!p;d}'seq 50 | awk '$0 ~ /1/ && $0 ~ /2/ || $0 !~ /1/ && $0 !~ /2/'
过滤出11以及之后的2行
seq 20 | grep -A2 11seq 20 | sed -n '/11/,+2p'seq 20 | awk '/11/{n=1} n && n++<=3'
步进过滤,过滤出每隔3行的记录,如3,6,9...

seq 10|sed -n '0~3p'seq 10|awk 'NR%3==0'
区间过滤,过滤出包含2到包含6的行

seq 20|sed -n '/2/,/6/p'seq 20|awk '/2/,/6/'
提取部分文本
echo 'hello,java'|grep -oP 'hello,\K(\w+)'echo 'hello,java'|sed -nE 's/hello,(\w+)/\1/p'echo 'hello,java'|awk 'match($0,/hello,(\w+)/,a){print a[1]}'
更新,java替换为bash

echo 'hello,java'|sed 's/java/bash/g'echo 'hello,java'|awk '{gsub(/java/,"bash",$0);print $0}'
插入,首行插入title

echo 'hello,java'|sed '1i\title'echo 'hello,java'|awk '{if(NR==1){print "title"} print $0}'
删除,删除包含java行

echo 'hello,java'|sed '/java/d'echo 'hello,java'|awk '{if(/java/){next} print $0}'
驼峰与下划线互转

echo "userId"|sed -E 's/([A-Z]+)/_\l\1/g'

echo "user_id"|sed -E 's/_(.)/\u\1/g'
echo "userId"|awk '{print tolower(gensub(/([A-Z]+)/,"_\\1","g",$0))}'

echo "user_id"|awk -F_ -v ORS= '{for(i=1;i<=NF;i++){print i==1 ? $i : toupper(substr($i,1,1)) substr($i,2)}}'
倒序输出seq 9|tac
seq 9|sed -n 'G;$p;h'seq 9|awk '{s=$0 "\n" s}END{print s}'
统计,总行数seq 20 | wc -lseq 20 | grep . -cseq 20 | sed -n '$='seq 20 | awk 'END{print NR}'
统计,分组计数seq 20|grep -o .|sort|uniq -c

seq 20|grep -o .|awk '{S[$0]++} END{for(k in S){print S[k],k}}'

实践

实践1:找最后10条异常日志

tac app.log |sed '/^\S/a\\'|awk -v RS= '/ERROR/ && ++n<=10{print;if(n>=10){exit}}'|tac

实践2:统计java代码中高频词

# uniq实现版
$ time find -name '*.java'|xargs sed -E 's/\b[A-Z]/\l&/g; s/[A-Z]/_\l&/g'|grep -w -oE '\w+'|pv -l|sort|uniq -c|sort -nrk1|head -n5
2.16M 0:00:03 [ 584k/s] [         <=>          ]
  56442 public
  46228 import
  45940 string
  42473 order
  41077 return

real    0m4.434s
user    0m4.719s
sys     0m2.911s

# awk实现版,比uniq实现快,理论上内存占用要高于uniq
$ time find -name '*.java'|xargs sed -E 's/\b[A-Z]/\l&/g; s/[A-Z]/_\l&/g'|grep -w -oE '\w+'|pv -l|awk '{S[$0]++}END{for(k in S){print S[k],k}}'|sort -nrk1|head -n5
2.16M 0:00:02 [1.03M/s] [         <=>          ]
56442 public
46228 import
45940 string
42473 order
41077 return

real    0m2.366s
user    0m2.324s
sys     0m3.050s

总结

熟练掌握文本处理命令十分重要,原因是命令的输入数据,以及命令执行后的输出结果,基本都是纯文本的,因此如果想比较轻松地使用Linux命令解决工作需求,就必须熟练掌握这些常见的文本处理命令。

另外,之前也分享过Linux中的常见文本命令使用技巧,更偏实际应用场景,感兴趣可前往查看:
原来awk真是神器啊
Linux文本命令技巧(上)
Linux文本命令技巧(下)
使用Linux命令快速查看某一行

知识延伸指引

  1. 下面这个斐波那契数列,是怎么计算出来的?
(echo 0;echo 1) > num.txt
tail -n+0 -f num.txt|awk 'NR>1{print pre+$0;fflush()}{pre=$0}' >> num.txt
  1. 尝试用-p调试下面两个xargs程序,理解输出不同的原因?
$ seq 6|xargs -n2|xargs -L1 printf "<%s> " 
<1> <2> <3> <4> <5> <6>

$ seq 6|xargs -n2|xargs -d'\n' -L1 printf "<%s> " 
<1 2> <3 4> <5 6>
  1. grep为啥默认不支持\dBRE、ERE、PCRE又是啥?

答案参考:这grep咋还不支持\d呢(BRE,ERE,PCRE)

  1. 为啥这条命令,sed不加-u,输出不了数据?为啥加了stdbuf -oL后又能输出数据了?
while sleep 1;do echo $((i++)); done|sed 's/.\+/&+1/g'|bc
while sleep 1;do echo $((i++)); done|sed -u 's/.\+/&+1/g'|bc
while sleep 1;do echo $((i++)); done|stdbuf -oL sed 's/.\+/&+1/g'|bc

答案参考:shell管道咋堵住了?
5. 下面这些文本命令分别是做什么用的?

tr cut paste comm join

答案参考:Linux文本命令技巧(上)

往期内容

Linux命令拾遗-入门篇
原来awk真是神器啊
Linux文本命令技巧(上)
Linux文本命令技巧(下)
字符编码解惑


长按关注【打码日记】


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存