find . -name "*.log" | rm 这条命令,直觉上应该是”找到所有 .log 文件,然后删掉”。实际跑一下:什么都不会发生,一个文件都删不掉,rm 也不会报任何错。

管道明明把 find 的结果传给了 rm,为什么 rm 完全不理会?这篇讲清楚背后的原因,以及支撑整个 Linux 命令行生态的设计哲学。

这是 Linux 命令系列的第六篇,承接 Linux 常见命令与工具地图grepfindtar/zip/unzip,以及姐妹篇 Linux 命令行选项设计约定——上一篇讲的是怎么给单个命令传参数,这篇讲怎么把多个命令组合起来干活。

管道的本质:把一个命令的输出接成另一个命令的输入

管道符 | 做的事情很纯粹:把左边命令的标准输出(stdout)直接接到右边命令的标准输入(stdin)。

1
2
3
4
ls | grep ".md"
# ls 把当前目录的文件列表写到 stdout
# | 把这份输出原样接到 grep 的 stdin
# grep 从 stdin 读入这份列表,从中过滤出包含 .md 的行

两个命令互相不知道对方的存在——ls 不知道自己的输出被谁读走了,grep 也不关心这些文本行是 ls 生成的还是别的什么命令生成的。它们唯一的约定是:数据以纯文本的形式流动。这个”互不知情、只认文本”的设计,正是管道能随意串联任意命令的原因。

关键区分:谁读 stdin,谁只认命令行参数

回到开头的问题:find . -name "*.log" | rm 为什么删不掉文件?

答案是:rm 根本不读 stdin。管道确实把 find 的结果传了过去,但 rm 压根没有”从标准输入读取要删除的文件列表”这个功能——它只认命令行参数,比如 rm file1.log file2.log。管道传过去的内容,rm 收到了也不会看,它只会检查自己的命令行参数里有没有文件名,没有就什么都不干、正常退出。

Linux 命令大致能分两类:

  • 过滤器类命令:默认从 stdin 读取数据,处理后写到 stdout,天生适合接在管道里。catgrepsedwcsort 都是这一类
  • 操作型命令:只认命令行参数,不读 stdin,因为它们的设计目的是”对指定的东西做一次性操作”,不是”处理一批流式数据”。rmchmodmkdircp 都是这一类

这个区分不是随意的——rm 删文件、chmod 改权限,这类操作要的是明确的目标列表,命令行参数比一份不确定长度的流式数据更适合表达”目标是谁”。

xargs 的意义:把 stdin 转换成命令行参数

xargs 存在的目的就是补上这个断层——把 stdin 里的内容转换成命令行参数,喂给那些只认参数的命令:

1
2
3
4
find . -name "*.log" | xargs rm
# find 把匹配到的文件路径列表写到 stdout
# xargs 从 stdin 读进这份列表,转换成 rm 的命令行参数
# 实际执行的是 rm file1.log file2.log file3.log ...

同样的管道,中间插一个 xargs,语义就通了:xargs 把”过滤器类命令的输出”转换成”操作型命令能理解的参数”,架起两类命令之间的桥。

xargs 核心参数

1
2
3
4
5
6
7
8
9
10
11
find . -name "*.tmp" | xargs -n 5 rm
# -n:每次调用命令时最多传几个参数,这里是每 5 个文件调一次 rm
# 不写 -n 的话,xargs 会尽量把所有参数塞进一次调用(受系统 ARG_MAX 限制)

find . -name "*.log" | xargs -I {} mv {} /backup/
# -I:自定义占位符,{} 代表 xargs 传进来的每一项
# 用 -I 之后参数位置更灵活,能把参数插到命令中间而不是只能接在最后

find . -name "*.log" -print0 | xargs -0 rm
# -0:配合 find 的 -print0 使用,用 null 字符分隔文件名而不是空格/换行
# 这个组合专门解决文件名里带空格的问题,前两篇 grep 和 find 文章里都提到过它

xargs -P:容易被忽略的并行执行

xargs 默认是串行的——一个一个调用命令,等上一个执行完再执行下一个。-P 可以指定并行度:

1
2
3
find . -name "*.jpg" | xargs -P 4 -n 1 convert -resize 50%
# -P 4:最多同时跑 4 个进程
# -n 1:每个进程只处理 1 个文件,这样才有 4 个独立任务可以并行

-P 有个容易踩的坑:它必须配合 -n 一起用,否则很可能不起作用——xargs 默认会把所有参数尽量塞进一次调用(一次 rm file1 file2 file3 ...),只有一次调用,-P 再高也没有第二个进程可以并行。想让并行真正生效,得先用 -n 把参数拆成多次独立调用,-P 才有事可做。

xargs 常用参数速查表

参数 作用
-n NUM 每次调用命令时最多传 NUM 个参数,不写则尽量一次塞满(受 ARG_MAX 限制)
-I REPLACE 自定义占位符,可以把参数插到命令中间而不是只能接在最后
-0 用 null 字符分隔输入项,配合 find -print0 处理带空格的文件名
-P NUM 最多同时跑 NUM 个进程,默认是 1(串行);必须配合 -n 才能真正并行
-t 执行前先把实际要跑的命令打印出来,调试危险操作时先加这个看一眼
-p 每次执行前都询问确认,比 -t 更保守,确认了才真正执行

xargs 和 find -exec 的对比

上一篇 find 文章里讲过 -exec 的两种写法(\;+),做的事情和 xargs 很像——都是”把找到的东西喂给下一个命令”。两者的区别:

1
2
3
4
5
find . -name "*.log" -exec rm {} +
# -exec 内建在 find 里,只服务 find 自己找到的结果

find . -name "*.log" | xargs rm
# xargs 是独立命令,管道前面可以是 find,也可以是 grep -l、ls,或者任何产生一行行文本的命令

-execfind 自带的能力,用起来更直接,但只能配合 findxargs 是通用工具,任何命令的输出只要能通过管道传过来,都能配合 xargs 转换成参数——适用范围更广。日常写 find ... -exec 就够用的场景不用特意换成 xargs,但如果数据来源不是 find(比如 grep -l ... | xargs),就只能用 xargs

Unix 设计哲学落地:小工具、文本接口、自由组合

前面这些具体机制,背后有一套统一的设计思想。1978 年,Unix 管道的发明者 Doug McIlroy 在 Bell System Technical Journal 上把这套思想总结成三句话:

Write programs that do one thing and do it well. Write programs to work together. Write programs to handle text streams, because that is a universal interface.

写只做一件事、并且做好这一件事的程序;写能相互协作的程序;写能处理文本流的程序,因为文本流是通用接口。

这三句话直接对应本文讲的内容:

  • 只做一件事grep 只管过滤文本,rm 只管删文件,没有哪个命令试图把所有功能都塞进自己
  • 相互协作:靠管道把小工具串起来,而不是靠一个大程序内置所有功能
  • 文本作为通用接口:不管是 ls 的输出还是 grep 的输出,都是纯文本,任何命令都能读懂任何命令的输出——这正是管道能随意组合任意命令的前提

这和图形界面软件的设计思路是相反的:图形应用倾向于把功能都装进一个软件里,命令行工具则倾向于拆成几十个只做一件事的小程序,需要什么功能现场用管道拼起来。前者上手快,后者组合起来的灵活度是图形界面很难达到的——今天学的这几个命令,明天可以用完全没预料到的方式重新拼装解决新问题。

Linux 命令系列:走到这里

这个系列从具体命令讲到了设计哲学:chmod/chown 管权限,grep 管文本搜索,find 管文件查找,tar/zip/unzip 管归档压缩,命令行选项设计约定 讲怎么给命令传参数,这篇讲怎么把命令组合起来。后续候选话题还有 rsync 的增量同步和包管理命令,写完再回来更新 Linux 常见命令与工具地图