一个数字的启示

打开终端,输入 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]

几乎所有命令的选项都在增长,而且几乎没有逆转的趋势。更值得注意的是,原本零选项的命令(如 mkdirmv)也开始积累选项。

为什么会这样?

第一个原因:文本流的阿喀琉斯之踵

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指出,这就是为什么命令要添加格式选项——因为用户不想记住 cutawk 在处理字段时的细微差异,也不想在每次管道操作时重新解析数据格式。

更糟糕的是,不同命令的文本输出格式互不兼容。wc -w 在某些系统上不能正确处理Unicode。有些命令输出制表符分隔,有些用空格,有些用固定宽度。用户被迫在脑中记住每个命令的怪癖,然后在管道的每个环节应用正确的解析逻辑。

讽刺的是,“文本是通用接口"这句话就像说"二进制是通用格式”——技术上正确,但毫无帮助。格式本身就有结构,而结构需要解析。

PowerShell选择了一条不同的路:传递结构化对象而不是文本。你不需要记住 cut -f4awk '{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惯例仍有差异,这导致了一些项目选择 clicktyper 这样的第三方库。

Rust社区推出了 clap,Go有 cobra,JavaScript有 oclifyargs。每个库都在试图平衡两个矛盾的目标:让开发者容易定义接口,同时让用户容易使用接口。

但讽刺的是,这些库的流行本身就在推动复杂度增长。以前写一个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 的层层子命令里,找到的是另一种秩序——不那么纯粹,但足够有用。

也许这就是计算机科学的真相:没有永恒的优雅,只有不断演进的有用。


参考资料

  1. McIlroy, M. D., Pinson, E. N., & Tague, B. A. (1978). Unix Time-Sharing System: Foreword. Bell System Technical Journal.
  2. Salus, P. H. (1994). A Quarter Century of UNIX. Addison-Wesley.
  3. Kernighan, B. W., & Pike, R. (1984). Program Design in the UNIX Environment.
  4. Luu, D. (2017). The growth of command line options, 1979-Present. danluu.com.
  5. Raymond, E. S. (2003). The Art of Unix Programming. Addison-Wesley.
  6. Prasad, A., Firshman, B., Tashian, C., & Parish, E. (2020). Command Line Interface Guidelines. clig.dev.
  7. Gancarz, M. (1994). The UNIX Philosophy. Digital Press.
  8. Gabriel, R. P. (1991). Worse is Better. Lisp: Good News, Bad News, How to Win Big.