摘自 Linux Shell 脚本攻略 第四章 让文本飞
使用grep在文件中搜索文本
在stdin中搜索匹配特定模式的文本行
$ echo -e "this is a word\nnext line" | grep word
this is a word
在文件中搜索匹配特定模式的文本行
$ grep bin coco.sh
#!/bin/bash
/USSR/bin/expect <<-OF &>/Devi/null
在多个文件中搜索匹配特定模式的文本行
$ grep "match_text" file1 file2 file3 ...
选项–color可以在输出行中着重标记出匹配到的模式。尽管该选项在命令行中的放置 位置没有强制要求,不过惯常作为第一个选项出现
$ grep --color=auto bin coco.sh
#!/bin/bash
/USSR/bin/expect <<-OF &>/Devi/null
grep命令默认使用基础正则表达式。这是先前描述的正则表达式的一个子集。选项-E可以 使grep使用扩展正则表达式
$ grep -E "bin" coco.sh
#!/bin/bash
/USSR/bin/expect <<-OF &>/Devi/null
选项-o可以只输出匹配到的文本
$ echo this is a line. |egrep -o "[a-z]+\."
line.
选项-v可以打印出不匹配match_pattern的所有行
$ echo -e "coco is handsome\nzues is handsome" |grep -v zues
coco is handsome
选项-c能够统计出匹配模式的文本行数
$ echo -e "1 2 3 4\nhello\n5 6" | egrep -c "[0-9]"
2
要统计文件中匹配项的数量,可以使用下面的技巧
$ echo -e "1 2 3 4\nhello\n5 6" | egrep -o "[0-9]" | wc -l
6
选项-n可以打印出匹配字符串所在行的行号
$ cat sample1.txt
gnu is not unix
linux is fun
bash is art
$ cat sample2.txt
planetlinux
$ cat sample1.txt |grep linux -n
2:linux is fun
如果涉及多个文件,该选项也会随输出结果打印出文件名
$ grep linux -n sample1.txt sample2.txt
sample1.txt:2:linux is fun
sample2.txt:1:planetlinux
选项-b可以打印出匹配出现在行中的偏移。配合选项-o可以打印出匹配所在的字符或 字节偏移
$ echo gnu is not unix | grep -b -o "not"
7:not
字节偏移(下标)从0开始计数
选项-l可以列出匹配模式所在的文件
$ grep -l linux sample1.txt sample2.txt coco.sh
sample1.txt
sample2.txt
递归搜索多个文件
$ grep 'bin' for_fun/ -R -n
for_fun/aa.json:81: 'Robin Zhu': {
for_fun/coco.py:15: 'Robin Zhu': {'issue_info': {'OPEN': 1}}, 'Guofeng Tang': {'issue_info': {'OPEN': 5}}, 'Zhiheng Cao': {
# 等价于下面这条命令
$ find for_fun/ -type f | xargs grep "bin"
for_fun/aa.json: 'Robin Zhu': {
for_fun/coco.py: 'Robin Zhu': {'issue_info': {'OPEN': 1}}, 'Guofeng Tang': {'issue_info': {'OPEN': 5}}, 'Zhiheng Cao': {
忽略模式中的大小写
$ echo hello world | grep -i "HELLO"
hello world
使用grep匹配多个模式
$ echo this is a line of text | grep -o -e "this" -e "line"
this
line
可以将多个模式定义在文件中。选项-f可以读取文件并使用其中的模式(一个模式一行)
$ cat pat_file
hello
cool
$ echo hello this is cool | grep -f pat_file
hello this is cool
在grep搜索中指定或排除文件
grep可以在搜索过程中使用通配符指定(include)或排除(exclude)某些文件
使用–include选项在目录中递归搜索所有的 .c和 .cpp文件
$ grep "main()" . -r --include=*.c --include=*.cpp
示例
在当前目录下递归查找 包含 bin 字符的 .sh 以及 .py 文件
$ grep --include=*.sh --include=*.py "bin" . -r
./for_fun/coco.py: 'Robin Zhu': {'issue_info': {'OPEN': 1}}, 'Guofeng Tang': {'issue_info': {'OPEN': 5}}, 'Zhiheng Cao': {
./coco.sh:#!/bin/bash
./coco.sh:/USSR/bin/expect <<-OF &>/Devi/null
./shift.sh:#!/bin/bash
./filestat.sh:# !/bin/bash
./debug.sh:#!/bin/bash
./cecho.sh:#!/bin/bash
./build1.sh:#!/bin/bash
./remove_duplicates.sh:# !/bin/bash
./a/remove_duplicates.sh:# !/bin/bash
在当前目录下递归查找 包含 bin 字符的 .sh 以及 .py 文件 但不包括 coco.sh
$ grep --include=*.sh --include=*.py --exclude=coco.sh "bin" . -r
./for_fun/coco.py: 'Robin Zhu': {'issue_info': {'OPEN': 1}}, 'Guofeng Tang': {'issue_info': {'OPEN': 5}}, 'Zhiheng Cao': {
./shift.sh:#!/bin/bash
./filestat.sh:# !/bin/bash
./debug.sh:#!/bin/bash
./cecho.sh:#!/bin/bash
./build1.sh:#!/bin/bash
./remove_duplicates.sh:# !/bin/bash
./a/remove_duplicates.sh:# !/bin/bash
在当前目录下递归查找 包含 bin 字符的 .sh 以及 .py 文件 但不包括 for_fun 文件夹
$ grep --include=*.sh --include=*.py --exclude-dir=for_fun/ "bin" . -r
./coco.sh:#!/bin/bash
./coco.sh:/USSR/bin/expect <<-OF &>/Devi/null
./shift.sh:#!/bin/bash
./filestat.sh:# !/bin/bash
./debug.sh:#!/bin/bash
./cecho.sh:#!/bin/bash
./build1.sh:#!/bin/bash
./remove_duplicates.sh:# !/bin/bash
./a/remove_duplicates.sh:# !/bin/bash
使用0值字节后缀的xargs与grep
xargs命令可以为其他命令提供命令行参数列表。当文件名作为命令行参数时,建议用0值 字节作为文件名终结符,而非空格。因为一些文件名中会包含空格字符,一旦它被误解为终结符, 那么单个文件名就会被视为两个(例如,New file.txt被解析成New和file.txt两个文件名)。这个问 题可以利用0值字节后缀来避免。我们使用xargs从命令(如grep和find)中接收stdin文本。 这些命令可以生成带有0值字节后缀的输出。为了指明输入中的文件名是以0值字节作为终结,需 要在xargs中使用选项-0
$ echo "test" > file1
$ echo "cool" > file2
$ echo "test" > file3
$ grep "test" file* -lZ | xargs -0 rm
$ ls file*
file2 filestat.sh
选项-l告诉grep只输出有匹配出现的文件名。选项-Z使得grep使用0值字节(\0)作为文 件名的终结符。这两个选项通常都是配合使用的。xargs的-0选项会使用0值字节作为输入的分 隔符
grep的静默输出 -q
$ cat silent_grep.sh
#!/bin/bash
# 文件名: silent_grep.sh
# 用途: 测试文件是否包含特定的文本内容
if [ $# -ne 2 ]; then
echo "Usage: $0 match_text filename"
exit 1
fi
match_text=$1
filename=$2
grep -q "$match_text" $filename
if [ $? -eq 0 ]; then
echo "The text exists in the file"
else
echo "Text does not exist in the file"
fi
$ ./silent_grep.sh bin coco.sh
The text exists in the file
打印出匹配文本之前或之后的行
- 选项-A可以打印匹配结果之后的行
$ seq 10 | grep 5 -A 3
5
6
7
8
- 选项-B可以打印匹配结果之前的行
$ seq 10 | grep 5 -B 3
2
3
4
5
- 选项-A和-B可以结合使用,或者也可以使用选项-C,它可以分别打印出匹配结果之前及之 后的n行
$ seq 10 | grep 5 -C 3
2
3
4
5
6
7
8
- 如果有多个匹配,那么使用–作为各部分之间的分隔
$ echo -e "a\nb\nc\na\nb\nc" | grep a -A 1
a
b
--
a
b
使用sed替换文本
sed是stream editor(流编辑器)的缩写。它最常见的用法是进行文本替换。这则攻略中包括
了大量sed命令的常见用法
sed可以使用另一个字符串来替换匹配模式。模式可以是简单的字符串或正则表达式
$ sed 's/pattern/replace_string/' file
选项-i会使得sed用修改后的数据替换原始文件
$ sed -i 's/text/replace/' file
g标记可以使sed执行全局替换
$ sed 's/pattern/replace_string/g' file
- /#g标记可以使sed替换第N次出现的匹配
$ echo thisthisthisthis | sed 's/this/THIS/2g'
thisTHISTHISTHIS
$ echo thisthisthisthis | sed 's/this/THIS/3g'
thisthisTHISTHIS
$ echo thisthisthisthis | sed 's/this/THIS/4g'
thisthisthisTHIS
sed命令会将s之后的字符视为命令分隔符
将默认分隔符修改为:
sed 's:text:replace:g'
将默认分割符修改为
sed 's|text|replace|g'
如果作为分隔符的字符出现在模式中,必须使用\对其进行转义
sed 's|te\|xt|replace|g'
删除空行
$ sed '/^$/d' file
直接在文件中替换
如果将文件名传递给sed,它会将文件内容输出到stdout。要是我们想就地(in place)修
改文件内容,可以使用选项-i
$ sed 's/PATTERN/replacement/' -i filename
示例
$ cat sed_data.txt
11 abc 111 this 9 file contains 111 11 88 numbers 0000
$ sed -i 's/\b[0-9]\{3\}\b/NUMBER/g' sed_data.txt
$ cat sed_data.txt
11 abc NUMBER this 9 file contains NUMBER 11 88 numbers 0000
上面的单行命令只替换了所有的3位数字。正则表达式\b[0-9]{3}\b用于匹配3位数字。[0-9] 表示数字取值范围是从0到9。{3}表示匹配之前的数字3次。{3}中的\用于转义{和}。\b表示 单词边界
已匹配字符串标记(&)
$ echo this is an example | sed 's/\w\+/[&]/g'
[this] [is] [an] [example]
在这个例子中,正则表达式\w+匹配每一个单词,然后我们用[&]替换它。&对应于之前所匹配 到的单词
子串匹配标记(\1)
$ echo this is digit 7 in a number | sed 's/digit \([0-9]\)/\1/'
this is 7 in a number
这条命令将digit 7替换为7。(pattern)用于匹配子串,在本例中匹配到的子串是7。子模式 被放入使用反斜线转义过的()中。对于匹配到的第一个子串,其对应的标记是\1,匹配到的第 二个子串是\2,往后以此类推
$ echo seven EIGHT | sed 's/\([a-z]\+\) \([A-Z]\+\)/\2 \1/'
EIGHT seven
([a-z]+)匹配第一个单词,([A-Z]+)匹配第二个单词。\1和\2分别用来引用这两个单 词。这种引用形式叫作向后引用(back reference)。在替换部分,它们的次序被更改为\2 \1, 因此就呈现出了逆序的结果
组合多个表达式
可以利用管道组合多个sed命令,多个模式之间可以用分号分隔,或是使用选项-e PATTERN:
# 以下三个相等
$ sed 'expression' | sed 'expression'
$ sed 'expression; expression'
$ sed -e 'expression' -e 'expression'
$ echo abc | sed 's/a/A/' | sed 's/c/C/'
AbC
$ echo abc | sed 's/a/A/;s/c/C/'
AbC
$ echo abc | sed -e 's/a/A/' -e 's/c/C/'
AbC
引用
sed表达式通常用单引号来引用。不过也可以使用双引号。shell会在调用sed前先扩展双引
号中的内容。如果想在sed表达式中使用变量,双引号就能派上用场了
$ echo hello world | sed "s/$text/HELLO/"
HELLO world
使用awk进行高级文本处理
awk命令可以处理数据流。它支持关联数组、递归函数、条件语句等功能
awk ‘BEGIN{ print “start” } pattern { commands } END{ print “end” }’ file
awk脚本通常由3部分组成:BEGIN、END和带模式匹配选项的公共语句块(common statement
block)。这3个部分都是可选的,可以不用出现在脚本中
输出文件行数
$ awk 'BEGIN {i=0}{i++}END{print i}' a.txt
4
$ awk "BEGIN {i=0}{i++}END{print i}" a.txt
4
当使用不带参数的print时,它会打印出当前行
$ echo -e "line1\nline2" | awk 'BEGIN {print "Start" } { print } END { print "End" }'
Start
line1
line2
End
print能够接受参数。这些参数以逗号分隔,在打印参数时则以空格作为参数之间的分隔符。
在awk的print语句中,双引号被当作拼接操作符(concatenation operator)使用
$ echo | awk '{ var1="v1"; var2="v2"; var3="v3"; print var1,var2,var3;}'
v1 v2 v3
echo命令向标准输出写入一行,因此awk的 { } 语句块中的语句只被执行一次。如果awk的 输入中包含多行,那么 { } 语句块中的语句也就会被执行相应的次数
对输出进行拼接
$ echo | awk '{ var1="v1"; var2="v2"; var3="v3"; print var1 "-" var2 "-" var3;}'
v1-v2-v3
特殊变量
- NR:表示记录编号,当awk将行作为记录时,该变量相当于当前行号。
- NF:表示字段数量,在处理当前记录时,相当于字段数量。默认的字段分隔符是空格。 $0:该变量包含当前记录的文本内容。
- $1:该变量包含第一个字段的文本内容。
- $2:该变量包含第二个字段的文本内容。
$ echo -e "line1 f2 f3\nline2 f4 f5\nline3 f6 f7" | awk '{print "Line No:"NR",No of fileds:"NF, "$0="$0, "$1="$1,"$2="$2,"$3="$3}'
Line No:1,No of fileds:3 $0=line1 f2 f3 $1=line1 $2=f2 $3=f3
Line No:2,No of fileds:3 $0=line2 f4 f5 $1=line2 $2=f4 $3=f5
Line No:3,No of fileds:3 $0=line3 f6 f7 $1=line3 $2=f6 $3=f7
$ seq 5 | awk 'BEGIN{ sum=0;print "Summation:" }{print $1"+";sum+=$1}END{print "==";print sum}'
Summation:
1+
2+
3+
4+
5+
==
15
将外部变量值传递给awk
借助选项-v,我们可以将外部值(并非来自stdin)传递给awk
$ VAR=10000
$ echo | awk -v VARIABLE=$VAR '{print VARIABLE}'
10000
$ var1="Variable1" ; var2="Variable2"
$ echo | awk '{print v1,v2}' v1=$var1 v2=$var2
Variable1 Variable2
$ awk '{ print v1,v2 }' v1=$var1 v2=$var2 file2
Variable1 Variable2
用getline读取行
awk默认读取文件中的所有行。如果只想读取某一行,可以使用getline函数。它可以用于
在BEGIN语句块中读取文件的头部信息,然后在主语句块中处理余下的实际数据
该函数的语法为:getline var。变量var中包含了特定行。如果调用时不带参数,我们可
以用 $0、$1和$2访问文本行的内容
$ seq 5 | awk 'BEGIN { getline; print "Read ahead first line", $0 } { print $0 }'
Read ahead first line 1
2
3
4
5
使用过滤模式对awk处理的行进行过滤
# 行号小于5的行
$ awk '$NF < 4' coco.sh
#!/bin/bash
/USSR/bin/expect <<-OF &>/Devi/null
# 行号在1到5之间的行
$ awk 'NR==1,NR==4' coco.sh
#!/bin/bash
/USSR/bin/expect <<-OF &>/Devi/null
# 包含模式为bin的行(可以用正则表达式来指定模式)
$ awk '/bin/' coco.sh
#!/bin/bash
/USSR/bin/expect <<-OF &>/Devi/null
# 不包含模式为linux的行
$ awk '!/bin/' coco.sh
spawn ssh amlogic@10.18.19.233
expect {
"yes/no" { send "yes\r";exp_continue }
"password:" { send "Linux2021\r" }
}
expect of
OF
设置字段分隔符
默认的字段分隔符是空格。我们也可以用选项-F指定不同的分隔符
$ awk -F: '{ print $NF }' /etc/passwd |head
/bin/bash
/usr/sbin/nologin
/usr/sbin/nologin
/usr/sbin/nologin
/bin/sync
/usr/sbin/nologin
/usr/sbin/nologin
/usr/sbin/nologin
/usr/sbin/nologin
/usr/sbin/nologin
# 等同于上面那条命令
$ awk 'BEGIN { FS=":" } { print $NF }' /etc/passwd
从awk中读取命令输出
awk可以调用命令并读取输出。把命令放入引号中,然后利用管道将命令输出传入getline
$ echo | awk '{"ls -l" |getline;print}'
total 391772
awk的关联数组
$ cat A.txt
apple
gold
iron
orange
silver
steel
$ awk '{a[$1]++}END{for (i in a){print i,a[i]}}' A.txt
steel 1
gold 1
silver 1
orange 1
iron 1
apple 1
在awk中使用循环
% awk -F : '{name[$1]=$5}END{for (i in name){print i name[i]}}' /etc/passwd |head -n 5
_appownerApplication Owner
rootSystem Administrator
_krb_changepwOpen Directory Kerberos Change Password Service
_appserverApplication Server
_applepayapplepay Account
awk内建的字符串处理函数
- length(string):返回字符串string的长度
- index(string, search_string):返回search_string在字符串string中出现的位置
- split(string, array, delimiter):以delimiter作为分隔符,分割字符串string,将生成的字符串存入数组array
- substr(string, start-position, end-position):返回字符串string中以start-position和end-position作为起止位置的子串
- sub(regex, replacement_str, string):将正则表达式regex匹配到的第一处内容替换成replacment_str
- gsub(regex, replacement_str, string):和sub()类似。不过该函数会替换正则表达式regex匹配到的所有内容
- match(regex, string):检查正则表达式regex是否能够在字符串string中找到匹配。如果能够找到,返回非0值;否则,返回0。match()有两个相关的特殊变量,分别是RSTART和RLENGTH。变量RSTART包含了匹配内容的起始位置,而变量RLENGTH包含了匹配内容的长度