在命令行里真正拉开效率差距的能力,不是会多少命令,而是能不能把命令"拼起来"——把小工具串成一条清晰的数据流。管道符
| 正是 Unix 哲学的核心:让每个小工具只做一件事( grep
只过滤、 awk 只提取字段、 sort
只排序),然后把它们串成一条可读可调试的流水线。本文从数据流模型(
stdin/stdout/stderr)讲起,系统梳理管道与重定向的语义差异(>、>>、2>、2>&1、<
分别干什么),然后补齐日志排查、文本过滤、统计汇总、批量处理里的典型套路(什么时候该用
grep/awk/sed/sort/uniq/wc/cut/tr,怎么逐层缩小范围),并用实战案例(
Nginx
日志分析、批量文件操作、安全删除)把"空格与换行"这类坑补上(find -print0
+ xargs -0
的正确用法)。你读完之后应该能把很多"要写脚本"的小需求,直接用一两行可读的命令解决,并且更容易看懂别人写的
one-liner 。
数据流模型: stdin/stdout/stderr 与文件描述符
三个标准流
每个 Linux 进程都有三个标准流:
| 流 | 文件描述符 | 默认行为 | 示例 |
|---|---|---|---|
| stdin | 0 | 从键盘读取输入 | cat(不带参数时等待用户输入) |
| stdout | 1 | 输出到屏幕 | echo "hello" |
| stderr | 2 | 错误输出到屏幕 | ls /nonexistent 2>&1 |
为什么要分 stdout 和 stderr?
- 正常输出和错误输出可以分别处理(如正常输出保存到文件,错误输出显示在屏幕)
- 管道
|只传递 stdout(不传递 stderr),这样错误信息不会污染数据流
示例: 1
2
3ls /nonexistent # 错误信息输出到 stderr(屏幕)
ls /nonexistent 2> err.log # 错误信息重定向到 err.log
ls /nonexistent 2>&1 # stderr 重定向到 stdout(合并到同一个流)
文件描述符( File Descriptor, FD)
文件描述符是进程打开文件时的"句柄",用整数表示:
0: stdin1: stdout2: stderr3+:进程自己打开的文件
查看进程打开的文件描述符: 1
2
3ls -l /proc/$$/fd # $$
是当前 shell 的 PID
输出示例: 1
2
3lrwx------ 1 user user 0 /proc/12345/fd/0 -> /dev/pts/0 # stdin
lrwx------ 1 user user 0 /proc/12345/fd/1 -> /dev/pts/0 # stdout
lrwx------ 1 user user 0 /proc/12345/fd/2 -> /dev/pts/0 # stderr
重定向:控制数据流的去向
输出重定向( stdout)
1 | echo "hello" > file.txt # 覆盖写入(文件存在会被清空) |
常见用法: 1
2ls -l > filelist.txt # 保存文件列表
date >> log.txt # 追加时间戳到日志
错误输出重定向( stderr)
1 | ls /nonexistent 2> err.log # 错误输出重定向到 err.log |
同时重定向 stdout 和 stderr
方法
1:2>&1(传统写法)
1 | command > output.log 2>&1 # stdout 和 stderr 都重定向到 output.log |
顺序很重要:
> output.log先把 stdout 重定向到 output.log2>&1再把 stderr 重定向到 stdout 的位置(也就是 output.log)
错误写法: 1
command 2>&1 > output.log # 错误! stderr 先重定向到 stdout(屏幕),然后 stdout 再重定向到文件
方法
2:&>(现代写法,推荐)
1 | command &> output.log # stdout 和 stderr 都重定向到 output.log |
丢弃输出(/dev/null)
/dev/null
是一个特殊的"黑洞"文件,写入的数据会被丢弃。
1 | command > /dev/null # 丢弃 stdout |
使用场景:
- 不想看到命令的输出(如定时任务里的脚本)
- 只关心命令是否成功(通过
$?判断退出码)
输入重定向( stdin)
1 | sort < input.txt # 从 input.txt 读取输入 |
Here-document(多行输入): 1
2
3
4
5cat <<EOF > config.txt
line 1
line 2
line 3
EOF
Here-string(单行输入): 1
grep "error" <<< "ERROR: something bad"
管道符:把命令串起来
管道的核心思想
Unix 哲学:每个工具只做一件事,做好一件事,然后通过管道组合起来。
示例: 1
cat access.log | grep "404" | wc -l
拆解: 1. cat access.log:输出日志内容( stdout) 2.
grep "404":从 stdin 读取,过滤出包含 "404" 的行( stdout)
3. wc -l:从 stdin 读取,统计行数( stdout)
为什么这么设计?
- 避免临时文件(数据在内存里流动,不写硬盘)
- 可读性强(每个步骤都很清晰)
- 易调试(可以逐步加管道,看每一步的输出)
调试管道:使用 tee
tee 可以把数据同时输出到屏幕和文件(类似"三通管")。
1 | cat access.log | grep "404" | tee filtered.log | wc -l |
tee filtered.log:把 grep 的输出保存到 filtered.log,同时传递给下一个命令- 这样可以看到中间结果,方便调试
文本处理工具链: grep/awk/sed/cut/tr/sort/uniq
grep:过滤行
grep 是最常用的文本过滤工具,用于查找匹配的行。
基本用法: 1
2grep "pattern" file # 在文件中查找匹配 pattern 的行
command | grep "pattern" # 在命令输出中查找
常用参数:
-i:忽略大小写-v:反向匹配(只显示不包含 pattern 的行)-n:显示行号-A N:显示匹配行及后面 N 行( After)-B N:显示匹配行及前面 N 行( Before)-C N:显示匹配行及前后各 N 行( Context)-E:扩展正则表达式(支持|、+、?等)-r:递归搜索目录
实战示例:
1. 查看日志中的错误
1 | grep -i "error" /var/log/syslog # 忽略大小写查找 error |
2. 查看错误的上下文
1 | grep -C 3 "OutOfMemoryError" app.log # 显示错误前后各 3 行 |
3. 递归搜索目录
1 | grep -r "TODO" /srv/project # 在项目目录中递归查找 TODO |
4. 统计匹配次数
1 | grep -c "ERROR" app.log # 统计包含 ERROR 的行数 |
awk:提取字段和聚合
awk 是用于处理列式文本(如日志、 CSV
、表格)的强大工具。
基本概念:
- awk 按行处理文本,每行按空格(或指定分隔符)分成多个字段
$1是第一个字段,$2是第二个字段,$0是整行
常用示例:
1. 提取字段
1 | # Nginx 日志格式: IP - - [时间] "GET /path HTTP/1.1" 200 1234 |
2. 过滤行(类似 grep)
1 | awk '/404/ {print $0}' access.log # 只显示包含 404 的行 |
3. 统计和聚合
1 | # 统计每个状态码的出现次数 |
4. 自定义分隔符
1 | # 以逗号分隔的 CSV 文件 |
5. awk 的高级应用
条件过滤 + 字段提取: 1
2# 只显示状态码为 500 的请求的 IP 和路径
awk '$9 == 500 {print $1, $7}' access.log
计算平均值: 1
2# 假设第 10 列是响应时间(毫秒),计算平均响应时间
awk '{sum += $10; count++} END {print sum/count}' access.log
按条件聚合: 1
2# 统计每个 IP 的总流量(假设第 10 列是字节数)
awk '{bytes[$1] += $10} END {for (ip in bytes) print ip, bytes[ip]}' access.log | sort -nr -k2
处理 CSV 文件: 1
2# 提取 CSV 文件的第 2 列和第 4 列,并过滤出第 3 列 > 100 的行
awk -F',' '$3 > 100 {print $2, $4}' data.csv
sed:文本替换和编辑
sed 是流编辑器( Stream
Editor),用于文本替换、删除、插入等操作。
常用示例:
1. 替换文本
1 | sed 's/foo/bar/' file.txt # 替换每行第一个 foo 为 bar |
2. 删除行
1 | sed '/pattern/d' file.txt # 删除包含 pattern 的行 |
3. 插入和追加
1 | sed '1i\First Line' file.txt # 在第一行前插入文本 |
4. sed 的高级应用
批量修改配置文件: 1
2# 修改 Nginx 配置中的端口( 80 改成 8080)
sed -i 's/listen 80;/listen 8080;/g' /etc/nginx/nginx.conf
提取特定范围的行: 1
sed -n '10,20p' file.txt # 只显示第 10-20 行(-n 不输出其他行, p 打印)
删除注释行和空行: 1
sed '/^#/d; /^$/d' config.conf # 删除以 # 开头的行和空行
原地修改文件(-i): 1
2sed -i 's/old/new/g' file.txt # 直接修改文件(不输出到屏幕)
sed -i.bak 's/old/new/g' file.txt # 修改前备份为 file.txt.bak
cut/tr/sort/uniq:简单高效的文本工具
cut:提取字段(简单场景)
1 | cut -d',' -f1 data.csv # 提取逗号分隔的第一列 |
tr:字符替换/删除
1 | echo "HELLO" | tr 'A-Z' 'a-z' # 转小写 |
sort:排序
1 | sort file.txt # 按字母顺序排序 |
uniq:去重(只能去除相邻重复)
1 | sort file.txt | uniq # 先排序,再去重 |
重要:uniq
只能去除相邻的重复行,所以通常要先
sort。
实战案例: Nginx 日志分析
假设你有一个 Nginx 日志文件 access.log,每行格式如下:
1
2192.168.1.100 - - [28/Jan/2025:12:00:00 +0000] "GET /api/users HTTP/1.1" 200 1234
192.168.1.101 - - [28/Jan/2025:12:00:01 +0000] "POST /api/login HTTP/1.1" 404 567
1. 统计访问最多的 IP
1 | awk '{print $1}' access.log | sort | uniq -c | sort -nr | head -10 |
拆解: 1. awk '{print $1}':提取 IP 地址(第一列) 2.
sort:排序(让相同的 IP 相邻) 3.
uniq -c:去重并统计次数 4.
sort -nr:按次数倒序排序(-n 数字排序,-r 反向) 5.
head -10:只显示前 10 个
2. 统计访问最多的 URL
1 | awk '{print $7}' access.log | sort | uniq -c | sort -nr | head -10 |
$7是请求路径(如/api/users)
3. 统计各状态码的出现次数
1 | awk '{print $9}' access.log | sort | uniq -c | sort -nr |
$9是状态码(如 200 、 404 、 500)
输出示例: 1
2
31234 200
567 404
123 500
4. 查找最近 1 小时的错误
1 | grep "28/Jan/2025:12:" access.log | grep -E " (4|5)[0-9]{2} " | tail -n 100 |
- 第一个
grep过滤时间 - 第二个
grep过滤 4xx 和 5xx 状态码 tail -n 100只显示最后 100 行
xargs:批量处理文件
xargs
用于把前一个命令的输出(通常是文件列表)转换成参数,传递给下一个命令。
为什么需要 xargs
问题:有些命令(如
rm、cp、mv)不支持从 stdin
读取参数。
1 | find . -name "*.tmp" # 输出文件列表 |
解决:用 xargs 把文件列表转成参数
1
find . -name "*.tmp" | xargs rm # ✅ 正确
基本用法
1 | echo "file1 file2 file3" | xargs rm # 删除三个文件 |
高级用法:-i 和替换符
{}
1 | find . -name "*.log" | xargs -i cp {} {}.bak # 给每个文件复制一个 .bak 备份 |
-i:启用替换符{}{}:代表每个输入的文件名{}.bak:在文件名后加.bak
处理包含空格的文件名(重要!)
问题:文件名包含空格时,xargs
会把它当成多个参数。
错误示例: 1
find . -name "*.txt" | xargs rm # 如果有文件名是 "my file.txt",会被当成 "my" 和 "file.txt" 两个文件
正确做法:使用 find -print0 +
xargs -0 1
find . -name "*.txt" -print0 | xargs -0 rm
-print0:用 null 字符(\0)分隔文件名(而不是换行)-0: xargs 用 null 字符作为分隔符
或者用 find -exec(更简单):
1
find . -name "*.txt" -exec rm {} +
并行处理( xargs -P)
如果有多个 CPU 核心,可以并行处理: 1
find . -name "*.json" -print0 | xargs -0 -P 4 -n 1 jq -c . > /dev/null
-P 4:最多同时运行 4 个进程-n 1:每次传递 1 个参数
适用场景:
- 批量处理大量文件(如图片压缩、视频转码、 JSON 验证)
- 充分利用多核 CPU,加快处理速度
find 命令与管道的结合
find
是查找文件的强大工具,结合管道可以实现复杂的批量操作。
find 的基本用法
1 | find /path -name "pattern" # 查找文件名匹配 pattern 的文件 |
find + grep:在文件内容中搜索
1 | find /srv/project -name "*.py" -exec grep -Hn "TODO" {} + |
- 查找所有
.py文件 - 在文件内容中搜索 "TODO"
-H显示文件名,-n显示行号
find + xargs:批量操作
1 | find /var/log -name "*.log" -mtime +30 -print0 | xargs -0 gzip |
- 查找 30 天前的日志文件
- 批量压缩
find + sed:批量替换
1 | find /srv -name "*.conf" -print0 | xargs -0 sed -i 's/old_domain/new_domain/g' |
- 查找所有
.conf文件 - 批量替换域名
实战案例:批量文件操作
案例 1:批量重命名文件
假设有一批文件
img_001.jpg、img_002.jpg,想改成
photo_001.jpg、photo_002.jpg。
1 | for file in img_*.jpg; do |
或者用 rename 命令(需要安装): 1
rename 's/img/photo/' img_*.jpg
案例 2:批量修改文件权限
1 | find /var/www/html -type f -exec chmod 644 {} + # 文件改成 644 |
案例 3:批量删除空文件
1 | find /tmp -type f -empty -delete # 删除所有空文件 |
案例 4:批量压缩日志文件
1 | find /var/log -name "*.log" -mtime +7 -exec gzip {} \; |
-mtime +7: 7 天前修改的文件-exec gzip {} \;:对每个文件执行 gzip 压缩
高级技巧
进程替换( Process Substitution)
语法:<(command)
用途:把命令的输出当作临时文件使用。
示例:比较两个排序后的文件(不生成临时文件)
1
diff <(sort file1.txt) <(sort file2.txt)
等价于: 1
2
3
4sort file1.txt > /tmp/sorted1
sort file2.txt > /tmp/sorted2
diff /tmp/sorted1 /tmp/sorted2
rm /tmp/sorted1 /tmp/sorted2
并行处理( xargs -P)
如果有多个 CPU 核心,可以并行处理多个文件。
1 | find . -name "*.json" -print0 | xargs -0 -P 8 -n 1 jq -c . > /dev/null |
-P 8:最多同时运行 8 个进程-n 1:每次传递 1 个参数给命令
安全与最佳实践
1. 永远不要解析 ls
的输出
错误示例: 1
ls | xargs rm # ❌ 文件名包含空格会出错
正确做法: 1
find . -maxdepth 1 -type f -print0 | xargs -0 rm
2. 删除前先预览
1 | find . -name "*.tmp" -print # 先看看要删除哪些文件 |
3. 使用
set -e 和 set -o pipefail(在脚本里)
1 |
|
4. 引号的重要性
错误示例: 1
2dir="my documents"
rm -rf $ dir # ❌ 会删除 "my" 和 "documents" 两个目录
正确做法: 1
rm -rf "$ dir" # ✅ 正确删除 "my documents" 目录
5. 避免命令注入(在脚本里处理用户输入)
危险示例(永远不要这样写): 1
2
3# 用户输入: filename; rm -rf /
filename=$1
cat $ filename # ❌ 命令注入!
安全做法: 1
2
3
4
5
6
7filename=$1
# 验证输入(只允许字母、数字、下划线、点、横杠)
if [[ ! "$ filename" =~ ^[a-zA-Z0-9._-]+$ ]]; then
echo "Invalid filename"
exit 1
fi
cat "$ filename" # ✅ 安全
更多实战案例
案例:生成服务器监控报告
假设需要生成每日的服务器监控报告,包含 CPU 、内存、磁盘、网络等信息。
1 |
|
案例:清理旧日志文件
1 |
|
案例:批量处理 CSV 文件
假设你有一个 data.csv 文件: 1
2
3
4name,age,city,salary
Alice,30,Beijing,10000
Bob,25,Shanghai,8000
Charlie,35,Guangzhou,12000
提取所有工资大于 9000 的员工: 1
awk -F',' 'NR>1 && $4 > 9000 {print $1, $4}' data.csv
NR>1:跳过标题行( NR 是行号)$4 > 9000:第 4 列(工资)大于 9000print $1, $4:打印姓名和工资
计算平均工资: 1
awk -F',' 'NR>1 {sum += $4; count++} END {print sum/count}' data.csv
按城市统计人数: 1
awk -F',' 'NR>1 {count[$3]++} END {for (city in count) print city, count[city]}' data.csv
案例:分析 Apache/Nginx 错误日志
假设你的错误日志 error.log 格式如下: 1
22025/01/28 12:00:00 [error] 1234#0: *567 connect() failed (111: Connection refused) while connecting to upstream
2025/01/28 12:00:01 [warn] 1234#0: *568 upstream server temporarily disabled
提取所有错误级别的日志: 1
grep '\[error\]' error.log
统计每种错误的出现次数: 1
grep '\[error\]' error.log | awk '{print $8, $9, $10}' | sort | uniq -c | sort -nr
查找最近 1 小时的错误: 1
grep "2025/01/28 $(date -d '1 hour ago' +%H):" error.log | grep '\[error\]'
案例:处理系统日志( syslog)
查找所有 SSH 登录失败记录: 1
grep "Failed password" /var/log/auth.log | awk '{print $1, $2, $3, $11}' | sort | uniq -c
- 提取日期、时间、 IP 地址
- 统计每个 IP 的失败次数
查找占用磁盘最多的目录: 1
du -sh /* 2>/dev/null | sort -hr | head -10
du -sh /*:查看根目录下各目录占用空间2>/dev/null:丢弃错误信息(如权限不足)sort -hr:按人性化大小倒序排序head -10:只显示前 10 个
命令行技巧与效率提升
使用命令历史( history)
1 | history # 查看命令历史 |
示例: 1
2mkdir /tmp/mydir # 创建目录
cd !$ # 相当于 cd /tmp/mydir
使用别名( alias)
1 | alias ll='ls -lah' # 创建别名 |
永久保存别名(编辑 ~/.bashrc):
1
2echo "alias ll='ls -lah'" >> ~/.bashrc
source ~/.bashrc
使用 Ctrl 快捷键
Ctrl+C:中断当前命令Ctrl+Z:暂停当前命令(放到后台)Ctrl+D:退出当前 shell(或结束输入)Ctrl+L:清屏(相当于clear)Ctrl+A:光标移到行首Ctrl+E:光标移到行尾Ctrl+U:删除光标前的所有内容Ctrl+K:删除光标后的所有内容Ctrl+R:搜索命令历史(增量搜索)
使用命令替换( Command Substitution)
语法:$(command) 或
`command`
示例: 1
2
3echo "当前时间:$(date)"
echo "当前目录:$(pwd)"
echo "CPU 核心数:$(nproc)"
在脚本里的应用: 1
2
3
4
5# 备份文件时加上时间戳
cp config.conf config.conf.$(date +%Y%m%d_%H%M%S)
# 统计日志文件行数并写入报告
echo "今日日志行数:$(wc -l < today.log)" > report.txt
Shell 脚本中的错误处理
检查命令是否成功: 1
2
3
4if grep "error" app.log > /dev/null; then
echo "发现错误!"
# 发送告警邮件或其他操作
fi
使用 || 和 &&
简化条件判断: 1
2command && echo "成功" || echo "失败"
mkdir /tmp/test || { echo "创建目录失败"; exit 1; }
安全的脚本模板: 1
2
3
4
5
set -euo pipefail # -e: 任何命令失败就退出; -u: 使用未定义变量就退出; -o pipefail: 管道中任何命令失败就退出
# 你的脚本逻辑
cat file.log | grep "error" | process_errors.sh
总结与扩展阅读
这篇文章涵盖了 Linux 文件操作和管道的核心内容: 1. ✅ 数据流模型(
stdin/stdout/stderr 、文件描述符) 2. ✅
重定向(>、>>、2>、2>&1、<)
3. ✅ 管道符(| 的原理和调试技巧) 4. ✅ 文本处理工具链(
grep/awk/sed/cut/tr/sort/uniq) 5. ✅ 实战案例( Nginx
日志分析、批量文件操作) 6. ✅ xargs 的正确用法(处理空格、并行处理) 7.
✅ 安全与最佳实践(不解析 ls 、删除前预览、正确引号)
扩展阅读:
- The Art of Command Line:命令行技巧大全
man bash:查看 Bash 的详细手册(重定向、管道等)man 1 awk:查看 awk 的详细手册
下一步:
- 《 Linux
用户管理》:学习如何管理用户/组、
/etc/passwd、/etc/shadow、 sudo 配置
到这里,建议已经从"会用管道"升级到"能写可读可调试的 one-liner 、能快速分析日志、能安全批量处理文件"。管道和文本处理是 Linux 的核心能力,掌握了它,你就能更高效地完成运维任务。
- 本文标题:Linux 文件操作深入解析
- 本文作者:Chen Kai
- 创建时间:2019-11-14 15:30:00
- 本文链接:https://www.chenk.top/Linux-%E6%96%87%E4%BB%B6%E6%93%8D%E4%BD%9C%E6%B7%B1%E5%85%A5%E8%A7%A3%E6%9E%90/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!