这是Linux命令拾遗系列的第二篇,本篇主要介绍Linux中与文本处理相关的命令,如xargs、grep、sed、awk等。
# 打印输入内容到标准输出
$ 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就不会有这种烦恼了。
# 显示前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
# 统计行数,单词数,字节数,之所以有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
根据正则搜索内容,它会一行一行的拿出数据中的内容,然后看这一行是否匹配正则,匹配则输出这一行的内容。
# 使用正则搜索,默认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 .
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
作用:将标准输入流中的数据,转换为命令参数,并执行命令。
引入这个命令的原因是,有些命令不支持处理标准输入的数据,而只支持命令参数,如杀死进程的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 $pid; done
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
一般用于替换修改文本数据,有流文本编辑器(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还可以是如下的形式:
# 第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
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
除了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
1
2
3
# 在最后一行之后追加一行,其中$表示最后一行
$ seq 3|sed '$ a\id'
1
2
3
id
# 将第一行整行直接修改为id
$ seq 3|sed '1 c\id'
id
2
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
运行过程:
保留空间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里面掺杂了操作保留空间的命令后,执行过程就变得非常烧脑了,你的大脑需要飞速的运转才行。
$ 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是一个强大的文本处理工具,本质上可以看做是一门脚本语言了,可以用来对文本进行过滤、替换等操作,还能实现简单的统计以及类似SQL的join功能等。
awk基本语法如下:
awk 'BEGIN{
//your code
}
pattern1{
//your code
}
pattern2{
//your code
}
END{
//your code
}'
如下所示,分别求奇数行与偶数行的和:
$ seq 1 5
1
2
3
4
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
这个程序还可以如下这样写:
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
可以看到:
如下,看看用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
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代码稍微有点长了,但逻辑还是很清晰的。
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地址转数字
$ 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.
$ 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了,因此,这哥仨也常被人合称为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 -n10 | seq 20 | grep -m10 '.*' | seq 20 | sed -n '1,10p' | seq 20 | awk 'NR<=10' |
过滤出包含1的行 | seq 20 | grep 1 | seq 20 | sed -n '/1/p' | seq 20 | awk '/1/' | |
过滤出不包含1的行 | seq 20 | grep -v 1 | seq 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 11 | seq 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 -l | seq 20 | grep . -c | seq 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}}' |
tac app.log |sed '/^\S/a\\'|awk -v RS= '/ERROR/ && ++n<=10{print;if(n>=10){exit}}'|tac
# 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中的常见文本命令使用技巧,更偏实际应用场景,感兴趣可前往查看:
(echo 0;echo 1) > num.txt
tail -n+0 -f num.txt|awk 'NR>1{print pre+$0;fflush()}{pre=$0}' >> num.txt
$ 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>
答案参考:这grep咋还不支持\d呢(BRE,ERE,PCRE)
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管道咋堵住了?
tr cut paste comm join
答案参考:Linux文本命令技巧(上)