0. 前言

在Linux中,一切皆文件,所以在Linux中避免不了和文件打交道,Linux有不少和文本处理相关的工具,其中最有名的当为三剑客:grep、awk、sed。三个工具配合正则表达式,基本上可以解决处理文本的绝大多数问题。

1. gerp

如果你忘记了自己的钥匙在哪里,就得自己去找;如果你忘记了文件中的内容,那么 grep 命令可以帮你查找。
grep 命令多配合管道符 | 以及正则来查找匹配文本。

  • 在 stdin 中搜索匹配特定模式的文本行

    1. $ echo -e 'this is a word\nnext line' | grep 'word'
    2. this is a word
  • 在文件中搜索匹配特定模式的文本行

    1. $ echo "This is the first line\nsecond line" > test.txt
    2. $ grep first test.txt # grep pattern filename
    3. This is the first line
  • 在多个文件中搜索匹配特定模式的文本行

    1. grep pattern file1 file2 file3 ...
  • 使用 --color=auto 着重输出匹配到的特定模式的文本

    1. $ grep --color=auto first test.txt
    2. This is the first line # 这里 first 应是带颜色的,代码块里无法上色
  • 使用 -o 选项只输出匹配到的文本

    1. # 使用 egrep 可以默认使用扩展正则表达式的命令 == grep -E
    2. $ echo "This is the first line\nsecond-line." | egrep -o "^T[a-z]+"
    3. This
    4. $ echo "This is the first line\nsecond-line." | grep -E -o "^T[a-z]+"
    5. This
  • 使用 -v 可以打印出不匹配的文本行,也就是翻转匹配结果

    1. $ "This is the first line\nsecond-line.\nthird line" | egrep -v "^T"
    2. second-line.
    3. third line
  • 使用 -c 可以统计出匹配模式的文本行数

    1. $ "This is the first line\nsecond-line.\nthird line" | egrep -v -c "^T"
    2. 2 # 这里配合了 -v 统计了匹配模式之外的文本行数

    这里值得注意的是 -c 只是统计了匹配的文本行数,和匹配到的次数并没有关系

    1. $ echo '1 2 3 4\nhello\n5 6' | grep -c '[0-9]'
    2. 2 # 匹配到了两行
    3. $ echo '1 2 3 4\nhello\n5 6' | grep -o '[0-9]' # 但是有6个数字被匹配到了
    4. 1
    5. 2
    6. 3
    7. 4
    8. 5
    9. 6
    10. # 如果想统计匹配到的次数,可以使用 -o 再结合 wc -l 命令
    11. $ echo '1 2 3 4\nhello\n5 6' | grep -o '[0-9]' | wc -l
    12. 6
  • 使用 -n 打印出匹配到的字符所在的文本行行号

    1. $ echo '1 2 3 4\nhello\n5 6' | grep -n '4'
    2. 1:1 2 3 4
    3. $ echo '1 2 3 4\nhello\n5 6' | grep -n '6'
    4. 3:5 6
  • 使用 -l 可以列出匹配模式所在的文件

    1. $ grep -l unix test.txt test1.txt
    2. test1.txt

    -L 命令得出与 -l 相反的结果

  • 使用 -i 忽略大小写

    1. $ echo HELLO world | grep -i -o hello
    2. HELLO
  • 使用 -R | -r 进行递归搜索

    1. $ grep test . -R -n # . 表示当前目录,匹配到了三个结果,分别打印出了所在文件和所在行
    2. ./run.py:24: pic_name = 'test.png'
    3. ./compare_pic.py:47: print('testing pass'.center(30, '-'))
    4. ./compare_pic.py:49: print('testing fail'.center(30, '-'))
  • 使用 -e 可以匹配多个指定模式

    1. $ echo this is the first line | grep -o -e this -e line
    2. this
    3. line
  • 使用 -f ,可以将多个匹配模式定义在文件中并读取使用其中的模式(一个模式一行)

    1. $ echo '^this\nhello' > pattern.txt
    2. $ echo 'this is the first line\nworld hello\nthird line' | grep -f pattern.txt
    3. this is the first line
    4. world hello
  • 使用 --include 配合通配符指定匹配的文件

    1. $ grep 'main' . -r --include *.py
    2. ./compare_pic.py:if __name__ == '__main__':

    与之相反的命令是 --exclude ,匹配这些文件之外的文件

    1. grep 'main' . -r --exclude README

    使用 -exclude-dir 可以排除目录

    1. $ grep 'main' . -r -exclude-dir imgs
  • 打印匹配到结果之后和之前的行,使用 -A | -B | -C

    1. $ seq 10 | grep 2 -A 3
    2. 2
    3. 3
    4. 4
    5. 5
    6. $ seq 10 | grep 5 -B 2
    7. 3
    8. 4
    9. 5
    10. $ seq 10 | grep 5 -C 2
    11. 3
    12. 4
    13. 5
    14. 6
    15. 7

    2. awk

    awk 命令可以处理数据流,它支持关联数组、条件语句等等功能

基本结构如下
awk 'BEGIN{ print "start" } pattern { commands } END{ print "end" } file这一串命令由三部分组成

  • BEGIN
  • END
  • 带模式匹配选项的公共语句块

这三部分都是可选的,不是必需
awk 以逐行的形式处理文件,BEGIN 之后的命令会先于公共语句块执行。对于匹配 PATTERN 的行,awk 会对其执行 PATTERN 之后的命令。最后,在处理完整个文件之后,awk 会执行 END 之后的命令。

  • 打印文件的行数
    1. $ awk "BEGIN { i=0 } { i++ } END { print i }" test.txt
    2. 2
    解读这条命令
  1. BEGIN 里面的语句定义了变量 i=0
  2. 每一行 执行i++
  3. END 打印变量 i

如果 PATTERN 没有提供,那么将会打印读取到的每一行文本,当 print 没有带参数的时候,默认打印当前行。

  1. $ echo -e 'line1\nline2' | awk 'BEGIN { print "start"} { print } END { print "end" }'
  2. start
  3. line1
  4. line2
  5. end

awk 中有一些特殊变量

  • NR:表示记录编号,当 awk 将行作为记录时,该变量相当于当前行号
  • NF:表示字段数量,在处理当前记录时,相当于字段数量。默认的字段分隔符是空格
  • $0:该变量包含当前记录的文本内容
  • $1:该变量包含第一个字段的文本内容
  • $2:该变量包含第二个字段的文本内容

🌰

  1. $ echo 'line1 named nancy\nline2 named roy\nline3 named lisa' | \
  2. awk '{ print "Line no:"NR",num of fields:"NF, "$0="$0, "$2="$2, "$3="$3 }'
  3. Line no:1,num of fields:3 $0=line1 named nancy $2=named $3=nancy
  4. Line no:2,num of fields:3 $0=line2 named roy $2=named $3=roy
  5. Line no:3,num of fields:3 $0=line3 named lisa $2=named $3=lisa

注意里面的变量名不要用 “” 包裹上哦
我们可以使用 $NF 来代表最后一个字段,倒数第二个字段用 $(NF-1) 表示,以此类推

  1. $ echo 'line1 named nancy\nline2 named roy\nline3 named lisa' | awk '{ print $NF}'
  2. nancy
  3. roy
  4. lisa
  • 传递外部变量给 awk

我们借助 -v 参数,将外部变量传递给 awk

  1. $ NAME='nancy'
  2. $ echo | awk -v MYNAME=$NAME '{ print MYNAME }'
  3. nancy

还有更为灵活的方式

  1. $ NAME='nancy' ; AGE=20
  2. $ echo | awk '{ print name,age }' name=$NAME age=$AGE
  3. nancy 20

变量以键值对的形式,使用空格进行分隔,作为 awk 的命令行参数紧跟在语句块结束之后

  • 对文本内容进行过滤

    1. $ awk 'NR < 5' # 行号小于5的行
    2. $ awk 'NR==1,NR==4' # 行号在1-5之间的行
  • 设置字段分隔符

NF 默认使用空格进行分割,但有时候文本是以特定的分隔符进行分割的,所以需要指定匹配的分隔符
-F 参数指定分隔符

  1. $ echo 'a----b----c----d' | awk -F"----" '{print $1 $2 $3 $4}'
  2. abcd

我们也可以在 BEGIN 语句块中使用 FS="" 来设置分隔符

  1. $ echo 'a----b----c----d' | awk 'BEGIN { FS="----" } { print NF}'
  2. 4
  • 在 awk 中关联数组并使用循环

    1. $ seq 5 | awk '{no[$1]=$+1} END { for (i in no) { print i,no[i]}}'
    2. 2 2
    3. 3 3
    4. 4 4
    5. 5 5
    6. 1 1
  • 内建字符串处理函数

    • length(string):返回字符串长度
    • index(string, search_str):返回search_str在字符串string中的索引位置
    • split(string, array, delimiter):以delimiter作为分隔符,分割字符串string,将生成的字符串组存入数组array中
    • substr(string, start, end):返回字符串string中 start起始位置和end结束为止的子字符串
    • sub(regex, replace_str, string):匹配到的第一处内容替换为replace_str
    • gsub(regex, replace_str, string):替换匹配到的所有

3. sed

sed 是 stream editor 的缩写,字面意思上看就是 流编辑器。这个工具常用来做文本的替换。

举一个最简单的实例

  1. $ echo 'hello,world' | sed 's/hello/hi/' # 将 hello 替换为 hi
  2. hi,world

sed 基本语法: sed s/pattren/replace_str/ filename
sed 可以读取文件中的文本,也可以从 stdin 中读出输入
sed 语法和 vim | vi 中的替换文本的命令十分类似,默认只打印出被替换的文本

  • 使用 -i 修改替换原始文件

    1. $ echo 'hello,world' > demo.txt
    2. $ sed -i 's/hello/hi/' demo.txt
  • 替换所有

上面只是替换了第一次匹配到的文本,下面替换所有符合匹配模式的文本

  1. $ echo 'this is a demo' | sed 's/is/IS/g'
  2. thIS IS a demo

g 标记可以使sed执行全局替换
#g 标记可以使sed执行第n次出现的匹配进行替换

  • 分隔符

sed 命令会将 s 之后的字符看做为命令分隔符,我们可以自行修改分隔符
sef 's:text:replace_str':g
需要注意的是:如果分隔符作为字符出现在匹配模式中,需要使用 \ 进行转义

  • 使用多个表达式进行替换

这里利用管道符 | 对多个匹配模式进行文本替换

  1. $ echo abc | sed 's/a/A/' | sed 's/c/C/'
  2. AbC