ICMAKE 第 2 部分
Icmake 源文件根据明确定义的语法编写,该语法与 C 编程语言的语法非常相似。这不是巧合。由于 C 编程语言在 Unix 操作系统中如此重要,我们假设许多使用 Unix 操作系统的人都熟悉这种语言。提供一种基于这种熟悉的编程语言的新工具,可以减轻所有人学习另一种方言的负担,从而简化新系统的使用,并让其新用户专注于其可能性,而不是其语法形式。
考虑到 icmake 的特定功能,我们将许多熟悉的 C 结构融入到 icmake 中:大多数 C 运算符都在 icmake 中实现,一些标准的 C 运行时函数也是如此。在这方面,icmake 的语法是 C 编程语言的子集。但是,我们自由地定义了两种通常在 C 中找不到的数据类型。有一种数据类型“string”(是的,它的变量包含字符串)和一种数据类型“list”,包含字符串列表。我们认为对 C 编程语言的这些扩展非常小,只需这一段话就可能足以对其进行定义。但是,在接下来的章节中将更详细地描述它们。此外,在 icmake 的语法中还可以找到 C++ 的一些元素:一些 icmake 函数已被重载;它们根据调用时参数的类型执行不同但可比较的任务。同样,我们认为这与“纯 C”语法略有不同,并认为这种做法非常符合 C++ 的理念。
预处理器的一项任务是去除 makefile 中的注释。Icmake 识别两种类型的注释:标准 C 风格的注释和行尾注释,Gnu C 编译器和 Microsoft 的 C 编译器也识别行尾注释。
标准注释必须以 /* 开头,并以 */ 结尾。这种类型的注释可以跨越多行。行尾注释以 // 开头,并在新行开始时结束。
预处理器会跳过以 #! 开头的行。包含此功能是为了允许使用可执行 makefile。除了 #! 指令外,icmake 还识别另外两个预处理器指令:#include 和 #define。所有预处理器指令都以“#”字符开头,该字符必须位于 makefile 中行的第一列。
#include 指令必须遵守以下语法
#include "filename"
或
#include <filename>
当预处理器 icm-pp 遇到此指令时,将读取“filename”。文件名可以包含路径规范。当文件名用双引号括起来时,icm-pp 会尝试完全按照所述方式访问此文件。当文件名用 < 和 > 括起来时,icm-pp 会尝试相对于环境变量 IM 指向的目录访问此文件。使用 #include 指令,可以将大型 icmake 脚本模块化,或者可以使用一组标准的 icmake 源脚本来实现特定的 icmake 脚本。
#define 指令是在 makefile 中包含常量的一种方法。该指令遵循以下语法
#define identifier redefinition-of-identifier
定义的名称(已定义常量的名称)必须是符合 C 编程语言的标识符:第一个字符必须是下划线或字母表中的字符;后续字符可以是下划线或字母数字。
#define 指令的重定义部分由空格、数字或任何适当的内容组成。预处理器只是将 #define 指令之后定义常量的所有出现替换为重定义部分。请注意,重定义不会进一步展开;重定义部分中已定义的名称不会被处理,而是保持原样。
另请注意,icm-pp 认为重定义部分是行中超出定义常量的所有字符。如果行中找到注释,则也包括注释。因此,通常不建议在包含 #define 指令的行上使用行尾注释。
常量可以在 makefile 中使用,以指示数字或字符串。Int 常量用数字字符表示;例如,13 是一个 int 常量。表示 int 常量的第二种方法是用单引号括起字符。常量的数值然后是字符的 ascii 码,例如,常量“A”的值为 65。引号之间的字符不能是“转义”字符,例如“\n”。在这种整数常量表示法中,只允许使用单个字符。
字符串常量用双引号标记之间的文本表示,例如,“a string”是一段文本。
Icmake 识别四种类型:int、string、list 和 void。这些类型用于以下目的
int:类型“int”用于表示数值型的 16 位有符号值。
string:类型“string”用于表示字符串,就像 C 中使用的字符串一样。
list:类型“list”用于由字符串列表组成的变量和函数的返回值。没有 list 常量。相反,列表始终必须在运行时构建。
void:类型“void”仅与函数一起使用,以指示这些函数不返回值。
类型 int、string 和 list 也用于定义变量和参数。Icmake 允许全局变量和局部变量。变量或参数的声明必须声明变量的类型;计数器变量将是 int 类型,而包含扩展名为“.c”的所有文件名的变量将是 list 类型。
icmake 的一些内置函数(请参阅有关 icmake 函数的部分)返回 int、string 或 list 类型的值。返回的值可以分配给相同类型的变量,也可以传递给另一个函数。
与内置函数类似,用户定义的函数被假定为返回 int、string 或 list 类型的值。int 类型是默认类型。函数可以定义为不返回值。此类函数具有“void”返回类型。
变量的定义遵循类似 C 的语法。参数的定义与 ansi-C 中相同。以下列表显示了类型的用法示例。请注意常量 55 和 “main.c”(字符串常量)的用法。
string myfun (int x, string y, list z) // a user-defined function { // of type string, having 3 int // parameters counter, // local variables: 2 ints, i; // 1 string and 1 list string name; list cfiles; counter = 55; // counter is set to 55 name = "main.c"; // name is set to string main.c return (name); // a string is returned to the } // caller
makefile 中的字符串用于表示文件名和显示的文本。Icmake 允许在字符串中使用许多特殊格式序列,以方便文本的显示。与 C 编程语言类似,这些序列称为转义序列。Icmake 识别以下转义序列:转义序列 操作
\a alert (bell) \b backspace character \f formfeed character \n newline \r carriage return character tab \v vertical tab \-other- literal -other-, e.g., \\
字符串中的转义序列由反斜杠字符 \ 后跟标识转义序列的字符标识。与 C 类似,Icmake 允许字符串连接。可以通过用空格字符(空格、制表符、换行符)分隔字符串常量来构建跨越多行文本的长字符串。
Icmake 识别六个控制语句
if 语句,包括 if-else
while 语句
for 语句
return 语句
break 语句
exit 语句
exit() 语句虽然在 C 中是一个函数,但却是 icmake 语言的一部分。exit 语句可以给定一个产生 int 值的表达式。如果后面跟着 int 表达式,则其值将作为 int 值返回给操作系统。否则,返回值未定义。其他流程控制语句与 C 编程语言中对应的语句类似。
Icmake 允许在 makefile 中构造用户定义函数。函数的定义必须遵循类似 ansi-C 的语法,但是,icmake 函数和 C 函数之间存在细微差别。本节重点介绍这些差异。
函数的定义必须遵循以下语法
可以选择指定函数的返回类型。类型为 void、int、string 或 list。默认返回类型为 int。
当函数使用 return 语句显式返回时,返回的值必须与返回类型匹配。如果函数未使用 returns 语句,则返回未定义的值。定义为 void 的函数也可以使用 return 语句,但不能带有表达式。
在可选的返回类型之后,必须跟函数名称。名称必须是标识符,即,第一个字符必须是下划线或字母表中的字符,可选的后续字符可以是下划线或字母数字。
在函数名称之后,需要一个 (。
可以跟一个参数列表,其中包含以 , 分隔的参数规范(这称为 ansi-C 参数列表)。参数规范由参数类型(int、string 或 list)和参数名称(标识符)组成。
与 C 相比,icmake 不允许用户定义的函数具有可变数量的参数。
在可选的参数列表之后,需要一个 )。
接下来,需要函数的代码:用 { 和 } 括起来的语句。
在代码块的第一个 { 之后,可以定义局部变量。局部变量的定义包括变量类型、一个或多个以逗号分隔的变量名称和一个分号。
与 C 相比,局部变量只能在函数代码块的外部花括号之后立即定义。变量不能在语句块中定义。
与 C 相比,icmake 将所有局部变量初始化为零。
Icmake 不允许前向引用。这意味着函数只能在定义后才能调用。接受递归函数调用。此外,调用函数的语句必须提供所需的确切参数数量,并且每个参数类型必须与函数的参数列表匹配。内置函数是预定义的,因此可以在函数中的任何位置使用。
makefile 的代码部分必须至少包含一个用户定义的函数,称为 main()。makefile 的执行从此函数开始。icmake 的运行时支持系统提供了函数 main() 可以使用的三个参数。这些参数用于保存 icmake 调用的命令行参数和环境设置。
这三个参数通常称为 argc、argv 和 envp。Argc 是一个 int 参数,保存命令行参数的数量。Argv 是一个列表,保存命令行参数本身。Envp 是一个列表,保存环境设置。下面给出了使用所有参数 argc、argv 和 envp 的 main() 函数的定义
int main(int argc, list argv, list envp) { // statement(s) }
当不需要检查命令行参数时,用户可能希望定义不带参数的 main() 函数。在这种情况下,main() 函数可以定义为
int main { //statements(s) }
也可以定义 main() 函数以仅使用第一个或前两个参数(argc 和 argv)。下面给出了一个示例 makefile,它打印其命令行参数。本例中使用的函数 printf() 和 element() 在下面的函数部分中讨论
传递给 main() 函数作为列表 argv 的参数是
void main (int argc, list argv) { int i; for (i = 0; i < argc; i++) printf ("Argument ", i, " is ", element (i, argv), "\n"); }
由 icm-exec 解释的二进制 makefile 的名称。这始终是第一个参数。
其余参数是在命令行上显式提供的那些参数。
例如,要向名为 try.im 的 makefile 提供参数 one、two 和 three,可以使用以下调用之一
icmake test - one two three or: icmake -i test.im one two three
在这两种情况下,函数 main() 的第一个 int 参数的值都将为 4。列表 argv 的第一个元素保存二进制 makefile 的名称 (test.bim);argv 的其余元素保存参数 one、two 和 three。
main() 的第三个参数 envp 是一个列表,其中包含环境的设置(环境变量)。此类变量的一个示例是 PATH,它指定操作系统在何处搜索可执行文件。envp 列表由元素对组成,其中每对的第一个元素保存变量名称(例如,字符串 PATH),而每对的第二个元素保存变量的值(例如,可能找到可执行文件的目录列表)。
下面给出了一个打印环境变量设置的 makefile 示例
void main (int argc, list argv, list envp) { int i; for (i = 0; i < sizeof (envp); i += 2) printf ("variable ", element (i, envp), " has value ", element (i + 1, envp)); }
Icmake 允许使用大量运算符来形成或组合表达式。二元运算符可以与以下操作数类型一起使用
每个二元运算符必须与两个相同类型的变量或常量一起使用,例如,不允许 int 和字符串相加;icmake 不执行默认类型转换。
某些运算符可能不能与某些类型一起使用,例如,不允许字符串减法,但允许字符串加法。
运算符具有一定的优先级;某些运算符在其他运算符之前求值。运算符的优先级与 C 使用的优先级相同。
下表总结了 icmake 识别的二元运算符
除赋值运算符外,所有二元运算符都是左结合的。赋值运算符是右结合的。表中顶部的运算符优先级最低;底部的运算符优先级最高。优先级不同的运算符用线条分隔。
下表总结了一元运算符。一元运算符的优先级高于二元运算符,并且是右结合的。例外是表达式嵌套运算符,它包围一个表达式,但不关联。
Icmake 识别三个逻辑运算符:逻辑与 (&&)、逻辑或 (||) 和逻辑非 (!)。这些运算符可用于组合或反转逻辑表达式。
逻辑非运算符反转表达式的逻辑结果。逻辑与运算符和逻辑或运算符对条件进行分组。Icmake 使用这些运算符评估组合条件,直到确定条件的结果为止,这与 C 类似
在条件 (c1 && c2) 中,如果 c1 产生零,则不评估 c2,因为当 c1 产生零时,组合条件只能失败。因此,仅当 c1 产生非零值时才评估 c2。
在条件 (c1 || c2) 中,如果 c1 产生非零值,则不评估 c2,因为当 c1 产生非零值时,组合条件只能成功。因此,仅当 c1 产生零时才评估 c2。
逻辑运算符可以与任何类型的表达式一起使用。int 常量或变量产生其整数表示形式。当字符串的长度为非零时,字符串常量或变量产生非零值;例如,字符串“a”产生非零值。
当列表中的字符串数量不为零时,列表或变量产生非零值,例如,在以下代码片段中,当找不到扩展名为“.c”的文件时,制作过程将停止
待续...
请在下一期《Linux Journal》中查找 IC Make 文章的第 3 部分(共 4 部分)。