ICMAKE 第 2 部分

作者:Frank B. Brokken
4. icmake 源文件的语法

Icmake 源文件根据明确定义的语法编写,该语法与 C 编程语言的语法非常相似。这不是巧合。由于 C 编程语言在 Unix 操作系统中如此重要,我们假设许多使用 Unix 操作系统的人都熟悉这种语言。提供一种基于这种熟悉的编程语言的新工具,可以减轻所有人学习另一种方言的负担,从而简化新系统的使用,并让其新用户专注于其可能性,而不是其语法形式。

考虑到 icmake 的特定功能,我们将许多熟悉的 C 结构融入到 icmake 中:大多数 C 运算符都在 icmake 中实现,一些标准的 C 运行时函数也是如此。在这方面,icmake 的语法是 C 编程语言的子集。但是,我们自由地定义了两种通常在 C 中找不到的数据类型。有一种数据类型“string”(是的,它的变量包含字符串)和一种数据类型“list”,包含字符串列表。我们认为对 C 编程语言的这些扩展非常小,只需这一段话就可能足以对其进行定义。但是,在接下来的章节中将更详细地描述它们。此外,在 icmake 的语法中还可以找到 C++ 的一些元素:一些 icmake 函数已被重载;它们根据调用时参数的类型执行不同但可比较的任务。同样,我们认为这与“纯 C”语法略有不同,并认为这种做法非常符合 C++ 的理念。

4.1. 注释和预处理器指令。

预处理器的一项任务是去除 makefile 中的注释。Icmake 识别两种类型的注释:标准 C 风格的注释和行尾注释,Gnu C 编译器和 Microsoft 的 C 编译器也识别行尾注释。

标准注释必须以 /* 开头,并以 */ 结尾。这种类型的注释可以跨越多行。行尾注释以 // 开头,并在新行开始时结束。

预处理器会跳过以 #! 开头的行。包含此功能是为了允许使用可执行 makefile。除了 #! 指令外,icmake 还识别另外两个预处理器指令:#include 和 #define。所有预处理器指令都以“#”字符开头,该字符必须位于 makefile 中行的第一列。

4.1.1 #include 指令。

#include 指令必须遵守以下语法

#include "filename"

#include <filename>

当预处理器 icm-pp 遇到此指令时,将读取“filename”。文件名可以包含路径规范。当文件名用双引号括起来时,icm-pp 会尝试完全按照所述方式访问此文件。当文件名用 < 和 > 括起来时,icm-pp 会尝试相对于环境变量 IM 指向的目录访问此文件。使用 #include 指令,可以将大型 icmake 脚本模块化,或者可以使用一组标准的 icmake 源脚本来实现特定的 icmake 脚本。

4.1.2. #define 指令。

#define 指令是在 makefile 中包含常量的一种方法。该指令遵循以下语法

#define identifier redefinition-of-identifier

定义的名称(已定义常量的名称)必须是符合 C 编程语言的标识符:第一个字符必须是下划线或字母表中的字符;后续字符可以是下划线或字母数字。

#define 指令的重定义部分由空格、数字或任何适当的内容组成。预处理器只是将 #define 指令之后定义常量的所有出现替换为重定义部分。请注意,重定义不会进一步展开;重定义部分中已定义的名称不会被处理,而是保持原样。

另请注意,icm-pp 认为重定义部分是行中超出定义常量的所有字符。如果行中找到注释,则也包括注释。因此,通常不建议在包含 #define 指令的行上使用行尾注释。

4.2. 常量、类型和变量

常量可以在 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
4.3. 字符串和转义序列

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 允许字符串连接。可以通过用空格字符(空格、制表符、换行符)分隔字符串常量来构建跨越多行文本的长字符串。

4.4. makefile 的代码

本节讨论 makefile 中可能出现的用户定义函数,并定义其他语法结构。

4.4.1. 流程控制语句

Icmake 识别六个控制语句

  • if 语句,包括 if-else

  • while 语句

  • for 语句

  • return 语句

  • break 语句

  • exit 语句

exit() 语句虽然在 C 中是一个函数,但却是 icmake 语言的一部分。exit 语句可以给定一个产生 int 值的表达式。如果后面跟着 int 表达式,则其值将作为 int 值返回给操作系统。否则,返回值未定义。其他流程控制语句与 C 编程语言中对应的语句类似。

4.4.2. 用户定义函数。

Icmake 允许在 makefile 中构造用户定义函数。函数的定义必须遵循类似 ansi-C 的语法,但是,icmake 函数和 C 函数之间存在细微差别。本节重点介绍这些差异。

函数的定义必须遵循以下语法

  1. 可以选择指定函数的返回类型。类型为 void、int、string 或 list。默认返回类型为 int。

    当函数使用 return 语句显式返回时,返回的值必须与返回类型匹配。如果函数未使用 returns 语句,则返回未定义的值。定义为 void 的函数也可以使用 return 语句,但不能带有表达式。

  2. 在可选的返回类型之后,必须跟函数名称。名称必须是标识符,即,第一个字符必须是下划线或字母表中的字符,可选的后续字符可以是下划线或字母数字。

  3. 在函数名称之后,需要一个 (。

  4. 可以跟一个参数列表,其中包含以 , 分隔的参数规范(这称为 ansi-C 参数列表)。参数规范由参数类型(int、string 或 list)和参数名称(标识符)组成。

    与 C 相比,icmake 不允许用户定义的函数具有可变数量的参数。

  5. 在可选的参数列表之后,需要一个 )。

  6. 接下来,需要函数的代码:用 { 和 } 括起来的语句。

  7. 在代码块的第一个 { 之后,可以定义局部变量。局部变量的定义包括变量类型、一个或多个以逗号分隔的变量名称和一个分号。

与 C 相比,局部变量只能在函数代码块的外部花括号之后立即定义。变量不能在语句块中定义。

与 C 相比,icmake 将所有局部变量初始化为零。

Icmake 不允许前向引用。这意味着函数只能在定义后才能调用。接受递归函数调用。此外,调用函数的语句必须提供所需的确切参数数量,并且每个参数类型必须与函数的参数列表匹配。内置函数是预定义的,因此可以在函数中的任何位置使用。

4.4.3. 用户定义函数 main()

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");
   }
  1. 由 icm-exec 解释的二进制 makefile 的名称。这始终是第一个参数。

  2. 其余参数是在命令行上显式提供的那些参数。

例如,要向名为 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));
        }
4.4.4. 表达式和运算符

Icmake 允许使用大量运算符来形成或组合表达式。二元运算符可以与以下操作数类型一起使用

操作数类型

  1. 每个二元运算符必须与两个相同类型的变量或常量一起使用,例如,不允许 int 和字符串相加;icmake 不执行默认类型转换。

  2. 某些运算符可能不能与某些类型一起使用,例如,不允许字符串减法,但允许字符串加法。

  3. 运算符具有一定的优先级;某些运算符在其他运算符之前求值。运算符的优先级与 C 使用的优先级相同。

下表总结了 icmake 识别的二元运算符

二元运算符

除赋值运算符外,所有二元运算符都是左结合的。赋值运算符是右结合的。表中顶部的运算符优先级最低;底部的运算符优先级最高。优先级不同的运算符用线条分隔。

下表总结了一元运算符。一元运算符的优先级高于二元运算符,并且是右结合的。例外是表达式嵌套运算符,它包围一个表达式,但不关联。

一元运算符

4.4.4.1. 逻辑运算符

Icmake 识别三个逻辑运算符:逻辑与 (&&)、逻辑或 (||) 和逻辑非 (!)。这些运算符可用于组合或反转逻辑表达式。

逻辑非运算符反转表达式的逻辑结果。逻辑与运算符和逻辑或运算符对条件进行分组。Icmake 使用这些运算符评估组合条件,直到确定条件的结果为止,这与 C 类似

  1. 在条件 (c1 && c2) 中,如果 c1 产生零,则不评估 c2,因为当 c1 产生零时,组合条件只能失败。因此,仅当 c1 产生非零值时才评估 c2。

  2. 在条件 (c1 || c2) 中,如果 c1 产生非零值,则不评估 c2,因为当 c1 产生非零值时,组合条件只能成功。因此,仅当 c1 产生零时才评估 c2。

逻辑运算符可以与任何类型的表达式一起使用。int 常量或变量产生其整数表示形式。当字符串的长度为非零时,字符串常量或变量产生非零值;例如,字符串“a”产生非零值。

当列表中的字符串数量不为零时,列表或变量产生非零值,例如,在以下代码片段中,当找不到扩展名为“.c”的文件时,制作过程将停止

列表

待续...

请在下一期《Linux Journal》中查找 IC Make 文章的第 3 部分(共 4 部分)。

加载 Disqus 评论