文本三剑客之 Grep

grep -- print lines matching a pattern

Created on 2017-10-07 17:07

grep 的来源

最初版本的 Unix 所使用的文本编辑器是ed(没错,那个Vi的祖先). 在ed 中, 有一个命令可以用来在文件中搜索所有包含指定模式的行,然后在终端打印出来(当然,在 那个上古时代,是没有现在的终端的,输出的结果是打印在纸张的). 而对应的 ed 语法是

g/re/p

g 代表 "global, 全局", re代表 "Regular Expression(正则表达式)", p 即是 "print", 即把结果打印出来。这个就是 grep 名字的来源,演变到今天, grep 已经 变成了 Unix 平台上非常重要的文本模式匹配工具了。

grep 简介

现代的 grep 功能是基于 ed 的搜索功能进行了扩展: grep 会对输入的文件进行搜 索,检测是否包含特定模式的行,如果找到匹配到特定模式的行,在默认情况下, grep 就会把匹配的行复制到标准输出。例如,可以使用 grep 搜索5 个长文件,查找所有包含字 符串 "Samray" 的行。或者是使用 cat 命令读取文件,然后使用 sort 命令对数据进 行排序,如何通过管道把数据传送给 grep, 选取所有以 what 开头的行

正则表达式

如果说强大的文本处理能力是 Unix命令行的皇冠的话,那么正则表达式就是皇冠上的那颗 璀璨明珠。除了搜索特定的字符串之外,grep 还可以使用正则表达式作为搜索模式,这时 候,grep 就变成了更加强大的工具了

调用 grep

初探 grep

grep 的语法是:

grep [options] pattern input_file

grep 的命令语法和其他的命令行工具相同,只是具体的选项不一样,其中 pattern 是要搜索的模式 , input_file 是输入的文件的名字,options 是指 grep 命令提供的诸多选项,而使用 [] 包裹 options 的意思是指 options 是可选的。

简单的例子

现在举一个简单的例子,在 Unix 系统中,每个用户的标识的基本信息都是保存在 /etc/password 文件中的,每个用户标识对应一行信息,如果想要找出某个特定用户的信 息,可以使用 grep 搜索这个文件:

grep samray /etc/passwd

一如典型的 Unix 命令, grep 只有在找到指定模式匹配的行的时候才会,不然就不会告 诉用户任何信息。

如果搜索的 pattern 包含标点符号或者是特殊字符的时候,应该使用单引号'引用它们, 这样 shell 才能够正确地解释命令,例如:

grep ':' /etc/passwd

通过管道使用 grep

如果想通过管道使用 grep, 就只需将输入的文件去掉,因为输入数据已经从文件输入变 成了从管道获取。例如搜索用户 samray 的登录记录:

last | grep samray

grep 选项用法详解

虽说 options 是可选的,但是不加 options 的选项只能算是基础用法,要想进一步了 解 grep 的高级用法,免不了要去了解 grep 的选项。我会使用《双城记》(a tale of two cities)的开头来展示选项的用法:

It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, it was the epoch of belief, it was the epoch of incredulity, it was the season of Light, it was the season of Darkness, it was the spring of hope, it was the winter of despair, we had everything before us, we had nothing before us, we were all going direct to Heaven, we were all going direct the other way

grep 版本: grep (GNU grep) 2.27

grep 选项分类

按照 grep 官方文档的说法,grep的选项可以分成以下的若干类:

关于 grep 的程序信息

这部分的选项是用来输出 grep 的信息的

输出使用说明

选项

-- help

说明

简短地展示 grep 的用法和命令行选项。我觉得这个是非常重要的选项, Unix 的命令行 一般都会有这个选项来展示该命令行的用法,如果碰到一个新的命令不知道怎么去使用,使 用这个选项来调用命令行是最佳的做法之一(另外一个就是使用 man命令). help 选项 总结了命令行的用法,只不过可能因为帮助信息都是英文的,所以有的同学会不太愿意查阅

用法

用法很简单

grep --help

然后就会输出相应的帮助信息

输出版本信息

选项
  • -V
  • --version
说明

输出 grep 的版本信息. 用处不大,一般是用来确定该版本是否有 bug

用法

这个用法就不用细说

grep --version

控制 grep 的输出

这部分的选项是用来控制 grep 的输出结果的

输出匹配行数

选项
  • -c
  • --count
说明

使用该选项不会输出匹配模式的行,取而代之的是输出匹配的行数。如果配合 -v(--invert-match) 选项使用,那么输出的就是不匹配的行数

用法

在《双城记》中搜索 It 的匹配个数

grep -c It a_tale_of_two_cities

结果是 1

在《双城记》中搜索 It 的不匹配个数

grep -c It a_tale_of_two_cities

结果是 5(总行数是6)

高亮匹配结果

选项
  • --color[=WHEN]
  • --colour[=WHEN]
说明

在终端高亮输出匹配的结果,而高亮的颜色定义在环境变量 GREP_COLORS, 如果环境变量 为空,那么默认值就是ms=01;31:mc=01;31:sl=:cx=:fn=35:ln=32:bn=32:se=36, 即使用 红色的 Bold 字体标记匹配的文本,用品红色标记匹配到的文件的文件名,用绿色标记行 号等等。而选项后接的 WHEN 值分别是 never, always, auto. 默认值是 auto

用法

在《双城记》中匹配 It, 无匹配高亮显示:

grep It --color='never' a_tale_of_two_cities

结果是:

It was the best of times, it was the worst of times, it was the age of wisdom

但是 It 是没有颜色高亮的

输出没有匹配到的文件的文件名

选项
  • -L
  • --files-without-match
说明

输出没有匹配到模式的文件的文件名,如果匹配到,那就什么都不输出

用法

除了《双城记》之外,现在增加一个空的文件 foo, 然后在 a_tale_of_two_cities foo, foo 两个文件匹配 It, 输出没有匹配到 It 的文件的文件名

grep It a_tale_of_two_cities foo -L

结果是: foo. 因为 foo 是空文件,没有匹配到 It

输出匹配到的文件的文件名

选项
  • -l
  • --files-with-match
说明

这个选项刚好和 -L 选项相反,是输出匹配到模式的文件的文件名,如果没有匹配到,那 就什么都不输出

用法

a_tale_of_two_citie,foo 两个文件匹配 It, 输出匹配到 It 的文件的文件名

grep It a_tale_of_two_cities foo -l

结果是 a_tale_of_two_cities.

最大匹配数

选项
  • -m num --max-count=num
说明

如果 grep 匹配到的模式的行数已经超过 num 行,那么 grep 就会停止读取文件。例如当num=1:

while grep -m 1 PATTERN
do
echo xxxx
done < FILE

那么在匹配一行之后, grep 就不会再去匹配。上面我们提到,grep 即可以从输入文件读取数据,也可以 从管道读取数据,但是 -m 这个选项可能对管道输入数据不适用,官方文档的例子:

# This probably will not work.
cat FILE |
while grep -m 1 PATTERN
do
echo xxxx
done

所以这个选项在管道操作的时候就尽量不要使用。至于具体原因可以参考 官方文档

用法

在《双城记》中不区分大小写,匹配三行的 It:

grep It a_tale_of_two_cities -i -m 3

结果是:

It was the best of times, it was the worst of times, it was the age of wisdom,
it was the age of foolishness, it was the epoch of belief, it was the epoch of
incredulity, it was the season of Light, it was the season of Darkness, it was

如果不对匹配的最大行数作限制的话:

grep It a_tale_of_two_cities -i 

那么结果会匹配到4行:

It was the best of times, it was the worst of times, it was the age of wisdom,
it was the age of foolishness, it was the epoch of belief, it was the epoch of
incredulity, it was the season of Light, it was the season of Darkness, it was
the spring of hope, it was the winter of despair, we had everything before us,

每行只输出匹配的部分

选项
  • -o
  • --only-matching
说明

每行只输出匹配(非空)的部分,每行一个匹配项。输出行使用和输入行相同的分隔符,如果 使用 -z (--null-data)选项,那么分隔符使用空字节

用法

在《双城记》中不区分大小写匹配 It, 只输出匹配项:

grep It a_tale_of_two_cities -io

结果是:

It
it
it
it
it
it
it
it
it
it
it

如果使用上 -z(--null-data) 选项的话:

grep It a_tale_of_two_cities -ioz

那么结果是:

Ititititititititititit%

静默输出结果

选项
  • -q
  • --quiet
  • --silent
说明

静默输出结果,即使找到匹配项,也不输出结果,只返回 0 状态,然后退出。如果出现 错误即打印错误信息

用法

在《双城记》中匹配 It, 并静默输出结果:

grep It a_tale_of_two_cities -q

结果是:无输出

静默错误提示

选项
  • -s
  • --no-messages
说明

不输出关于文件不存在和文件不可读的错误信息

用法

/tmp 目录匹配 'It':

grep It /tmp

结果是:

grep: /tmp: Is a directory

/tmp 目录匹配 'It', 并静默错误提示:

grep It /tmp -s

结果是: 无输出

grep 的行输出的前缀控制

这部分的选项是用来控制 grep 的输出行的前缀

字节偏移量

选项
  • -b
  • --byte-offset
说明

输出每个匹配项的字节位移值,从0 开始,如果一行有多个匹配值,那么输出第一个匹配值。 如果使用了 -o(only-matching) 选项,那么输出每一个匹配项的位移值

用法

在《双城记》中不分大小写匹配 It, 并输出每行第一个匹配值的位移值:

grep It a_tale_of_two_cities -bi

结果是:

0:It was the best of times, it was the worst of times, it was the age of wisdom,
79:it was the age of foolishness, it was the epoch of belief, it was the epoch of
158:incredulity, it was the season of Light, it was the season of Darkness, it was
237:the spring of hope, it was the winter of despair, we had everything before us,

第一行第一个 It 位移值是0, 下一行的第一个 it 的位移值是 79, 如此类推

在《双城记》中不分大小写匹配 It, 并输出每一个匹配值的位移值:

grep It a_tale_of_two_cities -bio

结果是:

0:It
26:it
53:it
79:it
110:it
138:it
166:it
171:it
199:it
230:it
257:it

在输出匹配项前加上文件名

选项
  • -H
  • --with-filenmae
说明

在每一行的匹配项前加上文件名。如果匹配的文件数目超过两个,该选项会默认添加

用法

在《双城记》中匹配 It, 并输出每行匹配项对应的文件名:

grep It a_tale_of_two_cities -H

结果是:

a_tale_of_two_cities:It was the best of times, it was the worst of times, it was the age of wisdom,

在输出匹配项前去掉文件名

选项
  • -h
  • --no-filenmae
说明

在每一行的匹配项前去掉文件名。如果匹配的文件只有1个,该选项会默认添加

用法

在《双城记》中匹配 It, 并输出每行匹配项对应的文件名:

grep It a_tale_of_two_cities -h

结果是:

It was the best of times, it was the worst of times, it was the age of wisdom,

在输出匹配项添加标签

选项

--label=LABEL

说明

在使用管道读取数据的时候,标识匹配项来自文件 LABEL. 这个选项在匹配压缩包的文件的 时候特别有用,特别是配合 egrep 使用, 即:

gzip -cd foo.gz | grep --label=foo -H something

输出匹配项对应的行号

选项
  • -n
  • --line-number
说明

在输出匹配项的时候,同时输出匹配项对应的行号

用法

在《双城记》中匹配 It, 并输出匹配项对应的行号:

grep It a_tale_of_two_cities -n

grep 的匹配控制

正则表达式匹配

选项
  • -e pattern
  • --regexp=pattern
说明

使用正则表达式作匹配模式,如果这个选项多次使用或者是使用上了 -f(--file) 选项, 那就搜索所有的给定的正则表达式

用法

在《双城记》中匹配 以it 开头的行并输出:

grep -e "^it" a_tale_of_two_cities

结果是

it was the age of foolishness, it was the epoch of belief, it was the epoch of

使用文件作匹配项

选项
  • -f file
  • --file=file
说明

当需要匹配的模式太多的时候,就将模式写入到文件,每行一个匹配模式,使用 -f 选项 指定模式文件

用法

现在我想要把《双城记》中匹配 worst 或者 foolishness 的行输出出来,对应的匹配模式在 pat 文件中: pat:

worst
foolishness
grep -f pat a_tale_of_two_cities

结果是:

It was the best of times, it was the worst of times, it was the age of wisdom,
it was the age of foolishness, it was the epoch of belief, it was the epoch of

忽略大小写匹配

选项
  • -i
  • -y
  • --gnore-case
说明

进行匹配模式的时候忽略大小写

用法

在《双城记》中忽略大小写匹配 it:

grep -i it a_tale_of_two_cities

结果是:

It was the best of times, it was the worst of times, it was the age of wisdom,
it was the age of foolishness, it was the epoch of belief, it was the epoch of
incredulity, it was the season of Light, it was the season of Darkness, it was
the spring of hope, it was the winter of despair, we had everything before us,

输出不匹配模式的行

选项
  • -v
  • --invert-match
说明

grep的基本用法相反,输出不匹配模式的行

用法

在《双城记》中匹配不包含 it的行并输出:

grep -v it a_tale_of_two_cities

结果是:

we had nothing before us, we were all going direct to Heaven, we were all going
direct the other way

匹配整个单词

选项
  • -w
  • word-regexp
说明

选出那些匹配整个单词的行。当使用 grep 匹配 we 的时候,包含werewe的行都 会选中,如果你只想要包含 we的行,你就需要指定 -w 选项

用法

在《双城记》中匹配包含单词 we 的行并输出:

grep we a_tale_of_two_cities -w

结果是:

the spring of hope, it was the winter of despair, we had everything before us,
we had nothing before us, we were all going direct to Heaven, we were all going

需要注意的是 were 是没有颜色高亮的。

匹配整行

选项
  • -x
  • --line-regexp
说明

-w 选项类似,选出匹配整行的匹配项,这个就好像使用正则表达式时候,由 ^$ 包裹着的内容一样

用法

在《双城记》中匹配 It was the best of times, it was the worst of times, it was the age of wisdom, 的行并输出:

grep -x "It was the best of times, it was the worst of times, it was the age of wisdom," a_tale_of_two_cities

结果是:

It was the best of times, it was the worst of times, it was the age of wisdom,

需要注意的是,整行的内容都是被高亮的,这说明是匹配了整行的

递归匹配

选项
  • -r
  • --recursive
说明

当在一个目录匹配模式的时候,会报错 grep: .: Is a directory, 所以,如果想要整个 目录进行模式匹配,就可以使用 -r选项,这个是我非常常用的选项之一

用法

在当前目录递归匹配 It 并输出结果:

grep it . -r

结果是:

./pat:worst
./a_tale_of_two_cities:It was the best of times, it was the worst of times, it was the age of wisdom,

grep 应用场景

关于 grep 的选项用法我就已经说了很多了,现在就来说一下 grep 具体的用法,例子 来源我自己的日常经验

日志搜索

对于一个健壮的系统而言,无论是 debug 还是调优,日志绝对是不可或缺的。现在就来分 享一下我自己一点 debug 心得。以一个线上网站为例,点击某个操作返回结果是 500, 服 务器异常, 但是不知道怎么定位到具体引发异常的函数,只要日志打得好,报异常捕捉到, 就可以使用 grep 定位异常,例如调用的时间是 "2017-10-07 17:03:56":

grep -l '2017-10-07 17:03' *.log

就可以找到对义的日志文件,再结合操作找到对应的函数,就可以定位到问题的源头。

去掉配置文件的注释

Linux 下非常多的程序都是通过配置文件来管理配置项,默认配置文件一般都会有很多注释 来解释用法,对于新手可能比较友好,但是如果你对该程序比较熟悉,那么你会觉得注释很 烦人,但是如果手工删掉注释,工作量实在太大,这个时候我们就可以使用 grep 来去掉 注释,以 nginx的默认配置文件为例:

#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
            index  index.html index.htm;
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }


    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}


    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}

}

我们想把注释的内容去掉,命令如下:

grep -Ev "#|^$" /etc/nginx/nginx.conf

这个时候输出到终端的配置都是“干干净净”,如果你想将内容写到一个新的文件,也很容易:

grep -Ev "#|^$" /etc/nginx/nginx.conf > new_file

这里的 -E选项是使用功能扩展的 grep 版本即 egrep, egrep 功能和 grep 一 样,但是某些特性更加强大。上面命令的意思是匹配所有不包含 #或者空行的行

递归查找文件并进行匹配

我经常会作各种笔记,不同的内容对应不同的文件,分别存放在不同的子目录下,但是我经 常忘了我记笔记的文件名,只是大概记得内容。例如我只是记得我写了不少关于 emacs lisp 的笔记,但是我不记得具体的文件名了,而我记笔记的目录又有很多子目录,我就可 以通过 findgrep 把文件名找出来:

find /home/samray -name '*.org' -print0 | xargs -0r grep -H 'emacs lisp'

江山代有才人出之 ripgrep

不可否认,grep 仍是 Unix 平台上不可或缺的工具,但是随着技术的发展,已经和 grep 功能类似但是更加强大的工具了,那就是 ripgrep. 使用 Rust 开发的跨平台 的模式匹配工具,结合了 The Silver Searcher 的可用性和 grep 的性能,现在是最 快的模式匹配工具,比所有的同类工具都要快: 运行截图

更多的信息可以查看 官方文档

参考