Bash 零基础教程

Bash grep 命令

grep(全局正则表达式打印,"global regular expression print" 的缩写)是 Bash 脚本中最基础也是最强大的命令行工具之一,专门用于在纯文本数据中搜索匹配正则表达式的行。它就像一个极其高效的模式扫描器,让你能够快速定位文件或数据流中的特定信息。

无论你是处理系统日志、配置文件、代码库还是任何其他基于文本的信息,理解 grep 都至关重要,因为它是类 Unix 环境下进行高效文本处理和分析的基础。它能够根据复杂的模式过滤和提取相关行,这使其成为系统管理员、开发人员和数据分析师不可或缺的利器。

本章将深入探讨 grep 的核心功能、常用选项,以及如何利用其模式匹配能力完成各种任务。

1. 理解 grep:基本语法与功能

grep 的核心作用是在输入文件中搜索包含指定模式(pattern)的行。当它找到匹配项时,会将整行内容打印到标准输出(屏幕)。如果没有指定文件,或者文件名使用连字符 (-) 代替,grep 就会从标准输入读取数据,这使得它在通过管道符与其他命令结合使用时异常灵活。

grep 的基本语法如下:

grep [选项] 模式 [文件...]
  • 模式 (pattern):这是 grep 要搜索的正则表达式或纯文本字符串(字面量)。
  • 文件... (file...):这是 grep 要在其中搜索的一个或多个文件。如果省略,grep 将从标准输入读取。
  • 选项 (options):用于修改 grep 的行为,例如允许不区分大小写的搜索、反向匹配、显示行号等。

让我们从最简单的用例开始:搜索纯文本字符串。

1.1 搜索字面量字符串

当模式是一个简单的字符序列时,grep 会在指定文件的每一行中寻找该序列的精确匹配项。

示例 1:搜索单个文件

假设你有一个名为 access.log 的文件,内容如下:

192.168.1.1 GET /index.html 200
10.0.0.5 POST /api/data 404
192.168.1.1 GET /images/logo.png 200
172.16.0.10 GET /admin 500
10.0.0.5 GET /about.html 200

要查找包含字符串 "192.168.1.1" 的所有行:

grep "192.168.1.1" access.log

输出:

192.168.1.1 GET /index.html 200
192.168.1.1 GET /images/logo.png 200

示例 2:搜索多个文件

假设你有 file1.txtfile2.txtfile3.txt

file1.txt:

apple banana cherry
date elderberry fig

file2.txt:

grape watermelon kiwi
lemon mango nectarine

file3.txt:

orange pear quince
raspberry strawberry tangerine

要在这些文件中查找所有出现的 "banana":

grep "banana" file1.txt file2.txt file3.txt

输出:

file1.txt:apple banana cherry

当搜索多个文件时,grep 会在每个匹配行的前面加上它所在的文件名。

1.2 从标准输入搜索(结合管道符使用 grep)

正如在模块 1 中学到的,管道符 (|) 允许将一个命令的输出作为另一个命令的输入。grep 经常以这种方式接收输入,使其成为命令链中强大的过滤器。

示例:过滤进程列表

要查找系统上与 "apache" 相关的所有正在运行的进程:

ps aux | grep "apache"

输出(示例):

root      1234  0.0  0.1  123456 6789 ?        Ss   Oct01   0:00 /usr/sbin/apache2 -k start
www-data  5678  0.0  0.0  123456 3456 ?        S    Oct01   0:00 /usr/sbin/apache2 -k start
www-data  5679  0.0  0.0  123456 3456 ?        S    Oct01   0:00 /usr/sbin/apache2 -k start
user      9876  0.0  0.0  123456 7890 pts/0    S+   10:30   0:00 grep apache

注意最后一行:grep apache。发生这种情况是因为 grep 本身也是一个正在运行并搜索 "apache" 的进程,所以它找到了自己的进程。要排除它,你可以完善模式或使用 grep -v。我们稍后会讲到 grep -v

2. 结合正则表达式使用 grep

grep 的真正威力在于它能够使用正则表达式 (REs) 作为搜索模式。正如上一章所讲,正则表达式是定义搜索模式的字符序列,允许进行比简单的字面量字符串更灵活、更强大的匹配。grep 默认主要使用基础正则表达式 (BRE),但可以通过 -E 选项切换到扩展正则表达式 (ERE),ERE 提供了更多功能且不需要对特殊字符进行转义。

让我们回顾一下关键的正则表达式概念以及 grep 是如何使用它们的。

2.1 基础正则表达式 (BRE)

默认情况下,grep 将模式解析为 BRE。这意味着某些元字符需要用反斜杠 (\) 转义才能被视为特殊字符,否则它们会被当作普通字面量字符。

  • ^ (脱字符):行首锚点

匹配一行的开头。

示例:access.log 中查找以 "192.168" 开头的行:

# 纠正:点号 . 在正则中是特殊字符。要匹配字面的点号,必须转义。
# 匹配以 "192.168" 开头的行
grep "^192\.168" access.log

输出:

192.168.1.1 GET /index.html 200
192.168.1.1 GET /images/logo.png 200
  • $(美元符号):行尾锚点

匹配一行的结尾。
示例:access.log 中查找以 "200" 结尾的行:

grep "200$" access.log

输出:

192.168.1.1 GET /index.html 200
192.168.1.1 GET /images/logo.png 200
10.0.0.5 GET /about.html 200
  • .(点号):任意单个字符

匹配任意单个字符(不包括换行符)。

示例: 查找包含 "GET"、后跟任意一个字符、再跟 "index.html" 的行:

grep "GET.index\.html" access.log

这会匹配 "GET /index.html"(这里的空格就是 . 匹配到的字符)。

输出:

192.168.1.1 GET /index.html 200
  • *(星号):零次或多次出现

匹配前面字符或分组的零次或多次出现。

示例: 查找包含 "GET"、后跟任意数量的任意字符、再跟 "html" 的行:

grep "GET.*html" access.log

输出:

192.168.1.1 GET /index.html 200
10.0.0.5 GET /about.html 200
  • [](方括号):字符集

匹配方括号内的任意单个字符。

示例: 查找状态码的最后一个数字是 "2" 或 "4" 或 "5" 的行:

grep "[245]00$" access.log

输出:

192.168.1.1 GET /index.html 200
10.0.0.5 POST /api/data 404
192.168.1.1 GET /images/logo.png 200
172.16.0.10 GET /admin 500
10.0.0.5 GET /about.html 200
  • [^](方括号内的脱字符):反向字符集

匹配在方括号内的任意单个字符。

示例: 查找 IP 地址的第二位数字不是 0 或 1 的行:

grep "[0-9]\.[^01]\." access.log

说明:这个模式仅为了演示进行了简化。一个稳健的 IP 匹配规则会复杂得多。

输出(假设的,取决于具体日志内容):

172.16.0.10 GET /admin 500

(如果你有 192.2.3.4,它会匹配第二段的 2。)

  • \{n\}(花括号):量词

精确匹配前面字符出现 n 次。在 BRE 中需要转义。

示例: 查找恰好连续出现三个 'o' 的行(例如 "fooo"):

echo "foo" | grep "o\{2\}" # 匹配
echo "fooo" | grep "o\{3\}" # 匹配
echo "foooo" | grep "o\{3\}" # 匹配,因为包含 'o' 跟着 'o',再跟着 'o'

2.2 使用 grep -E 开启扩展正则表达式 (ERE)

对于更复杂的模式,特别是那些涉及 +?|() 的模式,使用扩展正则表达式 (ERE) 通常更容易。你可以通过给 grep 加上 -E 选项(或者使用等效的 egrep 命令)来启用 ERE。在 ERE 中,诸如 +?|(){} 等元字符需要被转义。

  • + (加号):一次或多次出现

匹配前面字符或分组的一次或多次出现。

示例: 查找包含 "Error" 后跟一个或多个数字的行:

# 假设日志条目类似于 "Error500: Internal Server Error"
grep -E "Error[0-9]+" error.log
  • ?(问号):零次或一次出现

匹配前面字符或分组的零次或一次出现(使字符变为可选)。

示例: 查找包含 "color" 或 "colour" 的行:

grep -E "colou?r" text.txt
  • |(管道符):OR(或)运算符

匹配管道符前面或后面的模式。

示例:access.log 中查找包含 "ERROR" 或 "WARNING" 的行:

grep -E "ERROR|WARNING" access.log

输出(假设存在相关行):

172.16.0.10 GET /admin 500 ERROR: Server Fault
10.0.0.5 POST /api/data 404 WARNING: Resource not found
  • ()(圆括号):分组

将多个字符或表达式分组,以便对整个组应用量词或 OR 运算符。

示例: 查找包含大写或小写的 "apple" 或 "banana" 的行:

grep -E "(Apple|Banana)" fruit.txt # 仅区分大小写匹配时有效
grep -E -i "(Apple|Banana)" fruit.txt # 匹配 Apple, apple, Banana, banana 等

如果没有 ():grep -E "Apple|Banana" 的交替工作原理类似。当对多个字符应用量词时,分组会变得更强大,例如 (ab)+ 匹配 "ab"、"abab" 等。

词边界与字符类(通常配合 -E 或特定选项使用)

  • \b (词边界) / -w 选项:

\b 匹配单词开头或结尾的空字符串。在使用 BRE 时,通常需要对其进行转义:grep '\bword\b'。使用 -E 时,\b 通常直接生效。然而,对于整个单词的匹配,直接使用 grep -w 选项通常更简单。
示例: 仅查找完整单词 "admin",不查找 "administrator" 或 "administer":

grep -w "admin" access.log

输出:

172.16.0.10 GET /admin 500
  • 字符类(通常需要 -E-P 启用 Perl 兼容正则):
    • \d: 匹配任意数字 (0-9)。(在某些实现中需要 grep -Pgrep -E)。
    • \w: 匹配任意单词字符(字母数字 + 下划线)。(需要 grep -P)。
    • \s: 匹配任意空白字符(空格、制表符、换行符等)。(需要 grep -P)。
    • \S: 匹配任意非空白字符。(需要 grep -P)。

使用 POSIX 字符类 更具可移植性(在 grep 中原生支持):

    • [[:digit:]] 代表数字(等同于 [0-9])
    • [[:alpha:]] 代表字母字符
    • [[:alnum:]] 代表字母数字字符
    • [[:space:]] 代表空白字符
    • [[:upper:]] 代表大写字母
    • [[:lower:]] 代表小写字母

示例: 查找包含 "GET"、后跟一个空格、然后是路径的任意字母数字字符、以 ".html" 结尾的行:

grep -E "GET\s+[[:alnum:]\/]+\.html" access.log

\s+ 需要 ERE。[[:alnum:]\/]+ 匹配一个或多个字母数字字符或正斜杠。

输出:

192.168.1.1 GET /index.html 200
10.0.0.5 GET /about.html 200

3. grep 核心高阶选项

grep 提供了一套丰富的选项来控制其行为、优化搜索和修改输出格式。

  • -i, --ignore-case:忽略模式和输入数据中的大小写区别。

示例: 无论大小写,查找 "error"。

grep -i "error" access.log

这会匹配 "error"、"Error"、"ERROR" 等。

  • -v, --invert-match:反向匹配;选择不匹配该模式的行。

示例: 显示 access.log 中状态码不是 "200" 的所有行。

grep -v "200$" access.log

输出:

10.0.0.5 POST /api/data 404
172.16.0.10 GET /admin 500

优化我们之前的 ps aux | grep apache

ps aux | grep "apache" | grep -v "grep"

输出(排除了 grep 进程本身):

root      1234  0.0  0.1  123456 6789 ?        Ss   Oct01   0:00 /usr/sbin/apache2 -k start
www-data  5678  0.0  0.0  123456 3456 ?        S    Oct01   0:00 /usr/sbin/apache2 -k start
www-data  5679  0.0  0.0  123456 3456 ?        S    Oct01   0:00 /usr/sbin/apache2 -k start
  • -c, --count:抑制正常输出;而是打印每个输入文件中匹配行的数量统计

示例: 统计 access.log 中有多少行包含 "192.168.1.1"。

grep -c "192.168.1.1" access.log

输出:

2
  • -n, --line-number:在每行输出前面加上其在输入文件中的行号(从 1 开始)。

示例:access.log 中查找 "GET" 并显示行号。

grep -n "GET" access.log

输出:

1:192.168.1.1 GET /index.html 200
3:192.168.1.1 GET /images/logo.png 200
4:172.16.0.10 GET /admin 500
5:10.0.0.5 GET /about.html 200
  • -l, --files-with-matches:抑制正常输出;而是只打印包含匹配项的文件名
    示例: 当前目录中哪些文件包含 "error"?
grep -l "error" *.log

输出(假设 error.log 包含 "error"):

error.log
  • -r, --recursive / -R, --dereference-recursive递归搜索目录。-R 会遵循符号链接,而 -r 不会。
    示例: 在当前目录及其子目录下的所有 Python 文件中搜索 "TODO" 注释。
grep -r "TODO" *.py

纠正:*.py 只会应用于当前目录。要递归搜索所有 .py 文件,最好使用 find 和管道:

find . -name "*.py" | xargs grep "TODO"

然而,grep -r 就是为了遍历目录设计的。如果目标是搜索所有文件,grep -r "TODO" . 是可以的。如果只搜 .py 文件,那么 grep -r --include='*.py' "TODO" . 会更具体。让我们看一个更简单的递归例子。
示例: 在当前目录及其子目录中的所有文件中搜索字符串 "password"。

grep -r "password" .
  • 上下文控制选项:
    • -A N, --after-context=N:打印匹配行之后 (After)N 行尾随上下文。
    • -B N, --before-context=N:打印匹配行之前 (Before)N 行前导上下文。
    • -C N, --context=N:打印匹配行周围前后 (Context)N 行上下文。

示例: 如果在 access.log 中发现 "ERROR",显示错误行以及它前面 2 行和后面 2 行。

grep -C 2 "ERROR" access.log
  • -o, --only-matching打印匹配行中匹配到的部分(非空),并将每个匹配部分单独输出一行。
    示例:access.log 中提取所有 IP 地址(简化模式)。
grep -E -o "([0-9]{1,3}\.){3}[0-9]{1,3}" access.log

输出:

192.168.1.1
10.0.0.5
192.168.1.1
172.16.0.10
10.0.0.5

注意:这是一个简化的 IP 地址正则表达式;完全稳健的正则会更复杂,但这有效地说明了 -o 的作用。

  • -x, --line-regexp:仅选择那些与整行完全匹配的结果。 示例: 查找仅仅包含 "OK" 的行。
echo "OK" | grep -x "OK" # 匹配成功
echo "NOT OK" | grep -x "OK" # 不匹配

4. 实战案例:使用 grep 解析日志文件

本模块的实战案例是解析日志文件以识别潜在问题。grep 在这方面非常有用,它可以作为第一道防线,快速过滤出相关的日志条目。

让我们使用一个更贴近真实(虽然仍有简化)的 system.log 文件:

Oct  1 08:30:01 servername sshd[12345]: Accepted password for user1 from 192.168.1.10 port 54321 ssh2
Oct  1 08:30:05 servername kernel: Out of memory: Kill process 6789 (mysqld)
Oct  1 08:30:10 servername CRON[23456]: (root) CMD (command -v lsb_release >/dev/null && lsb_release -cs)
Oct  1 08:30:15 servername sshd[12346]: Failed password for user2 from 10.0.0.20 port 12345 ssh2
Oct  1 08:30:20 servername kernel: CPU load high, throttling processes.
Oct  1 08:30:25 servername servername systemd[1]: Started User Manager for UID 1000.
Oct  1 08:30:30 servername sshd[12347]: Failed password for invalid_user from 10.0.0.21 port 12346 ssh2
Oct  1 08:30:35 servername webserver[7890]: [error] Client 172.16.0.30: Request denied for resource /admin
Oct  1 08:30:40 servername kernel: INFO: task kworker/u12:0:22 blocked for more than 120 seconds.
Oct  1 08:30:45 servername webserver[7891]: [warning] Large file download detected from 172.16.0.31

场景 1:识别所有错误消息

我们想要找到所有包含 "error" 或 "ERROR" 的行。

grep -i "error" system.log

输出:

Oct  1 08:30:05 servername kernel: Out of memory: Kill process 6789 (mysqld)
Oct  1 08:30:35 servername webserver[7890]: [error] Client 172.16.0.30: Request denied for resource /admin

这里它同时找出了 "Out of memory"(因为 "memory" 这个单词如果我们不小心的话,在某些复杂匹配中容易引发歧义,但这里是因为我们只搜了 error)和显式的 [error] 消息。为了更精确,我们可能会使用单词边界,或者明确搜索 [error]

让我们精确地查找 [error]ERROR: 消息:

grep -E -i "\[error\]|ERROR:" system.log
  • \[error\] 使用反斜杠转义方括号,将它们视为字面量字符,而不是正则元字符。
  • ERROR: 是一个字面量字符串。
  • -E 启用了扩展正则以使用 | (OR) 运算符。
  • -i 使搜索不区分大小写。

输出:

Oct  1 08:30:35 servername webserver[7890]: [error] Client 172.16.0.30: Request denied for resource /admin

这样匹配就精确多了。

场景 2:查找失败的登录尝试

我们对提及 "Failed password" 的 sshd 条目感兴趣。

grep "Failed password" system.log

输出:

Oct  1 08:30:15 servername sshd[12346]: Failed password for user2 from 10.0.0.20 port 12345 ssh2
Oct  1 08:30:30 servername sshd[12347]: Failed password for invalid_user from 10.0.0.21 port 12346 ssh2

场景 3:监控特定的 IP 地址

查找与特定 IP 地址(例如 "10.0.0.20")相关的所有日志条目。

grep "10\.0\.0\.20" system.log

我们要转义点号,因为它们是正则元字符。

输出:

Oct  1 08:30:15 servername sshd[12346]: Failed password for user2 from 10.0.0.20 port 12345 ssh2

场景 4:识别指示系统问题的内核消息

(例如 "Out of memory" 或 "CPU load high")

grep -E "kernel: (Out of memory|CPU load high)" system.log

这里,() 将特定于内核消息的 OR 条件进行了分组。

输出:

Oct  1 08:30:05 servername kernel: Out of memory: Kill process 6789 (mysqld)
Oct  1 08:30:20 servername kernel: CPU load high, throttling processes.

这些例子展示了 grep 作为一个强大且灵活的初始日志分析工具的作用,它能让系统管理员在大批量的文本数据中迅速锁定关键事件和潜在问题。