60行C代码实现一个shell

美是有目共睹的 。Unix之美 , 稍微体会 , 便能得到 。
1969年 , Unix初始 , 没有fork , 没有exec , 没有pipe , 没有 “一切皆文件”  , 但是那时它已经是Unix了 。它简单 , 可塑 。
Melvin Conway在1963年的论文中叙述fork思想时就解释说并行路径要用结果来交互 , 也就是在汇合的join点来同步结果 。这个同步点所得到的 , 就是一个并行进程的 输出  。
在此之外 , Unix还有另一个原则 , 就是 组合小程序!
Unix把一系列功能单一的小程序组合成一个复杂的逻辑 , 这个原则有以下优势:

  • 每一个小程序都很容易编写 。
  • 每一个小程序可以分别完成 。
  • 每一个小程序可以分别迭代修复 。
  • 多个小程序可以自由组合 。
这是典型的模块化思想 , 小到统筹佐餐烧饭 , 大到组成生命的嘌呤嘧啶 , 都不自觉地和这种模块化思想相契机 , 原来这就是真理 。 程序尽量小 , 只做一件事并且做好它 。
Unix程序在自身的逻辑之外对外暴露的只有输入和输出 。那么 用输出连接另一个程序输入 就是一种好的方法 。所谓Conway的join点对于Unix进程指的就是输出 。
对外暴露的越少 , 程序越内聚 。这是一种范式 , 类似RISC处理器也是抽象出仅有的load和store来和内存交互 。
简单来讲 , Unix程序通过输入和输出来彼此连接 。下面是一幅来自Wiki的图示:
60行C代码实现一个shell

文章插图
 
详见Pipeline (Unix):
https://en.wikipedia.org/wiki/Pipeline_(Unix)
Unix的另一个原则 , 即著名的 “一切皆文件!” 连接输出和输入的那个管道在Unix中被实现为Pipe , 显然 , 它也是文件 , 一个FIFO文件 。
说实话 , 协作几个小程序形成一个大逻辑的思想还是来自于Convey , 在Convey的论文里 , 他称为 协程 ,  Pile可以说是直接实现了 Convey协程 之间的交互 。有关这段历史 , 请看:
http://www.softpanorama.org/Scripting/Piporama/history.shtml
用Pipe连接作为输出和输入连接Unix进程可以做成什么事情呢?让我们去感受一个再熟悉不过的实例 , 即数学式子:
 
60行C代码实现一个shell

文章插图
 
 
我们把运算符加号 , 乘号 , 除号(暂不考虑括号 , 稍后解释为什么)这些看作是程序(事实上它们也真的是) , 那么类似数字3 , 5 , 7 , 6就是这些程序的输入了 , 这个式子最终需要一个输出 , 获得这个输出的过程如下:
  1. 数字3 , 5是加号程序的输入 , 3+5执行 , 它获得输出8.
  2. 第1步中的输出8连同数字7作为乘号程序的输入 , 8 × 7执行 , 获得输出56.
  3. 第2步中的输出56连同数字6作为除号的输入 , …
这个数学式子的求值过程和pipe连接的Unix程序组合获得最终结果的过程完全一致 。
如果你相信数学可以描述整个世界 , 那么Pipe连同Unix程序同样是描述这个世界的语言。
在数学领域 , 程序 就是所有的运算符 , 加号 , 减号 , 乘号 , 除号 , 乘方 , 开方 , 求和 , 积分 , 求导…它们无一例外 ,  只做一件事 。
在Unix看来也同样 。它做的事情和下面的应该差不多 , 而且更多:
60行C代码实现一个shell

文章插图
 
写出上面的式子中每一个数学运算符的程序并不困难 , 比如加号程序:
// plus.c#include <stdio.h>int main(int argc, char **argv){ int a, b; a = atoi(argv[1]); b = atoi(argv[2]); a = a + b; printf("%dn", a);}同样 , 我们可以写出除法 , 直到偏导的程序 。然后我们通过pipe就能将它们组合成任意的数学式子 。
现在谈谈Unix组合程序的具体写法 , 如果我们要化简薛定谔方程 , 我们应该如何用Unix命令写出与上述式子等价的组合程序命令行呢?我们无法像数学家手写那样随意使用括号 , 显然 , 计算机并不认识它 。我们能够使用的只有两个符号:


推荐阅读