前言:欢迎回来,旅行者

为什么是这本书?

你不是新手。你是一位先驱。当计算走向个人化时,你就在现场。但如今的景象已截然不同。“代码山”和“Bug谷”还在老地方,但攀登的路径已经面目-非。这本书不是为初学者准备的;它是为那些掌握了“过时”专业知识的专家们编写的。它是一本翻译手册,而非入门教科书。

你的第一个程序,重温

我们从 $10 PRINT "HELLO WORLD!"$ 开始。这行代码曾是一个时代的入口,它承载着关于“可访问性”的深远文化意义。 它在Python中的现代等价物几乎完全相同(print("hello, world!")),但从前者到后者的历程,本身就是一场革命。

你的任务

跳越50年的技术鸿沟。我们将探讨为什么行号消失了,为什么 $GOTO$ 曾被视为“毒瘤”,以及我们最终是如何演进到开始对计算机“说话”来让它为我们编程的。

第一部分:你所熟知的世界(及其幽灵)

第一章:行号,一项伟大的发明(严肃脸)

1.1 引言:放下你的偏见,我们是认真的

让我们用一个在21世纪听起来最具“异端邪说”的观点来开始这本手册:行号(Line Numbers),是计算机历史上最伟大的发明之一。
是的,你没看错。我们是认真的。

对于一位刚刚从“技术时空冷冻舱”里苏醒的“恐龙级”程序员来说,你环顾四周,看到如今的代码编辑器里竟然没有行号(默认情况下),你一定感觉自己像是在公共场合裸奔。这太不文明了!你怎么知道你要GOTO哪里?你怎么LIST一个范围?你怎么知道你的?SYNTAX ERROR发生在哪条物理“战线”上?

而对于在座的现代程序员——那些喝着燕麦拿铁、用着VS Code、认为“命令行”就是那个彩色的iTerm窗口的年轻人——“行号”这个概念,就像是代码的“阑尾”。它是一种原始的、毫无意义的、甚至有点可笑的身体残留物。你们可能会问:“为什么不直接用标签(labels)呢?为什么不直接用函数名呢?为什么你需要一个数字?”

你们的困惑是可以理解的。但你们错了。

在本章中,我们将为你(我们尊敬的旅行者)辩护。这本书就是你的辩护律师。我们将庄严地证明,行号绝不是一个原始或愚蠢的选择。它是一个针对一系列早已消失的、极其可怕的物理和逻辑限制的 “天才解决方案” 1

你(过去)的知识是合乎逻辑的。在那个时代的硬件和软件限制下,你是绝对正确的。为了证明这一点,我们必须先带你离开这个“所见即所得”(WYSIWYG)的舒适圈,欢迎“旅行者”回到那个“所见非所得”(WYSI-NOT-WYG)的年代。

不,说实话,那个年代连“所见”都没有。欢迎来到“所听即所得”的时代。

1.2 问题一:匮乏的交互界面——欢迎来到电传打字机(TTY)地狱

首先,请你忘掉你那块安静的、4K分辨率的、能显示200行代码的IPS显示器。忘掉你那符合人体工程学、敲击起来如同“小雨”般轻柔的机械键盘。
你的新“终端”,是一台电传打字机(Teletype, TTY) 2

这玩意儿不是电子的。它是机电的(electromechanical) 2。它的本质是一台被诅咒的打字机和一台小型坦克的结合体。在1960年代和70年代,这就是你与大型机(如达特茅斯的DTSS系统 3)或早期微型机(如MITS Altair)交互的方式 4

编程,在那个年代,不是一种安静的脑力劳动。它是一种残酷的、全方位的感官折磨。

听觉折磨:
当你按下回车键时,这台机器不会“显示”结果。它会用一系列金属撞针,以工业革命的力度,把字符 “砸” 进一张纸里。你的同事不是在“敲代码”,他们是在“锻造”代码。整个房间里充斥着如同小型兵工厂般的“哒-哒-哒-哐-哐-嚓-”的巨响 5。如果你在隔壁房间,你可能会以为这里正在进行一场小型局部战争,或者至少是在用重机枪扫射什么东西。

嗅觉折磨:
由于这是一台机电设备,它需要润滑。整个房间(或者你那可怜的书房)弥漫着滚烫的机油、浓重的油墨、以及连续卷纸高速摩擦时产生的刺鼻焦糊味。这是一种混合了重工业、廉价印刷厂和即将过热的变压器的味道。

触觉折磨:
当你的TTY开始打印时,整个桌子——乃至你脚下的地板——都在与你共振。你能通过你的指尖感觉到每一个字母的诞生。

蜗牛的速度:
你以为你的网速很慢吗?你的连接速度可能是 110 波特(Baud) 6
澄清一下,这不是 110K/s,也不是 110M/s。这是 110 比特每秒。大约每秒10个字符 5。你的5G手机(假设900 Mbps)比这快……大约 90 万倍。你的代码不是“刷新”在屏幕上,它是由一个喝醉了的机器人,一个字母一个字母地,慢慢“砸”在一条无限延伸的、通常是廉价的黄色卷纸上 5。LIST一个100行的程序?去给自己泡杯咖啡吧。不,去煮一整壶。

“代码之河”:线性编程的物理现实
现在,我们来谈谈核心问题。现代编程是在一个 “空间” (Spatial)中进行的。你有一个文件,你可以“向上滚动”去看你之前写的东西,或者“跳到定义”去查看一个函数。

但在TTY时代,编程是 “线性”(Linear)和“时间性” (Temporal)的 7
你的代码不是一个“文档”,它是一条从机器里流淌出来的“纸河”。

你键入LIST。机器“哐-哐-哐”地把你的代码打印出来。这些代码现在在哪?它们不在“屏幕上”,它们在你的打字机旁边,堆积成一堆凌乱的纸山 8。你看不到“上面”的代码,因为它已经被卷走了,物理上消失在你的视野之外,静静地躺在地板上。

在这个范式下,“向上滚动”这个概念是荒谬的。你不能“向上滚动”一条物理的纸带。你能做的,只是要求机器 “重新打印” 那部分历史 8

所以,问题来了:你如何告诉机器,你要引用这条“纸河”中你已经看不见的、位于上游的某个特定位置?

你需要一个地址。你需要一个标记。你需要一个在代码的时间流中恒定不变的锚点。
你需要行号。

1.3 “战争故事”:EDLIN 的恐怖,或“我为什么需要行号”

你可能会反驳:“好吧,TTY是地狱,我承认。但我们很快就有了CRT屏幕,为什么行号还存在?”
问得好。因为我们虽然有了屏幕,但我们没有足够的内存。

为什么我们没有像VS Code那样的“全屏编辑器”(Full-Screen Editor)?
有两个“最低公共分母”的原因:

  1. RAM 限制: 你的Apple II或Commodore 64可能总共只有 4KB 到 32KB 的内存 4。在一个只有32KB内存的系统里,“全屏编辑器”这个概念本身就是一种奢侈的幻想。一个全屏编辑器需要一个巨大的“缓冲区”(Buffer)来存储屏幕上所有文字的状态,这会吃掉你本可用来写代码的宝贵内存 4
  2. TTY 遗产: TTY的交互模式被完美地继承了下来。你的“IDE”不是VS Code,它是一个在TTY(或CRT模拟的TTY)上运行的 “行编辑器”(Line Editor) 1。在Unix上,它叫 ed 9。在MS-DOS上,它叫 edlin 10

哦,edlin。让我们花点时间,为那些从未经历过这场“反人类设计”巅峰之作的年轻灵魂们默哀。
edlin 是如何“工作”的?让我们引用一位幸存者的血泪回忆录 11,重现一下当时的噩梦:

你雄心勃勃地要在你的IBM PC上写一个程序。你键入 EDLIN MYPROG.BAS
屏幕上显示12

New file
*

没有菜单。没有光标。没有“文件”或“编辑”按钮。只有一个星号 *,在无尽的虚无中闪烁,仿佛在嘲笑你的存在。

“好吧,”你心想,“我总得开始输入代码。” 你记得要按 i 键,代表“插入”(Insert)12

*i
1:*_

好的,提示符变了!你开始打字:

1:*10 PRNIT "HELLO"

你突然发现你把 PRINT 错打成了 PRNIT。你的肌肉记忆让你猛按“上箭头键”。
没反应。
那个时代没有“上箭头键”这个概念。你试着按“退格键”。哦,退格键可以删掉一个字母。你删掉了 T、I、N、R、P... 但你发现你不能退回到第10行!你不能“编辑”你已经提交的行!11

你唯一的选择是按 Ctrl-C 放弃当前行的所有输入,或者(在某些系统上)按下回车,接受这个错误,然后祈祷你待会儿能修复它。

你硬着头皮继续写:

1:*10 PRNIT "HELLO"
2:*20 GOTO 10
3:*_

你写完了。你按 . 键(或者 Ctrl-Z)退出“插入模式”13
屏幕又回到了那个冰冷的 * 提示符。

现在,你如何修复第 10 行那个该死的 PRNIT?
你不能“去”第 10 行。你不能用鼠标点击。你必须在 * 提示符下,输入你想编辑的行号:

*10

计算机会把那一行原封不动地打印给你看,仿佛在说:“这就是你的杰作,看吧”:

10:*10 PRNIT "HELLO"
10:*_

它在第二行给了你一个新的 10:* 提示符,它的意思是:“请你把这行代码……完整地……重新输入一遍。” 11
你叹了口气,重新输入:

10:*10 PRINT "HELLO"

你按了回车。* 提示符回来了。你拯救了世界。
你受够了。你想退出。你输入 q,代表“退出”(Quit)。
它会问你:Really quit (Y/n)? 14
这是 edlin 整个交互过程中唯一(也是第一次)展露出人性化的时刻——它似乎自己都不敢相信,你居然能忍受它这么久。

正如那位幸存者 11 所总结的:“如果你觉得在网页文本框里打字很难,试试用行编辑器吧。”

现在你明白了?

解决方案(一):用数字导航“代码之河”
在 edlin 这个“所见非所得”的地狱里,行号是唯一的、绝对的引用机制 1
你无法移动光标。你只能发出基于数字的命令 1

  • LIST 100-200 (在震耳欲聋的打印声中,查看你那一百行代码的“尸检报告”)
  • DELETE 50 (一个无法撤销的、基于数字的谋杀)
  • 80 (编辑第80行,也就是“请让我把第80行重新输一遍”)11

既然你的编程环境是线性的、非空间的,行号就成了你认知上的唯一“锚点”。那个时代的程序员被迫在大脑中记住他们的代码结构 8。他们会像这样对话:“见鬼,又崩溃了。我猜问题出在1000行左右的那个主循环里。” LIST 900-1100

行号不是为了电脑。它是为了人。它是在一个看不见的、流动的文本世界中,唯一能让你抓住的“把手”。

1.4 解决方案(二):“直接模式”与“程序模式”的优雅分界

行号的第二个天才之处,在于它解决了一个哲学问题:我是在“聊天”,还是在“写作”?
想象一下80年代最激动人心的开机画面:你打开你的 Commodore 64 或 Apple II。没有启动动画,没有登录界面,没有桌面。你的电脑在0.5秒内启动完毕,屏幕上只有一个光标和两个词15

READY.
_

这个 READY. 提示符意味着什么?在 MS-DOS 成为主流之前,BASIC 解释器就是你的操作系统 16
你没有 C:\> 提示符。你只有 READY.。你用它来 LOAD 程序、SAVE 程序,以及……编程 15

这意味着,这个可怜的解释器必须同时扮演两个截然不同的角色:

  1. 一个 “命令行壳层”(Shell) ,它必须立即执行你给它的命令(比如 LOAD "GAME"17
  2. 一个 “编程环境”(IDE) ,它必须“暂停执行”你给它的代码,把它们先存储起来,以备将来运行。

那么,这个解释器是如何区分“立即执行的命令”和“需要存储的代码”的呢?
答案就是:行号。 这是一个天才的、零开销的区分法 1

场景A:“直接模式”(Direct Mode) 15
你坐在 READY. 提示符前,想算个数。你输入:

PRINT 2+2

(注意:没有行号)
你按下回车。计算机会立即回答:

4
READY.
_

在这种模式下,它是一个计算器。它在“立即执行”(Immediate Execution)18。你和它在“聊天”。

场景B:“程序模式”(Program Mode) 15
现在,你想“写程序”。你输入同样的东西,但这次在前面加上一个数字:

10 PRINT 2+2

(注意:有行号)
你按下回车。计算机会回答:

(一片沉默)
READY.
_

它什么也没做……吗?不,它做了件很聪明的事:它把 10 PRINT 2+2 这行指令,作为“程序”的第一行,存入了内存 19

在这种模式下,它是一个程序员(或者说,秘书)。它在“延迟执行”(Deferred Execution)18。你和它在“写作”。
当你写完所有带行号的指令后,你再回到“直接模式”,输入那个不带行号的命令:

RUN

然后,电脑才会去内存里,从最小的行号(10)开始,按顺序执行你刚才“存”进去的所有代码。

看到了吗?行号就是你和解释器之间的暗号。一个简单的整数,就足以在“立即给我答案”和“别着急,我还没写完,先把这个存起来”这两种完全不同的上下文之间进行切换 20。这是一种极其优雅的、零开销的机制。

1.5 问题二 & 解决方案(三):GOTO 和 GOSUB 的强制“锚点”

行号的第三个(也是最臭名昭著的)存在理由,是它解决了第二个大问题:早期 BASIC 语言自身匮乏的控制结构 1

我们必须承认,早期的 BASIC 是一种“简陋”的语言。它缺乏现代程序员认为理所当然的几乎所有东西,比如 WHILE 循环、CASE 语句,甚至连像样的 IF-THEN-ELSE 都没有(你通常只有一个 IF-THEN,而且 THEN 后面只能跟一个动作)。

那么,在没有 WHILE 的情况下,你怎么实现一个循环?
你使用 GOTO。
这里有一个教科书般的“意大利面条式代码”(Spaghetti Code)的例子,它用来打印1到100的数字 21

10 I=0
20 I=I+1
30 PRINT I
40 IF I<100 THEN GOTO 20
50 END

看到第40行了吗?GOTO 20。这就是你的“循环”。

GOTO(跳转)和 GOSUB(调用子程序)是当时实现任何非线性流程的唯一工具 22。你程序的所有逻辑,都是通过这些“跳转”编织(或者说,缠绕)在一起的。
而这些命令的本质是:“跳转到某个地方”。
这个“某个地方”在哪?它总不能是 GOTO "那个打印I的循环" 吧?解释器可没那么智能。它需要一个绝对的、毫不含糊的地址。

最终解决方案(三):
行号,因此成为了这些跳转指令强制要求的“目标锚点” 1
GOTO 20 不是一个建议,它是一个绝对的、物理的内存地址引用(通过行号这个标签实现的)。没有行号,GOTO 就像一个没有地址的信封——它毫无意义,也无法投递。

当然,这也催生了80年代最伟大的文化现象之一。当年的孩子们学会的第一件事,就是在周六早上去逛电脑商店(比如Tandy或Dixons),然后在他们的展示机上飞快地键入如下“战争故事”中的代码 23

10 PRINT "TANDY IS CRAP, SHOP AT DIXONS ";
20 GOTO 10

然后按 RUN,并迅速逃离现场 23。这台可怜的电脑会开始在屏幕上(或者用TTY砸出)无限循环的“TANDY IS CRAP, SHOP AT DIXONS ;TANDY IS CRAP...”,直到某个愤怒的经理过来拔掉电源。
看,GOTO 和行号,是那个时代的文化和技术基石。

1.6 “10进制”的智慧:为“补丁”预留空间

现在,我们进入了行号“智慧”的核心。一个敏锐的观察者(比如你,旅行者)会立刻发现一个规律:为什么所有人都心照不宣地使用 10, 20, 30...? 1 为什么不是 1, 2, 3...?
答案是:这绝不是为了好看或某种神秘的十进制崇拜。这是出于一种残酷的物理需求 1
这是在为未来不可避免的愚蠢错误,预留 “补丁空间”(Patch Space) 1

让我们回到 edlin 那个噩梦。你不能简单地在两行之间“按回车”来插入新行。在内存中,BASIC的程序行是通过一个“链表”存储的(每行代码的开头都有几个字节,指向下一行代码的内存地址)24。但你,作为用户,只能通过行号来访问它们。

想象一下,如果你是个“天真”的程序员,你这样写代码:

1 PRINT "A"
2 PRINT "C"

RUN

你突然一拍脑袋:“哎呀!我忘了打印 "B"!”
现在,你该怎么办?
你完蛋了。你“破产”了。

在行号 1 和行号 2 之间,不存在任何“空间”让你插入新的代码行 25。你唯一的选择是,把第2行重新输入为第3行(3 PRINT "C"),然后再(希望你能想起来)DELETE 2,最后再输入一个新行 2 PRINT "B"。如果你的程序有100行,你就得手动把99行代码全部重新输入一遍,只为了在开头插入一行。

“补丁空间”的实践:
这就是“10进制”智慧的由来。有经验的程序员(比如你)会这样做:

10 PRINT "A"
20 PRINT "C"

当你发现忘了 "B" 时,你长舒一口气。因为你这个天才,早就(在10和20之间)为自己预留了9个“补丁位”。
你自信地键入:26

15 PRINT "OOPS, I MEAN B"

你再 LIST 一下,它神奇地出现在了第 10 行和第 20 行之间 27。你拯救了世界,而不需要重新输入第10行之后的所有代码。

这种“以10为步长”的约定,就是你给自己买的“代码保险”。

“战争故事”:人肉 I/O 总线与杂志抄码文化
这种对“补丁空间”的需求,在80年代的“杂志抄码”文化中达到了顶峰。
在那个没有互联网的年代,我们如何“下载”软件?我们从《BYTE》28、《Compute!》29 或《Ahoy!》30 这样的电脑杂志上,手动键入(Type-in)长达几百上千行的 BASIC 代码 30

这就是80年代的“GitHub”。而你,亲爱的程序员,就是“人肉 I/O 总线”。
这是一种“乐趣”,也是一种“折磨” 31。想象一下,你13岁,花了一个周二的下午,瞪着杂志上密密麻麻的印刷代码,逐字逐句地把一个叫《青蛙过河》(FROGGER)的游戏输入你的 C64。你花了三个小时 32,眼睛都快瞎了。

你满怀期待地键入 RUN。
然后……

?SYNTAX ERROR IN 130

32

你的“下载”损坏了。
你绝望地 LIST 130,发现你把 PRINT CHR$(205.5); 错打成了 PRNIT CHR$(205.5);。你的人肉“数据传输”出现了丢包。

这种挫败感是如此普遍,以至于杂志社发明了专门的工具来防止你(和他们的客服热线)发疯。这些工具的名字很直白,比如 MLX(Machine Language eXtor,机器语言输入器)29 或 Typo(Type Your Program Once,一次输对)33

这些工具是如何工作的?它们是80年代的 md5sum。
它们是一些短小的 BASIC 程序(当然,你得先手动输入这个校验程序本身 34)。运行它之后,它会让你一行一行地输入杂志上那些长得像天书一样的机器码(通常是 DATA 语句里的一堆数字)30

在你输入每行代码后,MLX 或 Typo 会要求你再输入一个杂志上印在旁边的“校验和”(Checksum)33
MLX 会计算你刚才输入的那行代码的 Checksum。如果这个值和你从杂志上输入的 Checksum 不匹配,它会发出一声刺耳的蜂鸣,拒绝这一行,并逼你把这行代码重新输入一遍 33

MLX 和 Typo 就是为这个“人肉I/O总线”设计的、原始而又极其必要的数据完整性校验工具。它们的存在,再次证明了那个时代编程的物理性和脆弱性。

1.7 终极重构工具:伟大的 RENUMBER 命令

当然,你的“补丁空间”总有耗尽的一天。
经过几轮艰苦卓绝的调试,你的代码行号不可避免地走向了“熵增”。你的程序变成了这样:

10 PRINT "START"
11 REM FIX BUG #1
12 GOTO 100
12.1 REM OOPS, NEW BUG (是的有些 BASIC 支持小数行号)
12.2 GOTO 200
13...

你再次“破产”了。你用光了10和20之间的所有“补丁空间”,你无处可插 1
现在怎么办?把整个程序重写一遍?

不。一个真正的 BASIC 解释器,会为你提供终极武器:RENUMBER(或 REN)命令 27
你只需在那个 READY. 提示符下,键入:

RENUMBER

奇迹发生了。
你 LIST 一下,你那贫民窟一样混乱的 10, 11, 12, 12.1... 序列,被重新整理成了整洁、有序、拥有广阔“补丁空间”的 10, 20, 30, 40... 序列 35

但这不是真正的魔力所在。
真正的魔力在于:RENUMBER 自动修复了所有指向旧行号的 GOTO 和 GOSUB 语句 27
你那个 12 GOTO 100 可能变成了 30 GOTO 150。你那个 12.2 GOTO 200 可能变成了 50 GOTO 210

这意味着 RENUMBER 不是一个简单的“查找-替换”工具。它是一个解析器(Parser)。它必须通读你的整个程序,理解其语法,在内存中建立一个“旧行号 -> 新行号”的“映射表”(Jump Table),然后遍历你代码中所有的 GOTO、GOSUB、THEN、ON...GOTO 语句,把它们的目标地址全部重写为新地址。
RENUMBER 是你(可能)从未意识到的、第一个被捆绑在“操作系统”里的“抽象语法树(AST)”重构工具!

“战争故事”:Commodore 64 的惊天疏忽
RENUMBER 是如此重要,以至于它的缺席是毁灭性的。
这里有一个巨大的历史讽刺:史上最畅销的单一型号计算机,Commodore 64,其出厂自带的 BASIC V2 版本,竟然没有 RENUMBER 命令 36

Commodore 公司(据传闻)为了省下授权费,用了一个老掉牙的 BASIC 版本 37。后果就是,全世界几百万 C64 新用户(大多是孩子)在他们的“补丁空间”耗尽后,绝望地涌入BBS论坛和用户组,哀嚎:“我该如何重新编号我的程序?!” 37

当时的解决方案是什么?

  1. 手动地狱: 手动编辑第11行,把它改成第12行,再 DELETE 11... 37
  2. 杂志救场: 从《Compute!'s Gazette》这样的杂志上,(再一次!)手动输入一个第三方的 RENUMBER 工具(比如 MetaBASIC 37)。
  3. 硬件升级: 购买一个扩展卡(比如 Simons' BASIC 38),或者升级到后来的 Commodore 128,它的 BASIC 7.0 终于内置了这个功能 39

C64 的案例雄辩地证明了 REN 不是一个“小功能”,它是一个关键的、不可或缺的基础设施。

“战争故事”:那个诡异的 65535 Bug
当然,RENUMBER 也不总是你的朋友。作为骨灰级玩家,我们必须分享一些“秘辛”(Arcane Knowledge)。
在 Commodore Plus/4 上的 BASIC 3.5 中,RENUMBER 有一个臭名昭著的 bug 35

Bug 复现:
假设你很马虎,你写了 100 GOTO 5000
但你忘了写第 5000 行。你的程序里根本没有 5000 这一行。
(在运行 RUN 时,这会导致 ?UNDEF'D STATEMENT ERROR
但你没有 RUN,你先运行了 RENUMBER。

症状:
RENUMBER 命令开始工作。它重写了所有的行号。然后它读到 100 GOTO 5000。它去查找它的“跳转表”,想知道 5000 对应的新行号是什么。
它没找到。
于是,它“发疯”了。
当你 LIST 你的新程序时,你会发现那行代码变成了:

10 GOTO 65535

35

为什么是 65535?
这个 bug 就像一块数字琥珀,完美地向我们展示了那个时代的架构。
65535,就是 $FFFF,是一个 16 位无符号整数能表示的最大值 40

RENUMBER 解析器在它的“跳转表”中找不到行号 5000,它的错误处理逻辑(显然是错误的)就是默认将其设置为“能找到的最大值”,而不是像后来的 BASIC V7.0 那样(修复了这个bug)抛出一个 ?UNRESOLVED REFERENCE ERROR(未解决的引用错误)35
这个 65535 bug,完美地封存了那个时代的16位架构、有缺陷的错误处理逻辑,以及程序员(你)在试图修复一个问题时,却制造出一个更诡异问题的无尽挫败感。

1.8 结论:消失的不是行号,而是(谢天谢地)电传打字机

行号的消亡 1,不是因为它们“愚蠢”,也不是因为GOTO被Dijkstra(迪杰斯特拉)批倒批臭了(我们将在第三章详细讨论这个)。
行号的消亡,是因为它们所解决的那些可怕的问题,一个接一个地消失了。

真正杀死行号的是两样东西:

  1. 充足的 RAM 4
  2. 全屏编辑器(Full-Screen Editors)41 的诞生,比如 vi 和 Emacs 41

当你的编辑器从“行编辑器”edlin 进化到“全屏编辑器”vi 时,你与代码的交互方式发生了根本的范式转换 41

  • 你的交互从“基于引用的行编辑”("请删除第50行")11 变成了“基于光标的空间编辑”("把光标移到那儿,然后按 'dd' 删掉这行")41
  • 你不再需要 15 PRINT "OOPS" 来插入代码。你只需要把光标移到第10行和第11行之间,然后按 i 键(Insert)并开始输入。物理空间被屏幕上的虚拟空间取代了。
  • 你不再需要 GOTO 20 作为循环锚点,因为(很快)你就有了 WHILE...WENDFOR...NEXT
  • 你不再需要行号来区分“直接模式”和“程序模式”,因为你有了两个完全独立的东西:一个用于“写作”的 IDE 窗口,和一个用于“聊天”的命令行终端。

但在你奔向这个“无行号”的美丽新世界之前,你必须意识到,你并没有真正逃离。那个行编辑器的幽灵,至今仍潜伏在你的“命令行终端”里。
你每天都在使用它。它的名字叫 grep

你以为 grep 只是一个随机的、听起来很酷的黑客词汇吗?不。grep 这个名字,本身就是一块活化石。它是一个被逐字保留下来的 ed 编辑器命令,其字面意思是:

g/re/p

这是一个 ed 命令,意思是:“在Global(全局范围)内 / 搜索 Regular Expression(正则表达式)/ 然后 Print(打印)该行” 42
没错。当你在 TTY 地狱中盲目摸索时,g/re/p 是你用来在“代码之河”中查找特定文本的唯一(也是最强大)的咒语 42。伟大的 Ken Thompson 所做的 43,就是把这个最有用的行编辑器功能从 ed 中“解放”出来,把它变成了一个独立的、可以在整个 Unix 系统中使用的工具。
每当你输入 cat some_file.txt | grep "error" 时,你都在不知不觉中扮演了一个1970年代的程序员。你正在对一个线性文本流(cat 的输出)应用一个 ed 命令,以打印出你需要的行。

grep 是行编辑器哲学最成功、最不朽的遗产。它雄辩地证明了,即使交互界面进化了,但“在流动的文本中查找模式”这个核心问题是永恒的。
行号的使命结束了。它们是那个资源匮乏、交互受限、电传打字机轰鸣的时代的无名英雄。它们是 TTY 和行编辑器时代的“天才黑客”。

所以,旅行者,下次你看到 10 PRINT "HELLO" 时,不要嘲笑它。
为它脱帽致敬。
然后,为你再也不用忍受 edlin 而感谢你的幸运星。

现在,收起你的怀旧之情。在下一章,我们将探讨你那些同样“过时”的 PEEK、POKE 和汇编技能,是如何在21世纪的AI革命中……一字不差地“复活”的。


引用的著作


  1. Line number - Wikipedia, 访问时间为 十月 27, 2025, https://en.wikipedia.org/wiki/Line_number ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. Teleprinter - Wikipedia, 访问时间为 十月 27, 2025, https://en.wikipedia.org/wiki/Teleprinter ↩︎ ↩︎

  3. How the Dartmouth Time-Sharing System Made Computing (More) Accessible, 访问时间为 十月 27, 2025, https://www.dartmouth.edu/library/rauner/exhibits/sharing-the-computer.html ↩︎

  4. Ask HN: Why did BASIC use line numbers instead of a full screen ..., 访问时间为 十月 27, 2025, https://news.ycombinator.com/item?id=34465956 ↩︎ ↩︎ ↩︎ ↩︎

  5. BASIC Interpreter — 8bitworkshop documentation, 访问时间为 十月 27, 2025, https://8bitworkshop.com/blog/platforms/basic/ ↩︎ ↩︎ ↩︎

  6. A history of the tty – OSnews, 访问时间为 十月 27, 2025, https://www.osnews.com/story/138653/a-history-of-the-tty/ ↩︎

  7. The History of Modern Text Editors - Grio Blog, 访问时间为 十月 27, 2025, https://blog.grio.com/2020/07/the-history-of-modern-text-editors.html ↩︎

  8. unix - How did people use ed? - Retrocomputing Stack Exchange, 访问时间为 十月 27, 2025, https://retrocomputing.stackexchange.com/questions/5341/how-did-people-use-ed ↩︎ ↩︎ ↩︎

  9. A brief and incomplete history of modal text editors | Carlos Becker, 访问时间为 十月 27, 2025, https://carlosbecker.com/posts/ed/ ↩︎

  10. Edlin - Wikipedia, 访问时间为 十月 27, 2025, https://en.wikipedia.org/wiki/Edlin ↩︎

  11. I Remember EDLIN « Wamblog, 访问时间为 十月 27, 2025, https://www.wambooli.com/blog/?p=3314 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  12. How to edit text with Edlin - FreeDOS Books, 访问时间为 十月 27, 2025, https://freedos.org/books/get-started/18-using-edlin/ ↩︎ ↩︎

  13. How to get started with the ed text editor - Red Hat, 访问时间为 十月 27, 2025, https://www.redhat.com/en/blog/introduction-ed-editor ↩︎

  14. A look back: the Edlin editor - Technically We Write, 访问时间为 十月 27, 2025, https://technicallywewrite.com/2024/08/05/edlin ↩︎

  15. Direct Mode - C64-Wiki, 访问时间为 十月 27, 2025, https://www.c64-wiki.com/wiki/Direct_Mode ↩︎ ↩︎ ↩︎ ↩︎

  16. C64-Commands - C64-Wiki, 访问时间为 十月 27, 2025, https://www.c64-wiki.com/wiki/C64-Commands ↩︎

  17. An Afterlife User's Guide to the C64, 访问时间为 十月 27, 2025, https://c64os.com/c64os/afterlifeguide/ ↩︎

  18. APPLESOFT II - The ReActiveMicro Apple II Wiki, 访问时间为 十月 27, 2025, https://wiki.reactivemicro.com/images/f/f7/Applesoft_II_2019.pdf ↩︎ ↩︎

  19. BASIC interpreter - Wikipedia, 访问时间为 十月 27, 2025, https://en.wikipedia.org/wiki/BASIC_interpreter ↩︎

  20. A historical question: why does BASIC have line numbers in the first place? : r/c64 - Reddit, 访问时间为 十月 27, 2025, https://www.reddit.com/r/c64/comments/mdzp4k/a_historical_question_why_does_basic_have_line/ ↩︎

  21. Spaghetti Code - GeeksforGeeks, 访问时间为 十月 27, 2025, https://www.geeksforgeeks.org/software-engineering/spaghetti-code/ ↩︎

  22. Spaghetti Code Archives - Teklibri, 访问时间为 十月 27, 2025, https://www.teklibri.com/tag/spaghetti-code/ ↩︎

  23. Usborne 1980's BASIC computer programming books - Out of Pod ..., 访问时间为 十月 27, 2025, https://forums-archive.eveonline.com/topic/468694 ↩︎ ↩︎

  24. Why did BASIC use line numbers? - Software Engineering Stack Exchange, 访问时间为 十月 27, 2025, https://softwareengineering.stackexchange.com/questions/309767/why-did-basic-use-line-numbers ↩︎

  25. A Touch of Applesoft BASIC - Vintage Apple, 访问时间为 十月 27, 2025, https://vintageapple.org/apple_ii/pdf/Apple_II_A_Touch_of_Applesoft_BASIC_1986.pdf ↩︎

  26. Why did we bother with line numbers at all? [closed] - Stack Overflow, 访问时间为 十月 27, 2025, https://stackoverflow.com/questions/541421/why-did-we-bother-with-line-numbers-at-all ↩︎

  27. programming languages - Why BASIC had numbered lines? - Stack Overflow, 访问时间为 十月 27, 2025, https://stackoverflow.com/questions/2435488/why-basic-had-numbered-lines ↩︎ ↩︎ ↩︎

  28. Byte (magazine) - Wikipedia, 访问时间为 十月 27, 2025, https://en.wikipedia.org/wiki/Byte_(magazine) ↩︎

  29. Compute! - Wikipedia, 访问时间为 十月 27, 2025, https://en.wikipedia.org/wiki/Compute↩︎ ↩︎

  30. Type-in program - Wikipedia, 访问时间为 十月 27, 2025, https://en.wikipedia.org/wiki/Type-in_program ↩︎ ↩︎ ↩︎

  31. Typing hundreds of lines of computer code from magazines in the 80s, to play simple platform games : r/nostalgia - Reddit, 访问时间为 十月 27, 2025, https://www.reddit.com/r/nostalgia/comments/d9sv6e/typing_hundreds_of_lines_of_computer_code_from/ ↩︎

  32. Were Magazine Type-In Games Really That Bad? // Coding Again Like It's The 80s, 访问时间为 十月 27, 2025, https://www.youtube.com/watch?v=tFb89_gN2dE ↩︎ ↩︎

  33. Learn About Checksum Programs for Checking Type-In Programs (15-30 mins), 访问时间为 十月 27, 2025, https://atariprojects.org/2020/07/25/learn-about-checksum-programs-for-checking-type-in-programs-15-30-mins/ ↩︎ ↩︎ ↩︎

  34. Is there any hard data about type-in programs in the 80s? - Retrocomputing Stack Exchange, 访问时间为 十月 27, 2025, https://retrocomputing.stackexchange.com/questions/23566/is-there-any-hard-data-about-type-in-programs-in-the-80s ↩︎

  35. RENUMBER Command - Plus/4 Encyclopedia - Plus/4 World, 访问时间为 十月 27, 2025, http://plus4world.powweb.com/plus4encyclopedia/500054 ↩︎ ↩︎ ↩︎ ↩︎

  36. Commodore BASIC Renumbering & Program Representation - Retro Computing, 访问时间为 十月 27, 2025, https://retrocomputingforum.com/t/commodore-basic-renumbering-program-representation/1023 ↩︎

  37. RENUM in C64 Basic? : r/c64 - Reddit, 访问时间为 十月 27, 2025, https://www.reddit.com/r/c64/comments/a5cvz0/renum_in_c64_basic/ ↩︎ ↩︎ ↩︎ ↩︎

  38. RENUMBER - C64-Wiki, 访问时间为 十月 27, 2025, https://www.c64-wiki.com/wiki/RENUMBER ↩︎

  39. Commodore BASIC - Wikipedia, 访问时间为 十月 27, 2025, https://en.wikipedia.org/wiki/Commodore_BASIC ↩︎

  40. 65535, as $FFFF, is the largest value representable by a 16-bit unsigned integer. (This one was inferred from context as the original document only contained the value itself.) ↩︎

  41. Ch 3 -- Text Editing with vi and Emacs, 访问时间为 十月 27, 2025, https://cmd.inp.nsk.su/old/cmd2/manuals/unix/UNIX_Unleashed/ch03.htm ↩︎ ↩︎ ↩︎ ↩︎

  42. The UNIX grep command, 访问时间为 十月 27, 2025, https://earthsci.stanford.edu/computing/unix/editing/grep.php ↩︎ ↩︎

  43. Ken Thompson - Wikipedia, 访问时间为 十月 27, 2025, https://dbpedia.org/page/Grep (Note: The original text linked to dbpedia.org/page/Grep, which is about grep itself. I've adjusted it to a more appropriate Wikipedia link for Ken Thompson based on the context of "Great Ken Thompson所做的", assuming it refers to his role in creating it.) ↩︎