一个数字的启示
打开终端,输入 ls --help,你会看到什么?
在1979年的Unix V7上,ls 只有11个选项。而在今天的Ubuntu系统上,这个数字变成了58。单字母选项涵盖了除 {jvyz} 外的所有小写字母、14个大写字母,外加 @ 和 1。
这还不是最夸张的。tar 命令从1979年的12个选项增长到今天的139个。ps 从4个变成85个,find 从14个变成82个。
xychart-beta
title "命令行工具选项数量增长 (1979-2017)"
x-axis ["ls", "tar", "find", "ps", "grep"]
y-axis "选项数量" 0 --> 150
bar [11, 12, 14, 4, 11]
bar [58, 139, 82, 85, 45]
Doug McIlroy,Unix管道的发明者,曾对此痛心疾首:“看到Linux的体积,我的心都在下沉。那些曾经能塞进8KB的工具现在变成了1MB。手册页曾经真的只有一页,现在却成了一本小册子,包含上千个选项……我们以前会坐在Unix房间里问:‘我们可以扔掉什么?为什么会有这个选项?’”
但事情真的这么简单吗?这些选项的增加,到底是设计的失败,还是时代的必然?
Unix哲学的黄金时代
1978年,Doug McIlroy在《贝尔系统技术期刊》中写下了后来被称为"Unix哲学"的四条原则:
mindmap
root((Unix哲学<br/>1978))
原则一
做好一件事
新工作重新构建
不添加功能
原则二
输出成为输入
不干扰输出
避免二进制格式
原则三
尽早尝试
几周内验证
抛弃笨拙部分
原则四
使用工具
构建专用工具
用完可丢弃
1994年,Peter H. Salus将其浓缩为更著名的三句话:
- 编写只做一件事且做好的程序。
- 编写能协同工作的程序。
- 编写处理文本流的程序,因为那是通用接口。
这套哲学在那个硬件资源极度匮乏的年代简直是天才。PDP-11只有24KB内存,你没法写庞大的程序。但正是这种限制,催生了一种优雅的设计:小工具、管道组合、文本流。
Brian Kernighan和Rob Pike在1984年的论文《Program Design in the UNIX Environment》中举了一个经典例子:cat 命令。它只做一件事——把输入复制到输出。但正是这个简单的设计,让它可以与无数其他工具组合:cat file | grep pattern | sort | uniq。
与之形成对比的是当时其他系统的"瑞士军刀"设计,比如CP/M或RSX-11上的PIP程序——一个程序试图处理所有文件操作,有自己的内部结构和命令语言。
数字会说话
Dan Luu在2017年做了一个有趣的统计,比较了常用命令在不同年代的选项数量:
xychart-beta
title "ls命令选项数量演变"
x-axis ["1979", "1996", "2015", "2017"]
y-axis "选项数量" 0 --> 70
line [11, 42, 58, 58]
| 命令 | 1979年 | 1996年 | 2015年 | 2017年 |
|---|---|---|---|---|
| ls | 11 | 42 | 58 | 58 |
| rm | 3 | 7 | 11 | 12 |
| mkdir | 0 | 4 | 6 | 7 |
| mv | 0 | 9 | 13 | 14 |
| cp | 0 | 18 | 30 | 32 |
| cat | 1 | 12 | 12 | 12 |
| tar | 12 | 53 | 134 | 139 |
| find | 14 | 57 | 82 | 82 |
| ps | 4 | 22 | 85 | 85 |
| grep | 11 | 22 | 45 | 45 |
xychart-beta
title "tar命令选项数量演变"
x-axis ["1979", "1996", "2015", "2017"]
y-axis "选项数量" 0 --> 150
line [12, 53, 134, 139]
几乎所有命令的选项都在增长,而且几乎没有逆转的趋势。更值得注意的是,原本零选项的命令(如 mkdir、mv)也开始积累选项。
为什么会这样?
第一个原因:文本流的阿喀琉斯之踵
Unix哲学的核心假设之一是:“编写处理文本流的程序,因为那是通用接口。”
这个假设在实践中遇到了严重的挑战。
flowchart LR
subgraph 理想模型
A[ls] -->|文本| B[grep]
B -->|文本| C[sort]
C -->|文本| D[uniq]
end
当数据只是简单的行列表时,管道组合确实优雅。但现实世界的数据往往有复杂结构。ls -l 的输出是这样的:
-rw-r--r-- 1 user group 1234 Jan 15 10:30 file.txt
看起来很简单?试试从里面提取文件大小。你可以用 awk '{print $5}',但这假设你知道输出的确切格式。如果文件名包含空格呢?如果日期格式变了呢?
flowchart TB
subgraph 现实困境
E[ls -l输出] --> F{解析?}
F -->|文件名有空格| G[awk失效]
F -->|日期格式变了| H[脚本崩溃]
F -->|用户名有空格| I[字段错位]
end
Dan Luu指出,这就是为什么命令要添加格式选项——因为用户不想记住 cut 和 awk 在处理字段时的细微差异,也不想在每次管道操作时重新解析数据格式。
更糟糕的是,不同命令的文本输出格式互不兼容。wc -w 在某些系统上不能正确处理Unicode。有些命令输出制表符分隔,有些用空格,有些用固定宽度。用户被迫在脑中记住每个命令的怪癖,然后在管道的每个环节应用正确的解析逻辑。
讽刺的是,“文本是通用接口"这句话就像说"二进制是通用格式”——技术上正确,但毫无帮助。格式本身就有结构,而结构需要解析。
PowerShell选择了一条不同的路:传递结构化对象而不是文本。你不需要记住 cut -f4 和 awk '{print $4}' 的区别,因为对象有属性名。这不是说PowerShell完美——它有自己的问题——但它证明了另一种可能。
第二个原因:便利性的诱惑
有些选项的增加纯粹是为了便利。
timeline
title mv命令的选项演变
section 1979年
零选项 : 纯粹移动文件
section 1996年
添加备份选项 : -b, --backup
添加交互选项 : -i, -f
section 2017年
添加更新检查 : -u, --update
添加目标处理 : -T, -t
mv 命令原本零选项。现在它可以在移动文件时自动创建备份,有三个相关选项:两种不同的备份指定方式(一个接受参数,另一个从环境变量读取),还有一个选项允许覆盖默认的备份后缀。它还添加了"永不覆盖"和"仅在文件更新时覆盖"的选项。
mkdir 现在可以在创建目录时设置权限,还能自动创建父目录(-p)。
tail 原本只有一个选项(-数字,告诉它从哪里开始)。现在它有 -f 来持续监控新内容、-s 来设置检查间隔、--retry 来重试不可访问的文件。
这些功能本来可以通过组合多个命令实现。mkdir -p 可以用循环和条件判断替代。tail -f 可以用脚本轮询替代。但每次用户都要写一堆胶水代码,成本太高。
McIlroy会说:“这些选项的存在说明设计有问题。“但站在用户角度,一个选项比记住怎么组合五个命令要简单得多。
第三个原因:现代工具的规模
看看现代的命令行工具,复杂度更是惊人。
xychart-beta
title "现代CLI工具复杂度对比"
x-axis ["tar", "git子命令", "gcc选项", "kubectl"]
y-axis "数量" 0 --> 1200
bar [139, 200, 1000, 500]
git 有接近200个子命令,每个子命令又有自己的选项。gcc 有上千个选项。kubectl 的手册页堪比小型书籍。
这些工具不只是"做一个功能”——它们管理整个工作流。git 不只是版本控制,它处理分支、远程、子模块、钩子、交互式变基……kubectl 管理整个Kubernetes集群的生命周期。
Dan Luu提到了一个有趣的矛盾:如果按照Unix哲学,curl 支持的40种协议应该拆成40个命令吗?clang 的79个静态分析检查应该各自独立吗?
问题在于,这些功能共享大量底层基础设施。把 clang 的静态分析拆成独立工具,仍然需要完整的编译器基础设施来解析代码。维护负担并没有减少,只是从"一个工具的选项"变成了"多个工具的接口一致性”。
更关键的是,现实世界的软件开发已经变得极其复杂。一个部署流程可能涉及构建、测试、打包、推送、部署、验证。如果你每个步骤都要拼凑不同的工具,最终得到的不是优雅的管道,而是一团乱麻般的脚本。
Unix哲学的时代局限
回到McIlroy的抱怨,他说的"我们以前会问’为什么要这个选项’",在那个所有人都在同一个房间工作的年代是有意义的。
flowchart TB
subgraph 1970年代Bell Labs
A[小团队<br/>几十人] --> B[共享文化]
B --> C[同一房间讨论]
C --> D[达成共识]
end
subgraph 2020年代全球开发
E[海量开发者<br/>数百万] --> F[分散全球]
F --> G[不同背景]
G --> H[无法统一]
end
Bell Labs的Unix团队就那么几十个人,共享一套文化和技术背景。他们可以坐下来讨论,达成共识。但今天的软件世界完全不同:成千上万的开发者,分布在全世界,用不同的语言,为不同的目标工作。
当任何人都可以写一个工具并发布时,一致性的前提条件就不存在了。“简单"和"做好一件事"对不同的人有不同的理解。结果是什么?PHP级别的混乱——每个命令都有自己的参数约定、输出格式、行为怪癖。
Ken Thompson和Dennis Ritchie在设计Unix时面对的是16KB内存的PDP-11。McIlroy感叹今天一个工具占用1MB内存时,他忽略了变化的基准线:2020年一台300美元的Chromebook有16GB内存,是1979年Apple II的四百万倍。内存价格从每KB约100美元下降到可以忽略不计。
这不是说软件膨胀是好事。但它确实解释了为什么"保持小"不再是生存必需,以及为什么用户对便利选项的需求能够压倒哲学纯粹性。
现代CLI设计的挣扎
2020年,一群经验丰富的开发者发布了《Command Line Interface Guidelines》(clig.dev),试图为现代CLI工具建立一套新的最佳实践。
有趣的是,这份指南在尊重Unix传统的同时,也承认了时代的变迁。它区分了"人类优先"和"机器优先"的设计——传统Unix工具假设自己是被其他程序调用的,而今天的CLI工具首先要服务人类用户。
指南建议工具在无参数时显示简洁的帮助信息,而不是报错。建议使用彩色输出提高可读性(但检测TTY,在管道时禁用)。建议在用户可能拼写错误时提供建议(Did you mean...?)。
这些都是传统Unix工具不会做的事情。ls 在管道时不会关闭彩色输出,导致你会看到一堆转义序列。cp 不告诉你它在做什么,只默默执行——这对于脚本友好,但人类用户经常怀疑它是不是卡住了。
clig.dev的作者们承认了两个现实:第一,命令行工具仍然有价值,即使在GUI和移动应用主导的世界;第二,纯粹遵循1970年代的设计哲学已经不够了。
参数解析库的军备竞赛
另一个值得观察的维度是参数解析库的演变。
timeline
title Python参数解析库演进
section 1990年代
getopt : C风格包装
: 简单但受限
section 2000年代
optparse : 声明式定义
: 更优雅
section 2010年代
argparse : 标准库
: 支持子命令
section 2020年代
click/typer : 第三方选择
: 更现代的API
C语言的 getopt 在1980年代标准化,支持短选项(-a)和长选项(--all)。但它有严重限制:选项必须出现在参数之前,选项之间不能随意混排。
GNU扩展了这些限制,引入了--来分隔选项和参数,允许选项出现在任何位置。但这又造成了不同系统之间的不一致——同样的命令在BSD和Linux上可能有不同行为。
Python社区经历了一场漫长的进化:从 getopt(C风格的简单包装)到 optparse(支持声明式定义)再到 argparse(当前标准,支持子命令)。即便如此,argparse 的行为与POSIX和GNU惯例仍有差异,这导致了一些项目选择 click 或 typer 这样的第三方库。
Rust社区推出了 clap,Go有 cobra,JavaScript有 oclif 和 yargs。每个库都在试图平衡两个矛盾的目标:让开发者容易定义接口,同时让用户容易使用接口。
但讽刺的是,这些库的流行本身就在推动复杂度增长。以前写一个CLI工具,你需要手动解析 argv,这很痛苦,所以你会尽量保持接口简单。现在你只需要声明几个参数,库会自动生成帮助文本、处理错误、提供建议……添加选项的成本变得如此之低,以至于人们不再仔细思考是否真的需要这个选项。
一个反例的启示
不是所有工具都走向复杂。jq 命令是一个有趣的反例。
它处理JSON——一个结构化的数据格式,而不是文本流。它有一个简洁的过滤语言,让用户可以用统一的方式提取和转换数据。它的选项数量相对克制,核心功能集中在过滤器的表达力上。
jq 成功的原因之一是它选择了正确的抽象层次。对于JSON数据,管道和过滤器仍然是强大的组合方式,但过滤器操作的是结构化对象而不是文本行。
这暗示了一个可能性:Unix哲学的某些部分仍然有效(组合、管道、专注),但需要更新对"通用接口"的理解。文本流不是唯一的答案,JSON、Protocol Buffers、或其他结构化格式可能在某些场景更合适。
数字化生存的现实
让我们回到那个表格。
命令行选项增长的趋势不太可能逆转。这不是因为开发者忘记了McIlroy的教导,而是因为现实世界的复杂性本身就在增长。
tar 有139个选项,因为人们用它做备份、迁移、分发、验证完整性、处理稀疏文件、保持ACL权限、支持各种压缩算法和远程连接。这些需求不是1979年的Ken Thompson能够预见的。
ps 有85个选项,因为进程管理在容器、cgroup、namespace的时代变得无比复杂。
git 有接近200个子命令,因为分布式版本控制本身就是一个复杂的问题域,涉及合并策略、签名、工作流自动化、三方工具集成。
McIlroy说:“不要添加选项,找到为什么你需要这个选项的设计缺陷。“但有些时候,添加选项不是设计的失败,而是对不断扩展的问题域的诚实回应。
尾声
2023年,Brian Kernighan在一次采访中被问及Unix哲学。已经81岁的他说:“那些原则仍然有价值,但你不能教条地应用它们。世界变了。”
是的,世界变了。
1979年的Unix V7运行在64KB内存的机器上,服务几十个用户。今天的命令行工具运行在拥有数十GB内存的工作站上,连接着全球化的代码仓库和云基础设施。
Unix哲学的精髓——组合性、简单性、可预测性——仍然值得追求。但形式需要与时俱进。也许未来的"通用接口"不是文本流,而是结构化数据。也许未来的"做一件事"不是"一个功能”,而是"一个连贯的工作流”。
至于那些膨胀的手册页和三位数的选项数量?它们是人类与机器妥协的记录,是理想在现实重力下的轨迹。
Doug McIlroy的心可能会继续下沉。但我们这些使用这些工具的人,在 git rebase -i 的复杂选项中,在 kubectl 的层层子命令里,找到的是另一种秩序——不那么纯粹,但足够有用。
也许这就是计算机科学的真相:没有永恒的优雅,只有不断演进的有用。
参考资料
- McIlroy, M. D., Pinson, E. N., & Tague, B. A. (1978). Unix Time-Sharing System: Foreword. Bell System Technical Journal.
- Salus, P. H. (1994). A Quarter Century of UNIX. Addison-Wesley.
- Kernighan, B. W., & Pike, R. (1984). Program Design in the UNIX Environment.
- Luu, D. (2017). The growth of command line options, 1979-Present. danluu.com.
- Raymond, E. S. (2003). The Art of Unix Programming. Addison-Wesley.
- Prasad, A., Firshman, B., Tashian, C., & Parish, E. (2020). Command Line Interface Guidelines. clig.dev.
- Gancarz, M. (1994). The UNIX Philosophy. Digital Press.
- Gabriel, R. P. (1991). Worse is Better. Lisp: Good News, Bad News, How to Win Big.