goawk,一家逆向翻译

2021-04-13 01:10:37

摘要:阅读读取AWK编程语言后,我受到启发,为AWK编写翻译。本文概述了AWK,描述了GoAwk如何工作,我如何接近测试,以及如何测量和提高其性能。

Awk是一种迷人的文本处理语言,Awk编程语言是一个奇妙的简明书籍描述它。 A,W和K在AWK代表三位原创创作者的姓氏:Alfred Aho,Peter Weinberger和Brian Kernighan。 Kernighan也是C编程语言的作者(“K& R”),两本书具有相同的每页 - 包装 - 拳击感。

Awk于1977年发布,这使得40岁以上!对于域的特定语言而言,仍然用于单行的特定于域的语言无处不在。

在实现我自己的小语言以及LOX中的鲍勃NYSTROM的LOX语言之后,我仍然有点“翻译高”。在阅读Awk书之后,我以为它会很有趣(对于一些讨厌“)写一个翻译的讨论者。它能有多难?

事实证明,在基本级别上工作并不是很难,但获得正确的POSIX AWK语义并快速使其迅速令人震惊。

首先,如果您已经了解了,请简要介绍AWK语言(跳至下一节)。

如果您不熟悉awk,这是一句话摘要:awk按行读取文本文件行,对于匹配模式表达式的每一行,它执行一个操作(通常打印输出)。

所以给出了一个例子输入文件(Web服务器日志文件),其中每行使用格式"时间戳方法路径IP状态时间" :

2018-11-07T07:56:34Z获得/约1.2.3.4 200 0.0132018-11-07T07:56:35Z Get / Contact 1.2.3.4 200 0.0202018-11-07T /联系人1.2.3.4 200 1.3092018- 11-07T07:56:40Z获取/Robots.txt 123.0.0.1 404 0.0042018-11-07T07:57:00Z GET /约2.3.4.5 200 0.0142018-11-07T08:00:00Z GET / ASDF 3.4.5.6 404 0.0052018- 11-07T08:00:01z GET / FDSA 3.4.5.6 404 0.0042018-11-07T08:00:02ZHEAD / 4.5.6.7 200 0.0008:00:15z GET / 4.5.6.7 200 0.00552018-11-07T08: 05:57Z GET / ROBOTS.TXT 201.12.34.56 404 0.0042018-11-07T08:05:58Z / 5.6.7.8 200 0.0072018-11-07T08:05:59Z GET / 5.6.7 200 0.049

如果我们希望看到所有命中的IP地址(字段4)到“关于”页面,我们可以写:

上面的模式是斜杠分隔的正则表达式/约/,动作是打印第四个字段($ 4)。默认情况下,awk将行拆分为空格的字段,但字段分隔符可以轻松配置,并且可以是正则表达式。

通常,正则表达式模式匹配整行,但您也可以匹配任意表达式。以上也会匹配URL / NOT-TO-OF-TO-OF,但您可以拧紧它以测试路径(字段3)正好" /关于&#34 ;:

如果我们想确定所有GET请求的平均响应时间(字段6),我们可以总结响应时间并计算GET请求的数量,然后在结束块中打印平均值 - 18毫秒,不错:

$ awk' / get / {总+ = $ 6; n ++}结束{print total / n}' server.log 0.0186667.

Warning: Can only detect less than 5000 characters

这一切都从Lexer开始,将AWK源代码转换为令牌流。 Lexer的肠道是扫描()方法,它跳过空格和注释,然后解析下一个令牌:例如,美元,数字或lparen。每个令牌都以其源代码位置(行和列)返回,因此解析器可以在语法错误消息中包含此信息。

代码的大部分(在Lexer.scan方法中)只是一个大型交换机语句,可在令牌的第一个字符上切换。这是一球片段:

// ...切换CH {案例' $' :Tok =美元案例' 0' ,' 1' ,' 2' ,' 3' ,' 4' ,' 5' ,' 6' ,' 7' ,' 8' ,' 9' ,'' :开始:=升。 offset - 2 getDigit:=假if ch!='。' {getdigit = true l。 Ch> =' 0' &&湖ch<' 9' {l。 Next()}如果l。 Ch =='' {l。 next()}} // ... tok =数字案例' {' :tok = lbrace case'}}}' :tok = rbrace case' =' :tok = l。选择(' =',分配,等于)// ...

关于AWK语法的古怪事物之一是解析/和/正则表达式/是模糊的 - 您必须知道解析上下文以知道是否返回div或正则表达式令牌。因此,Lexer公开了一种扫描方法,用于普通令牌和扫描程序的扫描方法,用于解析器,以便调用它预期正则表达式令牌的位置。

接下来是解析器,一个相当标准的递归 - 下降解析器,它创建了一个抽象语法树(AST)。我不喜欢学习如何驱动像YACC这样的解析器生成器或带来外部依赖性,因此GoAWK的解析器用爱滚动。

AST节点是简单的GO SCRECTS,IDPR和STMT分别由每个表达式和语句结构实现的接口。 AST节点也可以通过调用字符串()方法来漂亮打印自己 - 这对于调试解析器非常有用,您可以通过在命令行上指定-d来启用它:

$ goawk -d' begin {x = 4;打印x + 3; }' begin {x = 4打印(x + 3)} 7

Awk语法在地方有点古怪,并非最不重要的是,打印语句中的表达不支持>或者除了括号内。这应该使重定向或管道输出更简单。

打印x> y表示打印变量x重定向到名称y的文件

打印(x> y)表示打印boolean true(1)如果x大于y

我无法弄清楚代码中的recursive-descent树的两条路径做出更好的方法 - expr()和printexpr():

func(p * parser)expr()expr {返回p。 getLine()} Func(p * parser)printexpr()expr {return p。 _assign(p。printcond)}

构建函数调用特殊解析,以便在解析时间可以检查参数的数量(以及某些情况下)。例如,解析匹配(str,正则表达式):

案例f_match:p。下一个()p。期待(lparen)str:= p。 expr()p。 CommaneWlines()Regex:= p。 Regexstr(p. expr)p。期待(RPAREN)返回& callexpr {f_match,[] expr {str,regex}}

很多解析函数标志在无效语法或意外令牌时出错。它使生活更容易在每一步检查这些错误,而是用特殊的ParseError类型陷入顶层恢复。这避免了在递归下降代码中处理了大量的重复误差处理。以下是顶级Parseprogram函数如何实现:

func parseprogram(src []字节,config * parserconfig)(prog * program,err错误){defer func(){如果r:= recover(); r!= nil {//转换为parseerror或重新恐慌err = r。 ( * 解析错误 ) } }() // ... }

解析器实际上是解析器包的一部分。它确实基本类型检查数组与标量,并将整数索引分配给所有变量引用(以避免在执行时间下慢地图查找)。

我认为我完成了解析器的方式是非传统的:而不是在AST上进行全面传递,而不是进行解析器记录resolver以弄清楚类型的必要条件(函数调用列表和可变引用列表)。这可能比散步整个树更快,但它可能会使代码不太直接。

事实上,解析器是我写了一段时间的更难代码之一。这是一块Goawk来源,我并不是特别满意。它有效,但它很乱,我仍然不确定我已经涵盖了所有边缘案例。

复杂性来自于调用函数时,您不知道参数是否是调用站点的标量或数组。您必须仔细阅读所谓的函数中的类型(并且可能在其调用的函数中)以确定该类型。考虑这个awk程序:

函数g(b,y){返回f(b,y)}函数f(a,x){return a [x]}开始{c [1] = 2;打印f(c,1);打印g(c,1)}

该程序只需打印2两次。但是,当我们在内部打电话时,我们不知道参数的类型。它是迭代时尚的解决方案的一部分。 (请参阅“解析”中的“标准”。)

在找出未知的参数类型之后,解析程序将整数索引分配给所有变量引用,全局和本地。

解释器是一个简单的树步道翻译。解释器实现语句执行和表达式评估,输入/输出,函数调用和基本值类型。

语句执行在interp.go中启动execprogram,execprogram,它采用解析程序,设置解释器,然后执行开始块,模式和操作和结束块。执行操作包括评估模式表达式并确定它们是否与当前行匹配。这包括“范围模式”,如NR == 4,NR == 10,其匹配开始和停止模式之间的线。

由执行方法执行语句,该方法采用任何类型的STMT,执行一个大型开关,以确定它是什么样的语句,并执行该语句的行为。

Expression评估以相同的方式工作,除了它发生在eval方法中,它将expr和切换在表达式上。

大多数二进制表达式(除了短路和amp;&和||)通过EvalBinary评估,其中包含在操作员令牌上的另一个开关,如下所示:

Func(P * Interp)EvalBinary(Op令牌,L,R值)(值,错误){Switch Op {Case Add:返回num(l。num()+ r.num()),nil case子:返回num (l。num() - r。num()),nil案例等于:如果l。 istruestr()|| r。 Istruestr(){返回布尔值(p. toString(l)== p. toString(R)),否}否则{返回布尔值(l。n == r。n),nil} // ...}

在等于案例中,您可以看到AWK的“Strace Typed”性质:如果任一操作数肯定是“真实字符串”(不是用户输入的数字字符串),请执行字符串比较,否则执行数字比较。这意味着比较,如3 ==" foo"字符串比较是否为3 == 3.14是一个数字,这就是你所期望的。

AWK的关联阵列非常适合于Go Map [String]值类型,因此使得实现那些简单的映射。说到哪个,Go的垃圾收集者意味着我们不必担心编写自己的GC。

输入和输出在IO.GO中处理。所有I / O都会缓冲效率,我们使用Go的Bufio.scanner读取输入记录和Bufio.WrieRiter以缓冲输出。

输入记录通常是行(扫描仪的默认行为),但记录分隔符RS也可以设置为另一个字符以分割ON,或者为空字符串,这意味着在两个连续的纽丁(空白行)上拆分用于处理多线记录。这些方法仍然使用bufio.scanner,但具有自定义拆分功能,例如:

//拆分函数在给定的分隔字节型Bytesplitter struct {sep byte} func(s byteplitter)扫描(data [] byte,ateof bool)(authend int,token []字节,错误错误){如果ateof&amp ;& len(数据)== 0 {返回0,nil,nil}如果i:=字节。 IndexByte(数据,s。SEP); i> = 0 {//我们有一个完整的SEP终止记录返回I + 1,数据[0:i],nil} //如果在eof,我们有一个最终的,非终止的记录;如果ATEOF {返回LEN(数据),数据,零} //请求更多数据返回0,nil,nil}如果返回它

从打印或printf的输出可以被重定向到文件,附加到文件,或者将其管道向导:这是在getOutputStream中处理的。输入可以来自STDIN,文件或来自命令的PIPE。

CallBuiltin方法再次使用大型交换机语句来确定我们正在调用的awk函数,例如split()或sqrt()。内置拆分需要特殊处理,因为它需要一个非评估的数组参数。类似地,Sub和GSub实际上采用分配给的“Lvalue”参数。对于其余功能,我们首先评估参数并执行操作。

大多数功能都是使用Go的标准库的部分来实现。例如,SQRT()这样的数学函数使用标准数学包,split()使用字符串和regexp函数。 goawk重新使用Go的正则表达式,因此模糊的Regex语法可能不会与“一个真正的awk”相同。

谈到正则表达式,我使用简单的界限缓存缓存了正则表达式的汇编,这足以加快几乎所有的awk脚本:

//编译正则表达式字符串(或从Regex Cache获取)Func(P * Interp)CompunereGex(正则表达式字符串)(* Regexp。Regexp,错误){如果重新,OK:= p。 regexcache [正则表达式];好的{return re,nil} re,err:= regexp。编译(

......