文件查找与统计
# 第 6 章 文件查找与统计
# 目录介绍
# 6.1 find 基础查找
# 6.1.1 按名称搜索 -name / -iname
find 是 Linux 最强文件搜索命令——它真正遍历文件系统,不像 locate 依赖数据库:
# find 基本语法:find [路径] [表达式] [动作]
find . -name "*.log" # 当前目录下搜索所有 .log 文件
find /var/log -name "syslog*" # 指定路径搜索
# ===== -name:区分大小写 =====
find . -name "README.md" # 精确匹配(大小写敏感)
# ===== -iname:忽略大小写 =====
find . -iname "readme.md" # 匹配 ReadMe.MD、README.md 等
# ===== 通配符(用引号括起,防止 Shell 展开)=====
find . -name "*.py" # 以 .py 结尾
find . -name "test*" # 以 test 开头
find . -name "file?.txt" # ? = 匹配单个字符
find . -name "report[0-9].log" # 字符集:report0.log ~ report9.log
# ===== 路径匹配 -path(包含目录名)=====
find . -path "*/test/*.py" # 只在 test 子目录下找
find . -path "*/node_modules/*" -prune # 跳过 node_modules
# ⚠️ 不带路径默认从当前目录 . 开始
find -name "*.txt" # 默认路径 = .
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 6.1.2 按类型搜索 -type
#!/bin/bash
# ===== 常用类型 =====
find . -type f -name "*.sh" # f = 普通文件
find . -type d -name "log" # d = 目录
find . -type l -name "*.so" # l = 符号链接
find . -type s # s = socket 文件
find . -type p # p = 命名管道 FIFO
# ===== 实战:查找所有空目录 =====
find . -type d -empty # 空目录
# ===== 查找并列出所有符号链接及目标 =====
find . -type l -exec ls -l {} \;
# 或用 -ls 简写:
find . -type l -ls
# ===== 查找所有可执行脚本 =====
find . -type f -name "*.sh" -perm /111 # 有执行权限的 .sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-type 速查表:
| 缩写 | 文件类型 | 说明 |
|---|---|---|
f | 普通文件 | 最常用 |
d | 目录 | 排除文件只找目录 |
l | 符号链接 | 软链接 |
b | 块设备 | /dev/sda 等 |
c | 字符设备 | /dev/tty 等 |
s | socket | 套接字文件 |
p | 命名管道 | FIFO |
# 6.1.3 按大小搜索 -size
#!/bin/bash
# ===== 大小单位 =====
# c = 字节,k = KB,M = MB,G = GB
# 前缀 + = 大于, - = 小于, 不加 = 精确(近似)
find . -type f -size +100M # 大于 100MB
find . -type f -size -10k # 小于 10KB
find . -type f -size 0 # 空文件(0 字节)
find . -type f -size +1G -size -5G # 1GB~5GB 之间
# ===== 实战:找出大文件 =====
find /var/log -type f -size +50M \
-exec ls -lh {} \; | sort -k5 -h -r | head -10
# 找 /var/log 下 >50M 的文件,按大小降序显示 Top 10
# ===== 查找并删除空文件 =====
find . -type f -empty -delete # 删除所有空文件(-delete 是 find 内置)
# ===== 查找非空文件 =====
find . -type f ! -empty # ! 取反——非空
find . -type f -size +0 # 等价写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 6.1.4 按时间搜索 -mtime / -atime / -ctime
#!/bin/bash
# ===== 三种时间戳 =====
# mtime = 修改时间(内容被修改)——最常用
# atime = 访问时间(文件被读取)
# ctime = 状态变更时间(权限/所有者/链接数等元数据改变)
# 单位:+n = n天前,-n = n天内,n ≈ n 到 n+1 天前(有小数舍入)
find . -type f -mtime -7 # 最近 7 天内修改过的文件
find . -type f -mtime +30 # 30 天前修改过的文件
find . -type f -ctime -1 # 最近 24 小时内状态变更的
# ===== -mmin / -amin / -cmin:分钟级精度 =====
find . -type f -mmin -60 # 最近 60 分钟内修改的
find . -type f -mmin +1440 # 超过 1440 分钟(24h)之前的
# ===== -newer:比某个文件更新/更旧 =====
find . -type f -newer reference.txt # 比 reference.txt 更新的文件
find . -type f ! -newer reference.txt # 比 reference.txt 更旧的文件
# ===== -newerXY:精确比较(X=访问/修改/创建,Y=参考文件)=====
find . -type f -newermt "2025-01-01" # 2025-01-01 之后修改的
find . -type f -newermt "2025-01-01" ! -newermt "2025-06-01"
# 2025年1月1日到6月1日之间修改的
# ===== 实战:清理 N 天前的日志 =====
find /var/log -type f -name "*.log" -mtime +30 -delete
# 删除 30 天前的日志文件
find /var/log -type f -name "*.log" -mtime +7 -exec gzip {} \;
# 压缩 7 天前的日志
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 6.1.5 按权限/所有者/深度搜索
#!/bin/bash
# ===== 按权限 -perm =====
find . -type f -perm 644 # 精确匹配权限 644
find . -type f -perm -u=x # 所有者有执行权限(- 前缀:至少满足)
find . -type f -perm /u=w,g=w # 所有者或组有写权限(/ 前缀:任一满足)
find . -type f -perm /o=w # 其他用户有写权限(安全隐患!)
# ===== 按所有者 -user / -group =====
find . -type f -user root # 属于 root 的文件
find . -type f -group www-data # 属于 www-data 组
find . -type f -nouser # 无主文件(用户已删除)
# ===== 按深度 -maxdepth / -mindepth =====
find . -maxdepth 1 -name "*.txt" # 只搜索当前目录(不递归)
find . -maxdepth 2 -name "*.go" # 最多搜索 2 层
find . -mindepth 2 -name "*.py" # 最少 2 层深(跳过当前目录)
find . -mindepth 1 -maxdepth 3 -type f # 1~3 层深度
# ===== 按 inode 号搜索 =====
ls -i some_file # 查看 inode 号
find . -inum 123456 # 找硬链接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 6.1.6 逻辑运算组合条件
#!/bin/bash
# ===== 默认 AND:多个条件并列 =====
find . -name "*.log" -size +1M # .log 且 >1M
# ===== -o:OR(或)=====
find . \( -name "*.py" -o -name "*.go" \) # .py 或 .go(括号要转义)
find . -name "*.py" -o -name "*.go" # 省略括号也行(但优先级可能有问题)
# ===== ! :NOT(取反)=====
find . -type f ! -name "*.bak" # 所有文件但排除 .bak
find . ! -type d # 非目录(= 普通文件 + 其他)
# ===== 带括号的复杂逻辑 =====
find . \( -name "*.log" -o -name "*.tmp" \) -size +10M
# 翻译:( .log 或 .tmp ) 且 >10M
find . \( -name "*.jpg" -o -name "*.png" \) ! -size +5M
# 翻译:( .jpg 或 .png ) 且 不大于 5M
# ===== 实战:清理项目中的临时文件 =====
find . \( \
-name "*.pyc" -o \
-name "*.pyo" -o \
-name "__pycache__" -type d -o \
-name "*.egg-info" -type d \
\) -exec rm -rf {} +
# 批量删除 Python 编译缓存和 egg 信息
# ===== -prune:排除特定目录 =====
find . -path ./node_modules -prune -o -name "*.js" -print
# 搜索 .js 但跳过 node_modules 目录
# -prune 的意思是"如果 -path 匹配,则剪掉不往下递归"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 6.2 find 执行动作
# 6.2.1 -exec 对找到的每个文件执行命令
#!/bin/bash
# ===== 基本格式(两种终止符)=====
# -exec command {} \; 一次处理一个文件(每文件执行一次 command)
# -exec command {} + 一次处理一批文件(把所有文件做参数传入一次 command)
# ===== \; 版本——每文件一个进程 =====
find . -name "*.log" -exec rm {} \; # 每个文件开一次 rm(慢但安全)
find . -name "*.py" -exec chmod 644 {} \; # 修改权限
find . -name "*.bak" -exec mv {} /tmp/ \; # 移动到 /tmp
# ===== + 版本——批量传参(高效)=====
find . -name "*.log" -exec rm {} + # 一批文件删一次(快)
# 等价于 rm file1.log file2.log file3.log ...(受 ARG_MAX 限制)
# ===== 实战:统计所有 .go 文件总行数 =====
find . -name "*.go" -exec cat {} + | wc -l
# 等价于:cat file1.go file2.go ... | wc -l
# ===== {} 占位符说明 =====
# {} = 当前找到的文件名(find 自动替换)
# \; = 命令结束标记(必须转义,否则 Shell 会解释)
# + = 批量模式结束标记
# ===== 实战:查找并打包 =====
find . -name "*.jpg" -mtime -7 -exec tar -cf recent_photos.tar {} +
# 把最近 7 天的 jpg 打包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 6.2.2 xargs 批量处理——比 exec 更高效
xargs 从标准输入读取数据,拼接成命令参数。配合 find 使用是经典组合:
#!/bin/bash
# ===== 为什么要用 xargs?=====
# 管道传递的是 stdout,而很多命令(rm/cp/mv)不接受管道输入
# xargs 把 stdout 转为命令行参数
# ❌ 错误:管道不能直接传给 rm
find . -name "*.bak" | rm # rm 不接受管道输入
# rm: missing operand
# ✅ 正确:用 xargs 转成参数
find . -name "*.bak" | xargs rm # 把文件列表传给 rm
# ===== -I {}:自定义占位符——每次替换一个 =====
find . -name "*.sh" | xargs -I {} cp {} {}.bak
# 等价于 -exec cp {} {}.bak \;
# 将每个 .sh 文件复制为 .sh.bak
# ===== -n N:每批最多 N 个参数 =====
find . -name "*.log" | xargs -n 3 rm
# 每次传 3 个文件给 rm
# ===== -t:打印执行的命令(调试用)=====
find . -name "*.tmp" | xargs -t rm
# 输出:rm ./a.tmp ./b.tmp ./c.tmp ← 让你看到实际执行了什么
# ===== -p:交互模式——每次确认 =====
find . -name "*.bak" | xargs -p rm
# rm ./a.bak?... y ← 输入 y 才执行
# ===== -P N:并行执行 N 个进程 =====
find . -name "*.jpg" -size +1M | xargs -P 4 -I {} convert {} -resize 50% thumb_{}
# 用 4 个进程并行压缩图片
# ===== xargs 也可处理任意管道输入 =====
echo "file1.txt file2.txt file3.txt" | xargs ls -l
# 打印 3 个文件的信息
seq 1 10 | xargs -I NUM echo "Number: NUM"
# 输出:Number: 1 ... Number: 10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 6.2.3 -print0 与 -0——处理空格与特殊字符
文件名包含空格、换行、引号时,find 与 xargs 的默认行为会出错:
#!/bin/bash
# ===== 问题:文件名含空格 =====
# 文件:a b.txt c.txt(空格分隔的两个文件名)
# find 输出:./a b.txt\n./c.txt
# xargs 解析为:a, b.txt, c.txt ← 三个参数!
# 实际期望:a b.txt, c.txt ← 两个参数
# ===== 解决方案:-print0 + -0(NUL 字符分隔)=====
find . -name "*.txt" -print0 | xargs -0 rm
# find 用 \0 (NUL) 分隔 → xargs 用 -0 解析 NUL
# 这是「黄金组合」——绝对安全
# ===== 实战:查找所有 .mp4 并移动 =====
find /downloads -name "*.mp4" -print0 | xargs -0 -I {} mv {} /videos/
# ===== 查找含空格和特殊字符的文件 =====
find . -type f -name "* *" # 找文件名含空格的文件
find . -type f -print0 | xargs -0 ls -l # 安全列出所有文件
# ===== 批量重命名(处理含空格文件名)=====
find . -name "* *" -print0 | while IFS= read -r -d '' file; do
mv "$file" "${file// /_}" # 空格替换为下划线
done
# while read -d '' 是处理 NUL 分隔的另一种方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 6.2.4 -delete / -ok / -ls 快捷动作
#!/bin/bash
# ===== -delete:直接删除(比 -exec rm 更高效)=====
find . -name "*.tmp" -delete # find 内置删除
# 优点:不需要 fork 新进程,不会因 ARG_MAX 失败
# ⚠️ 务必先不带 -delete 预览结果!
# ===== -ok:每次确认版 -exec =====
find . -name "*.bak" -ok rm {} \;
# 对每个文件提示:< rm ... ./a.bak > ? ← 输入 y 确认
# ===== -ls:列出详细信息(类似 ls -l)=====
find . -type f -size +1M -ls
# 直接输出 ls -l 格式,比 -exec ls -l 更高效
# ===== -print:默认动作(通常省略也生效)=====
find . -name "*.log" # 默认 -print 打印结果
find . -name "*.log" -print # 显式写法,等价
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 6.2.5 find 实战案例集锦
#!/bin/bash
# ===== 案例 1:查找重复文件(按大小+md5)=====
find . -type f -size +0 -printf "%s %p\n" \
| sort -n \
| awk '{
size=$1; file=$2
if (size in seen && size == last_size) print "check:", file, seen[size]
else seen[size]=file
last_size=size
}'
# ===== 案例 2:查找并统计各类型文件数量 =====
find . -type f | sed 's/.*\.//' | sort | uniq -c | sort -rn | head -10
# 输出:Top 10 文件扩展名及数量
# 1234 js
# 567 ts
# 345 json
# ===== 案例 3:批量修改文件扩展名 =====
find . -name "*.jpeg" -print0 | while IFS= read -r -d '' file; do
mv "$file" "${file%.jpeg}.jpg"
done
# ===== 案例 4:查找最近修改的配置文件 =====
find /etc -type f -name "*.conf" -mmin -60 2>/dev/null
# 2>/dev/null 忽略权限不够的错误信息
# ===== 案例 5:一键给目录下所有 .sh 加执行权限 =====
find . -type f -name "*.sh" -exec chmod +x {} +
# ===== 案例 6:清理构建产物 =====
find . -type d \( -name "node_modules" -o -name "dist" -o -name ".next" \) \
-prune -exec rm -rf {} + 2>/dev/null
# -prune 配合 -exec:找到目录后,先剪枝(不递归进去),再删除目录本身
# ===== 案例 7:查找项目中未使用的图片(按引用搜索)=====
find . -name "*.png" -exec basename {} \; \
| while read img; do
grep -rq "$img" . --include="*.{js,ts,jsx,tsx,html}" || echo "Unused: $img"
done
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 6.3 sort 排序
# 6.3.1 基础排序:字母/数值/逆序
#!/bin/bash
# ===== sort 默认:按 ASCII 字母顺序 =====
cat words.txt | sort # A→Z 升序
# ===== -n:数值排序(否则 10 排在 2 前面)=====
echo -e "10\n2\n1\n20" | sort # 输出:1 10 2 20(字符串排序)
echo -e "10\n2\n1\n20" | sort -n # 输出:1 2 10 20(数值排序)
# ===== -r:逆序 reverse =====
echo -e "10\n2\n1\n20" | sort -nr # 输出:20 10 2 1
# ===== -f:忽略大小写 =====
echo -e "Apple\napple\nBanana" | sort -f # Apple apple Banana
# ===== -R:随机排序 =====
cat file.txt | sort -R # 随机打乱行
# ===== -b:忽略前导空白 =====
echo -e " apple\n banana\ncat" | sort -b # 忽略行首空格再排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 6.3.2 按列排序 -k 与分隔符 -t
#!/bin/bash
# ===== -k:指定排序的列(第 N 列)=====
# -k start[,end] 从 start 列排到 end 列(含)
sort -k2 data.txt # 按第 2 列排序(空格分隔)
sort -k2,2 data.txt # 严格只按第 2 列排
# ===== -t:指定分隔符 =====
sort -t ',' -k2 data.csv # 用逗号分隔,按第 2 列
sort -t ':' -k3 -n /etc/passwd # 按 UID(第 3 列)数值排序
# ===== 按列内的字符位置排 =====
# -k2.3 = 第 2 列的第 3 个字符开始
sort -k1.5 file.txt # 按第 1 列第 5 个字符开始
# ===== 排序选项可附加在 -k 之后 =====
sort -t ',' -k2nr,2 data.csv # 按第 2 列数值逆序
sort -t ':' -k3n,3 -k1,1 /etc/passwd # 先按 UID 数值,再按用户名
# ===== 实战:按文件大小排序 ls 输出 =====
ls -l | sort -k5 -n -r | head -5 # 最大的 5 个文件
ls -l | sort -k5,5 -n -r # 严格按第 5 列(文件大小)
# ===== 按月份排序(第4列是 "Jan" "Feb"...)=====
ls -l | sort -k6M # 按月份排序(-M = 月份排序)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 6.3.3 多列排序与稳定排序 -s
#!/bin/bash
# ===== 多列排序:多个 -k 选项,优先级从左到右 =====
# 先按第 2 列排,再按第 1 列排
sort -k2,2 -k1,1 data.txt
# ===== 实例:成绩单排序(先按科目,同科目内按分数降序)=====
cat scores.txt # 格式:张三 数学 92
sort -k2,2 -k3,3nr scores.txt
# 先按科目(第2列)字母序,同科目按分数(第3列)数值降序
# ===== -u:去重(去掉完全相同的行)=====
sort -u file.txt # 排序 + 去重
# ===== -s:稳定排序(stable)=====
# 保证相等元素的相对顺序不变
sort -s -k2 data.txt # 相同 key 时保持原始顺序
# ===== 组合:按第二列排序,相同第二列时保持第一列原有顺序 =====
sort -s -k2,2 data.txt
# ===== 实战:统计访问日志的请求方法 =====
awk '{print $6}' access.log | sort | uniq -c | sort -rn
# sort | uniq -c | sort -rn = 统计 + 计数 + 降序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 6.3.4 人类可读排序 -h 与月份排序 -M
#!/bin/bash
# ===== -h:人类可读大小排序(K/M/G)=====
du -h /home | sort -hr | head -10
# du 输出 "1.2G\t/path",sort -h 能正确比较 1.2G > 500M
# 不用 -h 的悲剧:
echo -e "1K\n1G\n1M\n10K" | sort -n # 靠第一个数字排 = 乱排
echo -e "1K\n1G\n1M\n10K" | sort -h # 正确:1K 10K 1M 1G
# ===== -V:版本号排序 =====
echo -e "v1.10.1\nv1.2.1\nv1.10.0" | sort -V
# 输出:v1.2.1 v1.10.0 v1.10.1(版本号语义排序)
# 普通排序:v1.10.0 v1.10.1 v1.2.1 ← 错误!
# ===== -M:月份排序(Jan/Feb/Mar...)=====
echo -e "Dec\nJan\nFeb" | sort -M # Jan Feb Dec
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 6.3.5 去重 -u 与检查排序 -c
#!/bin/bash
# ===== -u:输出去重后的行 =====
sort -u file.txt # 排序后去掉重复行
# 对比 uniq:sort -u 在排序阶段就去重(比 sort|uniq 略快)
# 但如果需要统计重复次数(uniq -c),还是要用 uniq
# ===== -c:检查是否已排序 =====
sort -c file.txt # 已排序则无输出,未排序则报错
sort -c -n numbers.txt # 检查数值排序
# ===== 实战:检查文件是否已按时间排序 =====
sort -c -t '[' -k2 app.log # 检查日志是否按时间戳排序
# ===== -o:输出到文件(可覆盖自身)=====
sort file.txt -o sorted.txt # 输出到新文件
sort file.txt -o file.txt # ⚠️ 直接覆盖自身(sort 允许)
# 对比:sort file.txt > file.txt 会清空 file.txt!(先重定向再读)
# 所以要用 sort 的 -o 选项来覆盖自身
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 6.4 uniq 去重与统计
# 6.4.1 uniq 的工作原理——必须先排序
#!/bin/bash
# ===== uniq 只合并「相邻」的重复行!=====
echo -e "a\na\nb\na\na" | uniq
# 输出:a b a ← 第三个 a 没被去掉!因为它跟 b 不连续
# ✅ 正确用法:先 sort 再 uniq
echo -e "a\na\nb\na\na" | sort | uniq
# 输出:a b ← 完全去重
# ===== 这是最常见的组合 =====
some_command | sort | uniq # 排序 + 去重
# ≈ sort -u,但 sort -u 不能配合 -c/-d 等 uni 标志
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
为什么一定要 sort?直观理解:
原始:a a b a a
↓ sort
a a a a b
↓ uniq
a b
1
2
3
4
5
2
3
4
5
# 6.4.2 -c 计数、-d 重复项、-u 唯一项
#!/bin/bash
# ===== -c:每行前面加出现次数 =====
echo -e "a\na\nb\na\na" | sort | uniq -c
# 输出:
# 4 a
# 1 b
# ===== -d:只显示重复出现的行(出现 >= 2 次)=====
echo -e "a\na\nb\na\na\nc" | sort | uniq -d
# 输出:a
# ===== -D:显示所有重复行(每行都打印)=====
echo -e "a\na\nb\na\na" | sort | uniq -D
# 输出:a a a a
# ===== -u:只显示唯一出现的行(只出现 1 次)=====
echo -e "a\na\nb\na\na\nc" | sort | uniq -u
# 输出:b c
# ===== -i:忽略大小写 =====
echo -e "APPLE\napple\nBanana" | sort | uniq -ci
# 输出:2 APPLE 1 Banana
# ===== -w N:只比较前 N 个字符 =====
echo -e "error:001\nerror:002\ninfo:003" | sort | uniq -w5 -c
# 输出:2 error:001 1 info:003(只比较前 5 个字符)
# ===== -s N:跳过前 N 个字符 =====
echo -e "[ERR] timeout\n[ERR] retry\n[INFO] ok" | sort | uniq -s5 -c
# 跳过 "[ERR]" (5字符) 后再比较
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 6.4.3 实战:日志中的 Top N 统计
#!/bin/bash
# ===== 场景 1:访问量 Top 10 的 IP =====
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10
# 管道拆解:
# awk '{print $1}' → 取 IP 列
# sort → 排序(uniq 前置条件)
# uniq -c → 统计每个 IP 出现次数
# sort -rn → 按次数降序
# head -10 → 取前 10
# ===== 场景 2:被访问 Top 10 的 URL =====
awk '{print $7}' access.log | sort | uniq -c | sort -rn | head -10
# ===== 场景 3:404 最多的 URL =====
awk '$9 == 404 {print $7}' access.log | sort | uniq -c | sort -rn | head -10
# ===== 场景 4:统计 HTTP 状态码分布 =====
awk '{print $9}' access.log | sort | uniq -c | sort -rn
# ===== 场景 5:找出只出现过一次的 IP(异常访问)=====
awk '{print $1}' access.log | sort | uniq -u
# ===== 场景 6:重复登录的用户 =====
awk '/login success/ {print $3}' auth.log | sort | uniq -d
# ===== 整个管道记忆口诀 =====
# 提取 → 排序 → 统计 → 再排序 → 截取
# awk → sort → uniq -c → sort -rn → head
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 6.5 cut / tr / paste 列处理
# 6.5.1 cut——按字符/字节/分隔符切列
#!/bin/bash
# ===== -c:按字符位置切 =====
echo "ABCDEFG" | cut -c3 # C(第 3 个字符)
echo "ABCDEFG" | cut -c3-5 # CDE(第 3~5 个)
echo "ABCDEFG" | cut -c3- # CDEFG(从第 3 到末尾)
echo "ABCDEFG" | cut -c-3 # ABC(开头到第 3 个)
# ===== -d + -f:按分隔符切字段 =====
echo "apple,banana,orange" | cut -d ',' -f2 # banana(第 2 字段)
echo "apple,banana,orange" | cut -d ',' -f1,3 # apple,orange
echo "apple,banana,orange" | cut -d ',' -f1-2 # apple,banana
echo "apple,banana,orange" | cut -d ',' -f2- # banana,orange
# ===== 实战:提取 passwd 的用户名和 Shell =====
cut -d ':' -f1,7 /etc/passwd
# 输出:root:/bin/bash nobody:/usr/sbin/nologin ...
# ===== 补充选择(不被分隔符包含时区补空)=====
echo "a" | cut -d ',' -f1,3 --complement # 不选字段 1 和 3
# --complement = 取反选择
# ===== 提取 csv 指定列 =====
cut -d ',' -f1,3,5 data.csv | head -5 # 提取第 1/3/5 列
# ===== cut vs awk 选取列对比 =====
cut -d ':' -f1,7 /etc/passwd # 简单列提取(快)
awk -F ':' '{print $1, $7}' /etc/passwd # 复杂处理(灵活,可加条件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 6.5.2 tr——字符级转换与删除
#!/bin/bash
# ===== tr 是从 stdin 到 stdout 的字符映射(不接受文件名参数)=====
# ===== 替换/转换:tr SET1 SET2 —— 一一对应 =====
echo "abc" | tr 'a' 'X' # Xbc
echo "abc" | tr 'abc' 'XYZ' # XYZ(a→X, b→Y, c→Z)
echo "hello world" | tr 'a-z' 'A-Z' # HELLO WORLD(大小写转换)
echo "HELLO" | tr 'A-Z' 'a-z' # hello
# ===== -d:删除字符 =====
echo "abc123def456" | tr -d '0-9' # abcdef(删除所有数字)
echo "a b c" | tr -d ' ' # abc(删除空格)
cat file.txt | tr -d '\r' # 删除 Windows 的回车符 \r
# ===== -s:压缩重复字符(squeeze)=====
echo "a b c" | tr -s ' ' # a b c(多个空格压缩为一个)
echo "aaabbbccc" | tr -s 'ab' # abccc(a 和 b 分别压缩)
echo -e "a\n\n\nb\n\nc" | tr -s '\n' # 压缩空行
# ===== -c(-C):取反(补集)=====
echo "abc123" | tr -cd '0-9' # 123(删除非数字)
echo "abc123" | tr -c '0-9' 'X' # X X X 1 2 3(把非数字替换成 X)
echo "hello 123" | tr -cd 'a-z' # hello(只保留字母)
# ===== 字符类 =====
echo "hello" | tr '[:lower:]' '[:upper:]' # HELLO
cat file.txt | tr -d '[:space:]' # 删除所有空白字符
cat file.txt | tr '[:punct:]' '_' # 所有标点变下划线
# ===== 实战:Windows 换行 → Unix 换行 =====
tr -d '\r' < windows.txt > unix.txt
# 或用 sed:sed -i 's/\r$//' file.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
tr 常用转义与字符类速查:
| 表示法 | 含义 |
|---|---|
\n | 换行 |
\t | Tab |
\r | 回车 |
\\ | 反斜杠 |
[:alnum:] | 字母 + 数字 |
[:alpha:] | 字母 |
[:digit:] | 数字 0-9 |
[:lower:] / [:upper:] | 小写/大写字母 |
[:space:] | 空格+Tab+换行+回车 |
[:punct:] | 标点符号 |
# 6.5.3 paste——文件横向合并
#!/bin/bash
# ===== paste 把多个文件横向拼接(默认用 Tab 分隔)=====
# 文件1 文件2 paste 结果
# a x a\tx
# b y b\ty
# c z c\tz
paste file1.txt file2.txt # 横向拼接
# ===== -d:指定分隔符 =====
paste -d ',' names.txt scores.txt # 用逗号拼接
paste -d '|' col1.txt col2.txt col3.txt # 用竖线拼接
paste -d '\n' file1.txt file2.txt # 交替行(用换行拼接)
# 输出:a x b y c z ← 交替排列
# ===== -s:按行拼接(serial)=====
# 把文件的所有行串成一行(用 Tab 分隔)
paste -s file.txt
# 输出:line1\tline2\tline3 ← 一行
# 多文件 -s:把每个文件变成一行
paste -s file1.txt file2.txt
# 输出:
# file1_line1\tfile1_line2\tfile1_line3
# file2_line1\tfile2_line2
# ===== 实战:生成 CSV =====
paste -d ',' names.txt ages.txt cities.txt > users.csv
# names.txt ages.txt cities.txt
# Alice 25 Beijing
# Bob 30 Shanghai → users.csv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 6.6 wc / 综合实战 / 陷阱
# 6.6.1 wc 行/词/字节统计
#!/bin/bash
# ===== wc 默认输出:行数 词数 字节数 文件名 =====
wc file.txt
# 输出:15 120 850 file.txt
# 行 词 字节
# ===== 单项统计 =====
wc -l file.txt # 只统计行数 (lines)
wc -w file.txt # 只统计词数 (words)
wc -c file.txt # 只统计字节数 (bytes)
wc -m file.txt # 只统计字符数 (characters,含多字节)
# ⚠️ -c vs -m:对中文等多字节字符有差异
echo "你好" | wc -c # 7 (UTF-8: 2×3字节 + 换行 = 7)
echo "你好" | wc -m # 3 (2个字符 + 换行 = 3)
# ===== 统计多个文件 =====
wc -l *.md
# 输出每文件行数 + 总计
# ===== 实战:统计项目代码行数 =====
find . -name "*.py" | xargs wc -l | tail -1 # 总行数
# 或:
find . -name "*.py" -exec cat {} + | wc -l
# 按文件类型分别统计:
find . -name "*.py" | xargs wc -l | sort -rn | head -10
# 按行数排 Top 10 的 .py 文件
# ===== 统计目录下文件数量 =====
find . -type f | wc -l # 文件数(包含子目录)
ls -1 | wc -l # 当前目录文件数(不含子目录)
find . -type d | wc -l # 目录数
# ===== 统计进程数 =====
ps aux | wc -l # 总进程数(含表头)
ps aux --no-headers | wc -l # 去掉表头
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 6.6.2 四大工具管道组合实战
#!/bin/bash
# ============================================
# 案例 1:日志文件中 Top 10 出现最多的关键词
# ============================================
grep -oP '\b\w{4,}\b' app.log | sort | uniq -c | sort -rn | head -10
# 拆解:grep -oP 提取所有长度>=4 的单词 → sort → uniq -c → sort -rn → head
# ============================================
# 案例 2:统计项目中各编程语言代码行数
# ============================================
find . -type f \( -name "*.py" -o -name "*.sh" -o -name "*.js" \) \
-exec wc -l {} + | awk '{
if (NF==1) total=$1 # 行数
else {
# 按扩展名分组累加
ext=tolower($2); gsub(/.*\./, "", ext)
lines[ext] += $1
}
} END {
for (ext in lines) printf "%-10s %8d lines\n", ext, lines[ext]
}' | sort -k2 -rn
# ============================================
# 案例 3:分析 GitHub 提交记录
# ============================================
git log --format="%an" | sort | uniq -c | sort -rn | head -10
# %an = author name
# 输出:Top 10 提交者
git log --format="%ad" --date=format:"%Y-%m" | sort | uniq -c
# 每个月提交次数统计
# ============================================
# 案例 4:查找占用空间最大的目录
# ============================================
du -sh */ | sort -hr | head -10
# du -sh */ = 每个子目录的大小(human readable)
# sort -hr = 按人类可读大小逆序排列
# ============================================
# 案例 5:文本数据预处理管道
# ============================================
# 功能:提取 csv 第 2 列 → 去空格 → 去重 → 统计 → 排序 → Top 5
cut -d ',' -f2 data.csv | tr -d ' ' | sort | uniq -c | sort -rn | head -5
# ============================================
# 案例 6:监控文件变化(find + 比较)
# ============================================
# 找出 1 小时内修改过的文件
find /var/www -type f -mmin -60 | xargs ls -lh | awk '{print $9, $5, $6, $7}'
# 输出:文件名 大小 日期 时间
# 生成需要备份的文件列表:
find /data -type f -mtime -1 | xargs -I {} cp {} /backup/daily/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 6.6.3 新手陷阱 Top 5
陷阱 1:find 路径顺序影响性能
# ❌ 先找所有文件再过滤名称——遍历所有文件
find . -type f -name "*.log"
# ✅ find 会优化,但养成好习惯:最严格的过滤放前面
find . -name "*.log" -type f
# 某些 find 版本(尤其是带 -O 优化级别)会自动重排条件
1
2
3
4
5
6
2
3
4
5
6
陷阱 2:sort | uniq 是必须的,不可省略 sort
# ❌ 没排序直接 uniq——只去相邻重复,去不干净
cat data.txt | uniq -c
# ✅ 永远先 sort
cat data.txt | sort | uniq -c
# 记住:uniq 前必须 sort(或数据本身就是有序的)
1
2
3
4
5
6
2
3
4
5
6
陷阱 3:xargs 遇到空格/引号会出错
# ❌ 文件名含空格时会解析错误
find . -name "*.txt" | xargs rm
# ✅ 黄金组合:-print0 + -0
find . -name "*.txt" -print0 | xargs -0 rm
1
2
3
4
5
2
3
4
5
陷阱 4:cut 不区分连续分隔符
# cut 把连续的多个相同分隔符合并为一个!
echo "a,,b" | cut -d ',' -f2 # 输出:空(想取空字段)
echo "a,,b" | cut -d ',' -f3 # 输出:b(count 跳到了第 3 位)
# ✅ awk 更精确
echo "a,,b" | awk -F ',' '{print $2}' # 输出:空字符串
echo "a,,b" | awk -F ',' '{print $3}' # 输出:b
1
2
3
4
5
6
7
2
3
4
5
6
7
陷阱 5:sort file > file 会清空文件
# ❌ Shell 先截断 file(重定向),再执行 sort——此时 file 已空
sort file.txt > file.txt # file.txt 变成空文件!
# ✅ 用 -o 选项覆盖自身
sort file.txt -o file.txt # sort 内置的输出到文件
1
2
3
4
5
2
3
4
5
# 6.6.4 综合思考题
- find + xargs 管道组合:如何找到最近 7 天修改过的、大于 10MB 的普通文件,并安全删除它们?
- 日志分析一条龙:从 Nginx 访问日志中统计"每个小时"的请求量和流量大小(提示:
cut时间字段 +awk分组累加)。 - sort 多列排序:有一个 csv 文件
学生,科目,分数,请写出按科目分组、组内按分数降序排列的命令。 - tr 实战:如何把 Windows 风格的文本文件(
\r\n换行)转为 Unix 风格(\n),同时把所有 Tab 转为 4 个空格? - uniq 陷阱:以下命令的输出是什么?
echo -e "apple\nApple\norange\nAPPLE" | sort -f | uniq -i -c1 - 综合管道:统计当前目录下所有
.md文件中出现频率最高的 20 个单词(忽略大小写,忽略标点)。
上次更新: 2026/06/17, 12:47:39