这是《与 Shell 对话》系列的第三篇。前两篇你学会了操作文件和搜索内容——每个命令都是独立使用的。但终端的真正威力来自把命令串起来。一篇的核心是两个符号:|(管道)和 >(重定向)。
上一篇:看与找——信息的读取与搜索 · 下一篇:面向系统的操作
| 关键词 | 一句话说明 |
|---|---|
| 标准输入(stdin) | 编号 0,命令默认的输入来源——通常是你的键盘 |
| 标准输出(stdout) | 编号 1,命令的正常输出目的地——通常是屏幕 |
| 标准错误(stderr) | 编号 2,命令的错误信息通道——也默认去屏幕,但可以单独处理 |
重定向 > | 把命令的输出从屏幕转到文件里——ls > 清单.txt 会把结果存下来 |
追加 >> | 和 > 一样,但不覆盖已有内容,而是追加到文件末尾 |
管道 | | 把左边命令的输出接到右边命令的输入——A | B,A 的结果变成 B 的处理对象 |
/dev/null | Unix 的"黑洞"——任何扔进去的东西都会消失,用于丢弃不需要的输出 |
| sort | 排序——sort -h 按人类可读数字排序,sort -n 按数值排序 |
| uniq | 去重——去掉相邻的重复行,通常和 sort 配合使用 |
| tee | T 型管道——把输出同时送到屏幕和文件,像三通水管 |
| xargs | 把管道送来的内容转换成命令的参数——"从流水线上拿起每个零件,交给下一个机器处理" |
在你输入一个命令并按回车之后,Shell 会创建三个"通道",连接你的命令和外界:
| 通道 | 编号 | 默认来源/去向 | 拿它做什么 |
|---|---|---|---|
| 标准输入(stdin) | 0 | 你的键盘 | 命令读入的数据 |
| 标准输出(stdout) | 1 | 屏幕 | 命令的正常输出 |
| 标准错误(stderr) | 2 | 屏幕 | 命令的错误信息 |
说人话就是:每个命令都有两个"出口"(正常结果和错误消息),默认都送到屏幕让你看到。而当你学会了重定向和管道,你就可以改变这些通道的方向——把结果存到文件里、把错误丢弃掉、把输出喂给另一个命令。
为什么编号重要?因为重定向时你会在符号前面写编号:2>&1 的意思是"把 2 号通道(stderr)塞进 1 号通道(stdout)"。不写编号时默认操作的是 stdout。
理解标准流是打通终端任督二脉的关键。它解释了为什么
ls 不存在的文件的错误消息和正常输出走的是不同的管子——这也是为什么你可以把正常结果保存到文件,同时让错误还在屏幕上显示的底层原因。
>ls -la ~/Desktop > 桌面清单.txt # 将 ls 的输出存入文件(不显示在屏幕)
cat 桌面清单.txt # 验证——文件里确实有内容
> 的意思是:把 stdout 从"屏幕"转向"文件"。
⚠️ > 会覆盖文件的已有内容。 如果文件原来有内容,使用 > 后原有内容会丢失。
echo "第一行" > 测试.txt # 创建文件,写入"第一行"
echo "第二行" > 测试.txt # 覆盖!现在文件里只有"第二行",第一行没了
这里的 echo 命令很简单——它把后面跟着的文字打印到屏幕。echo "第一行" 会输出「第一行」三个字。配上重定向 >,文字就不去屏幕、直接存入文件了。可以把 echo 理解为后续所有重定向和管道练习的「人工输入源」。
>>echo "第一行" >> 测试.txt # 写入"第一行"
echo "第二行" >> 测试.txt # 追加!文件现在有两行
>> 和 > 的唯一区别:>> 不覆盖,追加到末尾。想保留历史记录时用它。
2> 和 2>&1ls 存在的文件 不存在的文件 > 结果.txt
# stdout 去了文件,stderr 仍然在屏幕上显示
ls 存在的文件 不存在的文件 > 结果.txt 2> 错误.txt
# stdout → 结果.txt,stderr → 错误.txt,屏幕干干净净
ls 存在的文件 不存在的文件 &> 全部.txt
# stdout 和 stderr 合并进同一个文件(&> 是简写)
ls 存在的文件 不存在的文件 > 全部.txt 2>&1
# 更传统的写法:先把 stdout 指向文件,再把 stderr 指向 stdout
初学时最常用的组合是 2>/dev/null——把错误消息丢进黑洞:
grep -r "某个关键词" ~/Desktop 2>/dev/null
# 搜索桌面上的文件,忽略所有"权限不够"之类的错误提示
/dev/null是 Unix 里的一个特殊文件。任何写入它的内容都会被系统丢弃,读它则什么也读不到。它的用途就是"我不在乎这个输出,别给我看"。在搜索时屏蔽大量权限错误是最常见的用法。
| 写法 | 含义 |
|---|---|
命令 > 文件 | stdout → 文件(覆盖) |
命令 >> 文件 | stdout → 文件(追加) |
命令 2> 文件 | stderr → 文件(覆盖) |
命令 &> 文件 | stdout + stderr → 同一文件 |
命令 > 文件 2>&1 | 同上,传统写法 |
命令 2>/dev/null | 丢弃 stderr(只看正常输出) |
命令 > /dev/null 2>&1 | 丢弃所有输出(全静默模式) |
重定向是在命令和文件之间搬运数据。管道是在命令和命令之间搬运数据:
命令A | 命令B # 命令A 的输出 = 命令B 的输入
管道的哲学:每个命令只做一件事,但你可以自由组合它们。 ls 只管列文件,wc 只管数行数——但 ls | wc -l 就变成了"数一数有多少文件"。
前两篇里你已经用过几次管道了(ls | less、history | grep)。现在是时候正式理解它:
# 简单管道:两个命令
ls ~/Desktop | wc -l # 桌面上有多少项目?
grep -r "TODO" ~/Desktop | wc -l # 桌面文件里有多少个 TODO?
# 三级管道:三个命令串起来
ls -la ~/Desktop | grep "^-" | wc -l # 桌面上的文件(不含文件夹)有多少个?
# ls 列出来 → grep 只保留文件行 → wc 数一数
# 注意 grep "^-" 中的 ^ 是行首(第二篇学过的正则),- 是字面量。
# ls -la 输出中普通文件的第一个字符是 -,所以 "^-" 匹配以 - 开头的行。
# 四级管道:串四个命令
history | grep "ssh" | grep -v "scp" | head -10
# 历史记录 → 只保留带 ssh 的 → 去掉带 scp 的 → 只看前 10 条
这就是"Unix 哲学"在终端里的具体体现:每个命令做好一件事,然后用管道组合成新的工具。 你不需要一个叫"统计桌面文件数量"的程序——你把
ls和wc用管道连起来就行了。你自己"发明"了这个工具。
以下是你在日常使用中最常遇到的管道组合。每个都是 命令1 | 命令2 | 命令3... 的结构。
ls | wc -l# 数文件
ls ~/Desktop | wc -l # 桌面上有多少项目
ls ~/Downloads | wc -l # 下载文件夹里有多少文件
find ~/Desktop -name "*.md" | wc -l # 桌面上有多少 Markdown 文件
这是最简单的管道组合,但你每天都会用到。如果发现结果比预期多了一个——那是因为 ls | wc -l 可能会受到换行符的影响。不用担心,这个误差很小。
history | grephistory | grep "ssh" # 之前用过的所有 ssh 命令
history | grep "pmset" # 之前用过的电源管理命令
history | grep "brew install" # 用 Homebrew 装过哪些东西
history 命令列出你用过的所有命令记录。和 grep 组合后,你可以在历史中快速找到"我记得我上周敲过一条很长的命令,但我忘了是什么"——这是终端使用中最实用的场景之一。
du | sort -hsort 命令按字母或数字排序行:
du -sh ~/Desktop/* # 看桌面上一级各项目的大小
du -sh ~/Desktop/* | sort -h # 按大小从小到大排列(-h 理解 K/M/G)
du -sh ~/Desktop/* | sort -hr # 按大小从大到小排列(-r 是倒序)
# 显示最大的 5 个
du -sh ~/Desktop/* | sort -hr | head -5
# 找最大的文件——在整个用户文件夹里
du -sh ~/* | sort -hr | head -10
sort 的常用选项:
| 选项 | 含义 | 场景 |
|---|---|---|
sort -n | 按数值排序 | 数字排序而不是字典序(10 会排在 2 后面) |
sort -h | 按人类可读数字排序 | 理解 1K、2M、3G 这样的单位 |
sort -r | 倒序排列 | 最大的在前 |
sort -t, -k2 | 按逗号分隔的第二列排序 | 处理表格数据时 |
sort | uniquniq 去掉相邻的重复行。但实际数据中重复行往往不相邻,所以先 sort 再 uniq 是标准组合:
# 先用 echo 和重定向创建一个样本文件(刚学的技能!)
echo "成功" >> 操作日志.txt
echo "成功" >> 操作日志.txt
echo "失败" >> 操作日志.txt
echo "成功" >> 操作日志.txt
# 现在看看这个文件
cat 操作日志.txt
# 去重
sort 操作日志.txt | uniq # 去重后的结果
sort 操作日志.txt | uniq -c # 每种状态出现了几次
sort 操作日志.txt | uniq -d # 只显示重复的行
# 清理
rm 操作日志.txt
一个更实用的例子——找出桌面上所有文件的扩展名并统计频率:
ls -la ~/Desktop | grep "^-" | awk '{print $NF}' | grep -o '\.[^.]*$' | sort | uniq -c | sort -rn
这很长——但它的意思是:列出桌面文件 → 只看文件(不是文件夹)→ 提取最后一列(文件名)→ 只保留扩展名 → 排序 → 统计每种多少个 → 按数量倒序。你不需要记住它,只需要知道管道可以这样一层层组合。
| less任何输出太长时,送到 less 里翻页查看:
history | less # 命令历史太长,翻页看
ls -laR ~/Desktop | less # 递归列出桌面所有文件,翻页看
find ~ -name "*.md" | less # 找到所有 .md 文件,翻页看
在 less 里的操作和第二篇学的一模一样:空格翻页,/ 搜索,q 退出。
这五种管线模式——数、搜、排、去重、翻页——覆盖了日常终端使用中 90% 的管道场景。不用背,用的时候回来查。敲多了自然就记住了。
tee 的名字来自 T 型三通水管——它把输出同时送到两个地方:屏幕 和 文件。
ls ~/Desktop | tee 桌面清单.txt
# 结果同时在屏幕上显示,又存进了文件
ls ~/Desktop | tee -a 桌面清单.txt
# -a 追加模式(不覆盖已有内容)
# 实际场景:你想看 find 的结果,又想把它存下来
find ~/Desktop -name "*.md" | tee markdown文件清单.txt | wc -l
# 结果:屏幕显示文件列表,清单存到了文件,wc 数了数量
tee 的实际使用场景:你正在跑一个重要命令,输出很长,你想一边看结果一边把它存下来。或者你想在管道中间"偷看一眼"当前的数据流长什么样。
前面所有管道的组合中,右边的命令(如 wc、grep、sort)都接受从 stdin 读数据。但有些命令不接受 stdin——它们只接受参数。xargs 就是中间的桥梁:它把左边送来的每一行数据,转换成右边命令的参数。
# find 找到文件,xargs 把文件名作为参数传给 rm
find ~/Desktop -name "*.tmp" | xargs rm
# 等价于:rm 文件1.tmp 文件2.tmp 文件3.tmp ...
# 更安全的做法——先看看 xargs 会做什么
find ~/Desktop -name "*.tmp" | xargs echo
# xargs echo 会列出所有找到的文件——不会删除,只是打印
# 一个实用的例子——统计多个文件的总行数
find ~/Desktop -name "*.md" | xargs wc -l
# wc -l 可以接受多个文件参数,xargs 帮它把所有文件名传过去
xargs 的常用选项:
| 选项 | 含义 | 示例 |
|---|---|---|
-n 1 | 每次只传一个参数 | xargs -n 1 echo——每行单独处理 |
-p | 执行前询问确认 | xargs -p rm——安全的 rm |
-0 | 用空字符分隔(配合 find -print0) | 处理文件名中有空格的情况 |
xargs 配合
find -delete和rm时特别危险——因为你要删除的文件列表来自另一个命令的输出,你无法预判它会找到什么。永远先用| xargs echo看看 xargs 会拼出什么命令,确认无误后再去掉 echo。 这是一个让你在终端里安全活到老的习惯。
这个练习和之前不同——它没有目录结构要创建,直接用你电脑上的真实文件。每一个任务都是一个你可以重复使用的"工具"。
用一条命令查看桌面上的所有项目,统计数量,同时把清单保存到文件。
ls ~/Desktop | tee ~/Desktop/桌面清单.txt | wc -l
在桌面上找出最大的 5 个项目。
du -sh ~/Desktop/* | sort -hr | head -5
在桌面的 .md 文件中搜索"终端"这个词,统计出现了多少次(注意区分大小写)。
grep -rin "终端" ~/Desktop/领域 | wc -l
# 看看具体哪些文件包含
grep -rl "终端" ~/Desktop/领域
查看你最常用的 10 条命令是什么。
这一步需要用到 awk——一个文本处理工具,它的作用是提取每一行的第 2 列(即命令的名字,因为 history 的输出格式是"编号 命令 参数")。你不会 awk 完全没关系——这里只需要看懂管道的链条结构就行:
history | awk '{print $2}' | sort | uniq -c | sort -rn | head -10
# 解读这个管道的每一段:
# history → 列出所有命令记录
# awk '{print}' → 只保留"命令名字"那一列(去掉编号和参数)
# sort → 排序,让相同的命令排在一起
# uniq -c → 去重,同时统计每种命令出现多少次
# sort -rn → 按数字倒序排列(最常用的排最前)
# head -10 → 只看前 10 条
在 ~/.zshrc 中找到所有不是注释、也不是空行的有效配置行,统计有多少行。
grep -v "^#" ~/.zshrc | grep -v "^$" | wc -l
# 先去注释行,再去空行,然后数剩下的
到这里,你已经可以用终端做一件很"新潮"的事了——你不再依赖专门的应用来统计数据、分析文件和查看系统。一条几十个字的命令,就能完成一个专用小工具的工作。这就是管道的魔力。
| 符号/命令 | 作用 | 最常用法 |
|---|---|---|
> | 重定向 stdout 到文件(覆盖) | 命令 > 文件 |
>> | 重定向 stdout 到文件(追加) | 命令 >> 文件 |
2> | 重定向 stderr 到文件 | 命令 2> 错误文件 |
&> | 重定向 stdout + stderr 到同一文件 | 命令 &> 文件 |
2>/dev/null | 丢弃错误消息 | grep -r ... 2>/dev/null |
| | 管道——前一个命令的输出 → 后一个命令的输入 | A | B | C |
sort | 排序 | sort -h(人类可读) sort -n(数值) sort -r(倒序) |
uniq | 去重(需先 sort) | sort 文件 \| uniq -c(统计) |
tee | 同时输出到屏幕和文件 | 命令 | tee 文件 tee -a(追加) |
xargs | 将 stdin 转为命令参数 | find ... | xargs 命令 xargs -p(确认模式) |
<()——把命令的输出伪装成一个文件,让不接受管道但接受文件的命令使用-0 和 find -print0 防止参数分割错误> 文件 2>&1 和 2>&1 > 文件 效果完全不同,顺序有讲究<<——在终端中直接输入多行文本,cat << EOF ... EOFawk 和 sed——处理文本的两大神器,分别擅长列处理和行处理命令1 | 命令2 > 文件 2>/dev/null 这种混合写法