65.9K
CodeProject 正在变化。 阅读更多。
Home

将 AWK 嵌入 C 程序

starIconstarIconstarIconstarIconstarIcon

5.00/5 (14投票s)

2020 年 4 月 5 日

MIT

8分钟阅读

viewsIcon

21402

downloadIcon

301

一个 AWK 解释器被转换成一个 C 可调用库

引言

AWK 是一种极具影响力和声望的语言。最简短(也最有趣)的介绍,请查看蒙特利尔同胞 Julia Evans 的上述漫画。AWK 是由 Alfred Aho(著有 龙书)Peter Weinberger 和 Brian Kernighan(著有 K&R)创建的一种易于使用的脚本语言。有了如此显赫的“父母”,AWK 迅速成为最受欢迎的脚本语言之一也就不足为奇了。它还推广了联想数组等创新概念,远在它们成为主流之前。

AWK 最擅长处理需要“整理”才能放入数据库的数据。当然,你可以在将数据传递给数据库引擎之前先用 AWK 处理它,但如果你想使用像 SQLITE 这样的小型嵌入式数据库引擎,并且希望所有内容都在同一个程序中,那么将脚本语言嵌入到你的 C/C++ 程序中会更方便。我找不到一个可以嵌入 C 的 AWK 解释器,所以我决定自己做一个。本文将介绍由此产生的代码。对 AWK 有一些基本了解将有助于理解,但并非必需。如果你想重温 AWK 技能,可以查看这个 教程

示例用法

让我们从一个简短的示例开始,作为对该代码所能做什么的“执行摘要”。

流行的 Unix wc 程序可以计算文件中的行数、单词数和字节数。下面是使用我的 AWK 库的一个简化实现。

#include <awklib.h>

int main (int argc, char **argv)
{
  AWKINTERP* interp = awk_init (NULL);			//initialize an interpreter object
  awk_setprog (interp,							//set the AWK program
    "{wc += NF; bc += length($0)}\n"
    "END {print NR, wc, bc, ARGV[1]}");
  awk_compile (interp);							//compile the program
  if (argc > 1)
    awk_addarg (interp, argv[1]);				//add our argument as an interpreter argument
  awk_exec (interp);							//execute AWK code
  awk_end (interp);								//cleanup everything
}

该程序概述了使用 AWK 库需要遵循的基本步骤。

  • 首先,必须创建一个解释器对象。所有其他 API 函数都以解释器作为第一个参数。
  • 将程序传递给解释器。正如我们将看到的,有更多传递程序给解释器的方法,但这是最简单的一种。
  • 程序必须被“编译”。解释器实际上并没有编译它,但它会生成一个将在执行阶段使用的语法树。
  • 可以添加要由 AWK 程序处理的输入文件。在这种情况下,我们添加了 argv[1],这是 C 程序收到的第一个参数。
  • AWK 程序被执行。默认情况下,输出会发送到 stdout
  • 最后,解释器对象被删除。

设计决策

我必须回答的第一个问题是应该使用哪个版本的 AWK 代码。有许多 AWK 的变体:gawk、mawk 等。最终,我决定使用 Kernighan 博士的原始 onetrueawk。代码可以在 https://github.com/onetrueawk/awk 上找到,它给人一种置身于计算机科学博物馆的感觉。举个例子:一个测试文件似乎是一个 Unix 系统的 etc/passwd 文件。它看起来是这样的

/dev/rrp3:

17379	mel
16693	bwk	me
16116	ken	him	someone else
...
8895	dmr

假设 bwk 代表 Brian W. Kernighan,我让你猜 kendmr 分别代表谁 😊。为了增加复古的色彩,/dev/rrp3: 表示该文件位于 DEC RP03、RP04 或 RP06 上。RP06 在当时是巨大的磁盘,可存储 128MB 的数据。太棒了!

考虑到代码的历史质量,我本想将其保留为纯 C 项目。但不幸的是,这主要是由于错误处理问题而不可能实现。对于独立程序,出现错误时完全可以退出;嵌入式解释器没有这个选项。我的解决方案是将大部分代码包装在 try...catch 块中。这个将它保持为 C 产品(至少从外部看)的想法,是我没有将 API 组织成 C++ 对象的原因。必须完全重写的一部分是函数调用机制。

API 并不大;我试图将其保持在最低限度,并计划仅在真正需要时添加新函数。

API 描述

API 围绕着一个不透明的 AWKINTERP 对象构建,该对象代表 AWK 语言解释器。它的生命周期遵循一系列*不可逆*的状态转换:初始化、程序编译、程序执行和销毁。

解释器变量的访问通过 awksymb 结构进行。

struct awksymb {	
   const char *name;        //variable name
   const char *index;       //array index
   unsigned int flags;      //variable type flags
   double fval;             //numerical value
   char *sval;              //string value
};

相同的结构用于将参数传递给 AWK 可调用的 C 函数(参见 awk_setfunc API 调用)。

在 AWK 中,变量没有定义类型,或者更确切地说,它们都是字符串,当需要进行数值运算时,有时会被转换为数字。在 awksymb 结构中,flags 成员指示符号是string(在这种情况下 sval 成员有效)还是数字(值为 fval)。

数组在 AWK 中也很特别。如前所述,所有数组都是关联的,并由字符串“索引”。如果 flags 成员中设置了 AWK_ARRAY 标志,则该变量是一个数组,并且 index 成员表示数组索引。

以下是对每个 API 函数的简要描述。

awk_init

AWKINTERP* awk_init (const char **vars);

该函数初始化一个新的 AWK 解释器对象。它接受一个变量定义数组,其格式与独立 AWK 解释器的 -v 命令行参数相同。数组以 NULL string 结尾。

awk_setprog

int awk_setprog (AWKINTERP* pi, const char *prog);

设置解释器的程序文本。此函数对解释器只能调用一次。

awk_addprogfile

int awk_addprogfile (AWKINTERP* pi, const char *progfile);

将文件内容添加为 AWK 程序。其功能等同于独立解释器命令行上的 -f 开关。与 -f 开关一样,此函数可以重复调用以添加多个程序。

awk_compile

int awk_compile (AWKINTERP* pi);

编译使用 awk_setprogawk_addprogfile 函数指定的 AWK 语言程序。

awk_addarg

int awk_addarg (AWKINTERP* pi, const char *arg);

向解释器添加一个新参数。该参数可以是输入文件名,也可以是变量定义(如果其语法为 var=value)。参数可以在开始执行 AWK 程序之前的任何时间添加。

示例

    AWKINTERP *pi = awk_init (NULL);
    awk_setprog (pi, "{print pass+1 \"-\" NR, $0}");
    awk_compile (pi);
    awk_addarg (pi, "infile.txt");
    awk_addarg (pi, "pass=1");
    awk_addarg (pi, "infile.txt");

输出是(假设 infile.txt 有 25 行)

1 - 25
2 - 25

awk_exec

int awk_exec (AWKINTERP* pi);

执行已编译的程序。该函数返回 exit 语句返回的值,或者在发生错误时返回负错误代码。如果程序在没有 exit 语句的情况下终止,则返回值为 0。小的负值应视为保留用于错误情况。

示例

    AWKINTERP *pi = awk_init (NULL);
    awk_setprog (pi, "{print NR, $0}");
    awk_compile (pi);
    awk_addarg (pi, "infile.txt);
    awk_exec (pi);

awk_run

int awk_run (AWKINTERP* pi, const char *progfile);

此函数将 awk_setprogawk_compileawk_exec 函数的调用合并为一次调用。

如果程序在没有 exit 语句的情况下终止,则返回值为 0。否则,函数返回 exit 语句中指定的值。小的负值应视为保留用于错误情况。如果程序需要任何参数,可以在调用 awk_run 之前使用 awk_addarg 函数添加它们。

示例

    AWKINTERP *pi = awk_init (NULL);
    awk_addarg (pi, "infile.txt");
    awk_run (pi, "{print NR, $0}");

awk_end

void awk_end (AWKINTERP* pi);

释放解释器对象分配的所有内存。

awk_setinput

int awk_setinput (AWKINTERP* pi, const char *fname);

强制解释器从文件读取输入。默认情况下,解释器从 stdin 读取。此函数将输入重定向到另一个文件。

awk_infunc

使用用户定义的函数更改输入函数。

void awk_infunc (AWKINTERP* pi, inproc fn);

替换输入函数(默认为 getcfgetc)为用户定义的函数。inproc 函数与 getc 具有相同的签名。

typedef int (*inproc)();

它返回下一个字符或 EOF(如果没有更多字符)。

这是一个使用 AWK 处理内存中数据的示例。

    std::istrstream instr{
        "Record 1\n"
        "Record 2\n"
    };

    AWKINTERP *pi = awk_init (NULL);
    awk_setprog (pi, "{print NR, $0}");
    awk_compile (pi);
    awk_infunc (pi, []()->int {return instr.get (); });

awk_setoutput

int awk_setoutput (AWKINTERP* pi, const char *fname);

将解释器输出重定向到文件。默认情况下,解释器输出到 stdout。使用此函数,您可以将其重定向到另一个文件。

示例

    AWKINTERP *pi = awk_init (NULL);
    awk_setprog (pi, "BEGIN {print \"Output redirected\"}");
    awk_compile (pi);
    awk_setoutput (pi, "results.txt");
    awk_exec (pi);

awk_outfunc

void awk_outfunc (AWKINTERP* pi, outproc fn);

使用用户定义的函数更改输出函数。outproc 函数的签名是:

typedef int (*outproc)(const char *buf, size_t len);

示例

	std::ostringstream out;
    int strout (const char *buf, size_t sz)
    {
        out.write (buf, sz);
        return out.bad ()? - 1 : 1;
    }
...
    AWKINTERP *pi = awk_init (NULL);
    awk_setprog (pi, "BEGIN {print \"Output redirected\"}");
    awk_compile (pi);
    awk_outfunc (pi, strout);

awk_getvar

int awk_getvar (AWKINTERP *pi, awksymb* var);

检索 AWK 变量的值。

该函数成功时返回 1,否则返回负错误代码。

如果变量是数组且 index 成员为 NULL,则函数返回 AWK_ERR_ARRAY 错误代码。

对于 string 变量,设置 AWKSYMB_STR 标志,并且函数通过调用 malloc 分配所需的字符串内存。用户必须通过调用 free 来释放内存。

示例

	AWKINTERP *pi = awk_init (NULL);
    awksymb var{ "NR" };

    awk_setprog (pi, "{print NR, $0}\n");
    awk_compile (pi);
    awk_getvar (pi, &var);

awk_setvar

int awk_setvar (AWKINTERP *pi, awksymb* var);

更改 AWK 变量的值。该函数接受指向 awksymb 结构的指针,其中包含有关变量的信息。用户必须设置 awksymb 结构中的 flags 成员以指示哪些值是有效的(字符串或数值)。此外,对于数组成员,用户必须指定索引并设置 `AWKSYMB_ARR 标志。

如果变量不存在,则会创建它。

示例

	AWKINTERP *pi = awk_init (NULL);
    awksymb v{ "myvar", NULL, AWKSYMB_NUM, 25.0 };
    awk_setprog (pi, "{myvar++; print myvar}\n");
    awk_compile (interp);

    awk_compile (pi);
    awk_setvar (pi, &v);
    awk_exec (pi);  //output is "26"

awk_addfunc

向解释器添加一个用户定义的函数。

int awk_addfunc (AWKINTERP *pi, const char *name, awkfunc fn, int nargs);

参数

  • pi - 指向解释器对象的指针
  • name - 函数名称
  • fn - 指向 functype 的指针
  • nargs - 函数参数的数量

该函数成功时返回 1,否则返回负错误代码。

外部用户定义的函数可以像任何 AWK 用户定义的函数一样从 AWK 代码中调用。nargs 参数指定预期的参数数量,但与任何 AWK 函数一样,实际参数的数量可能不同。解释器将为任何缺失的参数提供 null 值。函数原型是:

typedef void (*awkfunc)(AWKINTERP *pinter, awksymb* ret, int nargs, awksymb* args);

函数可以通过将其设置在 ret 变量中并设置相应的标志来返回值。字符串值必须使用 malloc 分配。

只能在 AWK 程序编译后调用它。

示例

	void fact (AWKINTERP *pi, awksymb* ret, int nargs, awksymb* args)
    {
      int prod = 1;
      for (int i = 2; i <= args[0].fval; i++)
        prod *= i;
      ret->fval = prod;
      ret->flags = AWKSYMB_NUM;
    }
...
    awk_setprog (pi, " BEGIN {n = factorial(3); print n}");
    awk_compile (pi);
    awk_addfunc (pi, "factorial", fact, 1);
    awk_exec (pi);

最终想法

源代码已使用 Visual Studio 2017 编译。还有一个用于 gcc 的小型 makefile。语法分析器使用 YACC,因此如果您想完全重新构建,需要一个 YACC 编译器。不过,我包含了 YACC 生成的文件(ytab.cppytab.h),因此即使您没有 YACC 编译器也可以构建它。

以上是我嵌入式 AWK 解释器的介绍。它可以轻松地集成到 C/C++ 程序中,并与主机程序具有良好的通信。主机可以访问任何解释器变量,解释器可以调用主机程序定义的外部函数。从大小上看,解释器非常小。您可以预期开销约为 100 KB,与其他解释器相比,这是一个不错的数据(Lua 大约需要两倍)。

我将继续改进嵌入式 AWK 解释器。如果您想为该项目做出贡献或只是获取最新版本,您可以在 https://github.com/neacsum/awk 找到它。

历史

  • 2020 年 4 月 5 日 - 初始版本
© . All rights reserved.