第1 章引子
1.1 为什么要制作编程语言
本书的主题是自制编程语言。单说现在被广泛使用的编程语言,就有C、C++、Java、C#、Perl、Python、Ruby、PHP、Lisp、JavaScript 等。可能有人会质疑,既然已经有这么多语言了,真的有必要再特意创造一门新的语言吗?实际上,自制编程语言还是大有益处的。
1. 可以帮助理解编程语言的内部运行机制编程语言是程序员每天都要使用的工具。深刻地理解这个工具,对程序员来说非常重要。
一般来说,重新编写一个与已有程序相似的程序会被说成是“重复发明轮子”,这在行业内是不被认同的。但本书中想要实现的,偏偏是在众多语言存在的前提下再制作一门新的语言,正是“重复发明轮子”。这是深刻理解编程语言的最佳途径(缺点是要花很多时间)。
2. 能制作领域专用语言
比如在Unix 的世界中,有sed 和awk 两种历史悠久的专为文本处理定制的语言(后来在此方向上发展出了Perl 语言)。PHP 则是专门面向Web 程序开发的语言。如果掌握了制作编程语言的技术,就可以在必要的情况下制作出领域专用语言(DSL,Domain-Specific.Language)。
领域专用语言不一定会像Perl 与PHP 那么复杂,在很多情况下,如果能书写条件分支或者简单语句的话会方便许多,这也可以看作是一种专用领域。比如在业务流程处理等软件中,很多时候为了切换测试环境与生产环境的数据库,需要重写配置文件,而这一操作经常会引发问题(比如由于版本升级需要增加配置文件项目,此时必须与旧版本配置文件合并)。这时候我们可能就会想,如果能直接在配置文件中写 if 语句将其按域名分开就好了。
除此以外,我们在填写数据时可能希望能支持类似Excel 的简单算术公式,在玩游戏时希望能把游戏中的对话导出到一个外部文件中,等等。这些都可以看作专用领域并制作对应的DSL。
3. 可以用编程语言扩展应用程序
将以上两方面的考量进一步延伸,我们就会得到以通用语言扩展某个应用程序的构想。Emacs 这个编辑器就内置了Emacs.Lisp 这种Lisp 方言,从而为Emacs 的自定义提供了无限的可能性。同理,Microsoft.Office 也可以使用VBA进行扩展。
对于这类应用程序扩展语言,当然完全可以使用某种已有的编程语言(Lua等就在向这个方向发展),也可以在编写应用程序时从底层到扩展全部自己实现。这样就无需担心使用其他编程语言在版本升级时引起的兼容性问题了。
4. 说不定还会变成名人
如果自制的编程语言能在世界范围内得到广泛使用,那就太棒了。比如Ruby之父松本行弘先生就是世界名人。
不过坦白讲,通过自制编程语言来获得成功实在是太难了。即便语言被创造出来,如果没人用的话就不会产生相应的软件,这样就更不会有人用了。况且,即便真的因为发明了新的语言而变成了名人,通过这个赚到钱的希望也十分渺茫啊。其实我自己最近写的语法处理器都是免费发布的(不这样的话,语言没法普及呀)。
5. 自制编程语言非常有趣
啰嗦了这么多,说到底其实是因为自制编程语言非常有趣。自制一门编程语言确实是一件非常有意思的事。有人说过“想写出终极程序的程序员,最终都去写操作系统或者编程语言了”,你可以通过自制编程语言感受到接触最核心技术的乐趣。
让尽可能多的人感受到这种乐趣,这正是本书的目标。
1.2 自制编程语言并不是很难
一提起自制编程语言,很多人都会觉得这是一件非常难的事情。
. 比如,即便是一个很常见的赋值语句:
a1 = b1+ b2*0.5;
在自制编程语言时都必须考虑到以下几个要点。
1. 需要将 a1、 b1、 b2作为变量名解析出来。如果按照C 语言的语法规则,变量名只能由字母或下划线开头,从变量名第二个字符开始才允许出现字母或数字。所以首先必须扫描这个语句,然后将匹配上述语法规则的部分提取出来。
2. 0.5 是一个含有小数点的常量,在提取这类常量时,能否用“数字组合+ 小数点+ 数字组合”来概括所有常量的特征呢(还要考虑是否允许00.10 这样的数值)。当然我们的提取规则还要能处理2 这样不含小数点的数值。
3. 乘法运算符 *比 +拥有更高的运算优先级,语句必须被解析为 b1 + (b2 * 0.5)。
4. b2 * 0.5 的计算结果,必须在与 b1 进行加法运算前就应该取得。也就是说对于复杂的计算,需要保存很多类似这样的临时运算结果。
假如你已经有了一定的编程经验,肯定能想到上面这些难点,甚至可以说你的编程经验越丰富,就越能感受到这其中隐藏着极大的难题。
不过,编程语言的语法处理器在FORTRAN 诞生后已经经过了多年的研究,上面的这些难点都已经可以从前人那里找到解决方法*。
在本书中,上面1 ~ 3 的问题会用到名为yacc 及lex 的工具。问题1 和问题2用lex,问题3 通过yacc 解决。yacc 和lex 都是非常老的工具了,现在流行的LL语言大多内置了yacc。可能有人会说:“既然是以学习为目的去制作一门编程语言,如果还使用工具的话就太投机取巧了吧。”(这话很有道理。)所以在本书中,也会稍微介绍一下不使用这些工具的解决方法。
无论是使用工具,还是基于一些已有的解决方案自己编写,如果能掌握一些窍门的话,自制编程语言其实并不难。
那么你想不想试试自己制作一门编程语言呢?自己创造编程语言这件事情,不管怎么说都是很酷的吧。
*当然,在早年原始的研发条件下,人们为了开发第一个编程语言编译器还是花费了相当大的精力,据说实现初版的FORTRAN 编译器所花费的工时,累计达到了216 人月[1]。
1.3 本书的构成与面向读者
本书由以下的章节构成:
第1 章引子
第2 章试做一个计算器
第3~4 章制作无类型语言crowbar
第5 章中文支持与Unicode
第6~8 章制作静态类型的语言Diksam
第9 章应用篇
第1 章即是你正在阅读的章节。本章会对全书的构成以及讲解方式进行说明。
第2章通过制作一个简单的计算器,介绍yacc/lex 的基本使用方法。其实讲解yacc/lex 的部分,选择“计算器”为例实在有点老套,但确实没有比这更合适的题目了。此外还会介绍如何不依赖yacc,使用递归下降分析器(Recursive.Descent.Parser)来制作一个计算器。
从第3 章开始,会实际制作有一定行数规模的编程语言。
3 ~ 4 章会制作一个名为crowbar 的无类型解释型语言,6 ~ 8 章则主要制作名为Diksam 的支持静态类型的编译型语言(名字的由来会在后文提到)。在第5.章中,会针对使用编程语言时的中文支持与Unicode 问题进行说明。
第9 章阐释闭包(Closure)及异常处理机制等进阶功能。
本书中会使用C 语言作为编程语言语法处理器(编译器、解释器等)来编写语言(理由见后文中的具体说明)。而crowbar 与Diksam 最终都会累积为具备一定行数规模的程序(crowbar 约8000 行,Diksam 约2 万行)。
因此,阅读本书的读者最好具备两个条件:
1. 已经会C 语言
2. 具备阅读较长代码的能力
不过无论哪个条件都不是必须的。
对于 条件1 需要说一点的是,Java、C++、C# 等都是从C 语言发展出来的语言,所以对于已经学习过这些语言的人来说,读C 语言代码不会特别吃力。像预处理程序、指针等C 语言特有的知识,建议你借此机会一并学习一下。因为至少就现阶段来说,无论是专家还是业余爱好者,但凡是程序员都免不了要用到C 语言。而在crowbar 或Diksam 中,并没有使用很多C 语言特有的功能。比如说不会出现 *p++这种不易理解的写法,更多是写成数组下标的形式。
对于 条件2 要说的是,虽然一个语法处理器整体来看是个上规模的程序,但是其基础构成的部分并不会很庞大。本书不会对每一行代码逐一进行注释,而是侧重于介绍解决问题的思路,所以如果仅仅是想阅读一下本书的话,是不需要具备阅读较长代码的经验的。但若你最后不满足于书中的讲解,还想要自己去阅读一下crowbar 或者Diksam 源代码的话,因为代码行数很多,编程经验尚浅的朋友读起来可能会有压力。不过无论是业界还是外界人士,作为程序员总有一天会接触到大规模代码的程序,将本次实践作为入门的第一步也不是一件坏事。
综上所述:
如果你觉得自己不是本书所面向的读者,想办法加入其中不就行了?所以无需担心什么,门槛其实没有你想的那么高。凡是对语法处理器有兴趣的朋友都是本书面向的读者。
1.4 用什么语言来制作
如前文所述,本书将使用C 语言作为语法处理器的编写语言。
都什么年代了还用C 语言?可能会有人这样想吧。其实就连我自己也会这样想。
但本书还是使用了C 语言,其中一个理由是因为yacc/lex 都是面向C 语言的工具。
yacc/lex 本身是很老的工具。老工具虽然都有一些历史遗留问题,但也有其优点,即正是因为历史悠久,所以会积累下更详尽的技术文档。如前文所述,目前的LL 语言大多使用yacc。
另一个使用C 语言的理由是:想要降低“依赖程度”的话,C 语言是最适合的。
比如说用Java 编写软件,运行环境中必须安装JVM(Java 虚拟机)。如果用C# 则必须要安装.NET.Framework。在自制编程语言的理由中,我们曾经列举了“可以用编程语言扩展应用程序”这一条,并且提到,如果能在编写应用程序的时候从底层到扩展全部自己实现会更加放心,其目的就是为了不依赖JVM或.NET.Framework。这样在Java 或.NET 版本升级时也就无需操心了。
此外考虑到组合各种应用程序这个用途,C 语言在众多编程语言中可以说是最具通用性的。无论被组合的应用程序采用何种语言编写,毫无疑问都可以调用C 语言。
1.5 要制作怎样的语言
1.5.1 要设计怎样的语法
编程语言有很多种,C、C++、Java、C# 等都是 面向过程的编程语言( C++、Java、C# 虽然也被称为面向对象,但可以把面向对象看作是面向过程的一个派生)。目前看来,虽然面向过程的语言是主流,但还存在Haskell、ML 这样的 函数式编程语言 。函数式编程语言就是“变量值无法被更改”的一种语言*。
对于已经习惯了面向过程语言的人来说,肯定会想“变量值无法更改还怎么写程序呀”。其实这类语言已经编写出了很多实用的程序。在函数式编程的基础上发展出了如Prolog 这样的 逻辑编程语言 以及被称为 并行程序设计语言 的Erlang。
不过目前被广泛使用的仍然是面向过程的编程语言,本书中的代码示例使用的也都是面向过程的语言风格,当然里面还会加入面向对象的一些功能实现。在本书中,除了会有C++、Java、C# 这种基于类的面向对象之外,也会涵盖类似JavaScript 这种没有类的面向对象。
语法层面上,会使用类似C 语言的风格。crowbar 的示例代码如代码清单1-1所示,Diksam 的示例代码如代码清单1-2 所示。
代码清单1-1crowbar 版FizzBuzz
代码清单1-2Diksam 版FizzBuzz
*从这个定义来说,Lisp严格讲还不能算是函数式编程语言。
顺便说一下这个名为FizzBuzz 的小程序,其运行机制如下:
输出从1到100的数字,如果为3的倍数时,则将数字替换为Fizz,5的倍数时则输出Buzz,同时为3与5的倍数时输出FizzBuzz。
这个小程序引自下面的文章。文章大意是建议企业在面试程序员时,至少应聘者能写出这种程度的代码再考虑录用。
◎为什么自称程序员的人写不出程序?http://www.aoky.net/articles/jeff_atwood/why_cant_programmers_program.htm
看了示例就能明白,无论crowbar 还是Diksam,都是与C 语言非常类似的语言。
如上所述,本书虽然会创造一门新语言但仍然会用到C 语言,所以本书所面向的读者应该是已经掌握了C 语言的(还没有掌握的人可以先去学习一下)。因此如果选择C 语言风格的语法,读者应该会感到很亲切,更重要的是笔者本人已经习惯了Java、C# 这种以C 语言为基础的编程语言。
C 语言是很老的语言了,这门语言不是在前期经过严谨的设计,而是在项目中一边实践一边慢慢发展起来的,因此语法上难免有很多考虑不周的地方。比如在C 语言中赋值使用 =,即数学中的等号。而C 程序员在初学者阶段编写 if 语句时,肯定免不了会写成这样:
if (a = 0) { ← 应该写"==" 但是写成了"="… }
这样惨痛的教训至少也要经历一次吧。赋值在Pascal 等语言中,一般使用 :=。如果让一个没有编程经验的人来学习,Pascal 这种语法应该更加友好一些。
不过我现在是要制作一门新的编程语言,而使用这门新语言的人应该都已经习惯了C 语言的运算符,如果这里将赋值运算符定为 := 的话反而会引起混乱,说不定我自己就先头晕了。所以经验之谈是,语法上的些许优劣还是要给“习惯”让步的。
——出于这种考虑,我最终决定制作一门与C 语言类似的编程语言。
决定语法风格是编程语言创造者的特权。如果顾虑用户习惯,可以参考并整合已有的编程语言。当然,也可以完全不考虑用户的感受,去创造一门“理想的语言”。虽然我是以C 语言的语法为基础,但还是想到了以下几点可以改进的地方。
1. if条件在C语言中,如果按条件执行的语句只有一句,则 {}可以省略。但是这经常会造成混乱,很多项目的编码规范中都会规定必须包含 {}。因此最好在语法层面直接将 {}设置为不可省略(crowbar、Diksam均如此)。
2. 既然已经将 if 条件中的 {}设置为不可省略,那么 if后面的 ()要怎么办呢?(关于这一点,我起初在crowbar中尝试了一下省略 if的括号,结果发现在crowbar中 ()是不可省略的。)
3. 伴随着语言的逐步完善,考虑到要增加一些关键字(参考2.3.1节的补充知识),此时再处理与已存在程序的变量名相冲突的问题就比较麻烦,所以考虑在所有的变量前加上 $(Perl或PHP等的解决方式),或者将关键字全部以大写字母开头(Modula-2等的解决方式)。
4. switch case 语句中,最好能去掉忘了写 break就会进入下一个 case这种容易产生问题的设计(Java没有改进这一点,C#则做了一些半吊子的改进)。
5. switch case语句中,如果没有进入任何一个 case条件分支,也没有写 default分支,那么在运行时直接报错会不会更好一些(Pascal就是这样处理的)?
6. 编码规范通过缩进来约束怎么样?比如像Python那样通过缩进来表明逻辑结构。
7. 对于我来说,阅读Python风格的代码还有些吃力,因此是不是做成像C语言那样用花括号包裹语法块、把强制缩进的检查交给编译器去做比较好呢?
我希望读者朋友们也能够用好语言开发者的特权,不断去追求“更加理想的语言”。呃,虽然我这样讲可能会被说成是站着说话不腰疼吧。
1.5.2 要设计怎样的运行方式
程序员中应该无人不知,编程语言有编译型语言和解释型语言两种。
编译型语言中,C 和C++ 比较有代表性。这类语言通常会将程序员编写的程序源代码,最终输出为机器码的可执行文件。
但是想要输出机器码的话,必须首先掌握机器码才行。即便学习了机器码并写出了编译器,该编译器也无法输出供其他型号CPU 运行的文件*。
这类生成机器码的编程语言的优点是运行速度非常快,但是编译器性能优化的相关技术,学习起来非常有难度。另外,在自制编程语言的理由中曾经列举了“可以用编程语言扩展应用程序”这一点,而输出机器码的编译器并不适合这个用途。因此本书中会选择解释型语言。
虽说“解释型语言”只是一个词,但是其实现方法又分很多种。
解释型语言的“解释”一词源自英语的 interpreter ,是“能进行翻译的物体”的意思。编译器将源代码翻译为机器码,之后CPU 直接运行机器码就可以了。与此相对的解释型语言,则将程序员编写的源代码通过解释器这一程序一边解析一边运行——这种公式化的定义看起来只有简单的两个步骤,但现实中几乎不存在这么单纯的解释型语言(DOS 的批处理脚本或UNIX 的SHELL 脚本是最接近解释型语言的定义的)。虽说名为“解释型语言”,但其中的大多数都会将源代码临时转换为某种中间形态。比如有代码清单1-3 这样的代码。
代码清单1-3简单的if语句
*为了解决这个问题,一般的编译器都会将依赖CPU 生成的机器码的部分单独归为一个名为Backend 的模块,根据不同的CPU 可以更换相应的Backend,就可以支持其他型号的CPU 了。
从机器的角度看,源代码其实只是一些文字的排列组合而已,机器是无法直接运行的。现在大多数编程语言,都会将代码转换成一种叫分析树(parse.tree,也叫语法分析树或语法树)的东西。上面的代码如果做成分析树,则如图1-1 所示。
图1-1分析树示例
Perl*、Ruby 等语言,一旦将代码转换为分析树后,分析树将无法再还原回源代码。
本书第2 章以后所用到的语言crowbar 就是采用这种运行方式的语言。
对于这类语言来说,从源代码到分析树的构建过程还是得称为“编译”。但是这里的编译器是在程序启动时自动执行的。由于分析树会生成在内存里,因此不会生成目标代码或目标文件,所以程序员(用户)一般意识不到有编译器在执行。这类语言如果存在语法错误,会在刚开始运行时就被报出来,这正是源代码被一次性全部读入并构建分析树的证明。如果是纯粹的解释型语言,如批处理脚本或SHELL 脚本,则会运行到有语法错误的地方才会报错。
那么,相对于Perl、Ruby 这样的运行分析树型语言,在Java 等语言中,取代分析树的则是更底层的字节码,然后通过解释器运行字节码。字节码只是一些简单的数字排列,为了尽可能地让人读懂字节码,字节码中的所有指令都被加上了一些名为助记符(mnemonic)的字符,代码清单1-3 的源代码经过这样一番处理之后最终会变成代码清单1-4 的样子(源代码中的 printf 改为 System.out.println,并使用javap 输出)。
代码清单1-4Java 的字节码
本书第5 章以后所用到的语言Diksam,就是采用这种运行方式的语言。
在Java 中,编译器生成的字节码会被保存在class 文件中。但是在Diksam中,编译器会在程序启动时执行,因此字节码保存于内存中,不会生成类似class文件的东西。由此可以看出,从用户的角度出发,不需要意识到Diksam 内部其实有字节码在执行。Python 也是使用了类似的处理机制。
*Perl 6 还不知道什么时候出来,就不管它了:-)
补充知识 “用户”指的是谁?
前文曾写道“因此程序员(用户)一般意识不到有编译器在执行。”通常来说,用户是指使用程序员编写的程序的人,但是在这里,因为我们是要制作一门编程语言,所以本书中的用户应该是指使用我们制作的编程语言的人,即程序员。
这种指代在操作系统、类库、编程语言等面向程序员的文档中经常出现,不过可能有读者会有误解,在此特别补充说明一下。
补充知识 解释器并不会进行翻译
在很多入门书中,提到编译器与解释器时,一般会采用以下说明:
编译器会将源代码一次性全部翻译为机器码。
与此相对的解释器,不会事先做一次性翻译,而是在运行的同时,逐行分块地将源代码翻译为机器码。
请允许我说句老实话,这样的说明是完全错误的。
解释器会将源码或分析树解析为字节码这种中间形态,并且一边解析一边运行,但是解释器并不会将源码翻译为机器码。
Java 或.NET Framework 都具备在运行的同时将字节码转换为机器码的功能,这叫作“JIT(Just-In-Time)编译”技术,而这部分技术并不属于解释器。
那么解释器具体是如何运行程序的呢?读到后面你就会明白了。
1.6 环境搭建
1.6.1 搭建开发环境
本书的开发语言是C 语言,辅助工具是yacc 和lex。
UNIX(包含Linux 等)大部分都已经预装了开发所需的yacc 和lex,当然也有例外,而Windows 则默认没有预装。不过无需担心这些,我们完全可以全部使用自由软件来搭建一个可用的开发环境。
那么,下面我们就开始介绍这些软件的获取途径。
1. C 编译器
免费的C 编译器可以使用GNU 项目提供的GCC(GNU.Compiler.Collection)。
Linux 等免费的UNIX 环境下大多都预装了GCC*。Windows 下可以使用MinGW(Minimalist.GNU.for.Windows)。
可以从下面的URL 下载。
http://www.mingw.org/download.shtml
安装MinGW 时,UNIX 环境下的程序会将构建(build)时使用到的make 工具也一并安装。不过,安装完毕后可执行文件名有点奇怪,是 mingw32-make.exe,我将其复制并重命名为 gmake.exe以方便使用。
2. cygwin 或MSYS
cygwin 是可以运行在Windows 上的类UNIX 环境。比如说想在命令行提示符中列出当前文件夹内的文件时,Windows(DOS)会使用 DIR 指令,UNIX 则使用 ls指令。一般用惯了UNIX 的人,往往会在Windows 的命令行提示符中不自觉地敲出 ls却尴尬地发现指令不存在,而安装了cygwin 就可以避免这样的情况发生。那么对于不经常使用UNIX 的人还有必要装cygwin 吗?因为在后文中提到的bison 要使用UNIX 中的m4 工具,所以无论是cygwin 还是MSYS,至少还是要安装其中一个的*。MSYS 与cygwin 都是在Windows 上模拟UNIX 环境的软件。
cygwin 可以从下面的网址中获取:
http://cygwin.com/
MSYS 可从MinGW 页面中下载:
http://www.mingw.org/download.shtml
此外, 因为cygwin 也包含GCC,可以没有MinGW 而通过cygwin 安装GCC。但是使用cygwin 安装的GCC 编译,运行时需要依赖cygwin1.dll 文件,在其他机器运行还需要把DLL 也复制过去,所以还是使用MinGW 更方便。
3. bison
如果环境无法直接运行yacc,可以使用GNU 项目提供的bison。
http://gnuwin32.sourceforge.net/packages/bison.htm
*最近Linux 不预装GCC的情况似乎越来越多了。
*m4 其实也可以单独安装,但似乎没有独立的安装包,可能会非常麻烦。
4. flex
同理,如果环境无法直接运行lex,可以使用lex 的免费版flex。
http:/ /gnuwin32.sourceforge.net/packages/flex.htm
补充知识关于bison与flex的安装
bison由GNU项目提供。GNU项目是由理查德?斯托曼(Richard Matthew Stallman)创立的项目,目标在于建立一个完全相容于UNIX的自由软件环境。GNU项目提供的软件的许可证为GPL(通用公共许可协议,General Public License)。粗略地说,GPL是这样一种许可证:
发行GPL的程序时,必须公开源代码并且声明源代码的出处;
包含GPL源代码的程序,必须受GPL许可证条款约束;
程序即使以动态链接方式使用GPL程序,也必须受GPL许可证条款约束。不过这个限制在LGPL许可证(LesserGPL)中有所放宽。
也就是说,你的程序中只要用到GPL的程序,哪怕这部分再小,你的程序也会自动变成GPL程序,必须与源代码同时公开。这对于那些为了防止盗版而不得不采取一些措施的商用软件来说简直是致命的。因此也有人戏称GPL的这个特性是“GPL传染”或“GPL病毒”。
那么bison是否也是如此呢?后文会有说明,bison的作用是将用户编写的配置文件输出为C语言格式的代码。这里的C代码中会包含一些属于bison的代码。那么是不是说使用bison去制作编程语言,所做出的编程语言在发行上也必须遵守GPL许可证呢?关于bison输出的C代码这一点,是GPL的一个特例,可以不受GPL许可证约束。此处在GNU项目有关GPL的FAQ页面中有如下的记载:
碰巧的是,Bison也可以用于开发非自由软件。这是因为我们明确允许在Bison的输出结果中包含的Bison的标准解析程序可以不受限制。我们做此决定,是因为已经存在与Bison类似的工具被用于非自由软件的开发。
http://www.gnu.org/licenses/gpl-faq.ja.html
另一方面,flex则是遵循BSD许可证(Berkeley Software Distribution,加州大学伯克利分校开发的软件套件集合)的(不是修订版BSD)。BSD许可证的程序再次发行时,文档中必须要附加BSD的版权信息。
flex会像bison一样输出C代码,这里的C代码也像bison一样,会包含一些属于flex的代码。但是这部分代码并不需要附加BSD的版权信息。因为flex-2.5.34携带的COPYING文件中有这样的描述:
Note that the“ flex.skl” scanner skeleton carries no copyright notice. You are free to do whatever you please with scanners generated using flex; for them, you are not even bound by the above copyright.
1.6.2本书涉及的源代码以及编译器
本书所涉及的源代码,可以在作者的网站上下载:
http://avnpc.com/pages/devlang#download
在开始撰写本书之前,crowbar和Diksam就已经存在一些公开的版本了,本书所用到的代码都对其进行了重新的整理和修正,因此本书相关的代码将重新以book_ver作为版本号。比如本书最开始制作的crowbar的版本号就是crowbar.book_ver.0.1。