将 AWK 嵌入 C 程序





5.00/5 (14投票s)
一个 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,我让你猜 ken 和 dmr 分别代表谁 😊。为了增加复古的色彩,/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_setprog
或 awk_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_setprog
、awk_compile
和 awk_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);
替换输入函数(默认为 getc
或 fgetc
)为用户定义的函数。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.cpp 和 ytab.h),因此即使您没有 YACC 编译器也可以构建它。
以上是我嵌入式 AWK 解释器的介绍。它可以轻松地集成到 C/C++ 程序中,并与主机程序具有良好的通信。主机可以访问任何解释器变量,解释器可以调用主机程序定义的外部函数。从大小上看,解释器非常小。您可以预期开销约为 100 KB,与其他解释器相比,这是一个不错的数据(Lua 大约需要两倍)。
我将继续改进嵌入式 AWK 解释器。如果您想为该项目做出贡献或只是获取最新版本,您可以在 https://github.com/neacsum/awk 找到它。
历史
- 2020 年 4 月 5 日 - 初始版本