命令行有一种很特别的乐趣:一个只做一件小事的工具,单独看像玩具;一旦把它的输出交给下一个工具,它就可以变成抽样器、报表器、观察面板或批处理流程的一个零件。
所以这一篇不会只给你一份“冷门命令清单”。我们会从每个命令的输入、输出和退出状态出发,看它能不能进入管道,怎样受 locale、终端宽度与权限影响,以及在自动化中应该怎样限流、验证和清理。

本文的实操在一个使用 debian:bookworm-slim 的隔离容器中完成。测试文件只写入 /tmp/welearn-ch25,先运行只读观察,再进行受控写入,然后核对行数、排序、NUL 记录数和退出状态,最后删除目录并由 --rm 移除容器。这样测的原因是:命令能打印出东西,不等于它在边界条件下仍然正确。
一条命令通常由程序名、选项和操作数组成。例如 shuf -n 6 numbers.txt:shuf 是程序,-n 6 要求最多输出 6 条,numbers.txt 是输入对象。但真正决定它能否被组合的,是下面四个问题:
先确认程序来自哪里:
command -v rev
type printf
rev --versioncommand -v 适合在脚本中做依赖检查;type 还能告诉你某个名称是 Shell 内建命令、函数、别名还是外部程序。同名工具在不同系统上的选项可能不一样,所以先读 --help、man、info 或项目的上游说明,再把它写进脚本。这比从一条网络片段猜参数更可靠。
对非默认工具,还要把包名和可移植边界写清楚。例如 rev 和 column 属于 util-linux 工具集,但 Debian 精简环境会把 column 放在 bsdextrautils 包中;watch 来自 procps-ng。脚本若依赖它们,应在启动时明确报错,而不是执行到一半才发现命令不存在。
每个进程启动时通常都有三条标准流:标准输入的文件描述符是 0,标准输出是 1,标准错误是 2。命令在终端中运行时,它们往往连向键盘和屏幕;重定向和管道只是把这些端点换了地方。
producer | filter | formatter > report.txt在这条线上,producer 的 stdout 成了 filter 的 stdin,然后交给 formatter。最后的 stdout 写入文件,但三个进程的 stderr 默认仍会显示在终端。这是一个好设计:数据可以继续被机器处理,诊断则保留给人。
2>&1 会把 stderr 并入当时 stdout 的去向,|& 在 Bash 中是相近的简写。别在没有理由时把两者混在一起,否则错误文本可能混进 CSV、JSON 或后续命令的输入。如果目标是保存诊断,可以单独写 2>error.log。
要让命令适合组合,最好输出稳定、一条记录一行的纯文本,或明确说明使用 NUL 之类的分隔符。颜色、进度条、动画和清屏控制码适合人直接看,却不适合成为下游数据。

输出回答“程序看到了什么”,退出状态回答“这次执行是什么结果”。惯例上 0 表示成功,非 0 表示某种未完成状态,但每个程序对非 0 的定义不同。
grep -q 是一个很好的例子:找到匹配返回 0,没找到返回 1,读文件失败等错误返回 2。“没找到”是可预期的业务分支,不应和“根本没能搜索”混为一谈:
if grep -q 'READY' status.txt; then
printf '%s\n' '已就绪'
else
status=$?
if [ "$status" -eq 1 ]; then
printf '%s\n' '尚未就绪'
else
printf '%s\n' '无法读取状态' >&2
exit "$status"
fi
fi管道的默认状态是最后一个命令的状态。false | true 因此会得到 0。Bash 打开 set -o pipefail 后,会把最右侧的非 0 状态当作管道状态;${PIPESTATUS[@]} 可以查看各段。这两项都要明确标注为 Bash 语义,不要默认所有 /bin/sh 都支持。
pipefail 不是“打开后管道就永远正确”。某些下游命令会提前停止读取,上游因 SIGPIPE 结束反而是正常的限流结果。你需要理解每段的协议,再决定哪些状态可以接受。
文本命令并不是在真空中工作。LC_ALL、LC_CTYPE、LC_COLLATE 等 locale 设置会影响字符识别、排序和部分格式。隔离实测中,LC_ALL=C.UTF-8 rev 把 甲乙A 正确变成 A乙甲;强制 LC_ALL=C 后,同一份 UTF-8 输入返回 1,并报告不完整的多字节字符。
对自动化来说,不要简单地说“固定 C locale 就好”。如果处理内容可能有中文,应选择环境实际安装的 UTF-8 locale,例如 C.UTF-8;如果只需要字节级稳定排序,LC_ALL=C sort 又可能是合理选择。关键是先写清“按字符还是按字节”。
终端是另一个边界。watch、sl 和带颜色的输出会读取 TERM、终端宽度和是否连接 TTY。在脚本中可以用 [ -t 1 ] 判断 stdout 是否是终端:人在看时打开颜色,重定向到文件时输出纯文本。
权限决定你能读哪些输入、能写哪些目标。不要因为一条展示命令报“Permission denied”就立刻把整条管道交给 sudo。先用 id、namei -l、stat 或 ls -ld 确认是哪个路径节点拒绝,再把提权限制在真正需要的那一步。

sl、cowsay 和 figlet:玩具也能教会我们边界sl 会在交互终端里播放蒸汽机车动画,它最初就是对把 ls 误输为 sl 的趣味提醒。cowsay 把文本放进 ASCII 气泡,figlet 则用普通字符拼出大标题。在 Debian 系列环境可先查包信息,再由有权限的人安装:
apt-cache show sl cowsay figlet
sudo apt install sl cowsay figlet其他发行版应先用自己的包管理器搜索,不要假设包名、可执行路径或字体目录完全相同。Debian 的 cowsay 和 sl 可能位于 /usr/games,而这个目录未必在非交互环境的 PATH 里。
figlet -f small 'WE LEARN'
/usr/games/cowsay '命令已验证'
/usr/games/sl这三个工具有真实用途,但范围很窄:figlet 可以给人读的演示或本地启动脚本做分区,cowsay 可以让教学提示更醒目,sl 适合交互式体验。它们的输出依赖宽度、字符显示宽度和终端控制,不应混进日志协议、API 响应或需要严格解析的 stdout。中文在等宽字符终端中还可能让气泡边框轻微错位,这正好说明“看起来对”与“数据契约稳定”是两件事。

rev 和 tac:反转的是字符还是行rev 按行读取,然后把每一行的字符顺序反转;行与行的顺序不变。如果不给文件,它从 stdin 读取:
printf '%s\n' '甲乙A' '第二行' | LC_ALL=C.UTF-8 rev
A乙甲
行二第tac 做的是另一件事:它把记录顺序从最后一行倒着输出,但每行内部不变。所以查看末尾事件、从旧到新变为从新到旧时,tac 比 rev 合适。它们的名字都带着幽默,但数据单位完全不同。
printf '%s\n' '甲乙A' '第二行' | tac
第二行
甲乙Arev 是面向行的工具,会为一整行分配内存。处理一个没有换行的超大文件时,内存占用会成为边界。另外,反转字符不等于反转人眼看到的完整字形:组合字符、emoji 序列和双向文本可能需要知道 Unicode 字形簇的专用工具。
seq、shuf 和 factor:生成、抽样与拆解seq 把数值区间变成一行一个数的流。它可以只接收终点,也可以接收起点、步长和终点:
seq 5
seq 10 -2 2
seq -w 1 12-w 会用前导零补齐宽度,适合生成 01到12 这类文本标签。不要把浮点 seq 当作精密计算器;浮点表示和 locale 都可能让格式与预期不同。
shuf 会读入所有行,随机排列或选出指定数量。要从 1到20 中抽 6 个不重复数,可写:
seq 1 20 | shuf -n 6 | sort -nsort -n 必须明确指定数值排序;默认文本排序会把 10 放在 2 前面。shuf 适合训练样本、演示顺序和一般抽样,不应自动被当作密码、令牌或密钥生成器。安全随机有额外的威胁模型和 API 要求。
factor 接收命令行上的数,也可以从 stdin 读多个数,然后输出质因数。实测中 factor 2026 得到 2026: 2 1013。它是理解数据流的好玩示例,但大整数分解可能非常慢,不能把小数快速的经验外推到任意输入。

yes 不是免责点击器,而是无界生成器yes 会把参数用空格连接,然后一行接一行地持续输出;不给参数时默认输出 y。它没有自然终点,所以这条命令会一直运行:
yes READY安全的学习方式是给它一个明确消费上限:
yes READY | head -n 4
timeout 1s yes SPIN > /dev/null第一条中,head 读满 4 行后关闭管道,yes 再写时收到 SIGPIPE。隔离实测在 Bash 5.2 中看到分段状态为 141 0,开启 pipefail 后整条管道为 141。这不是“只打印了四行的失败”,而是下游完成后通知上游停止的流控结果。
timeout 则用时间边界结束命令。未使用 --preserve-status 时,超时通常返回 124。这个状态应该由调用方明确处理,不要用 || true 把“按计划超时”和“命令自身失败”一起吞掉。
不要使用 yes | rm -i ... 这类组合。交互确认本来是为了让人在高风险操作前复核对象,用无界 y 绕过它,等于在不验证路径的情况下放大删除范围。自动化应该使用工具的非交互选项、预演模式、显式允许列表和结果验证。

printf 和 column:先生成稳定数据,再给人看printf 的格式字符串由脚本控制,数据作为后续参数传入:
printf '%s\t%d\n' "$name" "$count"不要写 printf "$value"。如果 value 里有 %s、%b 等指令,它会被当成格式而不是普通数据。要原样输出外部文本,应写 printf '%s\n' "$value"。
column -t 会根据分隔符对齐字段,很适合把 TSV 在终端中显示成表格:
printf 'NAME\tCOUNT\nalpha\t12\nbeta\t3\ngamma\t27\n' \
| column -t -s "$(printf '\t')"但“对齐后的表格”是人类视图,TSV 才是更稳定的数据层。后续还要 sort、awk 或导入其他系统时,应保留 TSV,只在最后显示一步使用 column。否则一个更长的中文名称就可能改变空格数量,让后续分列失败。
util-linux 的新版 column 还有列名、右对齐、换行、隐藏列与 JSON 等能力,但发行版所带版本可能较旧。如果脚本使用这些长选项,应运行版本检查或功能探测,而不是只在一个环境看起来正常。
watch:重复观察快照,不要忘记它会清屏watch 按间隔重复执行一条命令,并在全屏终端中刷新结果:
watch -n 2 'date; printf "\n"; df -h /'
watch -d -n 1 'ps -eo pid,stat,comm --sort=pid | head -n 15'-n 设置秒级间隔,-d 高亮相邻两次的差异。这很适合在受控实验中观察文件大小、进程状态或空闲空间,但它只是不断重画的快照,不是持久日志或时序数据库。错过的中间状态不会被自动保留。
要退出通常按 Ctrl-C。运行的命令如果本身需要交互输入,会和 watch 的全屏控制冲突。命令字符串还会经过 Shell 解析,所以引用和变量展开发生在哪一层必须弄清。复杂观察任务更适合写成只读脚本,然后让 watch ./snapshot.sh调用它。
在 CI、定时任务或需要审计的环境中,不要把全屏刷新输出当日志。应按时间戳追加快照,或使用监控系统。为什么:控制码在文件中会变成噪声,覆盖刷新也不能证明历史上没有短暂异常。

tee:一份数据同时继续向前和留下证据tee 从 stdin 读取,把同一份数据写到 stdout 和一个或多个文件。它的位置像一个 T 形水管:
producer \
| tee raw.tsv \
| sort -t "$(printf '\t')" -k2,2nr \
| column -t -s "$(printf '\t')"这样做的价值是,你可以保留对原始输出的证据,同时让数据继续去排序和显示。如果下游结果不对,可以检查问题发生在 producer 还是后续转换。
默认情况下,tee file 会覆盖已有文件,tee -a file 才是追加。追加日志前先考虑文件大小、轮转、并发写入和敏感数据。tee 不会自动提供事务、加密或记录轮转。
有时人们写 printf ... | sudo tee /etc/file,因为重定向是当前 Shell 先打开目标,而 sudo tee 让真正的写入进程获得权限。这是受控配置发布技巧,不是泛用快捷键。写入系统路径前应先在普通目录生成候选文件,检查 diff 和语法,备份旧版,再只提权完成最后替换,并立即验证服务状态。
xargs -0:把数据流变成参数,先守住文件名边界管道连接的是字节流,但某些命令要的是一组 argv 参数。xargs 会从 stdin 读记录,分批组合成命令行。简单数字可以这样处理:
seq 1 5 | xargs -n 1 factor文件名不能默认按空格或换行分隔。Linux 文件名可以包含空格、Tab、换行、引号和反斜杠,只有 NUL 不能出现在路径名里。因此安全组合是成对出现的 find -print0 和 xargs -0:
find ./files -type f -print0 \
| sort -z \
| xargs -0 sha256sum --zero隔离实测创建了 plain.txt、space name.txt 和一个名称真正含换行的文件。对 find -print0 的输出数 NUL,得到 3 条;经 xargs -0 sha256sum --zero 后仍然是 3 条。这里要数的是分隔符,不是换行,因为换行已经可能属于文件名。
xargs 解决了参数长度上限:输入太多时,它会分批调用目标程序。但它不自动消除竞态。在 find 发现一个路径和目标命令真正打开它之间,目录可能被另一个进程修改。处理共享可写目录时,要同时考虑 find -execdir、目录权限、锁或应用级 API。
现在把前面的小工具连起来。这次实操不会去碰系统配置:所有输入都是专门创建的夹具,所有输出都在 /tmp/welearn-ch25/output,每一步都有可机器检查的断言。
第一条流水线完成“生成→抽样→排序→留存→汇总”:
seq 1 20 \
| shuf -n 6 \
| sort -n \
| tee output/sample.txt \
| paste -sd, -一次真实输出是 2,11,13,17,19,20。随机抽样每次可以不同,所以断言不比较这六个固定值,而是检查“恰好 6 行、互不重复、最小不低于 1、最大不高于 20”。这为什么重要:好测试检查不变式,而不是把随机结果假装成常量。
第二条流水线从 TSV 中保留表头,再按第二列数值降序,一边写入 report.tsv,一边交给 column:
printf 'NAME\tCOUNT\nalpha\t12\nbeta\t3\ngamma\t27\n' \
| { IFS= read -r header; printf '%s\n' "$header"; sort -t "$(printf '\t')" -k2,2nr; } \
| tee output/report.tsv \
| column -t -s "$(printf '\t')"终端视图显示 gamma 27、alpha 12、beta 3,断言则直接检查 TSV 的第二行是 gamma 和 27。这区分了人类显示与机器证据。
第三条流水线处理含空格和换行的文件名,连续数两次 NUL 记录:发现阶段是 3,校验和阶段仍然是 3。这证明数据经过 sort -z、xargs -0 和 sha256sum --zero 时没有因特殊字符被拆开。
第四条流水线专门检查无界输出。yes READY | head -n 4 实际写入 4 行;默认管道状态是 0,开启 pipefail 后分量是 141,0、整体是 141;timeout 0.05 yes SPIN 返回 124。我们同时验证输出数量和状态语义,因为只查一边会遗漏另一边的错误。
最后删除整个测试目录,断言路径不存在,再退出容器。清理不是补充动作,而是实验通过的一部分。

走到这里,你已经不需要靠背几百个命令证明熟悉 Linux。更可靠的能力是:看到一个问题时,知道如何把它拆成数据边界、权限边界和状态边界,然后用小工具组合出可验证的答案。
可以把前面所有技能收束为一条固定工作流:
先只读观察。用 pwd、id、stat、findmnt、ps、ss 或对应工具建立当前事实,把 stdout、stderr 和状态一起记下。为什么:没有基线,变更后就无法证明是变好还是变坏。
再缩小变更。在 /tmp 或专门的工作目录创建最小夹具,用普通用户先预演,需要提权时只提升最终一步。为什么:范围越小,误操作的爆炸半径越小。
然后验证契约。核对输出格式、退出状态、文件内容与权限,再重跑一次检查幂等性。为什么:进程返回 0 只说明它自己认为成功,不能代替业务结果核对。
遇到陌生问题时,先用 man -k 关键词、apropos 关键词、command --help 定位工具,再查上游文档与目标发行版的实际版本。一条管道变得很长、引用很难读、失败分支无法清晰表达时,把它写成有参数、日志、测试和清理的 Shell 脚本;当数据结构、并发、网络协议或错误恢复开始复杂,就换用 Python、Go 或其他更合适的语言。
sl 会让人笑一下,rev 会让我们重新看待“记录”,yes 则会用一条无穷流提醒我们:任何自动化都需要边界。真正值得带走的,正是这种把工具、契约、验证和安全边界一起思考的习惯。
最后清理并留下证据。删除临时文件,停止测试进程,恢复更改过的环境,保留精简日志、diff 和验证结果。为什么:一个无法清理的实验,会把下一次结果污染掉。