文本处理三剑客与文件查找
本文合并自:03-文本处理与文件查找.md、1-6-Shell编程与文本处理.md(文本处理部分)
本文档系统整理 Linux 下常用的文本处理与文件查找能力,包括:正则表达式(BRE / ERE)、grep 文本过滤、find / locate / which / whereis / type 文件查找、sed 流编辑器、awk 文本处理语言,以及 sort 排序、uniq 去重、comm 比较有序文件、diff 比较文件、wc 统计、cut 列切割、tr 字符替换/删除、paste / join / nl / rev / column / xargs / tee 等辅助工具。每个命令均给出常用选项、注意事项与课堂示例,最后给出「三剑客」组合实战与练习。
说明:
1-6-Shell编程与文本处理.md中的 Shell 编程语法部分(变量、流程控制、函数、数组、declare、命令历史、别名、环境变量、特殊符号、输入输出等)不在本文范围内。 详见 阶段0606-01-Shell脚本编程.md
学习路线:正则表达式是文本处理的基础语法,grep 负责「找行」,find 负责「找文件」,sed 负责「按行编辑」,awk 负责「按列计算」,其余小工具负责排序、去重、切列等辅助动作。四者通过管道组合,构成 Linux 运维最常用的文本处理流水线。
1. 正则表达式
正则表达式(Regular Expression,简称 regex / RE)用于字符串的模糊匹配,是 grep、sed、awk 共同的语法基础。
重要区分:正则表达式与 Shell 的通配符(globbing)是两套完全不同的语法。通配符由 Shell 解析,用于匹配文件名(如
ls *.txt、find -name "*.conf");正则表达式由grep、sed、awk等程序自身解析,用于匹配文件内容。例如*在通配符中表示「任意多个字符」,而在正则中表示「前一个字符出现 0 次或多次」。
1.1 正则表达式的两大流派
| 流派 | 全称 | 使用方式 |
|---|---|---|
| BRE(基本正则) | Basic Regular Expression | grep、sed 默认使用;+ ? { } ( ) \| 需转义才有元字符含义 |
| ERE(扩展正则) | Extended Regular Expression | grep -E(等价 egrep)、sed -r(或 sed -E)、awk 默认使用;+ ? { } ( ) \| 直接就是元字符 |
| PCRE(Perl 兼容正则) | Perl Compatible Regular Expression | grep -P 才支持;提供 \d、\d+? 非贪婪等语法 |
1.2 基础正则(BRE)元字符全表
| 元字符 | 含义 | 示例 |
|---|---|---|
. |
匹配任意单个字符(不匹配空行,但可匹配空格) | r..t 匹配 root、rest |
^ |
锚定行首 | ^root 以 root 开头的行 |
$ |
锚定行尾 | bash$ 以 bash 结尾的行 |
* |
前一个字符出现 0 次或多次(任意次) | ab* 匹配 a、ab、abb |
.* |
任意长度的任意字符(含长度 0) | ^r.*h$ r 开头 h 结尾 |
[] |
匹配括号内任意单个字符 | [Rr]oot 匹配 Root、root |
[^] |
匹配不在括号内的任意单个字符(取反) | [^0-9] 非数字字符 |
\{n\} |
前一个字符精确出现 n 次 | a\{3\} 匹配 aaa |
\{n,\} |
前一个字符至少出现 n 次 | a\{2,\} 匹配 aa、aaa… |
\{,m\} |
前一个字符最多出现 m 次 | a\{,3\} |
\{n,m\} |
前一个字符出现 n ~ m 次 | a\{1,3\} 匹配 a、aa、aaa |
\(\) |
分组(子表达式),供 \1 反向引用 |
\(ab\)\{2\} 匹配 abab |
\| |
或(GNU 扩展,BRE 中需转义) | root\|bin |
\ |
转义符,去除后一个字符的特殊含义 | \. 匹配真正的点号 |
\< |
锚定单词首(GNU 扩展) | \<root |
\> |
锚定单词尾(GNU 扩展) | root\> |
\b |
单词边界 | \broot\b 精确匹配单词 root |
\B |
非单词边界 | — |
1.3 扩展正则(ERE)与 BRE 的差异
ERE 中以下元字符不需要反斜杠转义,写法更直观:
| ERE 写法 | 等价的 BRE 写法 | 含义 |
|---|---|---|
+ |
\+ |
前一个字符出现 1 次或多次(至少 1 次) |
? |
\? |
前一个字符出现 0 次或 1 次(可有可无) |
{n,m} |
\{n,m\} |
前一个字符出现 n ~ m 次 |
() |
\(\) |
分组 / 子表达式 |
\| |
\\| |
或 |
ab+ # ab, abb, abbb...
(ab)+ # ab, abab, ababab...
a{1,3} # a, aa, aaa
其中
+、?、{n,m}、|、()属于扩展正则(ERE)元字符,必须配合grep -E或sed -r使用。在基本正则(BRE,即默认的grep、sed)中它们只是普通字符,需要写成\+、\?、\{n,m\}、\|、\(...\)才有元字符含义。
对比演示:
# BRE:查找连续出现 1 次以上的 a
grep 'a\+' /etc/passwd
# ERE:同样功能,写法更简洁
grep -E 'a+' /etc/passwd
# BRE 分组
grep '\(root\|bin\)' /etc/passwd
# ERE 分组
grep -E '(root|bin)' /etc/passwd
1.4 字符类与预定义字符类
自定义字符集合:
| 表达式 | 含义 |
|---|---|
[abc] |
匹配 a、b、c 中任意一个 |
[^abc] |
匹配除 a、b、c 外的任意字符 |
[a-z] |
匹配小写字母 |
[A-Z] |
匹配大写字母 |
[0-9] |
匹配数字 |
[a-zA-Z0-9_] |
匹配单词字符 |
POSIX 预定义字符类(写在 [] 内部,注意是双层中括号):
| 字符类 | 含义 |
|---|---|
[[:alnum:]] |
字母和数字,等价 [a-zA-Z0-9] |
[[:alpha:]] |
字母,等价 [a-zA-Z] |
[[:digit:]] |
数字,等价 [0-9] |
[[:lower:]] |
小写字母 |
[[:upper:]] |
大写字母 |
[[:blank:]] |
空格和制表符 |
[[:space:]] |
所有空白字符(空格、制表符、换行、回车、换页、垂直制表) |
[[:punct:]] |
标点符号 |
[[:print:]] |
可打印字符 |
[[:cntrl:]] |
控制字符 |
[[:xdigit:]] |
十六进制数字 [0-9a-fA-F] |
使用预定义字符类的最大好处是不受字符集(locale)影响。例如在某些 locale 下
[a-z]可能包含大写字母,而[[:lower:]]永远只匹配小写。
# 匹配包含数字的行
grep '[[:digit:]]' /etc/passwd
# 匹配以空白字符开头的行
grep '^[[:space:]]' /etc/fstab
# 注意:下面写法是错的,[:digit:] 只表示 : d i g t 这几个字符的集合
grep '[:digit:]' file # 错误写法
1.5 转义字符与方言差异
| 转义 | 含义 |
|---|---|
\. |
匹配普通点号 |
\n |
换行 |
\t |
制表符 |
\d |
数字 [0-9] |
\D |
非数字 |
\s |
空白字符 |
\S |
非空白字符 |
\w |
单词字符 [a-zA-Z0-9_] |
\W |
非单词字符 |
\b |
单词边界 |
\B |
非单词边界 |
注意方言差异:GNU grep / sed 支持
\w、\W、\b、\B、\s、\S;但\d、\D是 PCRE 专有语法,GNU grep(BRE/ERE)和 sed 都不支持,写了不会报错而是被当成普通字母d,导致静默匹配失败。需要\d时改用grep -P '\d'或等价的[0-9]/[[:digit:]]。
1.6 贪婪与非贪婪匹配
默认情况下数量表示符为最大匹配(贪婪),追加 ? 可启用最小匹配(非贪婪)。
.*b # 贪婪匹配
.*?b # 非贪婪匹配(仅 PCRE 支持)
非贪婪的
?同样是 PCRE 专有语法,grep 的 BRE/ERE 与 sed 都不支持:在grep -E中.*?会被理解为「.*出现 0 次或 1 次」,仍是贪婪结果。本文档涉及的 grep / sed 场景需要非贪婪时,可用grep -P,或改写为排除型字符类(如用[^b]*b代替.*?b)。
贪婪匹配演示:
# 贪婪:.* 会一直吃到最后一个 > 之前
echo "<a><b><c>" | grep -oE '<.*>'
# 输出:<a><b><c>
# 排除型字符类实现「非贪婪」效果
echo "<a><b><c>" | grep -oE '<[^>]*>'
# 输出:
# <a>
# <b>
# <c>
# PCRE 非贪婪写法
echo "<a><b><c>" | grep -oP '<.*?>'
1.7 反向引用
从左到右按左小括号计数,第 n 对括号匹配的内容可用 \n 引用。
echo "abcabcabcaabb" | grep -E '(a(bc)){2}\1' --color
echo "hello world, hello world, hello beautiful world" \
| grep -E --color '((hello) (world)), \1, \2 .* \3'
反向引用的典型用途是查找「重复出现」的内容:
# 查找连续重复的单词,如 "the the"
grep -E '\b(\w+) \1\b' file.txt
# 查找首尾字符相同的行
grep -E '^(.).*\1$' file.txt
1.8 常用正则案例
匹配 IP 地址
# 粗略匹配(不校验 0~255 范围),适合日志快速提取
grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' /var/log/secure
# 严格匹配 0~255 的合法 IPv4
grep -oE '\b((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\b' file
# 提取带掩码位的地址,如 192.168.1.10/24
grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}'
匹配邮箱
grep -oE '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' file
匹配中国大陆手机号
grep -oE '\b1[3-9][0-9]{9}\b' file
匹配日期
# YYYY-MM-DD
grep -oE '[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])' file
# YYYY/MM/DD 或 YYYY-MM-DD 都匹配
grep -oE '[0-9]{4}[-/][0-9]{2}[-/][0-9]{2}' file
# 时间 HH:MM:SS
grep -oE '([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]' file
匹配注释行与空行(配置文件清理必备)
# 空行(行首紧接行尾)
grep '^$' /etc/httpd/conf/httpd.conf
# 注释行(# 开头)
grep '^#' /etc/httpd/conf/httpd.conf
# 空行 + 注释行(含以空白开头的注释)
grep -E '^[[:space:]]*(#|$)' /etc/httpd/conf/httpd.conf
# 反向:显示真正生效的配置行(去掉空行和注释行)
grep -Ev '^[[:space:]]*(#|$)' /etc/httpd/conf/httpd.conf
其他常用模式
# 匹配以字母开头、长度 3~8 的用户名
grep -E '^[a-zA-Z][a-zA-Z0-9_]{2,7}$' names.txt
# 匹配 MAC 地址
grep -oE '([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}' file
# 匹配 URL
grep -oE 'https?://[a-zA-Z0-9./?=_%&-]+' file
# 匹配行尾空白(常用于代码检查)
grep -n '[[:space:]]\+$' script.sh
2. grep 家族
grep 用于在文本文件中过滤、筛选包含指定模式(字符串)的行。名字来源于 ed 编辑器命令 g/re/p(global / regular expression / print)。
2.1 grep / egrep / fgrep 的关系
| 命令 | 等价写法 | 说明 |
|---|---|---|
grep |
— | 使用基本正则(BRE) |
egrep |
grep -E |
使用扩展正则(ERE),extended grep |
fgrep |
grep -F |
不解析正则,把模式当固定字符串(fixed),速度最快 |
| — | grep -P |
使用 PCRE(Perl 兼容正则) |
egrep与fgrep在新版 GNU grep 中已标记为废弃(deprecated),运行时会输出警告,推荐统一使用grep -E和grep -F。
2.2 命令格式
grep [选项] 查找模式 [文件名1, 文件名2, ...]
grep [选项] [-e 查找模式 | -f 文件] [文件名1, 文件名2, ...]
2.3 常用选项全表
匹配控制:
| 选项 | 长选项 | 说明 |
|---|---|---|
-e 模式 |
--regexp= |
指定查找模式,可多次使用实现「或」;模式以 - 开头时必须用它 |
-f 文件 |
--file= |
从文件中读取模式,一行一个模式 |
-i |
--ignore-case |
匹配时忽略字母大小写 |
-v |
--invert-match |
反向匹配,只输出不包含匹配字符串的行 |
-w |
--word-regexp |
完全匹配单词(模式两侧必须是非单词字符) |
-x |
--line-regexp |
只输出整行严格匹配的行(整行没有任何其他字符) |
-E |
--extended-regexp |
使用扩展正则表达式(ERE) |
-F |
--fixed-strings |
把模式当作固定字符串,不解析正则 |
-G |
--basic-regexp |
使用基本正则表达式(默认) |
-P |
--perl-regexp |
使用 PCRE 正则 |
输出控制:
| 选项 | 长选项 | 说明 |
|---|---|---|
-c |
--count |
只输出包含匹配字符串的行数(统计有多少行匹配) |
-n |
--line-number |
在匹配行前加上该行在文件中的行号(首行编号为 1) |
-l |
--files-with-matches |
只输出含有匹配内容的文件名,找到即停止扫描该文件 |
-L |
--files-without-match |
只输出不含匹配内容的文件名 |
-o |
--only-matching |
只输出匹配到的部分,而不是整行;每个匹配独占一行 |
-q |
--quiet / --silent |
静默模式,不输出任何内容,只用退出状态码表示是否匹配,常用于脚本判断 |
-s |
--no-messages |
不显示文件不存在、无权限等错误信息 |
-h |
--no-filename |
多文件搜索时不显示文件名前缀 |
-H |
--with-filename |
强制显示文件名前缀(单文件时也显示) |
-m N |
--max-count=N |
每个文件最多输出 N 个匹配行后即停止 |
-b |
--byte-offset |
显示匹配内容的字节偏移量 |
--color[=auto] |
— | 高亮显示匹配到的内容 |
上下文控制:
| 选项 | 长选项 | 说明 |
|---|---|---|
-A N |
--after-context=N |
输出匹配行及其后 N 行(after) |
-B N |
--before-context=N |
输出匹配行及其前 N 行(before) |
-C N / -N |
--context=N |
输出匹配行前后各 N 行(context) |
文件与目录控制:
| 选项 | 长选项 | 说明 |
|---|---|---|
-r |
--recursive |
递归查询目录下所有子目录中的文件(不跟随符号链接) |
-R |
--dereference-recursive |
递归查询,且跟随符号链接 |
--include=GLOB |
— | 递归时只搜索匹配该通配符的文件,如 --include="*.conf" |
--exclude=GLOB |
— | 递归时排除匹配该通配符的文件 |
--exclude-dir=GLOB |
— | 递归时排除匹配该通配符的目录,如 --exclude-dir=.git |
-a |
--text |
把二进制文件当作文本处理 |
-I |
— | 忽略二进制文件 |
2.4 注意事项
- 文件名列表中可以使用通配符(如
*.txt),一次匹配多个相同格式的文件。 - 要查找目录中子目录内的文件,必须使用
-r/-R选项。 - 如果要搜索的字符串中包含空格,必须用单引号
'...'将字符串括起来。 - 正则模式建议始终用单引号包裹,防止 Shell 先把
*、$、?等符号展开或替换掉。 grep的退出状态码:0表示找到匹配,1表示未找到匹配,2表示发生错误(如文件不存在)。
2.5 基础示例
在 etcpasswd 文件中查找包含 root 的所有行:
grep root etcpasswd
统计 etcpasswd 文件中 root 字符串出现的行数:
grep -c 'root' etcpasswd
忽略大小写查找包含 root 的行,以下两种写法等价:
grep -E '[Rr]oot' etcpasswd
grep -i root etcpasswd
更多常用示例:
grep root /etc/passwd # 查找包含 root 的行
grep -n root /etc/passwd # 显示行号
grep -A 3 nfs /etc/passwd # 输出匹配行及其后 3 行
grep -rl root ./ # 递归查找并只列出含 root 的文件名
grep -v root /etc/passwd # 反向匹配,排除含 root 的行
grep -E 'a+' /etc/passwd # 使用扩展正则
2.6 各选项详细示例
# -w 完全匹配单词:只匹配独立的 root,不匹配 rootabc
grep -w root /etc/passwd
# -x 整行严格匹配
grep -x "root:x:0:0:root:/root:/bin/bash" /etc/passwd
# -o 只输出匹配部分:提取所有 IP
grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' /var/log/secure
# -c 统计匹配行数(注意:是行数,不是出现次数)
grep -c bash /etc/passwd
# -l / -L 找文件
grep -rl "PermitRootLogin" /etc/ssh/ # 含关键字的文件
grep -rL "PermitRootLogin" /etc/ssh/ # 不含关键字的文件
# -e 指定多个模式(或关系)
grep -e root -e bin /etc/passwd
# 等价的 ERE 写法
grep -E 'root|bin' /etc/passwd
# -f 从文件读取模式列表
cat > /tmp/patterns <<'EOF'
root
daemon
nobody
EOF
grep -f /tmp/patterns /etc/passwd
# -F 固定字符串匹配,特殊字符不再有正则含义
grep -F '192.168.1.1' /etc/hosts
grep -F '$HOME' script.sh
# -q 静默模式,用于脚本判断
if grep -q "^PermitRootLogin no" /etc/ssh/sshd_config; then
echo "已禁止 root 登录"
fi
# 上下文选项
grep -A 2 "ERROR" app.log # 匹配行 + 后 2 行
grep -B 2 "ERROR" app.log # 匹配行 + 前 2 行
grep -C 2 "ERROR" app.log # 匹配行 + 前后各 2 行
# --color 高亮
grep --color=auto root /etc/passwd
# --include / --exclude 限定文件范围
grep -r "Listen" /etc/ --include="*.conf"
grep -r "password" /etc/ --exclude="*.bak"
grep -r "TODO" /project/ --exclude-dir=.git --exclude-dir=node_modules
# -m 限制匹配数量,日志巨大时非常有用
grep -m 5 "ERROR" /var/log/messages
2.7 grep 与管道组合的典型用法
# 查看某个进程是否运行(grep -v grep 排除 grep 自身)
ps aux | grep nginx | grep -v grep
# 更优雅的写法:用字符类打断自身匹配
ps aux | grep "[n]ginx"
# 查看监听端口
ss -tnlp | grep ':80'
netstat -tnlp | grep ':22'
# 查看已加载的内核模块
lsmod | grep ext4
# 过滤已安装的软件包
rpm -qa | grep -i mysql
# 结合 wc 统计行数
cat /etc/passwd | grep root | wc -l
grep -c root /etc/passwd # 更高效的等价写法
# 多级过滤:先筛 IP 再排除本机
grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' access.log | grep -v '^127\.'
# 只看非注释非空行的有效配置
grep -Ev '^[[:space:]]*(#|$)' /etc/my.cnf
2.8 从日志中提取信息的实战案例
# 1. 统计 SSH 登录失败的来源 IP 及次数(TOP 10)
grep "Failed password" /var/log/secure \
| grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' \
| sort | uniq -c | sort -nr | head -10
# 2. 统计 SSH 爆破尝试的用户名
grep "Failed password for invalid user" /var/log/secure \
| awk '{print $(NF-5)}' | sort | uniq -c | sort -nr
# 3. 提取 Nginx 访问日志中的 404 请求 URL
grep ' 404 ' /var/log/nginx/access.log | awk '{print $7}' | sort | uniq -c | sort -nr
# 4. 查找某时间段的日志(结合时间字符串)
grep "2026-08-22 14:" /var/log/nginx/error.log
# 5. 统计各类日志级别出现次数
grep -oE '\[(DEBUG|INFO|WARN|ERROR|FATAL)\]' app.log | sort | uniq -c | sort -nr
# 6. 找出所有含异常堆栈的上下文
grep -A 10 "Exception" app.log
# 7. 实时监控日志中的错误(tail -f 配合 grep --line-buffered)
tail -f /var/log/messages | grep --line-buffered -i "error"
# 8. 排查配置文件中被注释掉的关键项
grep -n "^[[:space:]]*#.*ServerName" /etc/httpd/conf/httpd.conf
# 9. 统计访问日志中的独立 IP 数量
grep -oE '^([0-9]{1,3}\.){3}[0-9]{1,3}' access.log | sort -u | wc -l
# 10. 找出日志中响应时间超过 1 秒的请求(假设最后一列为耗时)
awk '$NF > 1' access.log | grep -v '^#'
3. 文件查找
Linux 中查找文件有两大思路:实时遍历磁盘(find,准确但慢)与查询预建数据库(locate,快但可能不是最新)。另有 which / whereis / type 专门用于查找命令本身。
3.1 find
find 是 Linux 中功能最强大的文件查找命令,可以按照名称、类型、属主/属组、权限、大小、时间等多种条件实时查找文件,会扫描指定范围。
命令格式
find 查找路径 [选项] 查找内容
find 范围 参数 关键字
若省略查找路径,默认为当前目录 .。
常用选项总览
| 选项 | 说明 |
|---|---|
-name |
按文件名查找(区分大小写) |
-iname |
按文件名查找,忽略大小写 |
-path |
按路径模式匹配(匹配整个路径而不只是文件名) |
-ipath |
按路径模式匹配,忽略大小写 |
-regex |
用正则表达式匹配整个路径 |
-type |
按文件类型查找 |
-uid |
按用户 UID 查找 |
-gid |
按组 GID 查找 |
-user |
按用户名查找 |
-group |
按组名查找 |
-nouser |
查找没有属主的文件(无头文件) |
-nogroup |
查找没有属组的文件 |
-perm |
按权限查找 |
-size |
按文件大小查找 |
-empty |
查找空文件或空目录 |
-mtime |
按内容修改时间查找(天) |
-atime |
按访问时间查找(天) |
-ctime |
按状态改变时间查找(天) |
-mmin |
按内容修改时间查找(分钟) |
-amin |
按访问时间查找(分钟) |
-cmin |
按状态改变时间查找(分钟) |
-newer 文件 |
查找比指定文件的 mtime 更新的文件 |
-anewer 文件 |
查找比指定文件的 atime 更新的文件 |
-cnewer 文件 |
查找比指定文件的 ctime 更新的文件 |
-newermt "日期" |
查找 mtime 晚于指定日期的文件 |
-maxdepth N |
限制查找的最大目录层级 |
-mindepth N |
限制查找的最小目录层级 |
-links N |
按硬链接数查找 |
-inum N |
按 inode 号查找 |
-prune |
剪枝,跳过指定目录不进入 |
-print |
打印结果(默认动作),以换行符分隔 |
-print0 |
打印结果,以 \0(NUL)分隔,供 xargs -0 使用 |
-exec |
对查找结果执行命令 |
-ok |
同 -exec,但执行每条命令前逐个询问确认 |
-delete |
直接删除查找到的文件(危险) |
-ls |
以 ls -dils 格式列出结果详情 |
-quit |
找到第一个结果后立即退出 |
综合示例:
find /etc -name grub.conf
find / -name "*.conf"
find /etc -type f -size +10M
find / -size +10M -user soul -type f -exec ls -hl "{}" \;
按名称查找(-name / -iname / -path / -regex)
# 查找 /etc 目录下名为 grub.conf 的文件
find /etc/ -name grub.conf
# 使用通配符查找 /etc 下所有 .conf 结尾的文件
# 注意:通配符必须加引号,否则 shell 会先展开导致出错
find /etc/ -name "*.conf"
# 查找 /etc 下所有隐藏文件
find /etc/ -name ".*"
# 忽略大小写查找
find /test/ -iname Aaa
-iname Aaa 的输出示例:
/test/aaa
/test/Aaa
/test/AAa
按路径匹配:
# -path 匹配整个路径,通配符 * 也会匹配 /
find /var -path "*log*"
# 排除某个目录(-path 配合 -prune)
find / -path "/proc" -prune -o -name "*.log" -print
# 排除多个目录
find / \( -path /proc -o -path /sys -o -path /dev \) -prune -o -name "core.*" -print
# 用正则匹配整个路径
find /etc -regex ".*\.\(conf\|cfg\)$"
find /var/log -regextype posix-extended -regex ".*/(messages|secure)"
通配符(注意不是正则表达式)
| 通配符 | 含义 |
|---|---|
? |
匹配单个任意字符 |
* |
匹配 0 个或多个任意字符 |
[a-z] |
匹配单个小写字母 |
[0-9] |
匹配单个数字 |
限制查找目录层级(-maxdepth / -mindepth)
# 只在 /test 的第 1 层查找 aaa
find /test/ -maxdepth 1 -name aaa
# 输出:/test/aaa
# 在 /test 的前 2 层查找 aaa
find /test/ -maxdepth 2 -name aaa
# 输出:
# /test/aaa
# /test/a/aaa
# 只查找第 2 层及更深的目录(跳过第 1 层)
find /test/ -mindepth 2 -name aaa
# 组合使用:只查第 2 层
find /test/ -mindepth 2 -maxdepth 2 -type d
-maxdepth/-mindepth属于「全局选项」,应写在其他测试条件之前,否则 GNU find 会给出警告。
按文件类型查找(-type)
| 类型字符 | 文件类型 |
|---|---|
f |
普通文件 |
d |
目录 |
l |
符号链接文件 |
b |
块设备文件 |
c |
字符设备文件 |
p |
管道文件(FIFO) |
s |
socket 文件 |
示例:
# 查找 /etc 下名为 grub.conf 的链接文件
find /etc -type l | grep grub.conf
find /etc -type l -name grub.conf
# 查找系统中各类文件
find / -type f # 普通文件
find / -type b # 块设备文件
find / -type s # socket 文件
find / -type c # 字符设备文件
find / -type p # 管道文件
find / -type d # 目录
find / -type l # 链接文件
按属主、属组、UID、GID 查找
find / -user a # 查找属主为用户 a 的文件
find / -group a # 查找属组为组 a 的文件
find / -uid 533 # 查找属主 UID 为 533 的文件
find / -gid 533 # 查找属组 GID 为 533 的文件
find / -nouser # 查找没有属主的文件(无头文件)
find / -nogroup # 查找没有属组的文件
没有属主或属组的文件通常是系统管理员需要重点关注的对象(例如用户被删除后遗留的文件)。
按权限查找(-perm)
精确匹配
# 查找权限恰好为 777 的文件
find / -perm 777
权限为 777 的文件通常也是管理员需要关注的。
+ 与 - 的区别
设有如下文件列表(课堂练习):
ll
total 0
-rwxr-xrwx. 1 root root 0 Jul 23 11:42 1
---------x. 1 root root 0 Jul 23 11:42 10
-rwx--xr--. 1 root root 0 Jul 23 11:42 2
-rw-rw--wx. 1 root root 0 Jul 23 11:42 3
-r-xrwx---. 1 root root 0 Jul 23 11:42 4
-r---w--w-. 1 root root 0 Jul 23 11:42 5
--wx-wx-wx. 1 root root 0 Jul 23 11:42 6
--w-r-x--x. 1 root root 0 Jul 23 11:42 7
---xrw-r-x. 1 root root 0 Jul 23 11:42 8
----rwxr--. 1 root root 0 Jul 23 11:42 9
+ 与 - 的含义如下:
+mode:将权限写成 9 位,位与位对比,只要有任意一位重合,就匹配。-mode:将权限写成 9 位,位与位对比,必须全部包含(所有指定的位都重合),才匹配。
# +422:只有文件 7 和 10 不符合(其余都有至少一位匹配 r-- -w- -w-)
find . -perm +422
# -422:只有文件 3 和 5 符合(同时包含 4、2、2 三个位)
find . -perm -422
版本提示:
-perm +mode语法在 GNU findutils 4.5.12 之后已被移除,现代系统(CentOS 7+)应改用-perm /mode,语义完全相同(任意一位匹配即可)。即上面的find . -perm +422在新系统上应写作find . -perm /422。-perm -mode(全部包含)语法则一直有效。
# 现代等价写法
find . -perm /422 # 任意一位匹配
find . -perm -422 # 全部包含
find . -perm 422 # 精确等于
权限还可用字母形式表示:
find /home -perm -u+s # 查找设置了 SUID 的文件
find / -perm -4000 -type f # 同上,数字形式
find / -perm -2000 -type f # 查找设置了 SGID 的文件
find /tmp -perm -1000 -type d # 查找设置了 Sticky 位的目录
find /home -perm -o+w -type f # 查找其他人可写的文件(安全隐患)
应用示例:假设 /etc/ 下所有文件的属主和属组都是 root:root,查找普通用户 user1 在 /etc/ 下能读又能写的文件:
find /etc/ -perm -006
关于权限位、SUID / SGID / Sticky、
umask与权限计算的完整说明,属于权限体系的旁支内容。 详见 阶段02 用户与权限
按大小查找(-size)
-size 后面跟数字和单位,+n 表示大于 n,-n 表示小于 n。
find / -size +500M # 大于 500 MB
find / -size +1G # 大于 1 GB
find /etc -size +50k # 大于 50 KB
find / -size -1M # 小于 1 MB
find / -size +3c # 大于 3 字节
大小单位说明:
| 单位 | 含义 |
|---|---|
b |
块(block),大小为 512 字节(注意:不是 byte,也不是文件系统 4096 字节大小的 block) |
c |
字节(byte) |
k |
KB(千字节) |
M |
MB(兆字节) |
G |
GB(吉字节) |
例如:
# 注意 b 在这里表示 block(512 字节),不是 byte
find /etc -size +50b
可以同时使用两个 -size 条件限定区间:
# 查找大于 80 MB 且小于 100 MB 的文件
find / -size +80M -size -100M
-size n(不带正负号)表示「向上取整后恰好等于 n 个单位」,例如-size 1M会匹配 0~1MB 之间的文件(因为不足 1M 也会被向上取整为 1M),实际使用中容易产生误解,建议明确使用+n/-n。
按空文件查找(-empty)
find /tmp -empty # 空文件和空目录
find /tmp -type f -empty # 只找空文件
find /tmp -type d -empty # 只找空目录
find /var/log -type f -empty -delete # 删除空日志文件
按时间查找(-mtime / -mmin / -newer)
三种时间戳
每个文件都有三种时间戳,可用 stat 查看:
stat 1 | tail -3
Access: 2014-07-23 11:56:42.297572398 +0800 # atime
Modify: 2014-07-23 11:56:44.836572907 +0800 # mtime
Change: 2014-07-23 11:56:44.856885177 +0800 # ctime
| 时间戳 | 含义 | 改变方式 |
|---|---|---|
atime |
访问时间(access time) | 使用 cat、tail、head、more、less、vi 等读取过文件(即使未修改);执行文件也会改变 |
mtime |
内容修改时间(modification time) | 使用 vi 修改过文件内容,或使用 echo 重定向写入内容 |
ctime |
状态改变时间(change time) | 文件内容、属主、属组、权限发生变化,或创建硬链接/软链接等 |
注意:
mtime改变时ctime也会随之改变;但ctime改变不一定会改变mtime(例如只修改权限时)。
三种时间戳与 inode 结构、链接数等文件系统元数据的关系,属于文件系统的旁支内容。 详见 阶段03 磁盘与文件系统
-mtime n 的精确语义
以“天”为单位,以当前时刻为基准向前推算,n 的三种写法含义如下:
| 写法 | 含义 |
|---|---|
-mtime n |
恰好在 n 天前的那一个 24 小时区间内被修改(即距今 n×24 小时到 (n+1)×24 小时之间) |
-mtime +n |
距今 超过 (n+1)×24 小时之前被修改(不含 n 本身那一个 24 小时区间,即 n+1 天前及更早) |
-mtime -n |
距今 n×24 小时以内被修改(从现在向前推 n 天,甚至到将来) |
时间轴示意图(以 -mtime 为例,数字代表天数):
1 2 3 4 5 6 7
|-- 2 --|
<------- -2 | | +2 --------------->
(过去) (现在)
-2:最近 2 天(0–48 小时之内)2:第 2 个 24 小时区间(48–72 小时之前)+2:72 小时之前(更早)
记忆口诀:-n 是「最近 n 天内」(新),+n 是「n+1 天以前」(旧),n 是「第 n 天那一天」(精确的一天区间)。运维中日志清理最常用的是 +n。
示例:
find / -mtime 0 # 当天这 24 小时内被修改过的文件
find / -mtime +1 # 修改时间在 48 小时之前(即 2 天前及更早)
find . -mtime -1 # 修改时间在最近 24 小时内(从现在往回推 24 小时,甚至包含将来)
find / -mtime 1 # 修改时间在 24–48 小时之前这一个区间内
实际场景示例:假设当前时间是 2014 年 7 月 23 日 14:35,要查找 7 月 20 日当天被修改过内容的文件:
# 表示 7 月 19 日 14:35 到 7 月 20 日 14:35 之间被修改的文件
find / -mtime 3
以分钟为单位:-mmin / -amin / -cmin
与 -mtime / -atime / -ctime 类似,但单位从“天”变为“分钟”:
| 选项 | 含义 |
|---|---|
-mmin n |
按内容修改时间查找(分钟) |
-amin n |
按访问时间查找(分钟) |
-cmin n |
按状态改变时间查找(分钟) |
示例:查找 30 分钟前到 150 分钟前(即 2 小时区间内)被修改过内容的文件:
find / -mmin +30 -mmin -150
其他时间用例:
find /var/log -mmin -10 # 最近 10 分钟被修改的文件(排障常用)
find /etc -ctime -1 # 最近一天内属性发生变化的文件
find /home -atime +90 # 90 天以上未被访问过的文件(清理候选)
按比较基准查找:-newer / -newermt
# 查找比 /etc/passwd 更新(mtime 更晚)的文件
find /etc -newer /etc/passwd
# 查找 atime 比参考文件更新的文件
find /etc -anewer /tmp/ref
# 查找 ctime 比参考文件更新的文件
find /etc -cnewer /tmp/ref
# 按绝对时间查找:2026-08-01 之后修改的文件
find /var/log -newermt "2026-08-01"
# 查找某个时间区间内修改的文件
find /var/log -newermt "2026-08-01" ! -newermt "2026-08-10"
# 配合 touch 造参考点
touch -d "2026-08-01 00:00:00" /tmp/timestamp
find /data -newer /tmp/timestamp
逻辑组合(-a / -o / -not)
| 运算符 | 等价写法 | 含义 |
|---|---|---|
-a |
-and |
逻辑与(默认,多个条件并列时可省略) |
-o |
-or |
逻辑或 |
-not |
! |
逻辑非 |
\( \) |
— | 分组,改变优先级;括号需转义或加引号 |
优先级:
-not>-a>-o。多个条件并列不写运算符时默认是-a。
# 与(默认):普通文件 且 大于 10M
find /var -type f -size +10M
find /var -type f -a -size +10M # 显式写法
# 或:.conf 或 .cfg 文件
find /etc -name "*.conf" -o -name "*.cfg"
# 非:非普通文件
find /etc -not -type f
find /etc ! -type f
# 分组:(.log 或 .txt)且 大于 1M
find /var \( -name "*.log" -o -name "*.txt" \) -a -size +1M
# 复杂组合:属主是 root 且(权限 777 或 设置了 SUID)
find / -user root \( -perm 777 -o -perm -4000 \) -type f 2>/dev/null
# 排除某目录后再查找
find / -path "/proc" -prune -o -name "*.core" -print
-exec、-ok 与 xargs 的区别
三者都用于「对查找结果做进一步处理」,区别如下:
| 方式 | 语法 | 执行特点 |
|---|---|---|
-exec cmd {} \; |
find 内建动作 | 每找到一个文件就调用一次命令,fork 次数多,效率低;能处理含空格/特殊字符的文件名 |
-exec cmd {} + |
find 内建动作 | 把多个结果拼成一条命令执行(类似 xargs),效率高 |
-ok cmd {} \; |
find 内建动作 | 同 -exec ... \;,但每次执行前都会询问 y/n 确认,安全但不适合批量 |
\| xargs cmd |
管道 + xargs | 把结果作为参数批量传给命令,效率高;默认按空白拆分,文件名含空格会出错 |
语法要点:
{}代表查找到的每一个文件(占位符)。\;是-exec的结束符,分号必须转义(也可写';'或";"),否则被 Shell 当作命令分隔符。+结尾时不需要转义。
# -exec 逐个执行
find /etc -name "*.conf" -exec ls -l {} \;
# -exec ... + 批量执行(推荐,效率高)
find /etc -name "*.conf" -exec ls -l {} +
# -ok 逐个确认(会提示 < ls ... /etc/xxx.conf > ?)
find /tmp -name "*.tmp" -ok rm {} \;
# xargs 批量执行
find /etc -name "*.conf" | xargs ls -l
# 结果太多时 -exec 与 xargs 都能避免「参数列表过长」错误
find /var/log -name "*.log" -mtime +30 | xargs rm -f
-print0 与 xargs -0 的配合
文件名中包含空格、换行、引号时,find | xargs 会把一个文件名拆成多个参数,导致误删或报错。正确做法是用 NUL 字符(\0)作分隔:
# 错误:文件名 "my file.txt" 会被拆成 "my" 和 "file.txt"
find . -name "*.txt" | xargs rm
# 正确写法一:-print0 配 xargs -0
find . -name "*.txt" -print0 | xargs -0 rm
# 正确写法二:直接用 -exec(本身就安全)
find . -name "*.txt" -exec rm {} +
# 结合 grep:在含空格文件名中搜索
find . -name "*.log" -print0 | xargs -0 grep "ERROR"
find 批量改名的安全写法
# 1. 用 -exec 配合 shell 完成改名:.txt -> .bak
find . -type f -name "*.txt" -exec sh -c 'mv "$0" "${0%.txt}.bak"' {} \;
# 2. 用 while read 循环(-print0 + read -d '' 处理特殊文件名)
find . -type f -name "*.txt" -print0 | while IFS= read -r -d '' f; do
mv -- "$f" "${f%.txt}.bak"
done
# 3. 使用 rename 命令(Perl 版 rename,语法为正则)
find . -type f -name "*.txt" -exec rename 's/\.txt$/.bak/' {} +
# 4. 批量给文件加统一前缀
find . -maxdepth 1 -type f -exec sh -c 'mv "$0" "backup_$(basename "$0")"' {} \;
# 5. 批量把文件名转为小写
find . -type f -exec sh -c 'mv "$0" "$(dirname "$0")/$(basename "$0" | tr "A-Z" "a-z")"' {} \;
find 批量删除的安全写法
删除是不可逆操作,务必遵循「先看后删」原则:
# 第 1 步:先只打印,确认结果无误(务必执行这一步!)
find /var/log -name "*.log" -mtime +30 -type f -print
# 第 2 步:确认后再删除,以下三种写法任选
find /var/log -name "*.log" -mtime +30 -type f -delete
find /var/log -name "*.log" -mtime +30 -type f -exec rm -f {} +
find /var/log -name "*.log" -mtime +30 -type f -print0 | xargs -0 rm -f
# 逐个确认删除(最安全)
find /var/log -name "*.log" -mtime +30 -ok rm {} \;
# 删除空目录(-depth 保证先处理子目录)
find /data -depth -type d -empty -delete
安全注意事项:
- 务必加
-type f,否则可能误删目录。 - 路径写绝对路径,避免在错误的当前目录执行。
-delete必须写在条件之后;写在前面会先删除整棵目录树(find . -delete -name "*.log"是灾难性写法)。- 变量拼接路径时要防止变量为空:
find "${DIR:?DIR未设置}" ...。 - 全盘查找时用
2>/dev/null屏蔽Permission denied噪音,但不要用它掩盖真实错误。
3.2 locate 与 updatedb
locate 通过系统预建数据库快速查找文件,速度远快于 find,但结果可能不是最新的。
locate hello
updatedb
数据库路径通常为
/var/lib/mlocate/mlocate.db。新建文件需执行updatedb后才能被 locate 找到。
| 命令 / 选项 | 说明 |
|---|---|
locate 关键字 |
在数据库中模糊查找路径包含关键字的文件 |
locate -i 关键字 |
忽略大小写 |
locate -n 10 关键字 |
只显示前 10 条结果 |
locate -c 关键字 |
只显示匹配数量 |
locate -r '正则' |
使用基本正则表达式匹配 |
locate -b '\name' |
只匹配文件名(basename),\ 表示精确匹配整个名字 |
locate -e 关键字 |
只显示当前实际存在的文件(过滤已删除的记录) |
updatedb |
手动更新数据库(需 root 权限,耗时较长) |
配置文件 /etc/updatedb.conf 可指定不索引的路径与文件系统类型:
cat /etc/updatedb.conf
# PRUNE_BIND_MOUNTS = "yes"
# PRUNEFS = "9p afs ... nfs proc sysfs tmpfs"
# PRUNEPATHS = "/afs /media /mnt /net /sfs /tmp /udev /var/cache/ccache /var/spool/cups ..."
find 与 locate 对比:
| 对比项 | find | locate |
|---|---|---|
| 原理 | 实时遍历磁盘 | 查询预建数据库 |
| 速度 | 慢 | 极快 |
| 实时性 | 完全准确 | 依赖 updatedb,新文件可能查不到 |
| 查找条件 | 极其丰富(类型、大小、时间、权限…) | 只能按路径名 |
| 后续处理 | 支持 -exec / -delete |
不支持 |
| CentOS 安装 | 默认自带(findutils) | 需安装 mlocate 包 |
# CentOS/RHEL 安装 locate
yum install -y mlocate
updatedb
3.3 which / whereis / type 的区别
这三个命令用于查找命令本身的位置,而不是普通文件。
which
在 $PATH 指定的目录中查找命令路径。
which ifconfig
which -a python # 显示 PATH 中所有同名命令,而不只是第一个
特点:只搜索 $PATH,只找可执行文件;找不到 Shell 内建命令和别名(部分发行版的 which 是别名包装,能显示 alias)。
whereis
查找命令路径、帮助文档等信息。
whereis ifconfig
| 选项 | 说明 |
|---|---|
-b |
只查找二进制程序 |
-m |
只查找 man 手册页 |
-s |
只查找源代码 |
-u |
查找不含上述某类文件的条目 |
特点:在固定的系统目录列表中查找二进制、man 手册与源码,速度快但不搜索用户自定义路径。
whereis -b ls # 只看二进制:/usr/bin/ls
whereis -m ls # 只看手册:/usr/share/man/man1/ls.1.gz
type
Bash 内建命令,用于判断一个名字究竟是什么(内建命令、别名、函数、外部程序)。
type cd # cd is a shell builtin
type ls # ls is aliased to `ls --color=auto'
type -a ls # 列出所有可能的形式
type -t ls # 只输出类型:alias / builtin / function / file / keyword
type -P ls # 强制只查 PATH 中的可执行文件路径
三者对比:
| 命令 | 查找范围 | 能否识别内建/别名/函数 | 典型用途 |
|---|---|---|---|
which |
$PATH 目录 |
一般不能(部分发行版可识别别名) | 快速拿到可执行文件路径 |
whereis |
固定系统目录 | 不能 | 同时找二进制、手册、源码 |
type |
Shell 全部命令解析规则 | 能 | 判断命令的真实身份,排查「命令被别名/函数覆盖」问题 |
# 典型排错场景:为什么 rm 会提示确认?
type rm
# rm is aliased to `rm -i'
4. sed 流编辑器
sed(Stream EDitor,流编辑器)是「Linux 三剑客」中负责按行编辑的工具,擅长对文本做删除、替换、插入、追加等批量修改,尤其适合非交互式地修改配置文件。
4.1 工作原理与模式空间
sed 的处理流程:
- 从输入(文件或标准输入)中读取一行,去掉行尾换行符,存入模式空间(pattern space)。
- 用脚本中的地址与命令对模式空间的内容进行处理。
- 处理结束后,自动把模式空间内容打印到标准输出(除非使用
-n抑制),并加回换行符。 - 清空模式空间,读取下一行,重复上述过程,直到输入结束。
两个缓冲区:
| 缓冲区 | 名称 | 说明 |
|---|---|---|
| 模式空间 | pattern space | 存放当前处理行,sed 命令默认操作的对象 |
| 保持空间 | hold space | 辅助缓冲区,初始为空(含一个空行),只能通过 h H g G x 与模式空间交换数据 |
关键结论:sed 默认不修改源文件,只把结果输出到屏幕;要修改源文件必须使用
-i选项。
4.2 基本格式与选项
sed 参数 "表达式" [操作文件]...
sed [选项] '地址command' file
表达式由两部分组成:
- 范围(地址):行号或正则表达式,未指定则默认所有行。
- 动作(命令):
d删除、s替换、p打印、i插入、a追加。
| 参数 | 作用 |
|---|---|
-e |
指定表达式(可多次使用,执行多个脚本) |
-n |
取消默认输出(quiet / silent),通常与 p 联用 |
-i |
直接修改原文件(in-place),慎用 |
-i.bak |
修改原文件的同时生成 .bak 后缀的备份 |
-r |
启用扩展正则表达式(ERE) |
-E |
同 -r(POSIX 标准写法,新版 GNU sed 支持) |
-f 脚本文件 |
从文件中读取 sed 脚本 |
-s |
把多个文件视为独立的流(默认视为一个连续的流) |
-z |
用 NUL 字符而不是换行符分隔行 |
--help |
查看帮助 |
# 多个 -e 表达式
sed -e '1d' -e 's/root/admin/g' /etc/passwd
# 用分号在一个表达式中写多个命令(等价写法)
sed '1d; s/root/admin/g' /etc/passwd
# 从脚本文件读取命令
cat > /tmp/my.sed <<'EOF'
/^#/d
/^$/d
s/root/admin/g
EOF
sed -f /tmp/my.sed /etc/passwd
4.3 地址定位(范围)
| 地址形式 | 含义 |
|---|---|
| (省略) | 所有行 |
n |
第 n 行 |
$ |
最后一行 |
n,m |
第 n 行到第 m 行 |
n,$ |
第 n 行到最后一行 |
n,+m |
从第 n 行开始,往后再匹配 m 行(共 m+1 行) |
n~step |
从第 n 行开始,每隔 step 行(步进);如 1~2 表示奇数行,0~2 表示偶数行 |
/正则/ |
匹配正则的所有行 |
\%正则% |
用 % 代替 / 作分隔符(正则中含 / 时很有用) |
/正则1/,/正则2/ |
从匹配正则1的行到匹配正则2的行(区间) |
/正则/,+n |
从匹配行开始往后 n 行 |
n,/正则/ |
从第 n 行开始到首次匹配正则的行 |
地址! |
取反,对不匹配该地址的行执行命令 |
first,~N |
从 first 行到下一个行号能被 N 整除的行 |
sed -n '5p' /etc/passwd # 第 5 行
sed -n '$p' /etc/passwd # 最后一行
sed -n '2,5p' /etc/passwd # 第 2~5 行
sed -n '5,$p' /etc/passwd # 第 5 行到末尾
sed -n '2,+3p' /etc/passwd # 第 2 行及其后 3 行(共 4 行)
sed -n '1~2p' /etc/passwd # 奇数行
sed -n '0~2p' /etc/passwd # 偶数行
sed -n '/root/p' /etc/passwd # 含 root 的行
sed -n '/^root/,/^bin/p' /etc/passwd # 从 root 行到 bin 行
sed -n '/root/,+2p' /etc/passwd # 匹配行及其后 2 行
sed -n '/root/!p' /etc/passwd # 不含 root 的行(取反)
sed -n '\%/bin/bash%p' /etc/passwd # 用 % 作分隔符,避免转义 /
4.4 常用动作(命令)全表
| 命令 | 全称 / 含义 | 说明 |
|---|---|---|
p |
打印模式空间内容,常配合 -n |
|
P |
Print first line | 打印模式空间中第一个换行符之前的内容 |
d |
delete | 删除模式空间内容,开始下一循环 |
D |
Delete first line | 删除模式空间中第一个换行符之前的内容,若还有剩余则不读新行重启循环 |
s |
substitute | 替换,格式 s/旧/新/标志 |
a text |
append | 在当前行之后追加文本 |
i text |
insert | 在当前行之前插入文本 |
c text |
change | 用文本替换整行(区间地址时整个区间替换为一次文本) |
y/abc/xyz/ |
transform | 按字符逐一映射转换(类似 tr),两侧字符数必须相等 |
q [退出码] |
quit | 打印当前模式空间后立即退出 sed |
Q [退出码] |
Quit | 立即退出,不打印当前行 |
r 文件 |
read file | 在当前行之后读入并输出指定文件的内容 |
R 文件 |
Read line | 每次只读入指定文件的一行 |
w 文件 |
write file | 把模式空间内容写入指定文件 |
W 文件 |
Write first line | 把模式空间第一行写入文件 |
n |
next | 打印模式空间(除非 -n),读入下一行到模式空间,继续执行后续命令 |
N |
Next append | 把下一行追加到模式空间(用 \n 连接),形成多行模式空间 |
h |
hold | 用模式空间内容覆盖保持空间 |
H |
Hold append | 把模式空间内容追加到保持空间 |
g |
get | 用保持空间内容覆盖模式空间 |
G |
Get append | 把保持空间内容追加到模式空间 |
x |
exchange | 交换模式空间与保持空间的内容 |
l |
list | 显示不可打印字符(以 \t、$ 等形式) |
= |
line number | 打印当前行号 |
b 标签 |
branch | 无条件跳转到标签 |
t 标签 |
test | 若上一次 s 替换成功则跳转到标签 |
T 标签 |
Test fail | 若上一次 s 替换失败则跳转 |
:标签 |
label | 定义跳转标签 |
{ } |
命令组 | 把多个命令组合到同一地址下 |
# |
注释 | sed 脚本中的注释 |
4.5 打印 p
sed -n '3p' /etc/passwd # 第 3 行
sed -n '3p;7p' /etc/passwd # 第 3 行和第 7 行
sed -n '3,5p;7p' /etc/passwd # 第 3~5 行和第 7 行
sed -n '/root/p' /etc/passwd # 包含 root 的行
sed -n '/^root/p' /etc/passwd # 以 root 开头的行
sed -n '/[0-9]/p' /etc/passwd # 包含数字的行
sed -n '/^[^[:blank:]]/p' passwd # 非空格开头的行
不加
-n时,匹配行会被打印两次(一次是p命令,一次是 sed 的默认输出),这是初学者最常见的困惑。
sed '/root/p' /etc/passwd # root 行出现两次
sed -n '/root/p' /etc/passwd # 正确:只出现一次
sed -n '$=' /etc/passwd # 用 = 打印总行数(等价 wc -l)
4.6 插入与追加 i / a
sed -i '2i content' passwd # 在第 2 行前插入
sed -i '2a content' passwd # 在第 2 行后追加
更多用法:
# 在文件首行前插入
sed '1i #!/bin/bash' script.sh
# 在最后一行后追加
sed '$a # end of file' script.sh
# 在匹配行前后插入
sed '/^\[mysqld\]/a max_connections = 1000' /etc/my.cnf
sed '/ServerName/i # 以下是主机名配置' /etc/httpd/conf/httpd.conf
# 每一行后都追加一行(默认地址为所有行)
sed 'a ---' file
# 插入多行(用 \n 或续行符)
sed '1i line1\nline2' file
sed '1i\
line1\
line2' file
# 用 c 命令整行替换
sed '/^SELINUX=/c SELINUX=disabled' /etc/selinux/config
sed '2,4c 这三行被合并替换为一行' file
GNU sed 允许
a text紧跟文本;标准 POSIX 写法是a\换行再写文本。为兼容性,脚本中推荐sed -e 'a\' -e 'text'或使用 GNU 简写并注明依赖。
4.7 删除 d
sed '3d' mypasswd # 删除第 3 行(不修改原文件)
sed -i '3d' mypasswd # 删除第 3 行(修改原文件)
sed -e '2,3d' passwd # 删除第 2~3 行
sed -e '1d;5d' passwd # 删除第 1 行和第 5 行
sed -e '/^$/d' passwd # 删除空行
sed -e '/^[[:blank:]]/d' /etc/passwd # 删除空格开头的行
sed -e '/^#/d;/^$/d' passwd # 删除注释行和空行
更多删除用法:
sed '$d' file # 删除最后一行
sed '2,$d' file # 只保留第 1 行
sed '/root/d' /etc/passwd # 删除含 root 的行
sed '/^#/!d' file # 取反:只保留注释行
sed -E '/^[[:space:]]*(#|$)/d' /etc/my.cnf # 删除空行、纯空白行和缩进注释行
sed '/^BEGIN$/,/^END$/d' file # 删除 BEGIN 到 END 之间的区块
sed '1~2d' file # 删除奇数行(只留偶数行)
4.8 替换 s
s 命令格式:s/查找模式/替换内容/标志。分隔符 / 可以换成任意字符(如 #、|、,),当模式中包含 / 时非常有用。
| 标志 | 作用 |
|---|---|
| (无) | 只替换每行第一个匹配 |
g |
global,替换每行所有匹配 |
数字 N |
只替换每行第 N 个匹配 |
Ng |
从第 N 个匹配开始,替换到行尾所有匹配 |
p |
替换成功后打印该行(配合 -n 只输出被修改的行) |
i / I |
匹配时忽略大小写 |
w 文件 |
把替换成功的行写入指定文件 |
e |
把替换结果作为 Shell 命令执行(GNU 扩展) |
m / M |
多行模式,^ $ 匹配每个内嵌行的首尾 |
替换内容中的特殊符号:
| 符号 | 含义 |
|---|---|
& |
代表整个被匹配到的内容 |
\1 ~ \9 |
反向引用第 n 个分组匹配的内容 |
\n |
换行符 |
\U / \L |
之后的内容转为大写 / 小写(GNU 扩展) |
\u / \l |
下一个字符转为大写 / 小写(GNU 扩展) |
基础示例:
sed -e 's/root/soul/' mypasswd # 每行第一个 root 替换为 soul
sed -e 's/root/soul/2' mypasswd # 每行第二个 root 替换为 soul
sed -e 's/root/soul/g' mypasswd # 全部替换
sed -e 's/^[ ]*//g' /etc/passwd # 删除行首空格
sed -e '1,10s/root/soul/g' mypasswd # 1~10 行替换
sed -e '10,$s/root/soul/g' mypasswd # 10 行到末尾替换
sed -e '10,$s/[a-z]/K/g' mypasswd # 小写替换为 K
sed -e "s/^.//" mypasswd # 删除每行第一个字符
sed -e "s/^./A/" mypasswd # 第一个字符替换为 A
sed -e 's/.$//' mypasswd # 删除每行最后一个字符
sed -r -e 's/.(.)$/\1/' mypasswd # 删除倒数第二个字符
sed -r -e 's/[0-9]+//2' passwd # 删除每行第二个数字
更换分隔符与使用 &:
# 替换路径时改用 # 作分隔符,免去大量转义
sed 's#/usr/local/bin#/opt/bin#g' file
sed 's|/etc/nginx|/usr/local/nginx/conf|g' file
# 等价的转义写法(可读性差)
sed 's/\/usr\/local\/bin/\/opt\/bin/g' file
# & 引用整个匹配:给所有数字加上括号
echo "abc 123 def 456" | sed -r 's/[0-9]+/(&)/g'
# 输出:abc (123) def (456)
# 给每行首尾加引号
sed 's/.*/"&"/' file
# 给匹配到的行加注释符
sed 's/^PermitRootLogin/#&/' /etc/ssh/sshd_config
反向引用与分组:
# 交换两个字段(用户名与 UID)
echo "root:0" | sed -r 's/(.*):(.*)/\2:\1/'
# 输出:0:root
# 提取版本号
echo "nginx version: nginx/1.20.1" | sed -r 's#.*/([0-9.]+)$#\1#'
# 大小写转换(GNU 扩展)
echo "hello world" | sed -r 's/(\w+)/\U\1/' # HELLO world
echo "hello world" | sed -r 's/(\w+)/\u\1/g' # Hello World
echo "HELLO" | sed -r 's/.*/\L&/' # hello
# 忽略大小写替换
sed 's/root/admin/Ig' /etc/passwd
# 只输出被修改的行
sed -n 's/root/admin/gp' /etc/passwd
# 把替换成功的行另存
sed -n 's/root/admin/gw /tmp/changed.txt' /etc/passwd
子表达式引用
| 模式 | 说明 |
|---|---|
\(...\) |
未加 -r 时需转义的小括号 |
(...) |
加 -r 后无需转义 |
\1 \2 ... |
依次引用第 1、2 ... 个子表达式匹配内容 |
4.9 按位置删除字符
echo 123456789ABCDEF | sed -r 's/^(..)...(.*)$/\1\2/' # 删除第 3~5 字符
echo 123456789ABCDEF | sed -r 's/^(.{2}).(.*)$/\1\2/' # 删除第 3 字符
echo 123456789ABCDEF | sed -r 's/^(.{2}).{10}(.*)$/\1\2/' # 删除第 3~12 字符
4.10 y 命令(字符映射)
# 小写转大写(逐字符映射,两侧长度必须相等)
sed 'y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/' file
# 把冒号换成逗号
sed 'y/:/,/' /etc/passwd
y与s的区别:y是逐字符一对一映射(类似tr),不支持正则;s是字符串替换,支持正则。
4.11 q / r / w / = / l 命令
# q:打印前 5 行后退出(比 sed -n '1,5p' 更高效,大文件不必读完)
sed '5q' bigfile
sed -n '1,5p;5q' bigfile
# q 带退出码,可用于脚本判断
sed '/ERROR/q1' app.log; echo $?
# r:在第 3 行后插入另一个文件的内容
sed '3r /tmp/insert.txt' file
sed '$r /tmp/footer.txt' file # 追加到末尾
sed '/^\[mysqld\]/r /tmp/mysqld.cnf' /etc/my.cnf
# w:把含 root 的行写入新文件
sed -n '/root/w /tmp/root_lines.txt' /etc/passwd
# =:打印行号
sed -n '/root/=' /etc/passwd # 只输出含 root 的行号
sed '=' file | sed 'N;s/\n/\t/' # 行号与内容并列(类似 nl)
# l:显示不可打印字符,排查 Windows 换行符(^M -> \r)
sed -n 'l' dosfile.txt
cat -A dosfile.txt # 等效手段
4.12 多行处理与保持空间
# N:把两行合并为一行(用 \t 连接)
seq 1 6 | sed 'N;s/\n/\t/'
# n:处理奇偶行,删除偶数行
seq 1 6 | sed 'n;d'
# G:每行后加一个空行(保持空间为空,追加一个空行)
sed 'G' file
# 连续两个 G:每行后加两个空行
sed 'G;G' file
# 删除所有空行后再每行后加一个空行(规范化行距)
sed '/^$/d;G' file
# x + 1!G + $!d:整个文件倒序输出(等价 tac)
sed '1!G;h;$!d' file
# 只保留最后 3 行(等价 tail -3)
sed -e ':a' -e '$q;N;4,$D;ba' file
# 把每行内容与上一行交换
sed -n '1h;1!{x;G;p}' file
# 合并连续的空行为一行(等价 cat -s)
sed '/^$/{N;/^\n$/D}' file
# H/G 收集所有行后统一处理(把整个文件读入模式空间)
sed ':a;N;$!ba;s/\n/,/g' file # 把所有换行替换为逗号
4.13 原地修改与备份
# 直接修改原文件(无备份,危险)
sed -i 's/old/new/g' file
# 修改并生成备份 file.bak(强烈推荐)
sed -i.bak 's/old/new/g' file
sed -i'.bak' 's/old/new/g' file
# 备份到带日期的文件名
sed -i".$(date +%F)" 's/old/new/g' file
# 批量修改多个文件
sed -i.bak 's/old/new/g' *.conf
find /etc -name "*.conf" -exec sed -i.bak 's/old/new/g' {} +
安全操作准则:
- 先不加
-i预览结果,确认无误后再加-i。 - 生产环境修改配置文件必须先备份:
cp file file.$(date +%F)或用-i.bak。 -i与-n一起使用会清空文件内容(因为不输出任何内容又写回原文件),务必小心。sed -i会改变文件的 inode(实为新建临时文件再改名),对硬链接、符号链接目标以及正被程序打开的文件需要额外注意。- 涉及
/etc/fstab、/boot/grub2/grub.cfg等关键文件时,改完务必校验语法。
# 反面示例:以下命令会把文件清空!
sed -n -i 's/old/new/g' file # 不要这样写
4.14 sed 实战案例
# 1. 删除配置文件中的注释与空行,只看有效配置
sed -e '/^#/d' -e '/^$/d' /etc/httpd/conf/httpd.conf
sed -E '/^[[:space:]]*(#|$)/d' /etc/my.cnf
# 2. 修改配置项(关闭 SELinux)
sed -i.bak 's/^SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
sed -i 's/^\(SELINUX=\).*/\1disabled/' /etc/selinux/config
# 3. 修改 SSH 端口与禁止 root 登录
sed -i.bak -e 's/^#\?Port .*/Port 2222/' \
-e 's/^#\?PermitRootLogin .*/PermitRootLogin no/' /etc/ssh/sshd_config
# 4. 注释掉某个配置项(前面加 #)
sed -i 's/^\(GSSAPIAuthentication\)/#\1/' /etc/ssh/sshd_config
# 5. 取消注释(打开某个配置项)
sed -i 's/^#\(net.ipv4.ip_forward\)/\1/' /etc/sysctl.conf
# 6. 在指定行前后插入配置
sed -i '/^\[mysqld\]/a character-set-server=utf8mb4' /etc/my.cnf
sed -i '/^http {/i # 由运维脚本自动生成' /etc/nginx/nginx.conf
# 7. 提取行范围
sed -n '10,20p' /var/log/messages # 第 10~20 行
sed -n '/2026-08-22 10:/,/2026-08-22 11:/p' app.log # 提取时间段日志
# 8. 删除行首行尾空白
sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' file
# 9. 转换 Windows 换行符为 Unix(去掉 \r)
sed -i 's/\r$//' dosfile.txt
sed -i 's/\x0D$//' dosfile.txt
# 10. 给文件每行加行号(等价 nl)
sed = file | sed 'N;s/\n/\t/'
# 11. 批量替换 IP 地址
sed -i 's/192\.168\.1\.100/10\.0\.0\.100/g' /etc/hosts
# 12. 只在特定区块内替换(限定地址范围)
sed '/^\[mysqld\]/,/^\[/ s/^port=.*/port=3307/' /etc/my.cnf
# 13. 打印文件的奇数行与偶数行
sed -n '1~2p' file # 奇数行
sed -n '0~2p' file # 偶数行
# 14. 在每行行首加统一前缀
sed 's/^/>> /' file
# 15. 统计并输出总行数
sed -n '$=' file
4.15 sed 综合实用示例
# 提取有线网卡名
ifconfig | sed -rn 's/^(e[0-9a-zA-Z]+).*$/\1/p'
# 截取 IP / 子网掩码(脚本)
#!/bin/bash
export LANG=en_US.UTF-8
export LANGUAGE=en_US:en
link=$(route -n | sed -n '/UG/p' | awk '{print $NF}')
ip=$(ifconfig $link | sed -n '/Mask/p' | sed -r 's/^([^0-9]+)([0-9.]+)(.*)([^0-9.]+)([0-9.]+$)/\2\/\5/')
echo $ip
ifconfig 属于已废弃的 net-tools 工具集,CentOS 7 及以上的最小化安装通常并不自带,推荐使用其现代替代品 ip 命令取 IP:
link=$(route -n | sed -n '/UG/p' | awk '{print $NF}')
ip=$(ip a s $link | sed -n '/inet /p' | awk '{print $2}')
echo $ip
ip a s是ip address show的缩写,输出中inet行的第 2 个字段即「IP/掩码位数」形式的地址。
以上示例涉及
ifconfig、ip、route等网络命令的完整用法与网卡配置,属于网络管理的旁支内容。 详见 阶段07 网络
5. awk 文本处理语言
5.1 awk 的定位
awk 不只是一个命令,而是一门完整的编程语言:它有变量、数组、流程控制、函数,专门为「按行读入、按字段处理、输出报表」这一类任务设计。
「三剑客」的分工可以这样理解:
| 工具 | 处理单位 | 擅长 |
|---|---|---|
grep |
行 | 找:判断一行要不要,做过滤和筛选 |
sed |
行(字符流) | 改:对行做替换、删除、插入、追加 |
awk |
行 + 字段 | 算:按列取值、判断、统计、计算、格式化报表 |
判断标准很简单:
- 只想把包含某个关键词的行挑出来 →
grep - 想把某个字符串批量替换掉、删掉某些行 →
sed - 涉及「第几列」「求和」「计数」「分组」「对齐输出」 →
awk
5.2 三种实现:awk / nawk / gawk
| 实现 | 说明 |
|---|---|
awk |
最初的 AWK(Aho、Weinberger、Kernighan 三人姓氏首字母),1977 年贝尔实验室 |
nawk |
new awk,1985 年的改进版本,增加了用户自定义函数、多文件处理等 |
gawk |
GNU awk,功能最全,Linux 发行版默认实现,/usr/bin/awk 通常是指向 gawk 的软链接 |
mawk |
Mike Brennan 的实现,功能少但执行速度最快,Debian/Ubuntu 常作为默认 awk |
# 查看当前系统的 awk 究竟是哪个实现
ls -l /usr/bin/awk
awk --version | head -1
本文档以 gawk 为准。Linux 中常用的就是 GNU 版本 gawk,本文所有示例默认在 gawk 下可用。
5.3 基本语法结构
awk [选项] 'BEGIN{初始化动作} /pattern/{处理动作} END{结束动作}' 文件...
三个部分都是可选的,可以只写其中任意一个或几个:
awk 'BEGIN{ FS=":" } { print $1 } END{ print NR }' /etc/passwd
| 组成部分 | 执行时机 | 典型用途 |
|---|---|---|
BEGIN{} |
读取第一行之前执行一次 | 设置 FS/OFS、初始化计数器、打印表头 |
pattern{action} |
每读入一行执行一次(pattern 为真时) | 主体处理逻辑 |
END{} |
读完所有行之后执行一次 | 输出汇总结果、总行数、平均值 |
规则的两种省略形式:
awk '/root/' /etc/passwd # 只有 pattern,省略 action,默认动作是 print $0
awk '{print $1}' /etc/passwd # 只有 action,省略 pattern,表示对每一行都执行
单引号包裹整个 awk 程序是硬性习惯:awk 程序里大量使用
$1、$0,若用双引号,Shell 会先把$1当成自己的位置参数替换成空。
5.4 工作流程
awk 的执行过程可以拆成 5 步:
- 执行
BEGIN{}块(此时还没有读任何数据)。 - 从输入中读取一条记录(默认以换行符
\n为记录分隔符,即读一行),整行存入$0。 - 按字段分隔符
FS(默认为空格/制表符)把$0切分成字段$1、$2、…、$NF,字段总数存入NF,已读记录数NR加 1。 - 拿这条记录逐条去匹配程序中所有的
pattern{action}规则:pattern 为真就执行对应的 action;一条记录可以同时命中多条规则,会按书写顺序都执行一遍。 - 回到第 2 步读下一条记录,直到输入结束;输入结束后执行
END{}块。
# 直观演示:一条记录命中两条规则,两个 action 都会执行
awk -F: '/root/{print "命中root: " $1} $3==0{print "UID为0: " $1}' /etc/passwd
5.5 内置变量详解
| 变量 | 含义 |
|---|---|
$0 |
当前完整记录(整行) |
$1 ~ $n |
当前记录的第 n 个字段 |
NF |
Number of Fields,当前记录的字段总数 |
NR |
Number of Records,已读取的记录总数(跨文件累加,相当于全局行号) |
FNR |
File Number of Records,当前文件内的记录号(换文件时重新从 1 开始) |
FS |
Field Separator,输入字段分隔符,默认空格/制表符 |
OFS |
Output Field Separator,输出字段分隔符,默认一个空格 |
RS |
Record Separator,输入记录分隔符,默认换行符 \n |
ORS |
Output Record Separator,输出记录分隔符,默认换行符 \n |
FILENAME |
当前正在处理的文件名 |
ARGC |
命令行参数个数(含 awk 本身) |
ARGV |
命令行参数数组,ARGV[0] 是 awk,ARGV[1] 起是文件名 |
ENVIRON |
环境变量数组,如 ENVIRON["HOME"] |
SUBSEP |
多维数组下标的连接符,默认是 \034(不可打印字符) |
$0 与 $1 ~ $NF
awk '{print $0}' /etc/passwd # 打印所有行(等价于 cat)
awk -F ":" '{print $3}' /etc/passwd # 打印第 3 列
awk -F ":" '{print $NF}' /etc/passwd # 打印最后一列
awk -F ":" '{print $(NF-1)}' /etc/passwd # 打印倒数第二列
awk -F ":" '{print $1 "\t" $3 "welcome"}' /etc/passwd # 多列拼接
$NF是「最后一个字段」,$(NF-1)是倒数第二个。注意括号不能省,$NF-1会被理解为「最后一个字段的值减 1」。
给字段赋值会重建 $0,此时字段间用 OFS 连接:
# 把第 1 列改成大写,$0 被重建
awk -F: -v OFS=: '{$1=toupper($1); print $0}' /etc/passwd
NF:字段数
awk -F ":" '{print NF}' /etc/passwd # 打印每行字段数
awk -F ":" 'NF > 4 {print $0}' /etc/passwd # 字段数大于 4 的行
awk 'NF==0' file # 找出空行
awk 'NF' file # 去掉空行(NF 为 0 即假)
NR 与 FNR
awk -F ":" 'NR == 8 {print $0}' /etc/passwd # 打印第 8 行
awk -F ":" 'NR >= 5 && NR <= 56 {print $0}' /etc/passwd # 打印 5~56 行
awk -F ":" 'END{print NR}' /etc/passwd # 打印总行数(等价 wc -l)
awk '{print NR": "$0}' file # 加行号
awk '{if (NR % 2 == 0) print $0}' /etc/passwd # 只打印偶数行
NR 与 FNR 的区别只在多文件时体现:
awk '{print FILENAME, NR, FNR, $0}' a.txt b.txt
处理 a.txt(3 行)和 b.txt(2 行)时:
a.txt 1 1 ...
a.txt 2 2 ...
a.txt 3 3 ...
b.txt 4 1 ... <-- NR 继续累加,FNR 归 1 重新计数
b.txt 5 2 ...
FS 与 OFS
awk 'BEGIN{FS=":"}{print $1}' /etc/passwd # BEGIN 中设置,等价于 -F:
awk -F: '{print $1}' /etc/passwd
# OFS 只在 $0 被「重建」时才生效
awk -F: -v OFS="---" '{$1=$1; print $0}' /etc/passwd
# 用 print 的逗号连接字段时,逗号会被替换为 OFS
awk -F: -v OFS="\t" '{print $1, $3, $7}' /etc/passwd
要点:
print $1, $3(逗号)用OFS连接;print $1 $3(无逗号)是字符串直接拼接,中间什么都没有。
RS 与 ORS
RS 决定「一条记录」怎么划分,改掉它可以处理非按行组织的数据:
# 以冒号为记录分隔符,把 /etc/passwd 的每个字段变成一行
awk 'BEGIN{RS=":"}{print NR, $0}' /etc/passwd
# RS="" 是特殊值:段落模式,以空行分隔记录,此时换行符自动成为字段分隔符之一
awk 'BEGIN{RS=""}{print "段落"NR": "$1}' paragraph.txt
# ORS 改变输出记录分隔符:把每行结果用逗号连接成一行
awk -F: '{print $1}' ORS="," /etc/passwd; echo
FILENAME
# 标出每一个匹配行来自哪个文件
awk '/error/{print FILENAME": "FNR": "$0}' /var/log/*.log
FILENAME在BEGIN{}块中是空的,因为那时还没打开任何文件。
ARGC 与 ARGV
awk 'BEGIN{
print "参数个数:", ARGC
for (i = 0; i < ARGC; i++) print "ARGV["i"] = " ARGV[i]
}' a.txt b.txt
输出:
参数个数: 3
ARGV[0] = awk
ARGV[1] = a.txt
ARGV[2] = b.txt
ENVIRON
awk 'BEGIN{print ENVIRON["HOME"]; print ENVIRON["PATH"]}'
awk 'BEGIN{print "当前用户: " ENVIRON["USER"]}'
SUBSEP
awk 的「多维数组」其实是用 SUBSEP 把多个下标拼成一个字符串:
# arr[1,2] 实际等价于 arr["1" SUBSEP "2"]
awk 'BEGIN{
arr[1,2] = "x"
for (k in arr) {
split(k, idx, SUBSEP)
print idx[1], idx[2], arr[k]
}
}'
5.6 常用选项
| 选项 | 作用 |
|---|---|
-F |
指定输入字段分隔符 FS |
-v |
在程序开始前定义变量,把 Shell 的值传进 awk |
-f |
从脚本文件读取 awk 程序 |
-F 指定分隔符
awk -F: '{print $1}' /etc/passwd # 单字符分隔符
awk -F, '{print $2}' data.csv
awk -F'\t' '{print $2}' data.tsv # 制表符
# 多字符分隔符:整个字符串作为一个分隔符
awk -F'::' '{print $2}' file
# 正则分隔符:分隔符本身可以是正则表达式
awk -F'[:,]' '{print $2}' file # 冒号或逗号都算分隔符
awk -F'[ \t]+' '{print $2}' file # 一个或多个空格/制表符
awk -F'[0-9]+' '{print $1}' file # 连续数字作为分隔符
awk 'BEGIN{FS="[^A-Za-z]+"}{print $1}' /etc/passwd # 打印每行第一个单词
特别注意:
-F后面若是单个空格(-F' '),awk 使用默认的「空白分隔」规则,会自动忽略行首空白并把连续空白当一个分隔符。若写成-F'[ ]'则严格按每一个空格切分,连续两个空格会切出空字段。
-v 传入变量
# 把 Shell 变量传给 awk(awk 内部无法直接读 Shell 变量)
name="root"
awk -v user="$name" -F: '$1 == user {print $0}' /etc/passwd
# 多个 -v
awk -v a=10 -v b=20 'BEGIN{print a + b}'
# 修改内置变量也可以用 -v
awk -v FS=: -v OFS="\t" '{$1=$1; print}' /etc/passwd
常见错误写法:
awk '$1 == "'$name'" {print}'(在单引号中拼接 Shell 变量)。这种写法一旦变量含空格或特殊字符就会出错,始终用-v传参。
-f 指定脚本文件
程序较长时写入独立文件,扩展名习惯用 .awk:
#!/bin/awk
BEGIN{
FS=":"
count=0
}
{
if($7 == "/sbin/nologin"){
count += 1
print $0
}
}
END{
print "count=" count
}
awk -f 1.awk /etc/passwd
也可以指定多个 -f 依次加载多个脚本,常用于把公共函数库单独抽出:
awk -f lib.awk -f main.awk data.txt
5.7 BEGIN 与 END 块
# BEGIN 不需要输入文件也能执行,可以把 awk 当计算器
awk 'BEGIN{print 3 * 4 + 2}'
awk 'BEGIN{printf "%.3f\n", 22/7}'
# BEGIN 打印表头,END 打印汇总
awk -F: 'BEGIN{print "用户名\tUID"}
{print $1"\t"$3}
END{print "共 " NR " 个用户"}' /etc/passwd
END 块中的 $0 与各字段仍保留最后一条记录的内容:
awk -F: 'END{print $NF}' /etc/passwd # 取到的是末行的末字段
awk 'END{print $0}' file # 等价于 tail -1
5.8 print 与 printf
| 命令 | 特点 |
|---|---|
print |
自动在末尾添加 ORS(默认换行),参数间的逗号替换为 OFS |
printf |
不自动换行,需要手写 \n;支持格式化,可控制宽度与对齐 |
print $1, $2 # 用 OFS 分隔,自动换行
print $1 $2 # 直接拼接,自动换行
printf "%s %s\n", $1, $2 # 格式化,需自己写 \n
常用格式化符:
| 格式符 | 含义 |
|---|---|
%s |
字符串 |
%d / %i |
十进制整数(小数会被截断,不是四舍五入) |
%f |
浮点数,默认 6 位小数 |
%.2f |
浮点数保留 2 位小数(会四舍五入) |
%e |
科学计数法 |
%c |
单个字符 |
%o / %x / %X |
八进制 / 小写十六进制 / 大写十六进制 |
%% |
一个百分号本身 |
%10s |
宽度 10,右对齐,不足补空格 |
%-10s |
宽度 10,左对齐(减号表示左对齐) |
%010d |
宽度 10,右对齐,不足补 0 |
%10.2f |
宽度 10,保留 2 位小数 |
%*s |
宽度由参数动态指定 |
转义序列:\n 换行、\t 制表符、\\ 反斜杠、\" 双引号、\r 回车。
格式化输出对齐表格:
awk -F: 'BEGIN{
printf "%-20s %-8s %-8s %-20s\n", "USERNAME", "UID", "GID", "SHELL"
printf "%-20s %-8s %-8s %-20s\n", "--------", "---", "---", "-----"
}
{
printf "%-20s %-8d %-8d %-20s\n", $1, $3, $4, $7
}' /etc/passwd
输出效果:
USERNAME UID GID SHELL
-------- --- --- -----
root 0 0 /bin/bash
bin 1 1 /sbin/nologin
daemon 2 2 /sbin/nologin
printf的格式字符串会被循环复用:如果参数个数多于格式符个数,awk 会把格式串重头再用一遍。例如printf "%s\n", $1, $2会输出两行。
5.9 pattern 的各种写法
pattern 就是「这一行要不要处理」的条件,写法非常多:
正则匹配整行 /re/
awk '/root/' /etc/passwd # 整行含 root
awk '/^root/{print $0}' /etc/passwd # 以 root 开头
awk '/nologin$/{print $1}' /etc/passwd # 以 nologin 结尾
awk '/^$/{print NR}' file # 空行的行号
awk -F: '/^(root|bin)/{print $1}' /etc/passwd # awk 使用 ERE,无需转义括号和竖线
指定字段匹配 $1 ~ /re/ 与 !~
awk -F: '$1 ~ /^r/{print $1}' /etc/passwd # 第 1 字段以 r 开头
awk -F: '$7 ~ /nologin/{print $1}' /etc/passwd # 第 7 字段含 nologin
awk -F: '$7 !~ /nologin/{print $1}' /etc/passwd # 第 7 字段不含 nologin
awk '$0 !~ /^#/' /etc/fstab # 非注释行
~是「匹配」,!~是「不匹配」。/re/单独作为 pattern 时等价于$0 ~ /re/。
关系表达式
| 运算符 | 含义 |
|---|---|
== |
等于 |
!= |
不等于 |
> >= < <= |
大小比较 |
awk -F: '$3 == 0{print $1}' /etc/passwd # UID 为 0
awk -F: '$3 > 1000{print $1, $3}' /etc/passwd # UID 大于 1000
awk -F: '$1 == "root"{print $0}' /etc/passwd # 字符串比较要加引号
awk 'NF != 7' /etc/passwd # 字段数不是 7 的异常行
类型陷阱:awk 会根据上下文自动判断数字/字符串。
$3 > 100按数字比较,$3 > "100"按字符串比较("9" > "100" 为真)。字段来自文件时若两边都像数字,按数字比较;只要一边是带引号的字符串常量,就按字符串比较。
逻辑运算 && / || / !
awk -F: '$3 >= 30 && $3 <= 40{print $1}' /etc/passwd # UID 在 30~40
awk -F: '$3 == 0 || $3 > 1000{print $1}' /etc/passwd # UID 为 0 或大于 1000
awk -F: '!($3 > 100){print $1}' /etc/passwd # UID 不大于 100
awk '/root/ && /bash/' /etc/passwd # 同时含 root 和 bash
awk -F: 'NR > 1 && $7 !~ /nologin/ {print $1}' /etc/passwd
范围模式 /start/,/end/
从匹配 start 的行开始,到匹配 end 的行结束(含首尾),可以多次触发:
awk '/^root/,/^daemon/' /etc/passwd # root 行到 daemon 行
awk 'NR==5,NR==10' /etc/passwd # 第 5 到第 10 行
awk '/BEGIN_MARK/,/END_MARK/{print}' file # 提取标记块
awk '/\[mysqld\]/,/^\[/{print}' my.cnf # 提取配置段
若 end 模式一直没匹配到,范围会一直延续到文件末尾。
表达式为真
awk 中「非 0 数字」和「非空字符串」为真,0 和空字符串为假:
awk 'NF' file # NF 非 0 即打印,效果是删除空行
awk '$3' data.txt # 第 3 字段非 0 非空的行
awk -F: '$3' /etc/passwd # 排除 UID 为 0 的行(root)
awk 'length > 80' file # 长度超过 80 的行(length 省略参数默认为 $0)
awk 'length($1) > 8' file # 第 1 字段长度超过 8
pattern 的组合小结
awk 'BEGIN{...}' # 只在开始执行
awk 'END{...}' # 只在结束执行
awk '/re/' # 正则
awk '表达式' # 表达式为真
awk '/re1/,/re2/' # 范围
awk 'pattern1 && pattern2' # 复合条件
5.10 流程控制
if / else if / else
awk -F: '{
if ($3 == 0)
print $1, "超级用户"
else if ($3 < 1000)
print $1, "系统用户"
else
print $1, "普通用户"
}' /etc/passwd
# 单行写法
awk -F: '{if($7=="/sbin/nologin") n++} END{print n}' /etc/passwd
# 三元运算符,等价于 if-else 的简写
awk -F: '{print $1, ($3==0 ? "root权限" : "普通")}' /etc/passwd
while
# 逐个打印每行的所有字段
awk '{
i = 1
while (i <= NF) {
print "字段" i ": " $i
i++
}
}' file
awk 'BEGIN{
i = 1
while (i <= 5) { print i; i++ }
}'
do-while
至少执行一次循环体,再判断条件:
awk 'BEGIN{
i = 1
do {
print i
i++
} while (i <= 5)
}'
for
# C 风格 for
awk '{for (i = 1; i <= NF; i++) printf "%s ", $i; print ""}' file
# 反向输出每行的字段(等价于按字段 rev)
awk '{for (i = NF; i >= 1; i--) printf "%s%s", $i, (i>1 ? OFS : ORS)}' file
# 打印 9x9 乘法表
awk 'BEGIN{
for (i = 1; i <= 9; i++) {
for (j = 1; j <= i; j++)
printf "%d*%d=%-4d", j, i, i*j
print ""
}
}'
for-in 遍历数组
awk -F: '{shell[$7]++} END{for (s in shell) print s, shell[s]}' /etc/passwd
注意:
for-in的遍历顺序是不确定的(基于哈希实现),绝不能依赖它。需要有序输出时,把结果管道给sort,或在 gawk 中用PROCINFO["sorted_in"]="@ind_str_asc"指定排序方式。
break / continue
# break 跳出整个循环
awk 'BEGIN{for(i=1;i<=10;i++){if(i==5) break; print i}}' # 输出 1 2 3 4
# continue 跳过本次迭代
awk 'BEGIN{for(i=1;i<=10;i++){if(i%2==0) continue; print i}}' # 输出奇数
next 与 nextfile
# next:立即结束当前行的处理,读入下一行(跳过后续所有规则)
awk '/^#/{next} /^$/{next} {print}' /etc/fstab # 跳过注释行和空行
awk 'NR==1{next} {sum+=$2} END{print sum}' data.csv # 跳过 CSV 表头再求和
# nextfile(gawk):立即放弃当前文件的剩余内容,切换到下一个文件
awk 'FNR>10{nextfile} {print FILENAME": "$0}' *.log # 每个文件只取前 10 行
next与continue的区别:continue只作用于 awk 程序内部的 for/while 循环,next作用于 awk 自身「读行」的主循环。
exit
# exit 立即停止读取输入,但仍会执行 END 块
awk 'NR==10{print; exit}' file # 打印第 10 行后立刻退出,大文件很省时
awk '/error/{found=1; exit} END{print found?"有错误":"无错误"}' app.log
# exit 可以带退出码,供 Shell 的 $? 判断
awk '/CRITICAL/{exit 1}' app.log; echo $?
5.11 数组
关联数组的本质
awk 只有一种数组:关联数组(哈希表)。它的下标本质上都是字符串,即使写成数字也会被转成字符串。
awk 'BEGIN{
a[1] = "x"
a["1"] = "y" # 覆盖了 a[1],因为下标 1 和 "1" 是同一个键
print a[1] # 输出 y
print length(a) # 输出 1,数组只有一个元素
}'
数组不需要预先声明,也不需要连续下标:
awk 'BEGIN{
fruit["apple"] = 3
fruit["banana"] = 5
fruit[100] = 1
print fruit["apple"], fruit[100]
}'
判断元素是否存在
# 正确写法:用 in 判断,不会创建元素
awk 'BEGIN{a["x"]=1; if ("x" in a) print "存在"}'
# 错误写法:if (a["y"] != "") 这种引用会「顺手创建」一个空元素
awk 'BEGIN{ if (a["y"]=="") ; print length(a) }' # 输出 1,y 被创建了
delete 删除元素
awk 'BEGIN{
a[1]=1; a[2]=2; a[3]=3
delete a[2] # 删除单个元素
print length(a) # 2
delete a # 清空整个数组(gawk 支持)
print length(a) # 0
}'
for-in 遍历
awk 'BEGIN{
a["x"]=1; a["y"]=2; a["z"]=3
for (k in a) print k, a[k]
}'
再次强调:遍历顺序不保证,需要有序请外接
sort。
经典写法一:数组去重
利用「同一个键只会存在一次」的特性,不排序即可去重,而且保持原始出现顺序:
# 去重(保持首次出现的顺序),等价于 sort -u 但不改变顺序
awk '!a[$0]++' file
# 按第 1 字段去重
awk '!a[$1]++' file
# 原理拆解:
# a[$0]++ 先取 a[$0] 的值(首次为 0,即假),再自增
# ! 取反:首次 !0 为真 -> 打印;之后 !1 为假 -> 不打印
对比三种去重方式:
sort -u file # 去重 + 排序,顺序被改变
sort file | uniq # 同上
awk '!a[$0]++' file # 去重,保持原顺序,只遍历一次
经典写法二:数组计数
# 统计每种 shell 被多少用户使用
awk -F: '{count[$7]++} END{for (s in count) print count[s], s}' /etc/passwd | sort -rn
# 统计每个单词出现次数
awk '{for(i=1;i<=NF;i++) w[$i]++} END{for(k in w) print w[k], k}' file | sort -rn | head
经典写法三:按字段分组求和
# 按第 1 列分组,对第 2 列求和
awk '{sum[$1] += $2} END{for (k in sum) print k, sum[k]}' data.txt
# 分组求和 + 计数 + 求平均
awk '{sum[$1]+=$2; cnt[$1]++}
END{for (k in sum) printf "%-10s 总和=%-10.2f 平均=%.2f\n", k, sum[k], sum[k]/cnt[k]}' data.txt
# 分组求最大值
awk '{if ($2 > max[$1]) max[$1] = $2} END{for (k in max) print k, max[k]}' data.txt
多维数组
# 按「部门+性别」二维分组统计
awk '{cnt[$1,$2]++}
END{for (k in cnt) {split(k, x, SUBSEP); print x[1], x[2], cnt[k]}}' staff.txt
# 判断多维元素是否存在
awk 'BEGIN{a[1,2]=1; if ((1,2) in a) print "存在"}'
5.12 常用内置函数
字符串函数
| 函数 | 说明 |
|---|---|
length([s]) |
返回字符串长度,省略参数则为 $0 的长度;参数为数组时返回元素个数 |
substr(s, m [, n]) |
从第 m 个字符开始截取 n 个字符(下标从 1 开始),省略 n 则截到末尾 |
index(s, t) |
返回 t 在 s 中第一次出现的位置,未找到返回 0 |
split(s, arr [, fs]) |
按 fs 把 s 切分到数组 arr,返回字段个数;省略 fs 则用 FS |
sub(re, rep [, target]) |
替换第一处匹配,返回替换次数;默认对 $0 操作 |
gsub(re, rep [, target]) |
全局替换所有匹配,返回替换次数 |
match(s, re) |
返回 re 在 s 中匹配的起始位置,同时设置 RSTART 和 RLENGTH |
toupper(s) |
转大写 |
tolower(s) |
转小写 |
sprintf(fmt, ...) |
按格式返回字符串(不打印) |
# length
awk '{print length($0), $0}' file
awk 'length > 80' file
awk -F: '{print $1, length($1)}' /etc/passwd
# substr:注意下标从 1 开始
awk 'BEGIN{print substr("Hello World", 7)}' # World
awk 'BEGIN{print substr("Hello World", 1, 5)}' # Hello
awk 'BEGIN{print substr("2026-08-22", 6, 2)}' # 08
awk -F: '{print substr($1, 1, 3)}' /etc/passwd # 每个用户名的前 3 个字符
# index
awk 'BEGIN{print index("Hello World", "World")}' # 7
awk 'BEGIN{print index("abc", "z")}' # 0
awk '{if (index($0, "error") > 0) print}' app.log
# split
awk 'BEGIN{n = split("a:b:c:d", arr, ":"); print n; print arr[1], arr[4]}' # 4 / a d
awk '{split($1, ip, "."); print ip[1]"."ip[2]".0.0"}' access.log # 取 IP 的 B 段
awk 'BEGIN{n=split("2026-08-22", d, "-"); print d[1]"年"d[2]"月"d[3]"日"}'
# sub / gsub
awk '{sub(/root/, "admin"); print}' /etc/passwd # 每行第一处
awk '{gsub(/root/, "admin"); print}' /etc/passwd # 每行所有处
awk -F: '{gsub(/o/, "0", $1); print $1}' /etc/passwd # 只对第 1 字段替换
awk '{n = gsub(/a/, "A"); print n, $0}' file # 返回替换次数
awk '{gsub(/^[ \t]+|[ \t]+$/, ""); print}' file # 去掉行首行尾空白
# match / RSTART / RLENGTH
awk 'BEGIN{
s = "order-12345-ok"
if (match(s, /[0-9]+/))
print "位置:" RSTART, "长度:" RLENGTH, "内容:" substr(s, RSTART, RLENGTH)
}'
# toupper / tolower
awk '{print toupper($0)}' file
awk -F: '{print toupper(substr($1,1,1)) substr($1,2)}' /etc/passwd # 首字母大写
# sprintf
awk '{s = sprintf("%-10s|%5.2f", $1, $2); print s}' data.txt
时间函数(gawk)
| 函数 | 说明 |
|---|---|
systime() |
当前时间的 Unix 时间戳(秒) |
strftime(fmt [, ts]) |
把时间戳按 fmt 格式化,省略 ts 则用当前时间 |
mktime("YYYY MM DD HH MM SS") |
把时间描述串转成时间戳 |
awk 'BEGIN{print systime()}'
awk 'BEGIN{print strftime("%Y-%m-%d %H:%M:%S")}'
awk 'BEGIN{print strftime("%F %T", systime() - 86400)}' # 昨天同一时刻
awk '{print strftime("%Y-%m-%d", $1), $2}' timestamp.log # 把首列时间戳转日期
awk 'BEGIN{print mktime("2026 08 22 00 00 00")}'
# 计算日志中每条记录距今过了多少小时
awk '{printf "%s 距今 %.1f 小时\n", $2, (systime()-$1)/3600}' timestamp.log
其他函数
# system():执行 Shell 命令,返回退出码(会刷新输出缓冲,性能较差,慎用)
awk -F: '{system("id " $1)}' /etc/passwd
awk '{system("mkdir -p " $1)}' dirlist.txt
awk 'BEGIN{system("date")}'
# 数学函数
awk 'BEGIN{print int(3.9), sqrt(16), exp(1), log(10), sin(0), cos(0)}'
awk 'BEGIN{srand(); print int(rand()*100)}' # 0~99 随机数,srand() 用于播种
int()是直接截断而非四舍五入:int(3.9)得 3。要四舍五入用int(x + 0.5)或printf "%.0f"。
getline 的几种形式
getline 用于「额外读一行」,形式不同,含义差别很大:
| 形式 | 含义 | 影响的变量 |
|---|---|---|
getline |
从当前输入读下一行到 $0 |
$0、NF、NR、FNR |
getline var |
从当前输入读下一行到 var | var、NR、FNR |
getline < "file" |
从文件读一行到 $0 |
$0、NF |
getline var < "file" |
从文件读一行到 var | 仅 var |
"cmd" \| getline |
执行命令,读其输出一行到 $0 |
$0、NF、NR |
"cmd" \| getline var |
执行命令,读其输出一行到 var | var、NR |
返回值:1 读取成功,0 到达文件末尾,-1 出错。
# 合并每两行为一行
awk '{if (getline nextline > 0) print $0, nextline; else print $0}' file
# 匹配到某行后,打印它的下一行
awk '/START/{getline; print}' file
# 从命令读取
awk 'BEGIN{"date +%Y" | getline year; print "今年是 " year}'
awk 'BEGIN{"hostname" | getline h; print "主机: " h}'
# 循环读取一个文件的全部内容
awk 'BEGIN{while ((getline line < "/etc/hostname") > 0) print line}'
while (("cmd" | getline line) > 0)的括号必须写全,否则|的优先级会让语义出错。
5.13 自定义函数
awk '
function max(a, b) {
return a > b ? a : b
}
function repeat(s, n, i, out) { # 多余的参数 i、out 用作局部变量
for (i = 1; i <= n; i++) out = out s
return out
}
{ print max($1, $2), repeat("-", 10) }
' data.txt
要点:
function关键字定义,函数名与(之间不能有空格(调用时也一样)。- awk 没有
local关键字,惯例是在参数列表后多写几个参数当局部变量,并用多个空格与真实参数隔开以示区分。 - 标量参数是值传递,数组参数是引用传递(函数内修改数组会影响调用者)。
- 用
-f加载函数库文件可在多个脚本间复用。
5.14 实战案例集
案例 1:统计日志中各 IP 的访问次数并取前十
awk '{ip[$1]++} END{for (k in ip) print ip[k], k}' access.log | sort -rn | head -10
逐段解释:$1 是 Nginx 默认日志格式的客户端 IP;ip[$1]++ 以 IP 为键累加;END 中遍历输出「次数 IP」;sort -rn 按第一列数字降序;head -10 取前十。
输出示例:
15230 192.168.1.100
8912 192.168.1.101
4455 10.0.0.23
用 gawk 的排序功能可以一条命令搞定,不用外接 sort:
awk '{ip[$1]++}
END{
PROCINFO["sorted_in"] = "@val_num_desc"
n = 0
for (k in ip) { print ip[k], k; if (++n >= 10) break }
}' access.log
案例 2:统计 Nginx 日志各状态码占比
awk '{code[$9]++; total++}
END{
printf "%-8s %-10s %-8s\n", "状态码", "次数", "占比"
for (c in code)
printf "%-8s %-10d %.2f%%\n", c, code[c], code[c]*100/total
print "总请求数: " total
}' access.log
$9 是默认组合日志格式中的 HTTP 状态码字段。输出示例:
状态码 次数 占比
200 45231 88.34%
404 3120 6.09%
500 412 0.80%
301 2456 4.79%
总请求数: 51219
案例 3:计算某列的和、平均值、最大最小值
awk '{
sum += $2
if (NR == 1 || $2 > max) max = $2
if (NR == 1 || $2 < min) min = $2
}
END{
printf "行数: %d\n和: %.2f\n平均: %.2f\n最大: %.2f\n最小: %.2f\n", \
NR, sum, sum/NR, max, min
}' data.txt
关键细节:
max/min必须用NR == 1来初始化。若直接写if ($2 > max),未初始化的max是空字符串(数值上视为 0),当所有值都是负数时结果会错。
统计磁盘使用率的最大值:
df -h | awk 'NR>1{gsub(/%/,"",$5); if ($5+0 > max) {max=$5; part=$6}}
END{print "使用率最高: " part " " max "%"}'
案例 4:按字段分组求和
# 数据:部门 姓名 工资
awk '{sum[$1] += $3; cnt[$1]++}
END{
printf "%-12s %-8s %-12s %-10s\n", "部门", "人数", "工资总额", "平均工资"
for (d in sum)
printf "%-12s %-8d %-12.2f %-10.2f\n", d, cnt[d], sum[d], sum[d]/cnt[d]
}' salary.txt | sort
案例 5:提取并去重
# 提取访问过的所有 URL 并去重
awk '{print $7}' access.log | awk '!a[$0]++'
# 一步完成:只在首次出现时输出
awk '!seen[$7]++{print $7}' access.log
# 统计独立访客数(UV)
awk '{ip[$1]} END{print "UV: " length(ip)}' access.log
案例 6:处理 /etc/passwd
# 列出所有可登录的普通用户
awk -F: '$3 >= 1000 && $7 !~ /nologin|false/{print $1}' /etc/passwd
# 统计每种 shell 的用户数
awk -F: '{s[$7]++} END{for (k in s) printf "%-20s %d\n", k, s[k]}' /etc/passwd
# 统计 /sbin/nologin 出现次数
awk 'BEGIN{FS=":"; count=0}{if($7 == "/sbin/nologin")count+=1}END{print count}' /etc/passwd
# 统计所有字段总数
awk -F ":" 'BEGIN{c=0}{c=c+NF}END{print c}' /etc/passwd
# 统计字段数大于 5 的行数
awk -F ":" 'BEGIN{count=0}(NF > 5){count+=1}END{print count}' /etc/passwd
# 找出 UID 重复的用户(危险配置)
awk -F: '{u[$3] = u[$3] " " $1; c[$3]++} END{for (k in c) if (c[k] > 1) print "UID " k ":" u[k]}' /etc/passwd
# 生成用户报表
awk -F: 'BEGIN{printf "%-18s%-8s%-8s%-24s\n","用户","UID","GID","家目录"}
{printf "%-18s%-8s%-8s%-24s\n",$1,$3,$4,$6}
END{print "共 " NR " 个用户"}' /etc/passwd
案例 7:处理 df 输出
# 找出使用率超过 80% 的分区
df -h | awk 'NR>1 && int($5) > 80 {print $6" 已用 "$5" 剩余 "$4}'
# 汇总所有分区的总容量(去掉单位 G)
df -h | awk 'NR>1 && $2 ~ /G$/{gsub(/G/,"",$2); total += $2}
END{printf "总容量: %.1fG\n", total}'
# inode 使用率检查
df -i | awk 'NR>1{gsub(/%/,"",$5); if ($5+0 > 85) print "inode告警: "$6" "$5"%"}'
案例 8:处理 ps 输出
# 内存占用 TOP5 进程
ps aux | awk 'NR>1{print $4"%", $11}' | sort -rn | head -5
# 按用户汇总 CPU 与内存占用
ps aux | awk 'NR>1{cpu[$1]+=$3; mem[$1]+=$4}
END{printf "%-12s%-10s%-10s\n","USER","CPU%","MEM%"
for (u in cpu) printf "%-12s%-10.1f%-10.1f\n", u, cpu[u], mem[u]}'
# 找出内存占用超过 5% 的进程并输出 PID(可接 xargs kill)
ps aux | awk '$4 > 5.0 {print $2}'
# 统计各状态的进程数
ps aux | awk 'NR>1{st[$8]++} END{for (s in st) print s, st[s]}'
案例 9:多文件处理与 FNR
# 每个文件单独统计行数
awk '{cnt[FILENAME]++} END{for (f in cnt) print f": "cnt[f]" 行"}' *.log
# 每个文件只处理前 5 行
awk 'FNR <= 5 {print FILENAME"("FNR"): "$0}' a.txt b.txt
# 跳过每个文件的表头(用 FNR==1 而不是 NR==1)
awk 'FNR == 1 {next} {sum += $2} END{print sum}' *.csv
# 每个文件分别求和
awk '{sum[FILENAME] += $2} END{for (f in sum) printf "%-20s %.2f\n", f, sum[f]}' *.txt
关键区别:多文件求和时若用
NR==1{next},只会跳过第一个文件的表头,后面文件的表头会被当数据参与计算。必须用FNR==1{next}。
案例 10:两文件关联比对的 NR==FNR 经典技巧
这是 awk 最重要的惯用法,必须彻底理解其原理。
awk 'NR==FNR{a[$1]; next} $1 in a' file1 file2
原理:awk 按顺序处理 file1、file2。NR 是全局行号(跨文件累加),FNR 是当前文件内行号(换文件归 1)。
- 处理 file1 时:两者同步递增,
NR == FNR恒为真 → 执行第一个块,把 file1 的键存进数组a,然后next跳过后续规则。 - 处理 file2 时:
NR已经累加上了 file1 的行数,一定大于FNR,NR == FNR恒为假 → 跳过第一个块,执行第二个规则$1 in a(pattern 为真则默认 print $0)。
于是一条命令完成了「先把小文件读进内存做字典,再拿大文件逐行查表」的操作,只需遍历一遍,无需排序。
常见变体:
# 1. 求交集:file2 中第 1 列在 file1 中出现过的行
awk 'NR==FNR{a[$1]; next} $1 in a' file1 file2
# 2. 求差集:file2 中第 1 列不在 file1 中的行
awk 'NR==FNR{a[$1]; next} !($1 in a)' file1 file2
# 3. 关联查询(类似 SQL JOIN):用 file1 的映射给 file2 补上一列
# file1: uid username file2: uid score
awk 'NR==FNR{name[$1]=$2; next} {print $1, name[$1], $2}' users.txt scores.txt
# 4. 按行号整行比对(不看字段)
awk 'NR==FNR{line[$0]; next} !($0 in line)' old.txt new.txt # new 中新增的行
# 5. 三个文件:用 FILENAME 或 ARGIND(gawk)区分
awk 'FILENAME=="a.txt"{a[$1]; next} FILENAME=="b.txt"{b[$1]; next} ($1 in a) && ($1 in b)' a.txt b.txt c.txt
注意:
NR==FNR在 file1 为空文件时会失效(空文件不产生任何记录,第一个块永远不执行,file2 的行反而会满足NR==FNR)。严格场景可改用FNR==NR && FILENAME==ARGV[1]或 gawk 的ARGIND==1。
实用示例:找出 A 服务器有、B 服务器没有的软件包:
rpm -qa | sort > /tmp/a.txt
ssh b_server 'rpm -qa' | sort > /tmp/b.txt
awk 'NR==FNR{p[$0]; next} !($0 in p)' /tmp/b.txt /tmp/a.txt
6. 排序、去重与比较
6.1 sort 排序
sort 对文本文件的各行进行排序,默认按字符编码升序排列,输出到标准输出,不修改源文件。
命令格式
sort [选项] 文件列表
基本控制选项
| 选项 | 说明 |
|---|---|
-m |
对已经排序好的文件进行合并,但不做排序 |
-c |
检查文件是否已排序;若未排序则显示错误信息,不进行排序 |
-u |
与 -c 一起使用时严格按顺序检查;单独使用时去重(只保留重复行的一行) |
-o 文件 |
将排序结果写入指定文件,文件不存在则创建 |
sort -c file # 检查是否已排序
sort -m a.txt b.txt # 合并两个已排序文件
sort -u file # 排序并去重
sort file -o file # 结果写回原文件(这是 sort 唯一安全的「原地修改」方式)
sort file > file会先清空文件导致数据丢失,必须用-o。sort -o内部做了特殊处理,允许输出文件与输入文件相同。
改变排序规则的选项
| 选项 | 说明 |
|---|---|
-n |
按数字大小排序(--numeric-sort) |
-r |
降序排列(默认升序),即 z–a、9–0 |
-d |
按字典顺序排序(只考虑字母、数字、空白),空白符排在最前 |
-f |
排序时忽略字母大小写 |
-b |
忽略每行前导空白(--ignore-leading-blanks) |
-i |
忽略非打印字符 |
-h |
按人类可读的数字排序,识别 K/M/G 后缀(--human-numeric-sort) |
-V |
按版本号排序(--version-sort) |
-R |
随机打乱顺序(--random-sort) |
-k |
指定按第几个字段排序 |
-t |
指定字段分隔符 |
-s |
稳定排序,禁用「最后手段比较」 |
-o |
输出到文件 |
--parallel=N |
使用 N 个并行线程排序 |
-S 大小 |
指定内存缓冲区大小,如 -S 2G |
-T 目录 |
指定临时文件目录 |
-z |
以 NUL 而非换行分隔行,配合 find -print0 |
-n 与默认字符排序的区别
printf "10\n9\n100\n2\n" | sort # 字符排序: 10 100 2 9
printf "10\n9\n100\n2\n" | sort -n # 数字排序: 2 9 10 100
这是最常见的坑:不加
-n时"10" < "9"(比较第一个字符 '1' < '9')。凡是排数字必须加-n。
-h 人类可读排序
du -sh /var/* | sort -h # 按实际大小排,正确处理 K/M/G 后缀
du -sh /var/* | sort -hr | head -5 # 最大的 5 个目录
对比:sort -n 遇到 1.5G 只会读到 1.5,把 900M 当 900 排在前面,结果完全错误。
-V 版本号排序
printf "v1.10\nv1.9\nv1.2\n" | sort # 错: v1.10 v1.2 v1.9
printf "v1.10\nv1.9\nv1.2\n" | sort -V # 对: v1.2 v1.9 v1.10
ls kernel-*.rpm | sort -V # 内核包按版本先后排列
-k 指定字段与 -t 指定分隔符
-k 的完整语法是 -k 起始字段[.起始字符][修饰符][,结束字段[.结束字符][修饰符]]。
# 按第 3 字段排序
sort -t: -k3 /etc/passwd
# 按第 3 字段数字排序
sort -t: -k3n /etc/passwd
sort -t: -n -k5 /etc/passwd
# 精确写法:只用第 2 字段排序,按数字
sort -k2,2n data.txt
# 按第 2 字段的第 3 个字符开始排
sort -k2.3 data.txt
-k2n 与 -k2,2n 的区别(重点):
-k2n:排序键是「从第 2 字段一直到行末」,中间的所有字段都参与比较。-k2,2n:排序键仅限第 2 字段。
# 数据
# apple 10 zzz
# apple 10 aaa
sort -k2n data.txt # 键是 "10 zzz" / "10 aaa",第 3 字段影响结果
sort -k2,2n data.txt # 键只有 "10",两行视为相等,保持原顺序
结论:养成写
-kN,Nn的习惯,只有明确需要「从某字段到行末」时才写-kN。
多级排序
多个 -k 依次生效,前一个键相等时才比较下一个键:
# 先按第 1 字段字母升序,第 1 字段相同再按第 3 字段数字降序
sort -t: -k1,1 -k3,3nr /etc/passwd
# 先按部门(第1列)升序,同部门内按工资(第3列)降序
sort -k1,1 -k3,3nr salary.txt
# 日志:先按日期,再按时间
sort -k1,1 -k2,2 app.log
# 修饰符可以逐键指定:n 数字、r 反向、f 忽略大小写、b 忽略空白
sort -t, -k2,2f -k1,1nr data.csv
稳定排序 -s
sort 默认在所有指定键都相等时,会拿整行做「最后手段比较」(last-resort comparison),这会打乱原始顺序。-s 禁用该行为,使相等元素保持输入顺序:
sort -s -k1,1 data.txt # 第 1 字段相同的行保持原有先后
sort -s -k2,2n access.log # 稳定排序,便于多次分级排序叠加
6.2 uniq 去重
uniq 用于比较相邻的行,从排序好的文件中去除重复行,只保留其中一行。
命令格式
uniq [选项] [输入文件 [输出文件]]
常用选项
| 选项 | 长选项 | 说明 |
|---|---|---|
-c |
--count |
在每行前加上该行出现的次数 |
-d |
--repeated |
只输出重复的行(每组只显示一次) |
-D |
--all-repeated |
输出重复行的所有副本 |
-u |
--unique |
只输出不重复(只出现一次)的行 |
-i |
--ignore-case |
比较时忽略大小写 |
-f n |
--skip-fields=n |
比较时忽略前 n 个字段 |
-s n |
--skip-chars=n |
比较时忽略前 n 个字符 |
-w n |
--check-chars=n |
只比较每行前 n 个字符 |
-z |
--zero-terminated |
以 NUL 分隔行 |
必须先 sort 再 uniq
这是 uniq 最重要的使用要点:uniq 只比较相邻的两行,它内部没有哈希表,不会记住之前出现过的所有行。
# 错误:a 出现了两次但不相邻,无法去掉
printf "a\nb\na\n" | uniq # 输出 a b a
# 正确:先排序让相同行挨在一起
printf "a\nb\na\n" | sort | uniq # 输出 a b
原因是 uniq 被设计为流式处理,内存占用恒定(只需缓存上一行),因此可以处理超大文件,代价就是要求输入有序。
不排序也要去重且保持原顺序时,用 awk:
awk '!a[$0]++' file # 保持首次出现顺序,代价是全部键要放进内存
统计词频的经典组合
sort file | uniq -c | sort -rn
三段含义:sort 让相同行相邻 → uniq -c 统计每组出现次数并前置 → sort -rn 按次数数值降序。
# 统计各 shell 使用人数
cut -d: -f7 /etc/passwd | sort | uniq -c | sort -rn
# 统计访问日志 TOP10 IP
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10
# 统计文章中的高频单词
tr -cs '[:alpha:]' '\n' < article.txt | tr 'A-Z' 'a-z' | sort | uniq -c | sort -rn | head -20
# 找出重复出现的行
sort file | uniq -d
# 找出只出现一次的行
sort file | uniq -u
# 只统计重复次数超过 1 的
sort file | uniq -c | awk '$1 > 1'
-f / -s / -w 示例
准备文件 123.txt:
123 456
122 456
133 456
456 123
557 123
789 456
789 567
567 567
567 123
# 显示文件中不重复的行
uniq -u 123.txt
# 跳过前 1 个字段后再比较,并在行首显示出现次数
uniq -cf 1 123.txt
# 跳过前 2 个字符后再比较,并在行首显示出现次数
uniq -cs 2 123.txt
# 只比较每行前 3 个字符
uniq -w 3 123.txt
# 忽略大小写去重
sort -f file | uniq -i
典型场景:日志行首是时间戳,想按「除时间戳外的内容」去重,就用
uniq -f 2跳过前两个字段(日期和时间)。
6.3 comm 比较有序文件
comm 对两个已经排序好的文件逐行进行比较,以三列形式输出。
命令格式
comm [-123] file1 file2
三列输出的含义
| 列 | 内容 |
|---|---|
| 第 1 列 | 仅在 file1 中出现的行 |
| 第 2 列 | 仅在 file2 中出现的行 |
| 第 3 列 | 两个文件中都出现的行 |
三列用制表符缩进区分:第 1 列不缩进,第 2 列缩进 1 个 Tab,第 3 列缩进 2 个 Tab。
抑制选项
| 选项 | 说明 |
|---|---|
-1 |
不显示第 1 列(仅 file1 独有的行) |
-2 |
不显示第 2 列(仅 file2 独有的行) |
-3 |
不显示第 3 列(两个文件共有的行) |
-1、-2、-3 可以组合使用:
| 组合 | 效果 |
|---|---|
comm -12 |
只显示交集(两文件共有) |
comm -13 |
只显示 file2 独有(file2 - file1) |
comm -23 |
只显示 file1 独有(file1 - file2) |
comm -3 |
只显示两文件的差异部分(对称差) |
示例
准备两个已排序的文件:
123.txt:
123
234
345
456
234.txt:
124
232
341
456
只显示两个文件中相同的行(即隐藏第 1、2 列):
comm -12 123.txt 234.txt
输出:
456
其他用法:
comm -23 123.txt 234.txt # 只在 123.txt 中的行:123 234 345
comm -13 123.txt 234.txt # 只在 234.txt 中的行:124 232 341
comm -3 123.txt 234.txt # 差异部分
输入必须有序
comm 硬性要求两个输入文件都按同一规则排序,否则结果毫无意义(并会打印 comm: file 1 is not in sorted order 警告)。
# 未排序的文件先排序再比较,用进程替换避免临时文件
comm -12 <(sort a.txt) <(sort b.txt)
# 比较两台服务器的软件包
comm -3 <(rpm -qa | sort) <(ssh other 'rpm -qa' | sort)
# 找出用户列表的差异
comm -13 <(cut -d: -f1 /etc/passwd | sort) <(sort userlist.txt)
comm与awk 'NR==FNR{...}'的选择:comm要求有序但内存占用恒定,适合超大文件;awk 不要求排序、只遍历一遍但要把一个文件放进内存,适合中小文件和需要关联取值的场景。
6.4 diff 比较文件
diff 用于比较两个文本文件并找出不同之处。与 comm 相比,diff 不要求文件预先排序,它会逐行比较并列出差异,同时提示如何修改才能使两个文件一致。若两个文件完全相同,则没有任何输出。
命令格式
diff [选项] file1 file2
常用选项
| 选项 | 说明 |
|---|---|
-b |
忽略空格数量造成的差别,例如 How are you 与 How are you 视为相同 |
-w |
忽略所有空白字符(包括行内空白全部忽略) |
-B |
忽略空行的增删 |
-i |
比较时忽略字母大小写 |
-c |
以上下文格式(context)输出,显示不同点上下各 3 行内容 |
-C n |
以上下文格式输出,显示不同点上下各 n 行内容,如 -C 4 |
-u |
以统一格式(unified)输出,最常用,也是 git diff 的格式 |
-U n |
统一格式,指定上下文行数 |
-y |
并排(side-by-side)显示两个文件 |
-W n |
配合 -y 指定输出总宽度 |
-r |
当比较的两个对象都是目录时,递归比较各子目录中的文件 |
-q |
只报告文件是否不同,不显示具体差异内容 |
-N |
把缺失的文件当作空文件处理(生成补丁时需要) |
-a |
把所有文件都当作文本文件比较 |
普通(默认)格式
diff a.txt b.txt
输出示例:
2c2
< hello world
---
> hello linux
4d3
< line to delete
6a6
> new line added
输出符号含义:
| 符号 | 含义 |
|---|---|
< |
该行来自 file1(左边的文件) |
> |
该行来自 file2(右边的文件) |
a |
add,需要在 file1 中追加行才能变成 file2 |
d |
delete,需要在 file1 中删除行才能变成 file2 |
c |
change,需要修改file1 的行才能变成 file2 |
--- |
分隔 < 和 > 两部分 |
指令行的格式是 [file1行号范围][a|d|c][file2行号范围]:
2c2:file1 第 2 行需改为 file2 第 2 行的内容4d3:删除 file1 第 4 行(对应 file2 第 3 行之后的位置)6a6:在 file1 第 6 行后追加 file2 的第 6 行
记忆要点:diff 的输出是「如何把 file1 改成 file2」的操作说明书,方向始终是从左到右。
-u 统一格式(最常用)
diff -u a.txt b.txt
--- a.txt 2026-08-22 10:00:00.000000000 +0800
+++ b.txt 2026-08-22 10:05:00.000000000 +0800
@@ -1,6 +1,6 @@
line one
-hello world
+hello linux
line three
-line to delete
line five
line six
+new line added
| 标记 | 含义 |
|---|---|
--- |
原文件(旧) |
+++ |
新文件 |
@@ -1,6 +1,6 @@ |
差异块(hunk)位置:旧文件从第 1 行起 6 行,新文件从第 1 行起 6 行 |
| 空格开头 | 上下文行,两文件都有 |
- 开头 |
旧文件独有(被删除) |
+ 开头 |
新文件独有(被新增) |
统一格式比普通格式紧凑(不重复输出上下文),且是 patch、git、代码审查的事实标准。
-c 上下文格式
diff -c a.txt b.txt
*** a.txt 2026-08-22 10:00:00
--- b.txt 2026-08-22 10:05:00
***************
*** 1,6 ****
line one
! hello world
line three
- line to delete
--- 1,6 ----
line one
! hello linux
line three
其中 ! 表示该行被修改,- 表示删除,+ 表示新增。它把两个文件的上下文分开完整列出,可读性好但更冗长。
-y 并排显示
diff -y a.txt b.txt
diff -y -W 120 a.txt b.txt # 指定总宽度 120
diff -y --suppress-common-lines a.txt b.txt # 只显示不同的行
line one line one
hello world | hello linux
line three line three
line to delete <
line five line five
> new line added
中间符号:| 两边不同,< 只有左边有,> 只有右边有,空白表示相同。
-r 递归比较目录与 -q 简报
diff -r dir1 dir2 # 递归比较两个目录的所有文件
diff -rq dir1 dir2 # 只列出哪些文件不同
diff -rq /etc /etc.bak | grep -v '^Only in' # 只看内容有差异的文件
# 输出示例
# Files dir1/a.conf and dir2/a.conf differ
# Only in dir1: extra.txt
# Only in dir2: newfile.txt
-i / -w 忽略差异
diff -i a.txt b.txt # 忽略大小写
diff -w a.txt b.txt # 忽略所有空白差异
diff -b a.txt b.txt # 忽略空格数量差异
diff -B /etc/passwd passwd # 忽略空行差异
diff -iwB a.txt b.txt # 组合使用,只关注实质内容差异
配合 patch 生成与应用补丁
完整流程:
# 第 1 步:生成补丁文件(必须用 -u 统一格式;-N 处理新增文件)
diff -uNr old_dir new_dir > my.patch
diff -u old.conf new.conf > conf.patch
# 第 2 步:预演,检查补丁能否干净应用(--dry-run 不真正修改文件)
patch --dry-run -p1 < my.patch
# 第 3 步:应用补丁
cd old_dir
patch -p1 < ../my.patch # -p1 去掉路径的第 1 层目录
patch old.conf < conf.patch # 单文件直接指定
# 第 4 步:需要撤销时反向应用
patch -R -p1 < my.patch
patch 常用选项:
| 选项 | 说明 |
|---|---|
-pN |
剥离补丁文件路径中的前 N 层目录 |
-R |
反向应用(撤销补丁) |
--dry-run |
只演练,不实际修改 |
-b |
应用前备份原文件(生成 .orig) |
-i 文件 |
指定补丁文件(等价于重定向输入) |
-N |
忽略已经应用过的补丁 |
-pN 的含义:补丁中路径是 --- old_dir/sub/a.txt 时,-p0 使用完整路径,-p1 去掉 old_dir/ 变成 sub/a.txt。因为打补丁时通常已经 cd 进了目标目录,所以 -p1 最常用。
实用场景:修改配置文件前留档、生成可复用的变更包:
cp /etc/nginx/nginx.conf /tmp/nginx.conf.orig
vim /etc/nginx/nginx.conf
diff -u /tmp/nginx.conf.orig /etc/nginx/nginx.conf > /tmp/nginx.patch
# 在另一台机器上应用同样的改动
scp /tmp/nginx.patch other:/tmp/
ssh other 'cd /etc/nginx && patch -b nginx.conf < /tmp/nginx.patch'
7. 统计、切割与转换
7.1 wc 统计
wc(word count)用于统计文件的行数、字数(单词数)与字节数。统计以空格作为单词分隔符,结果默认按 行数 字数 字节数 文件名 的顺序输出。
命令格式
wc [选项] [文件...]
常用选项
| 选项 | 长选项 | 说明 |
|---|---|---|
-l |
--lines |
统计行数 |
-w |
--words |
统计单词数 |
-c |
--bytes |
统计字节数 |
-m |
--chars |
统计字符数 |
-L |
--max-line-length |
统计最长一行的长度(不包含行结束符) |
说明:在 Linux 中字符串以
\0作为结束符;-c统计字节数,-m统计字符数。对纯 ASCII 文本两者相同;对 UTF-8 中文,一个汉字通常是 3 字节 1 字符,此时-c与-m差别很大。
wc -c /etc/passwd # 字节数
wc -w /etc/passwd # 单词数
wc -l /etc/passwd # 行数
wc -m file # 字符数
wc -L file # 最长行的长度
示例
统计 etcpasswd 文件的行数、单词数与字节数:
wc -lcw etcpasswd
输出:
40 72 1984 etcpasswd
结合管道使用,统计 /etc/passwd 中包含 root 的行数:
cat /etc/passwd | grep root | wc -l
统计多文件时的合计行
给多个文件时,wc 会为每个文件输出一行,并在最后追加一行 total 合计:
wc -l *.log
120 access.log
45 error.log
310 app.log
475 total
# 只要合计数
wc -l *.log | tail -1
wc -l *.log | awk 'END{print $1}'
# 统计项目代码总行数
find . -name "*.py" | xargs wc -l | tail -1
wc -l < file 与 wc -l file 的输出差异
这是一个常被忽略但很关键的细节:
wc -l /etc/passwd # 输出: 45 /etc/passwd (带文件名)
wc -l < /etc/passwd # 输出: 45 (只有数字)
cat /etc/passwd | wc -l # 输出: 45 (只有数字)
原因:wc -l file 是把文件名作为参数,wc 知道文件名所以会一并打印;wc -l < file 是 Shell 做重定向,wc 从标准输入读取,它并不知道数据来自哪个文件,所以只输出数字。
这个差异在脚本中赋值时非常重要:
# 错误:变量里带上了文件名
count=$(wc -l /etc/passwd)
echo "共 $count 行" # 输出: 共 45 /etc/passwd 行
# 正确写法一:重定向
count=$(wc -l < /etc/passwd)
# 正确写法二:取第一个字段
count=$(wc -l /etc/passwd | awk '{print $1}')
# 正确写法三:直接用 awk(还省了一个进程)
count=$(awk 'END{print NR}' /etc/passwd)
另一个细节:如果文件最后一行没有换行符,
wc -l会少算一行(它统计的是换行符个数)。awk 'END{print NR}'则会正确算入最后一行。
7.2 cut 列切割
cut 用于按指定分隔符对文件的每一行进行切割,并只输出指定的列(字段)。
命令格式
cut [选项] 文件
常用选项
| 选项 | 长选项 | 说明 |
|---|---|---|
-d |
--delimiter |
指定列分隔符(只能是单个字符),默认制表符 |
-f |
--fields |
指定要显示的列编号 |
-c |
--characters |
按字符位置截取 |
-b |
--bytes |
按字节位置截取 |
--complement |
取补集,输出未指定的那些列 | |
--output-delimiter=串 |
指定输出时使用的分隔符 | |
-s |
--only-delimited |
只输出含有分隔符的行(不含分隔符的行整行跳过) |
-n |
与 -b 配合,不拆分多字节字符 |
注意:
-d与-f必须配合使用,-d单独使用没有效果。-f、-c、-b三者互斥。
-f 的各种范围写法
| 写法 | 含义 |
|---|---|
-f1 |
只要第 1 列 |
-f1,3 |
第 1 列和第 3 列(逗号列举) |
-f1,3,7 |
第 1、3、7 列 |
-f2-4 |
第 2 到第 4 列(连续区间) |
-f2- |
从第 2 列到最后一列 |
-f-3 |
从第 1 列到第 3 列 |
-f1,4-6,9- |
混合写法 |
cut -d: -f1 /etc/passwd # 显示第 1 列
cut -d: -f1,2,5 /etc/passwd # 显示第 1、2、5 列
cut -d: -f1,3 /etc/passwd # 用户名和 UID
cut -d: -f2- /etc/passwd # 第 2 列到行末
cut -d: -f-3 /etc/passwd # 前 3 列
cut /etc/passwd -d ":" -f 2 # 选项与文件顺序可以互换
cat /etc/passwd | head -1 | cut -d ":" -f1 # 输出: root
重要限制:
cut -f无法调整输出顺序。cut -d: -f3,1的输出仍然是「第1列 第3列」的顺序,cut 会自动升序排列并去重列号。需要换序请用awk '{print $3, $1}'。
-c 按字符与 -b 按字节
cut -c1-5 file # 每行的第 1~5 个字符
cut -c1,5,9 file # 第 1、5、9 个字符
cut -c3- file # 从第 3 个字符到行末
cut -c-10 file # 前 10 个字符
cut -b1-3 file # 前 3 个字节
# 从日志时间戳中取出小时
awk '{print $4}' access.log | cut -c14-15
# 取 ls -l 输出中的权限部分
ls -l | cut -c1-10
对含中文的 UTF-8 文本,-c 按字符切、-b 按字节切,用 -b 可能把一个汉字切成乱码,应加 -n 或直接用 -c。
--complement 取补集
cut -d: -f1 --complement /etc/passwd # 输出除第 1 列外的所有列
cut -d: --complement -f2,3 /etc/passwd # 去掉密码位和 UID
cut -c1-3 --complement file # 去掉每行前 3 个字符
--output-delimiter 改变输出分隔符
cut 默认用输入分隔符输出,改成别的需要显式指定:
cut -d: -f1,3 --output-delimiter=' ' /etc/passwd # 用空格分隔
cut -d: -f1,3,7 --output-delimiter=$'\t' /etc/passwd # 用制表符分隔
cut -d: -f1,6 --output-delimiter=' -> ' /etc/passwd # 用任意字符串
# 巧用:把冒号分隔转成换行(每个字段一行)
echo "a:b:c" | cut -d: -f1- --output-delimiter=$'\n'
cut 的局限:无法处理连续多空格
这是 cut 最重要的局限。cut -d' ' 把每一个空格都视为一个分隔符,连续的空格之间会产生大量空字段:
# 数据:"root 1234 0.5"(列之间是多个空格对齐的)
echo "root 1234 0.5" | cut -d' ' -f2
# 输出为空!因为第 2 个字段是两个空格之间的空串
真实场景中 ls -l、ps aux、df -h、netstat 的输出都是用不定数量空格对齐的,cut 全部无能为力。
三种解决办法:
# 办法一(推荐):用 awk,它默认把「连续空白」视为一个分隔符,还会自动忽略行首空白
ps aux | awk '{print $2, $11}'
df -h | awk '{print $6, $5}'
ls -l | awk '{print $9, $5}'
# 办法二:先用 tr -s 把连续空格压缩成一个,再交给 cut
ps aux | tr -s ' ' | cut -d' ' -f2,11
df -h | tr -s ' ' | cut -d' ' -f5,6
# 办法三:用 awk 指定正则分隔符
awk -F'[ \t]+' '{print $2}' file
选型结论:
| 场景 | 用什么 |
|---|---|
分隔符固定为单个字符(:、,、\t) |
cut,更快更简洁 |
| 分隔符是不定数量空白 | awk |
| 需要调整列的输出顺序 | awk |
| 需要按条件筛选行再取列 | awk |
| 需要按字符位置截取固定宽度 | cut -c |
7.3 tr 字符替换与删除
tr(translate)用于从标准输入中读取字符串,并对字符进行替换或删除。它不接受文件名作为参数,只能通过管道或输入重定向提供数据。
命令格式
tr [选项] 字符集1 [字符集2]
tr [选项] 字符集1 [字符集2] < 输入文件
常用选项
| 选项 | 长选项 | 说明 |
|---|---|---|
-d |
--delete |
删除出现在字符集1中的所有字符 |
-s |
--squeeze-repeats |
压缩连续重复的字符为一个 |
-c |
--complement |
取字符集1的补集(对未列出的字符操作) |
-t |
--truncate-set1 |
把字符集1截断为与字符集2等长 |
字符集替换
tr 做的是逐字符一对一映射(不是字符串替换):字符集1的第 1 个字符换成字符集2的第 1 个字符,以此类推。
# 将 123.txt 中的冒号替换为点号
cat 123.txt | tr ":" "."
# 若内容为 a:b:c:d,输出:a.b.c.d
# 将 /etc/passwd 第一行中的 : 替换为 ,
cat /etc/passwd | head -1 | tr ":" ","
# 单字符替换
cat /etc/passwd | tr a b # 所有 a 替换为 b
# 多字符映射:a->1, b->2, c->3
echo "abc" | tr 'abc' '123' # 输出 123
注意
tr 'abc' 'xyz'不是把字符串 "abc" 换成 "xyz",而是 a→x、b→y、c→z 三条独立规则。要做字符串替换请用sed s///。
若字符集2比字符集1短,tr 会用字符集2的最后一个字符补齐:
echo "abcde" | tr 'abcde' 'xy' # 输出 xyyyy
echo "abcde" | tr -t 'abcde' 'xy' # -t 截断集1,输出 xycde
大小写转换
tr a-z A-Z < /etc/passwd # 小写转大写
tr A-Z a-z < /etc/passwd # 大写转小写
echo "Hello World" | tr 'a-z' 'A-Z' # HELLO WORLD
tr '[:lower:]' '[:upper:]' < file # 用字符类,更通用(支持本地化)
字符类
| 字符类 | 含义 |
|---|---|
[:alpha:] |
所有字母 |
[:digit:] |
所有数字 |
[:alnum:] |
字母和数字 |
[:lower:] |
小写字母 |
[:upper:] |
大写字母 |
[:space:] |
所有空白字符(空格、Tab、换行、回车、换页、垂直制表) |
[:blank:] |
空格和 Tab |
[:punct:] |
标点符号 |
[:print:] |
所有可打印字符(含空格) |
[:graph:] |
所有可打印字符(不含空格) |
[:cntrl:] |
控制字符 |
[:xdigit:] |
十六进制数字 |
tr 也支持转义序列:\n 换行、\t 制表符、\r 回车、\\ 反斜杠、\0 NUL、\NNN 八进制。
-d 删除字符
tr -d '0-9' < file # 删除所有数字
tr -d '[:digit:]' < file # 同上
tr -d ' ' < file # 删除所有空格
tr -d '[:space:]' < file # 删除所有空白(含换行,会变成一行)
tr -d '[:punct:]' < file # 删除所有标点
echo "hello123world" | tr -d '0-9' # helloworld
# 删除回车符:处理 Windows 换行(CRLF -> LF)
tr -d '\r' < win.txt > unix.txt
tr -d '\r' 的实用场景:从 Windows 拷贝来的脚本、配置文件、CSV,每行末尾多一个 \r(回车符),会导致:
- Shell 脚本报
/bin/bash^M: bad interpreter: No such file or directory - 字符串比较莫名失败(
"abc\r" != "abc") - 变量值末尾多出不可见字符,路径拼接出错
# 检测文件是否是 CRLF 换行
file script.sh # 显示 "with CRLF line terminators"
cat -A script.sh | head # 行尾显示 ^M$
# 三种修复方式
tr -d '\r' < script.sh > script_fixed.sh && mv script_fixed.sh script.sh
sed -i 's/\r$//' script.sh
dos2unix script.sh # 需安装 dos2unix 包
# 反向:Unix -> Windows
sed -i 's/$/\r/' unix.txt
注意
tr不能原地修改(tr -d '\r' < f > f会清空文件),必须写到新文件再替换。
-s 压缩重复字符
tr -s ' ' < file # 连续多个空格压成一个
echo "a b c" | tr -s ' ' # a b c
tr -s '\n' < file # 删除连续空行(多个换行压成一个)
tr -s '[:space:]' ' ' < file # 所有连续空白压成一个空格
ps aux | tr -s ' ' | cut -d' ' -f2,11 # 配合 cut 处理对齐输出
# 压缩并替换可以同时做
echo "a b" | tr -s ' ' '_' # a_b
-c 取补集
# 删除除字母和换行外的所有字符
tr -cd '[:alpha:]\n' < file
# 把所有非字母字符都变成换行 —— 提取单词的经典写法
tr -cs '[:alpha:]' '\n' < article.txt
# 完整词频统计
tr -cs '[:alpha:]' '\n' < article.txt | tr 'A-Z' 'a-z' | sort | uniq -c | sort -rn | head -20
# 生成随机密码:从 /dev/urandom 中筛出可用字符
tr -cd '[:alnum:]' < /dev/urandom | head -c 16; echo
tr不修改源文件,结果输出到标准输出。
7.4 paste 按列合并
paste 把多个文件按列并排拼接(cat 是按行纵向拼接,paste 是横向拼接)。
命令格式
paste [选项] 文件...
| 选项 | 说明 |
|---|---|
-d |
指定列之间的分隔符,默认制表符;可给多个字符循环使用 |
-s |
串行合并:把每个文件的所有行并成一行 |
# 横向合并两个文件
paste names.txt scores.txt
alice 90
bob 85
carol 92
paste -d: a.txt b.txt # 用冒号分隔
paste -d',' a.txt b.txt c.txt # 三个文件用逗号分隔
paste -d':-' a.txt b.txt c.txt # 多个分隔符循环使用:a:b-c
# -s 把文件所有行变成一行
paste -s -d, names.txt # alice,bob,carol
seq 1 10 | paste -s -d+ # 1+2+3+4+5+6+7+8+9+10
seq 1 10 | paste -s -d+ | bc # 55(求和)
# 每 3 行合并为一行(用 - 表示标准输入,重复 3 次)
seq 1 9 | paste - - -
1 2 3
4 5 6
7 8 9
# 把 /etc/passwd 的用户名和 shell 两列并排
paste <(cut -d: -f1 /etc/passwd) <(cut -d: -f7 /etc/passwd)
7.5 join 按字段关联
join 按公共字段把两个文件做关联(相当于 SQL 的 JOIN)。
命令格式
join [选项] file1 file2
| 选项 | 说明 |
|---|---|
-1 N |
指定 file1 的第 N 个字段作为连接字段 |
-2 N |
指定 file2 的第 N 个字段作为连接字段 |
-j N |
两个文件都用第 N 个字段(等价于 -1 N -2 N) |
-t 字符 |
指定字段分隔符(默认为空白) |
-a N |
同时输出 fileN 中未匹配的行(相当于 LEFT/RIGHT JOIN) |
-v N |
只输出 fileN 中未匹配的行 |
-o 格式 |
指定输出字段的顺序,如 -o 1.1,2.2,1.3 |
-e 串 |
用指定字符串填充缺失字段(配合 -a、-o) |
-i |
比较连接字段时忽略大小写 |
要求输入按连接字段有序
join 硬性要求两个文件都已按连接字段排序,否则会漏掉匹配并给出 join: input is not in sorted order 警告。
# 标准做法:先排序
sort -k1,1 users.txt > /tmp/u.sorted
sort -k1,1 scores.txt > /tmp/s.sorted
join /tmp/u.sorted /tmp/s.sorted
# 用进程替换更简洁
join <(sort -k1,1 users.txt) <(sort -k1,1 scores.txt)
# 指定分隔符时,排序也要用同样的分隔符
join -t: <(sort -t: -k1,1 a.txt) <(sort -t: -k1,1 b.txt)
示例
users.txt:
1 alice
2 bob
3 carol
scores.txt:
1 90
2 85
4 70
join users.txt scores.txt
1 alice 90
2 bob 85
# 内连接的结果只有两边都有的键(3 和 4 都被丢掉了)
# LEFT JOIN:保留 file1 全部行,缺失处用 NULL 填充
join -a1 -e 'NULL' -o 1.1,1.2,2.2 users.txt scores.txt
1 alice 90
2 bob 85
3 carol NULL
# FULL OUTER JOIN
join -a1 -a2 -e '-' -o 0,1.2,2.2 users.txt scores.txt
# 只看 file1 中没有匹配的行(反连接)
join -v1 users.txt scores.txt
# 用不同位置的字段连接:file1 的第 2 列 对 file2 的第 1 列
join -1 2 -2 1 <(sort -k2,2 a.txt) <(sort -k1,1 b.txt)
# 用冒号分隔的真实场景:关联 passwd 与 shadow
join -t: -1 1 -2 1 <(sort -t: -k1,1 /etc/passwd) <(sudo sort -t: -k1,1 /etc/shadow) | cut -d: -f1,3,9
-o 的字段写法:0 表示连接字段本身,1.2 表示 file1 的第 2 字段,2.3 表示 file2 的第 3 字段。
join与awk 'NR==FNR'的选择:join语义清晰、支持外连接,但要求排序;awk 不需要排序、逻辑更灵活,中小数据量首选 awk。
7.6 nl 添加行号
nl(number lines)为文件内容添加行号,比 cat -n 更灵活。
nl file
| 选项 | 说明 |
|---|---|
-b a |
所有行都编号(body all,等同 cat -n) |
-b t |
只对非空行编号(默认行为) |
-b pRE |
只对匹配正则 RE 的行编号 |
-n ln |
行号左对齐,不补零 |
-n rn |
行号右对齐,不补零(默认) |
-n rz |
行号右对齐,前面补零 |
-w N |
行号占用的字符宽度,默认 6 |
-s 串 |
行号与内容之间的分隔串,默认 Tab |
-v N |
起始行号 |
-i N |
行号递增步长 |
nl -b a file # 所有行编号(包括空行)
nl -b t file # 只给非空行编号(默认)
nl -b p'^#' file # 只给注释行编号
nl -n rz -w 4 file # 行号补零到 4 位:0001
nl -n ln file # 行号左对齐
nl -s': ' file # 行号后跟冒号加空格
nl -v 100 file # 从 100 开始编号
nl -i 10 file # 每次加 10
# 组合:非空行编号,补零 3 位,冒号分隔
nl -b t -n rz -w 3 -s ': ' /etc/passwd
三种加行号方式的对比:
| 命令 | 空行是否编号 | 说明 |
|---|---|---|
cat -n file |
是 | 最简单 |
cat -b file |
否 | 与 nl 默认行为一致 |
nl file |
否 | 格式最可控 |
awk '{print NR": "$0}' |
是 | 格式完全自定义 |
grep -n '' file |
是 | 用 grep 的行号 |
7.7 rev 反转每行字符
rev(reverse)把每一行的字符顺序颠倒。注意与 tac 区分:
| 命令 | 作用 |
|---|---|
rev |
反转每行内字符的顺序(横向反转) |
tac |
反转行的顺序,即倒序输出文件(纵向反转,是 cat 反写) |
echo "hello" | rev # olleh
rev file # 每行字符倒序
tac file # 行倒序输出
tac file | rev # 完全颠倒
实用技巧:rev 常用来「从右边取字段」,因为 cut 只能从左数:
# 取路径的最后一段(等价于 basename)
echo "/usr/local/bin/python3" | rev | cut -d/ -f1 | rev # python3
# 取倒数第二段
echo "/usr/local/bin/python3" | rev | cut -d/ -f2 | rev # bin
# 取域名的顶级域和二级域
echo "www.mail.example.com" | rev | cut -d. -f1,2 | rev # example.com
# 取文件扩展名
echo "archive.tar.gz" | rev | cut -d. -f1 | rev # gz
其他辅助小工具:
| 命令 | 作用 |
|---|---|
basename $(pwd) |
获取当前目录名 |
dirname $(pwd) |
获取当前路径,不含目录名 |
tac |
倒序输出文件内容 |
rev |
倒序输出每行字符 |
7.8 column 对齐输出
column 把不整齐的文本按列对齐,专门解决「输出看不清」的问题。
| 选项 | 说明 |
|---|---|
-t |
自动创建表格,把各列对齐 |
-s 字符 |
指定输入的分隔符(默认为空白) |
-o 串 |
指定输出的列分隔符(较新版本支持) |
-c N |
输出总宽度 |
-x |
先按行填充而不是按列填充 |
-N 名字 |
指定列名(较新版本) |
# 把冒号分隔的 passwd 对齐成表格
column -t -s: /etc/passwd
# 只看关心的几列,对齐显示
cut -d: -f1,3,4,7 /etc/passwd | column -t -s:
# CSV 对齐查看
column -t -s, data.csv
# 配合 awk 生成的报表
awk -F: '{print $1, $3, $7}' /etc/passwd | column -t
# mount 输出对齐
mount | column -t
# 加表头一起对齐
{ echo "USER UID SHELL"; awk -F: '{print $1, $3, $7}' /etc/passwd; } | column -t
输出效果:
USER UID SHELL
root 0 /bin/bash
bin 1 /sbin/nologin
daemon 2 /sbin/nologin
column -t是排查问题时的效率神器:任何列对不齐、看不清的输出,管道接一个column -t立刻可读。缺点是它会把连续的分隔符当一个,不适合处理有空字段的数据(此时 CSV 请用column -t -s,并确保无空字段,或改用awk printf手工对齐)。
7.9 xargs 构造参数列表
xargs 把标准输入的内容转换成命令行参数。这解决了一类根本问题:很多命令(如 rm、cp、mv、chmod)只接受参数而不读标准输入,无法直接放在管道右侧。
# 错误:rm 不读标准输入,什么也不会删
find . -name "*.tmp" | rm
# 正确:xargs 把文件名转成 rm 的参数
find . -name "*.tmp" | xargs rm
命令格式
命令1 | xargs [选项] 命令2
不指定命令2 时,默认执行 echo:
echo "a b c" | xargs # 输出 a b c
seq 1 5 | xargs # 把 5 行合成一行:1 2 3 4 5
常用选项
| 选项 | 长选项 | 说明 |
|---|---|---|
-n N |
--max-args=N |
每次最多传递 N 个参数 |
-I 占位符 |
--replace |
用占位符表示参数位置,可放在命令的任意位置;隐含 -n 1 |
-d 字符 |
--delimiter |
指定输入的分隔符(默认为空白和换行) |
-0 |
--null |
以 NUL 作为分隔符,配合 find -print0 |
-p |
--interactive |
每次执行前询问确认 |
-t |
--verbose |
执行前把命令打印到标准错误 |
-r |
--no-run-if-empty |
输入为空时不执行命令 |
-P N |
--max-procs=N |
最多并行运行 N 个进程 |
-L N |
--max-lines=N |
每次最多传递 N 行 |
-s N |
--max-chars=N |
每条命令行的最大字符数 |
-a 文件 |
--arg-file |
从文件读取参数而非标准输入 |
-E 串 |
指定输入结束标记 |
-n 限制每次参数个数
seq 1 10 | xargs -n 3
1 2 3
4 5 6
7 8 9
10
# 每次只传一个参数,命令会被执行多次
seq 1 3 | xargs -n 1 echo "处理:"
# 处理: 1
# 处理: 2
# 处理: 3
# 每次传 100 个文件给 rm,避免命令行过长
find /tmp -name "*.log" | xargs -n 100 rm
-I 指定参数位置
默认 xargs 把参数追加到命令末尾。参数需要放在中间时必须用 -I:
# 参数在末尾(默认)
ls *.txt | xargs cat
# 参数在中间:{} 是占位符(名字可任意,习惯用 {})
ls *.txt | xargs -I {} cp {} /backup/
ls *.txt | xargs -I {} mv {} {}.bak
cat hosts.txt | xargs -I {} ssh {} 'uptime'
cat urls.txt | xargs -I % curl -sO %
# 一个参数可以在命令中出现多次
ls *.jpg | xargs -I {} convert {} -resize 50% small_{}
cat servers.txt | xargs -I HOST sh -c 'echo "== HOST =="; ssh HOST df -h'
-I隐含-n 1:每个参数单独执行一次命令,所以进程数等于参数个数,性能不如默认的批量模式。只在确实需要控制参数位置时使用。
-0 配合 find -print0 处理带空格文件名
这是 xargs 最重要的实战要点。xargs 默认用空白(空格、Tab、换行)分隔参数,一旦文件名里含空格就会被拆成多个:
# 危险:文件 "my file.txt" 会被当成 "my" 和 "file.txt" 两个文件
find . -name "*.txt" | xargs rm
# rm: cannot remove 'my': No such file or directory
# rm: cannot remove 'file.txt': No such file or directory
更严重的是,如果目录里恰好有名为 my 的重要文件,它会被误删。
正确做法:让 find 用 NUL(\0)分隔输出,xargs 用 -0 按 NUL 读取。NUL 是文件名中唯一不可能出现的字符,所以绝对安全:
find . -name "*.txt" -print0 | xargs -0 rm
find . -type f -print0 | xargs -0 -n 50 md5sum
find /var/log -name "*.gz" -mtime +30 -print0 | xargs -0 rm -f
find . -type f -print0 | xargs -0 grep -l "TODO"
其他产生 NUL 分隔输出的命令:
grep -rlZ "pattern" . | xargs -0 sed -i 's/old/new/g' # grep -Z
sort -z file | xargs -0 ... # sort -z
规则:只要文件名可能来自用户输入或不受控目录,一律用
-print0 | xargs -0。这不是可选的最佳实践,而是安全底线。
-p / -t 确认与回显
# -t:执行前打印命令(调试神器)
find . -name "*.tmp" | xargs -t rm
# -p:逐条询问是否执行(危险操作前先用它)
find . -name "*.conf" | xargs -p rm
# 组合:先用 -t 看要执行什么,确认无误再去掉
find /data -name "*.bak" -print0 | xargs -0 -t -n 1 rm
-r 处理空输入
xargs 在输入为空时仍会执行一次命令(GNU xargs 默认行为对某些命令会出问题):
# 危险:如果 find 没有结果,会变成执行 "ls"(列出当前目录,看似无害)
# 但如果是 "rm -rf" 加了默认参数,就可能造成灾难
echo -n "" | xargs ls
# 安全:输入为空则完全不执行
find . -name "*.notexist" | xargs -r rm
find . -name "*.notexist" -print0 | xargs -0 -r rm
BSD/macOS 的 xargs 默认就不执行空输入,且不支持
-r。写跨平台脚本时要注意。
-d 指定分隔符
echo "a,b,c" | xargs -d, -n 1 echo # 用逗号分隔
echo "a:b:c" | xargs -d: echo
echo -e "a\nb\nc" | xargs -d'\n' -I {} echo "[{}]" # 只按换行分隔,允许参数含空格
-d'\n' 是处理「文件名含空格但不含换行」的次优方案(-0 才是最优)。
-P 并行执行
# 4 个进程并行压缩
find . -name "*.log" -print0 | xargs -0 -n 1 -P 4 gzip
# 并行 ping 一批主机(-P 0 表示尽可能多的并行)
cat hosts.txt | xargs -P 10 -I {} sh -c 'ping -c1 -W1 {} >/dev/null && echo "{} UP" || echo "{} DOWN"'
# 并行下载
cat urls.txt | xargs -P 5 -n 1 curl -sO
并行时多个进程的输出会交错混杂。需要干净输出时让每个任务写自己的文件,或改用 GNU
parallel(它自带输出缓冲)。
xargs 与 find -exec 的对比
# find -exec:每找到一个文件执行一次命令(\; 结尾)
find . -name "*.txt" -exec rm {} \;
# find -exec +:把多个文件攒成一条命令(+ 结尾),效率接近 xargs
find . -name "*.txt" -exec rm {} +
# xargs:批量传参
find . -name "*.txt" -print0 | xargs -0 rm
| 维度 | -exec cmd {} \; |
-exec cmd {} + |
xargs |
|---|---|---|---|
| 进程数 | 每个文件一个进程,慢 | 批量,快 | 批量,快 |
| 空格文件名 | 天然安全 | 天然安全 | 需 -print0 \| xargs -0 |
| 空结果 | 不执行 | 不执行 | 默认会执行,需 -r |
| 参数位置 | {} 可放任意位置 |
{} 只能在末尾 |
需 -I 才能放中间 |
| 并行 | 不支持 | 不支持 | 支持 -P |
| 管道组合 | 只能用于 find | 只能用于 find | 任何命令的输出都能用 |
| 复杂 Shell 语法 | 需包 sh -c |
需包 sh -c |
需包 sh -c |
选型建议:
- 只是 find 结果 + 简单命令,文件数不多 →
-exec cmd {} +(简洁安全) - 需要并行、需要参数在中间、数据源不是 find →
xargs - 命令必须逐个文件执行(如需要
{}出现多次)→-exec cmd {} \;或xargs -I {}
解决 Argument list too long
Linux 对单条命令的参数总长度有上限(由 ARG_MAX 决定,通常 2MB 左右)。文件数过多时会报错:
rm /tmp/cache/*
# -bash: /bin/rm: Argument list too long
ls /tmp/cache | wc -l
# 500000
查看限制:
getconf ARG_MAX # 单条命令参数最大字节数
ulimit -s # 栈大小,也会影响该限制
解决办法(按推荐顺序):
# 办法一:用 find + -delete(最快,无需启动外部进程)
find /tmp/cache -maxdepth 1 -type f -delete
# 办法二:用 find -print0 + xargs(xargs 会自动按 ARG_MAX 分批)
find /tmp/cache -maxdepth 1 -type f -print0 | xargs -0 rm -f
# 办法三:显式限制每批个数
find /tmp/cache -maxdepth 1 -type f -print0 | xargs -0 -n 1000 rm -f
# 办法四:find -exec +(也会自动分批)
find /tmp/cache -maxdepth 1 -type f -exec rm -f {} +
# 办法五:目录整体删了重建(文件极多时最快)
mv /tmp/cache /tmp/cache.old && mkdir /tmp/cache && rm -rf /tmp/cache.old
xargs 之所以能解决这个问题,是因为它会主动测量参数长度,超出系统限制就自动拆成多批执行。
注意:
ls | xargs rm依然不安全(文件名含空格/换行会出错),务必用find -print0。
7.10 tee 分流输出
tee 从标准输入读取数据,同时写到标准输出和文件(名字来自水管的 T 型三通接头)。它让数据在流过管道时被「顺手存档」。
命令格式
命令 | tee [选项] 文件...
| 选项 | 说明 |
|---|---|
-a |
--append,追加到文件而不是覆盖 |
-i |
--ignore-interrupts,忽略中断信号 |
-p |
诊断写入管道时的错误 |
# 既在屏幕上看到输出,又保存到文件
ls -l | tee filelist.txt
# 追加而不覆盖
echo "new entry" | tee -a log.txt
# 同时写多个文件
ls -l | tee f1.txt f2.txt f3.txt
# 只写文件不显示(等价于重定向,但保留了管道形式)
command | tee out.txt > /dev/null
在管道中间存档
tee 的核心价值是保留中间结果,便于排查每一步的处理是否正确:
# 一条流水线的每个阶段都留档
cat access.log \
| grep " 500 " | tee /tmp/step1_500.txt \
| awk '{print $1}' | tee /tmp/step2_ip.txt \
| sort | uniq -c | sort -rn | tee /tmp/step3_result.txt \
| head -10
# 长时间运行的任务:既实时看输出,又完整存日志
make 2>&1 | tee build.log
./deploy.sh 2>&1 | tee -a deploy_$(date +%F).log
# 用进程替换让 tee 分流给不同的命令
cat access.log | tee >(grep " 404 " > 404.log) >(grep " 500 " > 500.log) | wc -l
配合 sudo 写文件的技巧
这是 tee 最经典的用途。理解这个问题需要知道:重定向是由 Shell 完成的,不是命令完成的。
# 失败:sudo 提权的是 echo,而 > 是当前用户的 Shell 执行的
sudo echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
# -bash: /etc/sysctl.conf: Permission denied
解决办法是让有权限的进程本身去写文件,tee 正好可以被 sudo 提权:
# 正确:sudo 提权 tee,由 tee 去写文件
echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.conf
# 覆盖写入(不加 -a)
echo "nameserver 8.8.8.8" | sudo tee /etc/resolv.conf
# 不想在屏幕上看到回显
echo "content" | sudo tee /etc/some.conf > /dev/null
# 写多行内容用 here-document
cat <<'EOF' | sudo tee /etc/yum.repos.d/local.repo > /dev/null
[local]
name=Local Repo
baseurl=file:///mnt/cdrom
enabled=1
gpgcheck=0
EOF
# 把一条命令的输出存到特权目录
dmesg | sudo tee /var/log/dmesg.snapshot > /dev/null
其他等价方案:
sudo sh -c 'echo "content" >> /etc/some.conf' # 让 sudo 起一个 Shell 来做重定向
sudo bash -c 'cat > /etc/some.conf' <<'EOF'
content
EOF
tee方案比sudo sh -c更安全:后者要把整条命令交给 Shell 再解析一遍,如果内容里含$、反引号、引号,很容易被二次展开出问题。
8. 三剑客组合实战
8.1 grep / sed / awk 选型对照表
| 维度 | grep | sed | awk |
|---|---|---|---|
| 本质 | 行过滤器 | 流编辑器 | 编程语言 |
| 处理单位 | 整行 | 整行(字符流) | 行 + 字段 |
| 核心能力 | 找(匹配即输出) | 改(替换/删除/插入) | 算(取列/统计/格式化) |
| 是否懂「列」 | 不懂 | 不懂(只能靠正则分组模拟) | 天然懂 |
| 能否算数 | 不能 | 不能 | 能(变量、数组、算术) |
| 能否保存状态 | 不能 | 有限(保持空间 h/H/g/G/x) | 能(变量、关联数组) |
| 正则方言 | BRE / ERE(-E) / PCRE(-P) | BRE / ERE(-r) | ERE(默认就是扩展正则) |
| 性能 | 最快 | 快 | 稍慢(但一次遍历常胜多次管道) |
| 原地修改文件 | 不能 | 能(-i) | 需 gawk -i inplace 或重定向 |
选型决策:
| 需求 | 用什么 |
|---|---|
| 挑出含关键词的行 | grep |
| 排除某些行(注释、空行) | grep -v 或 sed -e '/^#/d;/^$/d' |
| 统计匹配行数 | grep -c |
| 只要文件名 | grep -rl |
| 批量替换字符串 | sed -i 's///g' |
| 删除/插入/追加行 | sed d / i / a |
| 取第 N 列 | awk '{print $N}'(列固定单字符分隔时 cut 更快) |
| 按列值筛选行 | awk '$3 > 100' |
| 求和、平均、计数、分组 | awk |
| 格式化对齐报表 | awk printf |
| 两文件关联比对 | awk 'NR==FNR' |
| 只要前 N 行就退出 | awk 'NR==N{print;exit}' 或 head |
一句话总结:能用 grep 解决的不要用 sed,能用 sed 解决的不要用 awk;但只要涉及「第几列」或「算数」,直接上 awk。
8.2 组合实战:从源文档整理的基础组合
# grep + wc:统计包含 root 的行数
cat /etc/passwd | grep root | wc -l
grep -c root /etc/passwd # 更高效,少一个进程
# cut + 管道:只输出第一行的第 1 个字段
cat /etc/passwd | head -1 | cut -d ":" -f1
# tr + 管道:把第一行的冒号换成逗号
cat /etc/passwd | head -1 | tr ":" ","
# find + grep:查找 /etc 下名为 grub.conf 的链接文件
find /etc -type l | grep grub.conf
find /etc -type l -name grub.conf # 更高效,交给 find 自己筛
# sed + awk:提取有线网卡名
ifconfig | sed -rn 's/^(e[0-9a-zA-Z]+).*$/\1/p'
# route + sed + awk:截取默认网关所在网卡的 IP / 掩码
link=$(route -n | sed -n '/UG/p' | awk '{print $NF}')
ip=$(ifconfig $link | sed -n '/Mask/p' \
| sed -r 's/^([^0-9]+)([0-9.]+)(.*)([^0-9.]+)([0-9.]+$)/\2\/\5/')
echo $ip
# 现代替代写法(推荐,ifconfig 已废弃)
link=$(route -n | sed -n '/UG/p' | awk '{print $NF}')
ip=$(ip a s $link | sed -n '/inet /p' | awk '{print $2}')
echo $ip
# grep + 正则反向引用
echo "abcabcabcaabb" | grep -E '(a(bc)){2}\1' --color
echo "hello world, hello world, hello beautiful world" \
| grep -E --color '((hello) (world)), \1, \2 .* \3'
# sed 字段换位:把 /etc/passwd 每行首末字段互换
cat /etc/passwd > 1_passwd.txt
sed -r 's/^([^:]+)([^a-zA-Z]+)([^:]+)(.*)\2([^:]+)$/\1\2\5\4\2\3/' 1_passwd.txt
# find 多条件综合 + exec
find / -size +5M -perm 644 -user root -type f 2>/dev/null -exec ls -hl {} \;
find / -size +10M -user soul -type f -exec ls -hl "{}" \;
8.3 案例:分析 Nginx / Apache 访问日志
假设日志为 Nginx 默认的 combined 格式:
192.168.1.100 - - [22/Aug/2026:10:23:45 +0800] "GET /api/user?id=1 HTTP/1.1" 200 1234 "-" "Mozilla/5.0" 0.023
$1 $4 $6 $7 $8 $9 $10
字段对应:$1 客户端 IP,$4 时间,$6 请求方法,$7 URL,$9 状态码,$10 响应字节数。
PV(页面浏览量)
需求:统计总请求数。
wc -l access.log
awk 'END{print "PV: " NR}' access.log
# 只统计某天的 PV
grep "22/Aug/2026" access.log | wc -l
awk '/22\/Aug\/2026/{n++} END{print "PV: " n}' access.log
# 排除静态资源,只算真实页面 PV
awk '$7 !~ /\.(jpg|jpeg|png|gif|css|js|ico|woff2?)$/{n++} END{print "页面PV: " n}' access.log
UV(独立访客数)
需求:统计去重后的客户端 IP 数量。
awk '{print $1}' access.log | sort -u | wc -l
# 一次遍历完成(更快,无需排序)
awk '{ip[$1]} END{print "UV: " length(ip)}' access.log
# 按天统计 UV
awk '{split($4, d, ":"); gsub(/\[/, "", d[1]); key=d[1]"|"$1; if(!(key in seen)){seen[key]; uv[d[1]]++}}
END{for (day in uv) print day, uv[day]}' access.log | sort
TOP10 IP
需求:找出访问次数最多的 10 个 IP,用于识别爬虫或攻击源。
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10
逐段解释:awk '{print $1}' 只取 IP 列 → sort 让相同 IP 相邻 → uniq -c 计数 → sort -rn 按次数降序 → head -10 取前十。
# awk 一次遍历版(大文件明显更快,省掉两次 sort)
awk '{c[$1]++} END{for (i in c) print c[i], i}' access.log | sort -rn | head -10
# 带占比的完整报表
awk '{c[$1]++; t++}
END{printf "%-18s %-10s %-8s\n","IP","次数","占比"
for (i in c) printf "%-18s %-10d %.2f%%\n", i, c[i], c[i]*100/t}' access.log \
| sort -k2,2rn | head -10
输出示例:
IP 次数 占比
192.168.1.100 15230 29.73%
192.168.1.101 8912 17.40%
10.0.0.23 4455 8.70%
TOP10 URL
需求:找出被访问最多的 URL,定位热点接口。
awk '{print $7}' access.log | sort | uniq -c | sort -rn | head -10
# 去掉 URL 的查询参数,按接口路径聚合
awk '{split($7, u, "?"); c[u[1]]++} END{for (i in c) print c[i], i}' access.log | sort -rn | head -10
# TOP10 最慢的接口(按平均响应时间,$NF 为响应时间)
awk '{split($7,u,"?"); sum[u[1]]+=$NF; cnt[u[1]]++}
END{for (i in sum) printf "%.4f %d %s\n", sum[i]/cnt[i], cnt[i], i}' access.log \
| sort -rn | head -10
# TOP10 流量消耗最大的 URL($10 为响应字节数)
awk '{byte[$7] += $10} END{for (u in byte) printf "%.2fMB %s\n", byte[u]/1024/1024, u}' access.log \
| sort -rn | head -10
状态码分布
需求:查看各 HTTP 状态码的数量与占比,判断服务健康度。
awk '{print $9}' access.log | sort | uniq -c | sort -rn
# 带占比
awk '{c[$9]++; t++}
END{printf "%-8s%-10s%-8s\n","状态码","次数","占比"
for (i in c) printf "%-8s%-10d%.2f%%\n", i, c[i], c[i]*100/t
print "总计: " t}' access.log | sort -k2,2rn
# 4xx / 5xx 分类汇总
awk '{c[substr($9,1,1)"xx"]++} END{for (i in c) print i, c[i]}' access.log | sort
# 列出所有 5xx 请求的 URL 及次数(定位故障接口)
awk '$9 ~ /^5/{c[$7]++} END{for (u in c) print c[u], u}' access.log | sort -rn | head
# 404 的 URL 排行(找死链)
awk '$9 == 404 {c[$7]++} END{for (u in c) print c[u], u}' access.log | sort -rn | head -20
各时段流量
需求:找出流量高峰时段,用于容量规划。
# 按小时统计请求数(时间字段形如 [22/Aug/2026:10:23:45)
awk '{split($4, t, ":"); c[t[2]]++}
END{for (h in c) printf "%s时 %d\n", h, c[h]}' access.log | sort
# 带柱状图,直观看峰值
awk '{split($4,t,":"); c[t[2]]++}
END{for (h=0; h<24; h++) {
hh = sprintf("%02d", h)
n = (hh in c) ? c[hh] : 0
printf "%s时 %6d ", hh, n
for (i = 0; i < n/100; i++) printf "#"
print ""
}}' access.log
# 按分钟统计(定位突发流量的精确时刻)
awk '{split($4, t, ":"); print t[2]":"t[3]}' access.log | sort | uniq -c | sort -rn | head -10
# 按小时统计带宽消耗
awk '{split($4,t,":"); byte[t[2]] += $10}
END{for (h in byte) printf "%s时 %.2f MB\n", h, byte[h]/1024/1024}' access.log | sort
输出示例:
00时 120 #
09时 4520 #############################################
10时 15230 ###############################...
一次遍历出全部指标(推荐写法)
大文件时最重要的优化:只读一遍文件,一次算出所有指标。
awk '{
pv++
ip[$1]++
code[$9]++
url[$7]++
bytes += $10
split($4, t, ":"); hour[t[2]]++
}
END{
print "===== 访问概况 ====="
printf "PV : %d\n", pv
printf "UV : %d\n", length(ip)
printf "总流量: %.2f GB\n", bytes/1024/1024/1024
printf "平均每次请求: %.2f KB\n", bytes/pv/1024
print "\n===== 状态码分布 ====="
for (c in code) printf " %-6s %-8d %.2f%%\n", c, code[c], code[c]*100/pv
print "\n===== 各时段请求 ====="
for (h = 0; h < 24; h++) {
hh = sprintf("%02d", h)
if (hh in hour) printf " %s时 %d\n", hh, hour[hh]
}
}' access.log
8.4 案例:从 ps 与 netstat 输出中提取并统计
ps 输出分析
需求:找出资源占用异常的进程。
# 内存占用 TOP10($4 是 %MEM,$11 是命令)
ps aux | awk 'NR>1{print $4"%", $2, $11}' | sort -rn | head -10
# CPU 占用 TOP10($3 是 %CPU)
ps aux | awk 'NR>1{print $3"%", $2, $11}' | sort -rn | head -10
# 按用户汇总资源占用
ps aux | awk 'NR>1{cpu[$1]+=$3; mem[$1]+=$4; cnt[$1]++}
END{printf "%-12s%-8s%-10s%-10s\n","USER","进程数","CPU%","MEM%"
for (u in cpu) printf "%-12s%-8d%-10.1f%-10.1f\n", u, cnt[u], cpu[u], mem[u]}' \
| sort -k3,3rn
# 按进程名汇总(把同名的多个进程合起来算,如 nginx 的多个 worker)
ps aux | awk 'NR>1{n=$11; sub(/.*\//,"",n); mem[n]+=$4; cnt[n]++}
END{for (p in mem) printf "%.1f%% %d个 %s\n", mem[p], cnt[p], p}' | sort -rn | head
# 统计各状态进程数($8 是 STAT)
ps aux | awk 'NR>1{st[substr($8,1,1)]++} END{for (s in st) print s, st[s]}'
# 找出僵尸进程
ps aux | awk '$8 ~ /Z/{print "僵尸进程 PID:"$2, "父进程需处理:", $11}'
# 找出运行超过 1 天的进程
ps -eo pid,etime,cmd | awk '$2 ~ /-/{print}'
# 提取内存超 5% 的 PID 并批量结束(先用 echo 验证再去掉)
ps aux | awk '$4 > 5.0 {print $2}' | xargs -r echo kill -15
netstat / ss 输出分析
需求:统计连接状态、找出连接数最多的来源 IP。
# 统计各 TCP 连接状态的数量(排查 TIME_WAIT 堆积、SYN_RECV 攻击)
netstat -ant | awk 'NR>2{state[$6]++} END{for (s in state) print s, state[s]}' | sort -k2,2rn
ss -ant | awk 'NR>1{state[$1]++} END{for (s in state) print s, state[s]}'
输出示例:
ESTABLISHED 245
TIME_WAIT 1832
LISTEN 18
SYN_RECV 3
# 统计连接数最多的客户端 IP($5 是对端 地址:端口)
netstat -ant | awk 'NR>2 && $6=="ESTABLISHED"{split($5,a,":"); ip[a[1]]++}
END{for (i in ip) print ip[i], i}' | sort -rn | head -10
# 用 rev + cut 处理 IPv6 也能正确取到 IP(从右边切掉端口)
netstat -ant | awk 'NR>2{print $5}' | rev | cut -d: -f2- | rev | sort | uniq -c | sort -rn | head
# 按本地端口统计(看哪个服务连接最多)
netstat -ant | awk 'NR>2{split($4,a,":"); port[a[length(a)]]++}
END{for (p in port) print port[p], p}' | sort -rn | head
# 找出监听中的端口及对应进程
netstat -antlp | awk '$6=="LISTEN"{split($4,a,":"); printf "%-8s %s\n", a[length(a)], $7}' | sort -n
# 单 IP 连接数超过 100 就告警(简易防护判断)
netstat -ant | awk 'NR>2 && $6=="ESTABLISHED"{split($5,a,":"); c[a[1]]++}
END{for (i in c) if (c[i] > 100) print "告警: "i" 连接数 "c[i]}'
8.5 案例:批量修改配置文件并先备份
需求:把一批配置文件里的旧 IP 换成新 IP,必须可回滚。
# 第 1 步:先看会改哪些文件(只列文件名,不做修改)
grep -rl "192.168.1.100" /etc/myapp/
# 第 2 步:看具体会改哪些行(带行号,人工确认)
grep -rn "192.168.1.100" /etc/myapp/
# 第 3 步:用 sed 预演(不加 -i,只把结果打到屏幕)
grep -rl "192.168.1.100" /etc/myapp/ | xargs sed 's/192\.168\.1\.100/10\.0\.0\.50/g' | grep "10.0.0.50"
# 第 4 步:备份 + 修改(sed -i 加后缀会自动生成备份文件)
grep -rlZ "192.168.1.100" /etc/myapp/ | xargs -0 sed -i.bak_$(date +%F) 's/192\.168\.1\.100/10\.0\.0\.50/g'
# 第 5 步:验证
grep -rn "10.0.0.50" /etc/myapp/
grep -rn "192.168.1.100" /etc/myapp/ # 应无输出
# 第 6 步:出问题时回滚
find /etc/myapp -name "*.bak_*" | while read f; do mv "$f" "${f%.bak_*}"; done
关键点解释:
sed -i.bak会把原文件另存为原名.bak再修改,这是最省事的备份方式。- IP 中的
.在正则里是「任意字符」,必须转义成\.,否则192.168.1.100也会匹配192x168y1z100。 grep -rlZ+xargs -0保证文件名含空格时安全。- 只用
grep -rl找出确实包含目标的文件再交给 sed,避免对整个目录里的每个文件都执行 sed(大目录时性能差距巨大,且不会无意改动 mtime)。
更完整的备份方案:
#!/bin/bash
# 批量替换配置,整目录打包备份
set -e
CONF_DIR="/etc/myapp"
OLD="192.168.1.100"
NEW="10.0.0.50"
BAK="/root/backup/myapp_$(date +%F_%H%M%S).tar.gz"
mkdir -p "$(dirname "$BAK")"
tar czf "$BAK" "$CONF_DIR"
echo "已备份到 $BAK"
files=$(grep -rl "$OLD" "$CONF_DIR" || true)
if [ -z "$files" ]; then
echo "没有需要修改的文件"
exit 0
fi
echo "将修改以下文件:"
echo "$files"
echo "$files" | xargs sed -i "s/${OLD//./\\.}/${NEW//./\\.}/g"
echo "完成,剩余匹配数: $(grep -rc "$OLD" "$CONF_DIR" | awk -F: '{s+=$2} END{print s+0}')"
其他常见批量修改场景:
# 批量注释掉某个配置项
sed -i 's/^\(SELINUX=enforcing\)/#\1/' /etc/selinux/config
# 批量取消注释
sed -i 's/^#\(PermitRootLogin\)/\1/' /etc/ssh/sshd_config
# 修改配置值(在原值位置替换,保留缩进)
sed -i -r 's/^(\s*max_connections\s*=\s*).*/\1500/' /etc/my.cnf
# 在某段配置后插入新行
sed -i '/^\[mysqld\]/a slow_query_log = 1' /etc/my.cnf
# 删除所有注释行和空行,得到干净配置(只查看,不修改)
grep -Ev '^\s*(#|$)' /etc/nginx/nginx.conf
# 批量给所有 .sh 文件加执行权限
find /opt/scripts -name "*.sh" -print0 | xargs -0 chmod +x
8.6 案例:提取 IP、邮箱、URL 的正则
提取 IP 地址
# 粗略版(能匹配 999.999.999.999 这类非法 IP,但日常够用)
grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' file
# 加单词边界,避免匹配到版本号中间的片段
grep -Eo '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b' file
# 严格版:每段限制 0-255
grep -Eo '\b((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\b' file
# 提取并统计出现次数
grep -Eo '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b' /var/log/messages | sort | uniq -c | sort -rn | head
# 从 ip a 输出中提取本机 IP(排除回环)
ip a | grep -Eo 'inet ([0-9]{1,3}\.){3}[0-9]{1,3}' | awk '{print $2}' | grep -v '^127\.'
# 用 awk 提取并按 C 段聚合
grep -Eo '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b' access.log \
| awk -F. '{print $1"."$2"."$3".0/24"}' | sort | uniq -c | sort -rn | head
提取邮箱
grep -Eo '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' file
# 提取并去重排序
grep -Eo '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' file | sort -u
# 按域名分组统计
grep -Eo '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' file \
| awk -F@ '{c[$2]++} END{for (d in c) print c[d], d}' | sort -rn
提取 URL
# 提取 http/https URL
grep -Eo 'https?://[A-Za-z0-9./?=_%:&#~+-]+' file
# 只提取域名部分
grep -Eo 'https?://[^/"]+' file | sed -r 's#https?://##' | sort -u
# 从 HTML 中提取所有链接的 href
grep -Eo 'href="[^"]+"' index.html | sed -r 's/href="([^"]+)"/\1/'
# 统计外链域名
grep -Eo 'https?://[^/"]+' index.html | awk -F/ '{print $3}' | sort | uniq -c | sort -rn
其他常用提取正则
# MAC 地址
grep -Eo '([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}' file
# 日期 YYYY-MM-DD
grep -Eo '[0-9]{4}-[0-9]{2}-[0-9]{2}' file
# 时间 HH:MM:SS
grep -Eo '[0-9]{2}:[0-9]{2}:[0-9]{2}' file
# 中国手机号
grep -Eo '\b1[3-9][0-9]{9}\b' file
# 纯数字(含小数)
grep -Eo '[0-9]+\.?[0-9]*' file
# 引号中的内容
grep -Eo '"[^"]*"' file
# 提取 URL 中的某个查询参数值
grep -Eo 'id=[0-9]+' access.log | cut -d= -f2 | sort -n | uniq
grep -o是提取的关键:它只输出匹配到的部分而不是整行。若一行有多处匹配,会分成多行输出,正好方便后续sort | uniq -c。
8.7 案例:统计代码行数
需求:统计项目的代码规模,区分总行数、空行、注释行、有效代码行。
# 最简单:所有 .py 文件的总行数
find . -name "*.py" -print0 | xargs -0 wc -l | tail -1
# 排除依赖目录
find . -name "*.py" -not -path "*/venv/*" -not -path "*/.git/*" -print0 | xargs -0 wc -l | tail -1
# 分类统计:总行 / 空行 / 注释 / 有效代码
find . -name "*.py" -not -path "*/venv/*" -print0 \
| xargs -0 awk '{
total++
if ($0 ~ /^[[:space:]]*$/) blank++
else if ($0 ~ /^[[:space:]]*#/) comment++
else code++
}
END{
printf "总行数 : %d\n", total
printf "空行 : %d (%.1f%%)\n", blank, blank*100/total
printf "注释行 : %d (%.1f%%)\n", comment, comment*100/total
printf "有效代码: %d (%.1f%%)\n", code, code*100/total
}'
输出示例:
总行数 : 12480
空行 : 1892 (15.2%)
注释行 : 2103 (16.8%)
有效代码: 8485 (68.0%)
# 按文件类型分别统计
find . -type f -not -path "*/.git/*" | awk -F. 'NF>1{print $NF}' | sort | uniq -c | sort -rn | head
# 按扩展名统计代码行数
find . -type f \( -name "*.py" -o -name "*.js" -o -name "*.sh" \) -not -path "*/.git/*" -print0 \
| xargs -0 awk '{ext = FILENAME; sub(/.*\./,"",ext); lines[ext]++}
END{for (e in lines) printf "%-8s %d 行\n", e, lines[e]}' | sort -k2,2rn
# 找出最长的 10 个文件
find . -name "*.py" -print0 | xargs -0 wc -l | sort -rn | sed -n '2,11p'
# 统计每个作者的提交行数(配合 git)
git log --pretty=format:'%an' --numstat \
| awk 'NF==1{author=$0} NF==3{add[author]+=$1; del[author]+=$2}
END{for (a in add) printf "%-20s +%-8d -%-8d\n", a, add[a], del[a]}' | sort -k2,2r
# 找出超过 80 字符的行(代码规范检查)
find . -name "*.py" -print0 | xargs -0 awk 'length > 80 {print FILENAME":"FNR": "length" 字符"}'
# 找出含 TODO / FIXME 的位置
grep -rn --include="*.py" -E 'TODO|FIXME|XXX' . | awk -F: '{print $1":"$2}' | wc -l
grep -rn --include="*.py" -E 'TODO|FIXME' . | column -t -s:
8.8 案例:处理 CSV
假设 sales.csv:
date,region,product,qty,price
2026-08-01,华东,A,10,99.50
2026-08-01,华北,B,5,199.00
2026-08-02,华东,A,8,99.50
2026-08-02,华南,C,12,49.90
# 查看表头及列号(排查「第几列是什么」的必备第一步)
head -1 sales.csv | tr ',' '\n' | nl
# 对齐显示,方便肉眼检查
column -t -s, sales.csv
# 取指定列
cut -d, -f2,4 sales.csv
awk -F, '{print $2, $4}' sales.csv
# 跳过表头后按条件筛选
awk -F, 'NR>1 && $4 > 8 {print}' sales.csv
# 计算总销售额(数量 × 单价)
awk -F, 'NR>1{total += $4 * $5} END{printf "总销售额: %.2f\n", total}' sales.csv
# 按地区分组统计销售额
awk -F, 'NR>1{amt[$2] += $4 * $5; qty[$2] += $4}
END{printf "%-8s%-10s%-12s\n","地区","数量","金额"
for (r in amt) printf "%-8s%-10d%-12.2f\n", r, qty[r], amt[r]}' sales.csv \
| sort -k3,3rn
# 按日期 + 产品二维统计
awk -F, 'NR>1{k=$1 SUBSEP $3; s[k] += $4}
END{for (k in s) {split(k,a,SUBSEP); print a[1], a[2], s[k]}}' sales.csv | sort
# 增加一列(计算列)并输出新 CSV
awk -F, -v OFS=, 'NR==1{print $0, "amount"; next} {print $0, $4*$5}' sales.csv > sales_with_amount.csv
# 转置行列(小表格)
awk -F, '{for (i=1; i<=NF; i++) a[i,NR]=$i; if (NF>nf) nf=NF}
END{for (i=1; i<=nf; i++) {
for (j=1; j<=NR; j++) printf "%s%s", a[i,j], (j<NR ? "," : "\n")
}}' sales.csv
# 检查数据质量:找出字段数不对的行
awk -F, 'NF != 5 {print "第"NR"行字段数异常("NF"): "$0}' sales.csv
# 找出某列为空的行
awk -F, 'NR>1 && ($4 == "" || $5 == ""){print "第"NR"行有空值: "$0}' sales.csv
# CSV 转 TSV
tr ',' '\t' < sales.csv > sales.tsv
awk -F, -v OFS='\t' '{$1=$1; print}' sales.csv > sales.tsv
# 去重(按第 1、3 列组合去重)
awk -F, '!seen[$1,$3]++' sales.csv
CSV 的重要陷阱:标准 CSV 允许字段内含逗号(用双引号包裹,如
"北京,朝阳区"),也允许字段内含换行。这种情况下cut -d,和awk -F,都会切错。应对办法:
- gawk 4+ 可用
--csv选项(gawk 5.3 起)正确解析。- 或用 FPAT 指定「字段长什么样」而不是「分隔符长什么样」:
gawk -v FPAT='([^,]*)|("[^"]*")' '{print $2}' quoted.csv- 数据复杂时改用专门工具:
csvkit(csvcut、csvstat)、mlr(Miller)、q,或直接用 Python 的csv模块。
8.9 案例:比对两台服务器的软件包差异
需求:新装的服务器和已上线的服务器行为不一致,怀疑缺包,需列出差异。
# 第 1 步:分别采集包列表(务必排序,comm/join 都要求有序)
rpm -qa --qf '%{NAME}\n' | sort > /tmp/pkg_local.txt
ssh root@server_b "rpm -qa --qf '%{NAME}\n' | sort" > /tmp/pkg_remote.txt
# Debian 系
dpkg-query -W -f='${Package}\n' | sort > /tmp/pkg_local.txt
# 第 2 步:三种视角看差异
comm -3 /tmp/pkg_local.txt /tmp/pkg_remote.txt # 全部差异(左列本地独有,右列远端独有)
comm -23 /tmp/pkg_local.txt /tmp/pkg_remote.txt # 只在本地有(远端缺这些)
comm -13 /tmp/pkg_local.txt /tmp/pkg_remote.txt # 只在远端有(本地缺这些)
comm -12 /tmp/pkg_local.txt /tmp/pkg_remote.txt | wc -l # 共有多少包
# 第 3 步:统计
echo "本地: $(wc -l < /tmp/pkg_local.txt) 个包"
echo "远端: $(wc -l < /tmp/pkg_remote.txt) 个包"
echo "本地独有: $(comm -23 /tmp/pkg_local.txt /tmp/pkg_remote.txt | wc -l) 个"
echo "远端独有: $(comm -13 /tmp/pkg_local.txt /tmp/pkg_remote.txt | wc -l) 个"
# 第 4 步:生成补齐命令
comm -23 /tmp/pkg_local.txt /tmp/pkg_remote.txt | xargs -r echo "ssh server_b yum install -y"
用 awk 的 NR==FNR 做同样的事(不需要排序):
# 本地有、远端没有的包
awk 'NR==FNR{p[$0]; next} !($0 in p)' /tmp/pkg_remote.txt /tmp/pkg_local.txt
# 一次输出双向差异,带清晰标注
awk 'NR==FNR{a[$0]; next} {b[$0]}
END{
print "=== 仅本地有(远端需安装)==="
for (p in a) if (!(p in b)) print " " p
print "=== 仅远端有(本地缺少)==="
for (p in b) if (!(p in a)) print " " p
}' /tmp/pkg_local.txt /tmp/pkg_remote.txt
版本差异比对(同名包但版本不同,这往往是真正的问题所在):
rpm -qa --qf '%{NAME} %{VERSION}-%{RELEASE}\n' | sort > /tmp/v_local.txt
ssh server_b "rpm -qa --qf '%{NAME} %{VERSION}-%{RELEASE}\n' | sort" > /tmp/v_remote.txt
# 用 join 关联同名包,列出版本不同的
join /tmp/v_local.txt /tmp/v_remote.txt \
| awk '$2 != $3 {printf "%-30s 本地:%-24s 远端:%s\n", $1, $2, $3}'
# 用 awk 一次完成
awk 'NR==FNR{v[$1]=$2; next}
($1 in v) && v[$1] != $2 {printf "%-30s 本地:%-24s 远端:%s\n", $1, v[$1], $2}' \
/tmp/v_local.txt /tmp/v_remote.txt
同理可用于比对配置、用户、服务、内核模块:
# 比对用户列表
comm -3 <(cut -d: -f1 /etc/passwd | sort) <(ssh server_b 'cut -d: -f1 /etc/passwd' | sort)
# 比对已启用的服务
comm -3 <(systemctl list-unit-files --state=enabled --no-legend | awk '{print $1}' | sort) \
<(ssh server_b 'systemctl list-unit-files --state=enabled --no-legend' | awk '{print $1}' | sort)
# 比对配置文件内容
diff -u <(ssh server_a 'cat /etc/nginx/nginx.conf') <(ssh server_b 'cat /etc/nginx/nginx.conf')
# 比对内核参数
diff <(sysctl -a 2>/dev/null | sort) <(ssh server_b 'sysctl -a 2>/dev/null' | sort)
8.10 案例:从 /var/log/secure 找出暴力破解来源 IP
需求:服务器可能正在被 SSH 暴力破解,需找出攻击 IP 及尝试次数,为封禁提供依据。
日志样本(/var/log/secure,Debian 系为 /var/log/auth.log):
Aug 22 10:23:45 web1 sshd[12345]: Failed password for root from 203.0.113.42 port 51234 ssh2
Aug 22 10:23:47 web1 sshd[12346]: Failed password for invalid user admin from 198.51.100.7 port 51240 ssh2
Aug 22 10:24:01 web1 sshd[12350]: Accepted password for deploy from 192.168.1.50 port 51250 ssh2
# 第 1 步:最基础版 —— 提取失败登录的来源 IP 并统计
grep "Failed password" /var/log/secure \
| awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -20
解释:Failed password ... from IP port NNNN ssh2 中,从行尾数:$NF=ssh2、$(NF-1)=端口号、$(NF-2)=port、$(NF-3)=IP。用 $(NF-3) 而不是固定列号,因为「Failed password for root」和「Failed password for invalid user admin」的前面字段数不同,从右数才稳定。
# 第 2 步:更稳健版 —— 直接用正则提取 from 后面的 IP
grep -oE 'Failed password.* from ([0-9]{1,3}\.){3}[0-9]{1,3}' /var/log/secure \
| grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | sort | uniq -c | sort -rn | head -20
# 或用 sed 提取
sed -n 's/.*Failed password.* from \([0-9.]*\) port.*/\1/p' /var/log/secure \
| sort | uniq -c | sort -rn | head -20
# 第 3 步:完整分析报表(一次遍历,同时统计 IP、用户名、时间分布)
awk '/Failed password/{
ip = $(NF-3)
fail[ip]++
total++
# 提取被尝试的用户名
if ($0 ~ /invalid user/) { user = $(NF-5); invalid[user]++ }
else { user = $(NF-5); valid[user]++ }
# 按小时统计
split($3, t, ":"); hour[t[1]]++
}
/Accepted password/{ ok[$(NF-3)]++ }
END{
printf "===== 失败登录总数: %d =====\n\n", total
print "【攻击来源 IP TOP20】"
printf " %-18s %-10s %s\n", "IP", "失败次数", "是否曾成功"
n = 0
for (i in fail) print fail[i], i > "/tmp/_fail_sorted"
close("/tmp/_fail_sorted")
while (("sort -rn /tmp/_fail_sorted | head -20" | getline line) > 0) {
split(line, f, " ")
printf " %-18s %-10s %s\n", f[2], f[1], (f[2] in ok ? "!!! 是 !!!" : "否")
}
print "\n【被尝试的用户名】"
for (u in invalid) printf " %-20s %d 次 (不存在的用户)\n", u, invalid[u]
for (u in valid) printf " %-20s %d 次 (真实存在的用户)\n", u, valid[u]
print "\n【各小时失败次数】"
for (h in hour) printf " %s时 %d\n", h, hour[h]
}' /var/log/secure
# 第 4 步:分项细查
# 统计被尝试次数最多的用户名(看攻击者在猜什么账号)
grep "Failed password" /var/log/secure | awk '{print $(NF-5)}' | sort | uniq -c | sort -rn | head
# 只看针对 root 的攻击
grep "Failed password for root" /var/log/secure | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn
# 找出「先失败多次后来成功」的 IP —— 这是最危险的信号,可能已被攻破
comm -12 \
<(grep "Failed password" /var/log/secure | awk '{print $(NF-3)}' | sort -u) \
<(grep "Accepted password" /var/log/secure | awk '{print $(NF-3)}' | sort -u)
# 统计不存在的用户名尝试(纯扫描行为)
grep "invalid user" /var/log/secure | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head
# 按攻击 IP 的 C 段聚合(识别同一网段的分布式扫描)
grep "Failed password" /var/log/secure | awk '{print $(NF-3)}' \
| awk -F. '{print $1"."$2"."$3".0/24"}' | sort | uniq -c | sort -rn | head
# 查看某个 IP 的完整攻击时间线
grep "203.0.113.42" /var/log/secure | awk '{print $1, $2, $3, $6, $(NF-5)}'
# 统计成功登录记录(安全审计)
grep "Accepted" /var/log/secure | awk '{print $1,$2,$3, "用户:"$(NF-5), "来自:"$(NF-3)}'
第 5 步:生成封禁命令
# 失败超过 20 次的 IP,生成 firewalld 封禁命令(先 echo 确认再执行)
grep "Failed password" /var/log/secure | awk '{print $(NF-3)}' | sort | uniq -c \
| awk '$1 > 20 {print $2}' \
| xargs -r -I {} echo "firewall-cmd --permanent --add-rich-rule=\"rule family=ipv4 source address={} drop\""
# 写入 hosts.deny(简单有效)
grep "Failed password" /var/log/secure | awk '{print $(NF-3)}' | sort | uniq -c \
| awk '$1 > 20 {print "sshd: " $2}' | sudo tee -a /etc/hosts.deny
# iptables 版本
grep "Failed password" /var/log/secure | awk '{print $(NF-3)}' | sort -u \
| xargs -r -I {} sudo iptables -A INPUT -s {} -j DROP
# 排除自己的内网网段,避免误封
grep "Failed password" /var/log/secure | awk '{print $(NF-3)}' \
| grep -Ev '^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)' \
| sort | uniq -c | sort -rn
输出示例:
===== 失败登录总数: 3847 =====
【攻击来源 IP TOP20】
IP 失败次数 是否曾成功
203.0.113.42 1832 否
198.51.100.7 945 否
203.0.113.99 612 !!! 是 !!!
【被尝试的用户名】
root 2103 次 (真实存在的用户)
admin 412 次 (不存在的用户)
test 288 次 (不存在的用户)
生产环境的长期方案是部署
fail2ban(自动读日志并动态封禁)、禁用 root 直接登录(PermitRootLogin no)、改用密钥认证(PasswordAuthentication no)、修改 SSH 默认端口。上面的命令用于应急排查和取证。
8.11 性能提示
处理 GB 级日志时,写法差异可能带来数倍到数十倍的性能差距。
避免 cat 管道套娃(UUOC)
# 差:多一个 cat 进程,多一次管道拷贝
cat access.log | grep "500" | wc -l
cat file | awk '{print $1}'
cat file | sed 's/a/b/'
# 好:命令直接读文件(可以 mmap、可以 seek、少一次数据拷贝)
grep -c "500" access.log
awk '{print $1}' file
sed 's/a/b/' file
# grep 自带计数,比管道给 wc 更快
grep -c "pattern" file # 而不是 grep "pattern" file | wc -l
cat file | cmd 被称为 UUOC(Useless Use Of Cat)。除了多一个进程,更重要的是:命令拿到的是管道而非文件,无法使用 mmap、无法预读优化、无法知道文件大小。对大文件差距明显。
(唯一合理用 cat 的场景:需要把多个文件当一个流处理,且命令不支持多文件参数。)
awk 一次遍历胜过多次管道
# 差:读了 4 遍文件,起了 8 个进程
pv=$(wc -l < access.log)
uv=$(awk '{print $1}' access.log | sort -u | wc -l)
err=$(grep -c " 500 " access.log)
bytes=$(awk '{s+=$10} END{print s}' access.log)
# 好:读 1 遍文件,1 个进程
eval $(awk '{pv++; ip[$1]; if ($9==500) err++; b+=$10}
END{printf "pv=%d uv=%d err=%d bytes=%d\n", pv, length(ip), err, b}' access.log)
echo "PV=$pv UV=$uv ERR=$err BYTES=$bytes"
同理,能省掉的 sort 一定要省:
# 差:sort 需要把全部数据读进内存(或写临时文件)后才能输出第一行
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10
# 好:awk 用哈希表计数,只在最后对「去重后的 key」排序,数据量小得多
awk '{c[$1]++} END{for (i in c) print c[i], i}' access.log | sort -rn | head -10
如果有 100 万行日志但只有 1 万个不同 IP,前者要排序 100 万行,后者只排序 1 万行。
其他一次遍历的技巧:
# 用 exit 提前结束,不读完整个文件
awk '/pattern/{print; exit}' huge.log # 找到第一个就退出
grep -m 1 "pattern" huge.log # grep 的等价选项
awk 'NR>1000{exit} {print}' huge.log # 只要前 1000 行
# 用 grep 先粗筛再交给 awk 精算(grep 比 awk 快很多)
grep " 500 " access.log | awk '{c[$1]++} END{for(i in c) print c[i], i}'
# 比 awk '$9==500{c[$1]++}...' 在 500 占比很低时更快
# 从文件末尾读取而不是全文扫描
tail -10000 access.log | awk '...'
tac access.log | awk 'NR>10000{exit} ...'
LC_ALL=C 对 sort 与 grep 的加速
这是最容易被忽略、收益却最大的一个优化。
# 加速 sort(可达 2~10 倍)
LC_ALL=C sort bigfile > sorted.txt
# 加速 grep(尤其是 UTF-8 环境下的字符类匹配)
LC_ALL=C grep "pattern" bigfile
# 整条流水线一起设置
export LC_ALL=C
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head
加速原理:
-
字符串比较的复杂度不同。在
en_US.UTF-8之类的本地化环境下,sort必须调用strcoll()按语言排序规则比较:要考虑多字节字符解码、大小写等价、重音符号次序、以及每种语言各自的排序习惯(如德语 ä 排在 a 附近)。这是一个查表 + 多轮比较的复杂过程。在LC_ALL=C下,比较退化为strcmp(),即逐字节比较字节序数值,一条 CPU 指令级的操作。 -
grep 的字符类和多字节处理简化。UTF-8 环境下 grep 要处理变长编码(一个字符 1~4 字节),
.、[a-z]、[[:alpha:]]都要按「字符」而非「字节」匹配,还要考虑不同 Unicode 码位。LC_ALL=C下每个字节就是一个字符,可以直接用 Boyer-Moore 等字节级快速算法。 -
省去本地化数据加载:不必读取 locale 定义文件、不必构建 collation 表。
代价与注意事项:
| 影响 | 说明 |
|---|---|
| 排序结果顺序会变 | LC_ALL=C 下大写字母全部排在小写字母之前(因为 ASCII 中 A=65 < a=97)。en_US.UTF-8 下则是 a A b B 这样交替 |
| 中文排序变成按 UTF-8 字节序 | 不再是拼音序或笔画序 |
| 不影响功能正确性 | 只影响「顺序」,去重、计数、匹配的结果都正确 |
# 对比看差异
printf "b\nA\na\nB\n" | LC_ALL=en_US.UTF-8 sort # a A b B
printf "b\nA\na\nB\n" | LC_ALL=C sort # A B a b
结论:做统计、去重、程序内部比较时一律加 LC_ALL=C;只有需要给人看的、要求符合语言习惯的排序(如中文人名列表)才用本地化 locale。
mawk 与 gawk 的性能差异
| 实现 | 相对速度 | 特点 |
|---|---|---|
mawk |
最快(约 gawk 的 2~5 倍) | 功能少:无 gensub、无 strftime、无 PROCINFO、正则支持较弱、不支持 --csv |
gawk |
中等 | 功能最全,Linux 默认,有 -i inplace、时间函数、排序控制、网络 IO |
busybox awk |
快但功能极简 | 嵌入式/容器基础镜像常见 |
original-awk |
慢 | 最原始的 one true awk |
# 检查系统上有哪些实现
which awk mawk gawk 2>/dev/null
ls -l /usr/bin/awk
awk --version | head -1
# 简单基准测试
time gawk '{c[$1]++} END{print length(c)}' bigfile
time mawk '{c[$1]++} END{print length(c)}' bigfile
选择策略:
- 简单的取列、计数、求和(只用
$N、NR、NF、数组、printf)→ 用mawk,性能提升明显。 - 用到
strftime、gensub、PROCINFO["sorted_in"]、asort、-i inplace、复杂正则 → 必须用gawk。 - 写要分发给别人的脚本 → 只用 POSIX awk 的公共子集,或显式调用
gawk并在文档中说明依赖。
其他通用性能建议:
# 1. 尽早过滤,减少后续处理的数据量
grep "2026-08-22" huge.log | awk '{...}' # 先缩小范围
# 2. 用 fgrep / grep -F 匹配固定字符串(无需正则引擎)
grep -F "192.168.1.1" bigfile # 比 grep 快
# 3. sort 给足内存、指定快速的临时目录、开并行
LC_ALL=C sort -S 4G -T /dev/shm --parallel=8 bigfile > sorted.txt
# 4. 压缩日志直接流式处理,不要先解压到磁盘
zcat access.log.gz | awk '{...}'
zgrep "pattern" access.log.*.gz
# 5. 多文件并行处理
ls *.log | xargs -P 8 -I {} sh -c 'awk "{c[\$1]++} END{for(i in c) print c[i], i}" {} > {}.stat'
# 6. 只要行数时用 wc -l(高度优化的 C 实现),别用 awk END{print NR}
wc -l < bigfile
练习
A 组:源文档原有练习
A1 grep 基础
1. 在 etcpasswd 文件中查找包含 root 的所有行。
grep root etcpasswd
2. 统计 etcpasswd 文件中 root 字符串出现的行数。
grep -c 'root' etcpasswd
3. 忽略大小写查找包含 root 的行,写出两种等价写法。
grep -E '[Rr]oot' etcpasswd
grep -i root etcpasswd
4. 写出常用 grep 命令:递归查找、显示行号、只输出文件名、完全匹配单词、显示匹配行后 3 行、反向匹配、使用扩展正则。
grep root /etc/passwd
grep -n root /etc/passwd
grep -A 3 nfs /etc/passwd
grep -rl root ./
grep -v root /etc/passwd
grep -E 'a+' /etc/passwd
A2 sort 排序
5. 拷贝 etcpasswd 为 mfile,取前 10 行存为 more_h10,再对 more_h10 升序排序。
cp etcpasswd mfile
head mfile > more_h10
sort more_h10
输出:
admx34admvaradmsbinnologin
binx11binbinsbinnologin
daemonx22daemonsbinsbinnologin
haltx70haltsbinsbinhalt
lpx47lpvarspoollpdsbinnologin
mailx812mailvarspoolmailsbinnologin
rootx00rootrootbinbash
shutdownx60shutdownsbinsbinshutdown
syncx50syncsbinbinsync
uucpx1014uucpvarspooluucpsbinnologin
6. 按数字对 /etc/passwd 排序;以冒号为分隔符按第 5 列数字排序。
sort -n /etc/passwd
sort -t: -n -k5 /etc/passwd
A3 uniq 去重
7. 给定 123.txt(内容见下),显示文件中不重复的行。
123 456
122 456
133 456
456 123
557 123
789 456
789 567
567 567
567 123
uniq -u 123.txt
8. 跳过前 1 个字段后再比较,并在行首显示出现次数。
uniq -cf 1 123.txt
9. 跳过前 2 个字符后再比较,并在行首显示出现次数。
uniq -cs 2 123.txt
10. 为什么 uniq 通常要先配合 sort 使用?
答:uniq 只比较相邻的行,它是流式处理、内存占用恒定,不会记住之前出现过的所有行。因此文件中不相邻的重复行无法被去掉,必须先用 sort 把相同的行排到一起。
A4 comm 比较有序文件
11. 给定两个已排序文件 123.txt(123 234 345 456)与 234.txt(124 232 341 456),只显示两个文件中相同的行。
comm -12 123.txt 234.txt
输出:
456
12. comm 输出的三列分别代表什么?-1、-2、-3 的作用是什么?
答:第 1 列是仅在 file1 中出现的行,第 2 列是仅在 file2 中出现的行,第 3 列是两文件都出现的行。-1/-2/-3 分别抑制(不显示)对应的列,可组合使用。
A5 diff 比较文件
13. 比较 /etc/passwd 与 passwd,忽略空行差异。
diff -B /etc/passwd passwd
14. diff 与 comm 的关键区别是什么?
答:diff 不要求文件预先排序,逐行比较并给出「如何把 file1 改成 file2」的操作说明;comm 要求两个文件都已排序,输出的是集合关系(独有/共有)。若两个文件完全相同,diff 没有任何输出。
15. 写出 -b、-c、-C n、-i、-r 各选项的作用。
答:-b 忽略空格数量造成的差别(How are you 与 How are you 视为相同);-c 以上下文格式输出,显示不同点上下各 3 行;-C n 上下文格式,显示上下各 n 行(如 -C 4);-i 比较时忽略大小写;-r 比较目录时递归比较各子目录中的文件。
A6 wc 统计
16. 统计 etcpasswd 文件的行数、单词数与字节数。
wc -lcw etcpasswd
输出:
40 72 1984 etcpasswd
17. 结合管道统计 /etc/passwd 中包含 root 的行数。
cat /etc/passwd | grep root | wc -l
18. 分别统计 /etc/passwd 的字节数、单词数、行数。
wc -c /etc/passwd
wc -w /etc/passwd
wc -l /etc/passwd
19. -c 与 -m 有什么区别?
答:在 Linux 中字符串以 \0 作为结束符;-c 统计字节数,-m 统计字符数。纯 ASCII 时两者相同,UTF-8 中文时一个汉字通常是 3 字节 1 字符。
A7 cut 列切割
20. 将 /etc/passwd 以 : 作为分隔符,只输出第 2 列。
cut /etc/passwd -d ":" -f 2
21. 结合管道,只输出 /etc/passwd 第一行的第 1 个字段。
cat /etc/passwd | head -1 | cut -d ":" -f1
输出:
root
22. 显示 /etc/passwd 的第 1 列;显示第 1、2、5 列。
cut -d: -f1 /etc/passwd
cut -d: -f1,2,5 /etc/passwd
23. -d 与 -f 的使用注意点是什么?
答:-d 与 -f 必须配合使用,单独使用 -d 没有效果或会报错。另外 -d 只能是单个字符,且 cut 无法调整输出列的顺序。
A8 tr 字符替换与删除
24. 将 123.txt 中的冒号 : 替换为点号 .(若内容为 a:b:c:d)。
cat 123.txt | tr ":" "."
输出:
a.b.c.d
25. 将 /etc/passwd 第一行中的 : 替换为 ,。
cat /etc/passwd | head -1 | tr ":" ","
26. 把 /etc/passwd 中所有 a 替换为 b;把 /etc/passwd 小写转大写。
cat /etc/passwd | tr a b
tr a-z A-Z < /etc/passwd
27. tr 会修改源文件吗?
答:不会。tr 不修改源文件,结果输出到标准输出;且它不接受文件名作为参数,只能通过管道或输入重定向提供数据。
A9 find 文件查找
28. 查找 /etc 目录下名为 grub.conf 的文件;用通配符查找 /etc 下所有 .conf 结尾的文件;查找 /etc 下所有隐藏文件;忽略大小写查找 /test/ 下的 Aaa。
find /etc/ -name grub.conf
find /etc/ -name "*.conf" # 通配符必须加引号,否则 shell 会先展开导致出错
find /etc/ -name ".*"
find /test/ -iname Aaa
-iname Aaa 输出示例:
/test/aaa
/test/Aaa
/test/AAa
29. 只在 /test 的第 1 层查找 aaa;在 /test 的前 2 层查找 aaa。
find /test/ -maxdepth 1 -name aaa # 输出:/test/aaa
find /test/ -maxdepth 2 -name aaa # 输出:/test/aaa 与 /test/a/aaa
30. 查找 /etc 下名为 grub.conf 的链接文件,写出两种方式。
find /etc -type l | grep grub.conf
find /etc -type l -name grub.conf
31. 按属主、属组、UID、GID 查找,以及查找无属主/无属组文件。
find / -user a
find / -group a
find / -uid 533
find / -gid 533
find / -nouser
find / -nogroup
32. 查找权限恰好为 777 的文件。+mode 与 -mode 有什么区别?给定文件 1~10(权限见正文),find . -perm +422 与 find . -perm -422 分别匹配哪些文件?
find / -perm 777
find . -perm +422 # 只有文件 7 和 10 不符合
find . -perm -422 # 只有文件 3 和 5 符合
答:+mode 把权限写成 9 位逐位对比,只要有任意一位重合就匹配;-mode 必须全部包含所有指定的位才匹配。
33. 假设 /etc/ 下所有文件属主属组都是 root:root,查找普通用户 user1 在 /etc/ 下能读又能写的文件。
find /etc/ -perm -006
34. 按大小查找:大于 500MB、大于 1GB、大于 50KB、小于 1MB、大于 3 字节、大于 50 个 block、大于 80MB 且小于 100MB。
find / -size +500M
find / -size +1G
find /etc -size +50k
find / -size -1M
find / -size +3c
find /etc -size +50b # 注意 b 是 block(512 字节),不是 byte
find / -size +80M -size -100M
35. 用 stat 查看文件的三种时间戳,并说明 atime / mtime / ctime 的改变方式。
stat 1 | tail -3
答:atime 是访问时间,用 cat、tail、head、more、less、vi 读取或执行文件会改变;mtime 是内容修改时间,用 vi 修改内容或 echo 重定向写入会改变;ctime 是状态改变时间,内容/属主/属组/权限变化或创建硬链接软链接都会改变。mtime 改变时 ctime 也会改变,但 ctime 改变不一定改变 mtime(如只改权限)。
36. 说明 -mtime n、-mtime +n、-mtime -n 的精确语义,并写出对应命令。
答:-mtime n 表示恰好在 n 天前的那一个 24 小时区间内被修改(距今 n×24 到 (n+1)×24 小时之间);-mtime +n 表示距今超过 (n+1)×24 小时之前被修改;-mtime -n 表示距今 n×24 小时以内被修改。
find / -mtime 0 # 当天这 24 小时内被修改过
find / -mtime +1 # 48 小时之前(2 天前及更早)
find . -mtime -1 # 最近 24 小时内
find / -mtime 1 # 24~48 小时之前这一个区间
37. 假设当前是 2014 年 7 月 23 日 14:35,要查找 7 月 20 日当天被修改过内容的文件,命令怎么写?
find / -mtime 3 # 即 7 月 19 日 14:35 到 7 月 20 日 14:35 之间
38. 查找 30 分钟前到 150 分钟前(2 小时区间内)被修改过内容的文件。
find / -mmin +30 -mmin -150
39. find 多条件综合:查找大于 5M、权限 644、属主 root 的普通文件并 ls -hl 显示。
find / -size +5M -perm 644 -user root -type f 2>/dev/null -exec ls -hl {} \;
find的多个条件默认是「与」关系,可任意叠加。全盘查找时普通用户必然遇到无权访问的目录,用2>/dev/null把标准错误丢弃即可屏蔽大量Permission denied干扰信息。
40. 查找大于 10M、属主为 soul 的普通文件并显示详情。
find / -size +10M -user soul -type f -exec ls -hl "{}" \;
A10 which / whereis / locate
41. 分别用 which、whereis 查找 ifconfig;用 locate 查找 hello 并说明找不到新建文件时怎么办。
which ifconfig
whereis ifconfig
locate hello
updatedb # locate 依赖预建数据库(通常在 /var/lib/mlocate/mlocate.db),新建文件需先 updatedb
A11 正则表达式
42. 写出匹配 ab/abb/abbb、匹配 ab/abab/ababab、匹配 a/aa/aaa 的正则。
ab+ # ab, abb, abbb...
(ab)+ # ab, abab, ababab...
a{1,3} # a, aa, aaa
+、?、{n,m}、|、()属于扩展正则(ERE)元字符,必须配合grep -E或sed -r。在基本正则(BRE)中它们是普通字符,要写成\+、\?、\{n,m\}、\|、\(...\)。
43. 用反向引用匹配 abcabcabcaabb 中的重复结构;匹配含重复短语的句子。
echo "abcabcabcaabb" | grep -E '(a(bc)){2}\1' --color
echo "hello world, hello world, hello beautiful world" \
| grep -E --color '((hello) (world)), \1, \2 .* \3'
答:从左到右按左小括号计数,第 n 对括号匹配的内容用 \n 引用。
*44. \d、\D 在 GNU grep / sed 中能用吗?非贪婪 `.?` 能用吗?**
答:都不能。GNU grep(BRE/ERE)与 sed 支持 \w、\W、\b、\B、\s、\S,但 \d、\D 是 PCRE 专有语法,写了不会报错而是被当成普通字母 d,导致静默匹配失败;需要时改用 grep -P '\d' 或等价的 [0-9] / [[:digit:]]。非贪婪 ? 也是 PCRE 专有:grep -E 中 .*? 会被理解为「.* 出现 0 次或 1 次」,仍是贪婪结果,需用 grep -P 或改写为排除型字符类(如用 [^b]*b 代替 .*?b)。
A12 sed 流编辑器
45. 用 sed 打印:第 3 行;第 3 和第 7 行;第 3~5 行和第 7 行;含 root 的行;以 root 开头的行;含数字的行;非空格开头的行。
sed -n '3p' /etc/passwd
sed -n '3p;7p' /etc/passwd
sed -n '3,5p;7p' /etc/passwd
sed -n '/root/p' /etc/passwd
sed -n '/^root/p' /etc/passwd
sed -n '/[0-9]/p' /etc/passwd
sed -n '/^[^[:blank:]]/p' passwd
46. 在 passwd 第 2 行前插入内容;在第 2 行后追加内容。
sed -i '2i content' passwd
sed -i '2a content' passwd
47. 用 sed 删除:第 3 行(不改原文件 / 改原文件);第 2~3 行;第 1 行和第 5 行;空行;空格开头的行;注释行和空行。
sed '3d' mypasswd
sed -i '3d' mypasswd
sed -e '2,3d' passwd
sed -e '1d;5d' passwd
sed -e '/^$/d' passwd
sed -e '/^[[:blank:]]/d' /etc/passwd
sed -e '/^#/d;/^$/d' passwd
48. 用 sed 替换:每行第一个 root;每行第二个 root;全部 root;删除行首空格;1~10 行替换;10 行到末尾替换;10 行到末尾小写换 K;删除每行第一个字符;第一个字符换 A;删除每行最后一个字符;删除倒数第二个字符;删除每行第二个数字。
sed -e 's/root/soul/' mypasswd
sed -e 's/root/soul/2' mypasswd
sed -e 's/root/soul/g' mypasswd
sed -e 's/^[ ]*//g' /etc/passwd
sed -e '1,10s/root/soul/g' mypasswd
sed -e '10,$s/root/soul/g' mypasswd
sed -e '10,$s/[a-z]/K/g' mypasswd
sed -e "s/^.//" mypasswd
sed -e "s/^./A/" mypasswd
sed -e 's/.$//' mypasswd
sed -r -e 's/.(.)$/\1/' mypasswd
sed -r -e 's/[0-9]+//2' passwd
49. 对 123456789ABCDEF 按位置删除字符:删除第 3~5 字符;删除第 3 字符;删除第 3~12 字符。
echo 123456789ABCDEF | sed -r 's/^(..)...(.*)$/\1\2/'
echo 123456789ABCDEF | sed -r 's/^(.{2}).(.*)$/\1\2/'
echo 123456789ABCDEF | sed -r 's/^(.{2}).{10}(.*)$/\1\2/'
50. 用 sed 提取有线网卡名。
ifconfig | sed -rn 's/^(e[0-9a-zA-Z]+).*$/\1/p'
51. 写脚本截取默认网关网卡的 IP / 子网掩码,并给出 ip 命令的现代替代写法。
#!/bin/bash
export LANG=en_US.UTF-8
export LANGUAGE=en_US:en
link=$(route -n | sed -n '/UG/p' | awk '{print $NF}')
ip=$(ifconfig $link | sed -n '/Mask/p' | sed -r 's/^([^0-9]+)([0-9.]+)(.*)([^0-9.]+)([0-9.]+$)/\2\/\5/')
echo $ip
ifconfig 属于已废弃的 net-tools 工具集,CentOS 7 及以上最小化安装通常不自带,推荐用 ip 命令:
link=$(route -n | sed -n '/UG/p' | awk '{print $NF}')
ip=$(ip a s $link | sed -n '/inet /p' | awk '{print $2}')
echo $ip
ip a s是ip address show的缩写,输出中inet行的第 2 个字段即「IP/掩码位数」形式的地址。
A13 awk 练习
52. 写出 awk 的基本结构,说明 BEGIN、END、$0、$1~$n、NR、NF、FNR、FS、OFS、RS、ORS 的含义。
awk 'BEGIN{ FS=":" } { print $1 } END{ print NR }' /etc/passwd
答:BEGIN 读取记录前执行;END 读取全部记录后执行;$0 当前完整记录;$1~$n 当前记录第 n 个字段;NR 已读取记录数(行号);NF 当前记录字段数;FNR 当前文件的记录数;FS 输入字段分隔符;OFS 输出字段分隔符;RS 输入记录分隔符(默认换行);ORS 输出记录分隔符。
53. 用 awk 完成下列各题:打印第 3 列;打印所有行;打印最后一列;多列拼接;打印第 8 行;打印总行数;打印每行字段数;字段数大于 4 的行;UID 在 30~40 的用户;打印 5~56 行;打印偶数行;打印每行第一个单词。
awk -F ":" '{print $3}' /etc/passwd
awk '{print $0}' /etc/passwd
awk -F ":" '{print $NF}' /etc/passwd
awk -F ":" '{print $1 "\t" $3 "welcome"}' /etc/passwd
awk -F ":" 'NR == 8 {print $0}' /etc/passwd
awk -F ":" 'END{print NR}' /etc/passwd
awk -F ":" '{print NF}' /etc/passwd
awk -F ":" 'NF > 4 {print $0}' /etc/passwd
awk -F ":" '$3 >= 30 && $3 <= 40{print $1}' /etc/passwd
awk -F ":" 'NR >= 5 && NR <= 56 {print $0}' /etc/passwd
awk '{if (NR % 2 == 0)print $0}' /etc/passwd
awk 'BEGIN{FS="[^A-Za-z]+"}{print $1}' /etc/passwd
54. 写一个 awk 脚本文件,统计并打印 shell 为 /sbin/nologin 的行及其数量,然后用 -f 调用。
#!/bin/awk
BEGIN{
FS=":"
count=0
}
{
if($7 == "/sbin/nologin"){
count += 1
print $0
}
}
END{
print "count=" count
}
awk -f 1.awk /etc/passwd
55. 用一行 awk 统计 /sbin/nologin 出现次数;统计所有字段总数;统计字段数大于 5 的行数。
awk 'BEGIN{FS=":"; count=0}{if($7 == "/sbin/nologin")count+=1}END{print count}' /etc/passwd
awk -F ":" 'BEGIN{c=0}{c=c+NF}END{print c}' /etc/passwd
awk -F ":" 'BEGIN{count=0}(NF > 5){count+=1}END{print count}' /etc/passwd
56. 【综合练习 20.5】用 awk 打印:UID 在 800~1000 的用户;第 6~9 行;最后一行的最后一个字段。
awk -F: '$3 >= 800 && $3 <= 1000 {print $1}' /etc/passwd # UID 在 800~1000 的用户
awk -F: 'NR >= 6 && NR <= 9 {print $0}' /etc/passwd # 打印第 6~9 行
awk -F: 'END{print $NF}' /etc/passwd # 最后一行的最后一个字段
END块中$0及各字段仍保留最后一条记录的内容,因此END{print $NF}取到的是末行末字段。
A14 综合练习
57. 【综合练习 20.1】用 read 取成绩,用 if/elif/else 构成数值判断阶梯,输出 A/B/C/D。
read -p "score: " score
if [ "$score" -ge 90 ]; then
echo "A"
elif [ "$score" -ge 80 ]; then
echo "B"
elif [ "$score" -ge 70 ]; then
echo "C"
else
echo "D"
fi
58. 【综合练习 20.2】写一个猜数字游戏:综合运用 $RANDOM、while 循环、read 交互与 exit。
#!/bin/bash
target=$((RANDOM % 100 + 1)) # 1 ~ 100 的随机数
max=$((RANDOM % 4 + 4)) # 4 ~ 7 次随机猜测机会
count=0
while [ $count -lt $max ]; do
read -p "guess: " guess
if [ "$guess" -eq "$target" ]; then
echo "right!"
exit 0
elif [ "$guess" -gt "$target" ]; then
echo "too big"
else
echo "too small"
fi
((count++))
done
echo "game over"
该脚本缺少输入校验:一旦输入非数字,
[ "$guess" -eq "$target" ]会直接报integer expression expected并继续消耗次数。生产脚本中应先用[[ "$guess" =~ ^[0-9]+$ ]]之类的判断过滤非法输入。
59. 【综合练习 20.3】find 多条件综合:查找大于 5M、权限 644、属主 root 的普通文件,屏蔽权限错误,并用 ls -hl 列出。
find / -size +5M -perm 644 -user root -type f 2>/dev/null -exec ls -hl {} \;
find的多个条件默认是「与」关系,可任意叠加。全盘查找时普通用户必然遇到无权访问的目录,用2>/dev/null把标准错误丢弃,即可屏蔽大量Permission denied干扰信息。
60. 【综合练习 20.4】用 sed 反向引用把 /etc/passwd 每行的首末字段互换。
cat /etc/passwd > 1_passwd.txt
sed -r 's/^([^:]+)([^a-zA-Z]+)([^:]+)(.*)\2([^:]+)$/\1\2\5\4\2\3/' 1_passwd.txt
五个子表达式依次对应「首字段、分隔符、第 2 字段、中间内容、末字段」,替换部分把
\3与\5调换位置即完成换位;\2在匹配模式中被复用,保证前后分隔符一致。
B 组:补充动手练习
B1 awk 分组统计
61. 统计 /etc/passwd 中每种登录 shell 各被多少用户使用,按人数降序输出。
awk -F: '{c[$7]++} END{for (s in c) print c[s], s}' /etc/passwd | sort -rn
62. 给定 salary.txt(部门 姓名 工资),统计每个部门的人数、工资总额与平均工资,格式化对齐输出。
awk '{sum[$1] += $3; cnt[$1]++}
END{
printf "%-12s%-8s%-14s%-12s\n", "部门", "人数", "工资总额", "平均工资"
for (d in sum)
printf "%-12s%-8d%-14.2f%-12.2f\n", d, cnt[d], sum[d], sum[d]/cnt[d]
}' salary.txt | sort
63. 统计访问日志中每个 IP 消耗的总流量(第 10 字段是字节数),换算为 MB,取前 5 名。
awk '{b[$1] += $10} END{for (i in b) printf "%.2f MB %s\n", b[i]/1024/1024, i}' access.log \
| sort -rn | head -5
64. 找出 /etc/passwd 中 UID 的最大值、最小值和平均值。
awk -F: '{
sum += $3
if (NR == 1 || $3 > max) max = $3
if (NR == 1 || $3 < min) min = $3
}
END{printf "最大: %d\n最小: %d\n平均: %.2f\n", max, min, sum/NR}' /etc/passwd
65. 按「状态码 + 请求方法」二维分组统计访问日志(提示:用多维数组和 SUBSEP)。
awk '{gsub(/"/, "", $6); c[$9, $6]++}
END{for (k in c) {split(k, a, SUBSEP); printf "%-6s %-8s %d\n", a[1], a[2], c[k]}}' access.log \
| sort -k3,3rn
B2 awk 数组去重
66. 去掉文件中的重复行,但保持原始出现顺序(不能用 sort)。
awk '!a[$0]++' file
原理:a[$0]++ 先取值再自增,首次取到 0(假),!0 为真则打印;之后取到 ≥1(真),!1 为假不打印。
67. 按第 2 列去重,只保留每个值第一次出现的那一行。
awk '!seen[$2]++' file
68. 统计访问日志中的独立访客数(UV),不使用 sort -u。
awk '{ip[$1]} END{print "UV: " length(ip)}' access.log
69. 找出文件中出现次数超过 1 次的行(不排序)。
awk '{c[$0]++} END{for (l in c) if (c[l] > 1) print c[l], l}' file
70. 对比 sort -u、sort | uniq、awk '!a[$0]++' 三种去重方式的差异。
答:
| 方式 | 是否排序 | 顺序 | 内存 | 遍历次数 |
|---|---|---|---|---|
sort -u |
是 | 被改变 | 需缓冲/临时文件 | 多趟 |
sort \| uniq |
是 | 被改变 | 同上 + 一个进程 | 多趟 |
awk '!a[$0]++' |
否 | 保持原序 | 全部唯一键进内存 | 1 趟 |
结论:需要保持顺序或追求速度用 awk;数据量极大超过内存则必须用 sort(它会用磁盘临时文件)。
B3 awk NR==FNR 关联
71. 有 vip.txt(一列 IP)和 access.log,只输出来自 VIP 名单 IP 的访问记录。
awk 'NR==FNR{vip[$1]; next} $1 in vip' vip.txt access.log
72. 反过来,输出不在名单中的 IP 的访问记录。
awk 'NR==FNR{vip[$1]; next} !($1 in vip)' vip.txt access.log
73. users.txt(uid 用户名)与 scores.txt(uid 分数),关联输出「uid 用户名 分数」。
awk 'NR==FNR{name[$1]=$2; next} {print $1, ($1 in name ? name[$1] : "未知"), $2}' users.txt scores.txt
74. 详细解释 NR==FNR 为什么能区分第一个文件和第二个文件。
答:NR 是全局行号(跨文件累加),FNR 是当前文件内行号(换文件归 1)。处理第一个文件时两者同步递增,NR == FNR 恒为真;处理第二个文件时 NR 已经累加了第一个文件的行数,必然大于 FNR,NR == FNR 恒为假。配合 next 跳过后续规则,就实现了「第一个文件建字典,第二个文件查字典」。注意第一个文件为空时该技巧会失效。
75. 找出 new.txt 相对 old.txt 新增的整行(不看字段)。
awk 'NR==FNR{line[$0]; next} !($0 in line)' old.txt new.txt
76. 比对本机与远端的 rpm 包,列出本机独有的包(用两种方法)。
# 方法一:awk(不需排序)
awk 'NR==FNR{p[$0]; next} !($0 in p)' /tmp/pkg_remote.txt /tmp/pkg_local.txt
# 方法二:comm(要求有序)
comm -23 <(sort /tmp/pkg_local.txt) <(sort /tmp/pkg_remote.txt)
B4 sort 多级排序
77. 对 /etc/passwd 先按 shell(第 7 列)字母升序,同 shell 内按 UID(第 3 列)数字降序排列。
sort -t: -k7,7 -k3,3nr /etc/passwd
78. 解释 -k2n 与 -k2,2n 的区别。
答:-k2n 的排序键是「从第 2 字段一直到行末」,后续字段都参与比较;-k2,2n 的排序键只有第 2 字段。给定两行 apple 10 zzz 与 apple 10 aaa,-k2n 会因第 3 字段不同而排序,-k2,2n 则认为两行相等。推荐始终写 -kN,Nn。
79. 按磁盘占用从大到小列出 /var 下各目录(要正确处理 K/M/G 后缀)。
du -sh /var/* 2>/dev/null | sort -hr | head -10
答:必须用 -h(human-numeric)。用 -n 会把 900M 当 900、1.5G 当 1.5,结果完全错误。
80. 按内核版本号先后顺序列出 rpm 包。
rpm -qa 'kernel*' | sort -V
81. 把排序结果安全地写回原文件。
sort file -o file # 正确
# sort file > file # 错误!会先清空文件导致数据丢失
82. 排序一个 10GB 的文件,写出性能最优的命令。
LC_ALL=C sort -S 4G -T /dev/shm --parallel=8 bigfile -o sorted.txt
答:LC_ALL=C 让比较退化为逐字节 strcmp(可快数倍);-S 给足内存缓冲减少落盘;-T 指定快速的临时目录;--parallel 开多线程。
B5 uniq 词频
83. 统计 /etc/passwd 中各 shell 的使用人数(用 sort + uniq 组合)。
cut -d: -f7 /etc/passwd | sort | uniq -c | sort -rn
84. 统计一篇英文文章的高频单词 TOP20(忽略大小写,忽略标点)。
tr -cs '[:alpha:]' '\n' < article.txt | tr 'A-Z' 'a-z' | sort | uniq -c | sort -rn | head -20
逐段解释:tr -cs '[:alpha:]' '\n' 把所有非字母字符压缩并替换成换行(一个单词一行)→ tr 'A-Z' 'a-z' 统一小写 → sort 让相同单词相邻 → uniq -c 计数 → sort -rn 降序 → head -20 取前 20。
85. 找出日志中只出现过一次的错误信息(可能是偶发问题)。
grep "ERROR" app.log | awk '{$1=""; $2=""; print}' | sort | uniq -u
86. 找出重复出现的行,并只显示重复次数大于 3 的。
sort file | uniq -c | awk '$1 > 3'
87. 日志行首是「日期 时间」两个字段,要按「除时间戳外的内容」去重统计,怎么写?
sort -k3 app.log | uniq -cf 2 | sort -rn
答:uniq -f 2 跳过前 2 个字段(日期和时间)再比较。注意排序也要从第 3 字段开始,否则相同内容不会相邻。
B6 cut 与 tr 组合
88. 从 ps aux 输出中取出 PID 和命令名(注意列之间是不定数量空格)。
# 方法一(推荐):awk 天然处理连续空白
ps aux | awk '{print $2, $11}'
# 方法二:先用 tr -s 压缩空格再交给 cut
ps aux | tr -s ' ' | cut -d' ' -f2,11
89. 为什么 ps aux | cut -d' ' -f2 取不到 PID?
答:cut -d' ' 把每一个空格都当分隔符,连续空格之间会产生大量空字段,所以第 2 个字段往往是空串。ps aux、ls -l、df -h、netstat 的输出都是用不定数量空格对齐的,cut 全部无能为力,应改用 awk(默认把连续空白视为一个分隔符)或先 tr -s ' ' 压缩。
90. 把 Windows 格式(CRLF)的脚本转换为 Unix 格式(LF),写出三种方法。
tr -d '\r' < script.sh > script_fixed.sh && mv script_fixed.sh script.sh
sed -i 's/\r$//' script.sh
dos2unix script.sh
答:注意 tr 不能原地修改(tr -d '\r' < f > f 会清空文件),必须写到新文件再替换。检测方法:file script.sh 会显示 "with CRLF line terminators",或 cat -A script.sh 看到行尾的 ^M$。CRLF 的典型症状是 /bin/bash^M: bad interpreter。
91. 从 /etc/passwd 中提取用户名并全部转为大写。
cut -d: -f1 /etc/passwd | tr 'a-z' 'A-Z'
cut -d: -f1 /etc/passwd | tr '[:lower:]' '[:upper:]'
awk -F: '{print toupper($1)}' /etc/passwd
92. 生成一个 16 位随机密码(只含字母和数字)。
tr -cd '[:alnum:]' < /dev/urandom | head -c 16; echo
93. 取出路径 /usr/local/bin/python3 的最后一段(不用 basename)。
echo "/usr/local/bin/python3" | rev | cut -d/ -f1 | rev
echo "/usr/local/bin/python3" | awk -F/ '{print $NF}'
答:cut 只能从左数,所以先 rev 反转、取第 1 段、再 rev 转回来。
94. 删除文件中所有数字和标点,只保留字母、空格和换行。
tr -cd '[:alpha:] \n' < file
tr -d '[:digit:][:punct:]' < file
B7 xargs 批量处理带空格文件名
95. 删除当前目录下所有 .tmp 文件,文件名可能含空格,写出安全的命令。
find . -name "*.tmp" -print0 | xargs -0 rm -f
# 或
find . -name "*.tmp" -delete
find . -name "*.tmp" -exec rm -f {} +
*96. 为什么 `find . -name ".txt" | xargs rm` 是危险的?**
答:xargs 默认用空白(空格、Tab、换行)分隔参数,文件名 my file.txt 会被拆成 my 和 file.txt 两个参数。轻则报错,重则误删恰好叫 my 的其他文件。必须用 -print0 | xargs -0,因为 NUL 是文件名中唯一不可能出现的字符。
97. 把所有 .log 文件复制到 /backup/(文件名含空格),要求参数放在命令中间。
find . -name "*.log" -print0 | xargs -0 -I {} cp {} /backup/
98. 4 个进程并行压缩所有 .log 文件。
find . -name "*.log" -print0 | xargs -0 -n 1 -P 4 gzip
*99. 遇到 Argument list too long 时(`rm /tmp/cache/` 报错,目录下 50 万个文件),写出至少三种解决办法。**
find /tmp/cache -maxdepth 1 -type f -delete # 最快
find /tmp/cache -maxdepth 1 -type f -print0 | xargs -0 rm -f # xargs 自动分批
find /tmp/cache -maxdepth 1 -type f -exec rm -f {} + # -exec + 也会分批
mv /tmp/cache /tmp/cache.old && mkdir /tmp/cache && rm -rf /tmp/cache.old # 整体替换
答:原因是单条命令的参数总长度受 ARG_MAX 限制(getconf ARG_MAX 可查,通常 2MB)。xargs 会主动测量参数长度并自动拆批,所以能解决。
100. xargs 与 find -exec 各自适合什么场景?
答:
- 只是 find 结果 + 简单命令 →
-exec cmd {} +(简洁、天然处理空格、自动分批) - 需要并行(
-P)、参数需在命令中间(-I)、数据源不是 find →xargs - 命令必须逐个文件执行、
{}需出现多次 →-exec cmd {} \;或xargs -I {} -exec cmd {} \;每个文件起一个进程,文件多时非常慢,应尽量用+
101. 用 xargs 批量替换多个文件中的字符串(只处理确实包含目标的文件)。
grep -rlZ "old_string" . | xargs -0 -r sed -i 's/old_string/new_string/g'
答:grep -rl 只列出包含目标的文件名,-Z 用 NUL 分隔,xargs -0 安全接收,-r 保证无匹配时不执行 sed。
102. 为什么要加 -r?
答:GNU xargs 在输入为空时仍会执行一次命令(不带参数)。若命令是 rm -rf 且有默认参数,可能造成灾难。-r(--no-run-if-empty)保证输入为空则完全不执行。注意 BSD/macOS 的 xargs 默认就不执行空输入且不支持 -r。
B8 三剑客综合分析日志
103. 一条命令统计 Nginx 日志的 TOP10 IP,并对比 sort|uniq 与 awk 数组两种写法的性能差异。
# 写法一:管道
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10
# 写法二:awk 哈希计数(推荐)
awk '{c[$1]++} END{for (i in c) print c[i], i}' access.log | sort -rn | head -10
答:若有 100 万行日志但只有 1 万个不同 IP,写法一要排序 100 万行,写法二只对 1 万个去重后的 key 排序,快得多。
104. 统计各状态码的数量与占比,输出对齐报表。
awk '{c[$9]++; t++}
END{printf "%-8s%-10s%-8s\n", "状态码", "次数", "占比"
for (i in c) printf "%-8s%-10d%.2f%%\n", i, c[i], c[i]*100/t
print "总计: " t}' access.log | sort -k2,2rn
105. 找出返回 5xx 的请求 URL 排行前 10。
awk '$9 ~ /^5/{split($7,u,"?"); c[u[1]]++} END{for (i in c) print c[i], i}' access.log \
| sort -rn | head -10
106. 按小时统计请求量并画出简易柱状图。
awk '{split($4, t, ":"); c[t[2]]++}
END{for (h=0; h<24; h++) {
hh = sprintf("%02d", h); n = (hh in c) ? c[hh] : 0
printf "%s时 %6d ", hh, n
for (i=0; i<n/100; i++) printf "#"
print ""
}}' access.log
107. 一次遍历同时算出 PV、UV、总流量、状态码分布。
awk '{pv++; ip[$1]; code[$9]++; bytes += $10}
END{
printf "PV: %d\nUV: %d\n总流量: %.2f GB\n", pv, length(ip), bytes/1024/1024/1024
for (c in code) printf " %-6s %d (%.2f%%)\n", c, code[c], code[c]*100/pv
}' access.log
答:这是大文件处理的核心优化——只读一遍文件、只起一个进程,比分别执行 4 条命令快数倍。
108. 从 /var/log/secure 找出 SSH 暴力破解的来源 IP 及次数(前 20)。
grep "Failed password" /var/log/secure | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -20
答:用 $(NF-3) 从右边数字段,因为「Failed password for root from IP」和「Failed password for invalid user admin from IP」左侧字段数不同,从左数会取错。
109. 找出「先失败多次、后来又成功」的 IP(最危险的信号)。
comm -12 \
<(grep "Failed password" /var/log/secure | awk '{print $(NF-3)}' | sort -u) \
<(grep "Accepted password" /var/log/secure | awk '{print $(NF-3)}' | sort -u)
110. 生成封禁失败超过 20 次的 IP 的命令,并排除内网网段避免误封。
grep "Failed password" /var/log/secure | awk '{print $(NF-3)}' \
| grep -Ev '^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)' \
| sort | uniq -c | awk '$1 > 20 {print "sshd: " $2}' | sudo tee -a /etc/hosts.deny
答:注意用 sudo tee 而不是 sudo echo >>,因为重定向是当前 Shell 做的,不会被 sudo 提权。
111. 统计项目中 Python 代码的总行数、空行、注释行、有效代码行。
find . -name "*.py" -not -path "*/venv/*" -print0 \
| xargs -0 awk '{
total++
if ($0 ~ /^[[:space:]]*$/) blank++
else if ($0 ~ /^[[:space:]]*#/) comment++
else code++
}
END{printf "总行:%d 空行:%d 注释:%d 有效:%d (%.1f%%)\n", \
total, blank, comment, code, code*100/total}'
112. 从访问日志中提取所有 IP 并按 C 段聚合统计(识别同网段分布式扫描)。
grep -Eo '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b' access.log \
| awk -F. '{print $1"."$2"."$3".0/24"}' | sort | uniq -c | sort -rn | head
113. 批量修改配置文件前先备份,并说明每一步的作用。
grep -rn "192.168.1.100" /etc/myapp/ # 1. 确认会改哪些行
grep -rl "192.168.1.100" /etc/myapp/ # 2. 确认会改哪些文件
grep -rlZ "192.168.1.100" /etc/myapp/ \
| xargs -0 sed -i.bak 's/192\.168\.1\.100/10\.0\.0\.50/g' # 3. 备份并替换
grep -rn "192.168.1.100" /etc/myapp/ # 4. 验证(应无输出)
答:sed -i.bak 自动生成 .bak 备份;IP 中的 . 必须转义为 \.,否则会匹配 192x168y1z100;用 grep -rl 先筛出目标文件,避免对整个目录每个文件都跑 sed。
114. 把一条流水线的每个中间结果都存档,便于排查。
cat access.log \
| grep " 500 " | tee /tmp/s1.txt \
| awk '{print $1}' | tee /tmp/s2.txt \
| sort | uniq -c | sort -rn | tee /tmp/s3.txt \
| head -10
115. 对比 grep、sed、awk 的适用场景,给出选型原则。
答:grep 是行过滤器,负责「找」,最快;sed 是流编辑器,负责「改」,能原地修改文件(-i);awk 是编程语言,负责「算」,天然懂列、能做变量数组和算术。原则:能用 grep 的不用 sed,能用 sed 的不用 awk;但一旦涉及「第几列」或「算数」,直接上 awk。
本章小结
正则速查表(BRE 与 ERE 差异)
通用元字符(BRE 与 ERE 写法相同):
| 元字符 | 含义 |
|---|---|
. |
任意单个字符 |
^ |
行首 |
$ |
行尾 |
* |
前一个字符出现 0 次或多次 |
[abc] |
匹配 a、b、c 中任意一个 |
[^abc] |
匹配除 a、b、c 外的任意字符 |
[a-z] [0-9] |
范围匹配 |
\ |
转义符 |
BRE 与 ERE 的写法差异(这是最容易出错的地方):
| 功能 | BRE(grep、sed) |
ERE(grep -E、sed -r、awk) |
|---|---|---|
| 1 次或多次 | \+ |
+ |
| 0 次或 1 次 | \? |
? |
| 恰好 n 次 | a\{n\} |
a{n} |
| n 到 m 次 | a\{n,m\} |
a{n,m} |
| 至少 n 次 | a\{n,\} |
a{n,} |
| 或 | \| |
\| |
| 分组 | \(...\) |
(...) |
| 反向引用 | \1 |
\1 |
记忆规律:BRE 需要转义才有元字符含义,ERE 直接写就是元字符。
awk默认使用 ERE,所以 awk 里写/^(root\|bin)/无需转义括号。
POSIX 字符类(三剑客通用,注意是双层方括号):
| 字符类 | 含义 |
|---|---|
[[:alnum:]] |
字母和数字 |
[[:alpha:]] |
字母 |
[[:digit:]] |
数字 |
[[:lower:]] / [[:upper:]] |
小写 / 大写字母 |
[[:blank:]] |
空格和制表符 |
[[:space:]] |
所有空白字符(含换行) |
[[:punct:]] |
标点符号 |
[[:print:]] / [[:graph:]] |
可打印字符(含 / 不含空格) |
[[:cntrl:]] |
控制字符 |
[[:xdigit:]] |
十六进制数字 |
GNU 扩展与 PCRE 专有(方言陷阱):
| 写法 | GNU grep/sed | 说明 |
|---|---|---|
\w \W |
支持 | 单词字符 [a-zA-Z0-9_] / 非单词字符 |
\s \S |
支持 | 空白 / 非空白 |
\b \B |
支持 | 单词边界 / 非单词边界 |
\< \> |
支持 | 单词开头 / 结尾(GNU 特有) |
\d \D |
不支持 | PCRE 专有,需 grep -P 或改用 [0-9] |
.*? 非贪婪 |
不支持 | PCRE 专有,grep -E 中会被当成「.* 出现 0/1 次」 |
\K (?=) (?!) |
不支持 | PCRE 专有,需 grep -P |
严重陷阱:
\d在 GNU grep/sed 中不报错,而是被当作普通字母d,导致静默匹配失败。排查正则不生效时优先检查这一点。
grep 常用选项速查表
| 选项 | 作用 |
|---|---|
-i |
忽略大小写 |
-v |
反向匹配(输出不匹配的行) |
-c |
只输出匹配的行数 |
-n |
显示行号 |
-l |
只输出含匹配的文件名 |
-L |
只输出不含匹配的文件名 |
-r / -R |
递归查找目录 |
-w |
完全匹配单词 |
-x |
整行严格匹配 |
-o |
只输出匹配到的部分(提取用) |
-q |
静默模式,用退出码判断(脚本用) |
-A n / -B n / -C n |
输出匹配行后 / 前 / 前后 n 行 |
-E |
使用扩展正则(等价 egrep) |
-F |
固定字符串匹配,不用正则(最快,等价 fgrep) |
-P |
使用 PCRE 正则 |
-e |
指定多个模式 |
-f 文件 |
从文件读取模式列表 |
-m n |
匹配 n 次后停止 |
-Z |
文件名用 NUL 分隔(配合 xargs -0) |
--include / --exclude |
递归时限定 / 排除文件名模式 |
sed 常用选项与动作速查表
| 选项 | 作用 |
|---|---|
-n |
取消默认输出,通常与 p 联用 |
-e |
指定表达式(可多个) |
-i |
直接修改原文件;-i.bak 同时生成备份 |
-r / -E |
启用扩展正则 |
-f |
从文件读取 sed 脚本 |
-s |
多文件时视为独立文件(不连续编号) |
| 动作 | 作用 |
|---|---|
p |
打印 |
d |
删除 |
s/old/new/ |
替换(g 全部、N 第 N 个、i 忽略大小写、p 打印、w 写文件) |
i\ |
在行前插入 |
a\ |
在行后追加 |
c\ |
替换整行 |
y/abc/xyz/ |
字符转换(类似 tr) |
q |
退出 |
= |
打印行号 |
n / N |
读入下一行 / 追加下一行到模式空间 |
h / H / g / G / x |
保持空间操作(存 / 追加存 / 取 / 追加取 / 交换) |
& |
在替换串中引用整个匹配内容 |
\1 ~ \9 |
引用子表达式 |
地址(范围)写法:3(第 3 行)、3,5(3 到 5 行)、3;7(3 和 7 行)、$(末行)、/re/(匹配行)、/re1/,/re2/(范围)、1~2(奇数行,GNU)、3,+2(第 3 行起 3 行)、/re/!(取反)。
awk 内置变量速查表
| 变量 | 含义 | 默认值 |
|---|---|---|
$0 |
当前整条记录 | — |
$1 ~ $NF |
各字段 | — |
NF |
当前记录的字段数 | — |
NR |
已读记录总数(跨文件累加) | — |
FNR |
当前文件内的记录号 | — |
FS |
输入字段分隔符 | 空格/Tab |
OFS |
输出字段分隔符 | 空格 |
RS |
输入记录分隔符 | \n |
ORS |
输出记录分隔符 | \n |
FILENAME |
当前文件名 | — |
ARGC / ARGV |
命令行参数个数 / 数组 | — |
ENVIRON |
环境变量数组 | — |
SUBSEP |
多维数组下标连接符 | \034 |
RSTART / RLENGTH |
match() 匹配的起始位置 / 长度 |
— |
CONVFMT / OFMT |
数值转字符串 / print 输出格式 |
%.6g |
IGNORECASE |
设为 1 则正则忽略大小写(gawk) | 0 |
PROCINFO["sorted_in"] |
控制 for-in 遍历顺序(gawk) |
— |
awk 常用选项:-F 指定分隔符(支持多字符和正则)、-v 传入变量、-f 指定脚本文件、-i inplace 原地修改(gawk)。
awk 内置函数速记:length substr index split sub gsub match toupper tolower sprintf systime strftime mktime system getline int sqrt rand srand。
辅助命令一句话速查表
| 命令 | 一句话 | 最常用写法 |
|---|---|---|
sort |
排序 | sort -k2,2nr file |
uniq |
去相邻重复行(必须先 sort) | sort f \| uniq -c \| sort -rn |
comm |
比较两个有序文件,三列输出 | comm -12 a b(交集) |
diff |
比较两文件差异,给出改法 | diff -u old new > p.patch |
patch |
应用 diff 生成的补丁 | patch -p1 < p.patch |
wc |
数行/词/字节 | wc -l < file(只出数字) |
cut |
按单字符分隔符切列 | cut -d: -f1,3 file |
tr |
逐字符替换/删除/压缩 | tr -d '\r' < win > unix |
paste |
按列横向拼接文件 | paste -s -d, file |
join |
按公共字段关联(要求有序) | join -1 1 -2 1 a b |
nl |
加行号(可控格式) | nl -b a -n rz -w 3 file |
rev |
反转每行字符(tac 反转行) |
... \| rev \| cut -d/ -f1 \| rev |
column |
对齐成表格 | ... \| column -t |
xargs |
把标准输入变成命令参数 | find . -print0 \| xargs -0 -r cmd |
tee |
分流:屏幕 + 文件 | echo x \| sudo tee -a /etc/f |
head / tail |
取头 / 取尾 | tail -f app.log |
tac |
倒序输出行 | tac file |
find |
实时按条件查文件 | find / -type f -size +10M |
locate |
查预建数据库(需 updatedb) |
locate name |
which / whereis |
找命令路径 / 找相关文件 | which awk |
basename / dirname |
取文件名 / 取目录名 | basename $(pwd) |
选型决策提示
按需求选工具:
| 需求 | 首选 | 备选 |
|---|---|---|
| 找含关键词的行 | grep |
awk '/re/' |
| 排除注释和空行 | grep -Ev '^\s*(#\|$)' |
sed '/^#/d;/^$/d' |
| 统计匹配行数 | grep -c |
awk '/re/{n++}END{print n}' |
| 提取匹配片段 | grep -o |
sed -n 's/.*\(re\).*/\1/p' |
| 批量替换字符串 | sed -i 's///g' |
awk '{gsub()}' |
| 删除/插入/追加行 | sed d/i/a |
— |
| 取固定单字符分隔的列 | cut -d: -f1 |
awk -F: '{print $1}' |
| 取空白对齐的列 | awk '{print $2}' |
tr -s ' ' \| cut |
| 换列顺序 | awk '{print $3,$1}' |
— |
| 按列值筛选行 | awk '$3>100' |
— |
| 求和/平均/计数/分组 | awk 数组 |
— |
| 去重(要排序) | sort -u |
sort \| uniq |
| 去重(保持原序) | awk '!a[$0]++' |
— |
| 词频统计 | sort \| uniq -c \| sort -rn |
awk 数组 + sort |
| 两文件求交集/差集 | comm(有序) |
awk 'NR==FNR'(任意序) |
| 两文件关联取值 | awk 'NR==FNR' |
join(有序) |
| 看两文件具体差异 | diff -u / diff -y |
— |
| 批量对文件执行命令 | find -exec cmd {} + |
xargs -0 |
| 需要并行 | xargs -P |
GNU parallel |
| 对齐显示输出 | column -t |
awk printf |
性能决策:
- 不写
cat file | cmd,直接cmd file。 - 能在
grep层先粗筛就先筛,再交给awk(grep 比 awk 快)。 awk一次遍历算出所有指标,胜过多条命令各读一遍文件。- 用
awk数组计数后再sort,比先sort全量数据快得多。 - 大文件排序/匹配加
LC_ALL=C(比较退化为逐字节strcmp,可快数倍)。 sort大文件给足-S内存、指定-T快速临时目录、开--parallel。- 只要前 N 行就用
head或awk 'NR>N{exit}',别读完整个文件。 - 简单取列计数可换
mawk(比 gawk 快 2~5 倍);用到strftime/gensub/PROCINFO必须gawk。 - 只数行数用
wc -l(高度优化的 C 实现)。
安全决策:
- 文件名可能含空格 → 一律
find -print0 | xargs -0。 xargs加-r,防止空输入时误执行。sed -i之前先不加-i预演,或用-i.bak留备份。- 批量删除前先把
rm换成echo或加xargs -p/-t确认。 - 写特权文件用
... | sudo tee,不要用sudo echo >>(重定向由当前 Shell 执行,不会被提权)。 - 正则中的
.(如 IP、域名、文件扩展名)必须转义为\.。 sort file > file会清空文件,必须用sort file -o file。tr不能原地修改,tr ... < f > f会清空文件。
此处评论已关闭