宠物商店项目
我喜欢宠物项目,它们是使用图书馆和其他您在工作中无法使用的技术的很好借口。 最近,我一直在从事一个较大的宠物项目,该项目需要解析Go文件。在使用此类工具之前,我曾使用过ANTLR ,但不幸的是, ANTLR的 Go目标性能很差。 因此,我开始搜索用纯Go语言编写的替代方法,并遇到了这种方法,它采用了不同的方法来创建Go分析器,但是在我们了解该库与其他库之间的区别之前,让我们介绍一些基本概念关于解析。
抱歉,正在解析?
为了“形式化”,这是维基百科必须说的:
解析,语法分析或语法分析是按照自然语法,计算机语言或数据结构分析一串符号的过程,符合形式语法的规则。 解析一词来自拉丁语pars(orationis),意思是(语音的一部分)。
现在,让我们尝试通过一个示例将其分解,如果您使用Go,您可能已经熟悉go.mod文件了。 但是如果您不使用它,go.mod文件将用于管理您的项目依赖项。 这是一个简单的go.mod文件:
module github.com/matang28/go -latest
go 1.13
require (
github.com/some/dependency1 v1 .2 .3
github.com/some/dependency2 v5 .1 .0 // indirect
)
replace github.com/some/dependency1 => github.com/some/dependency1@dev latest
在解析此文件之前,我们需要做两件事:
- 从文件本身获取的符号流。
- 语法定义将由解析器用来验证语法。
第一个正式称为Lexer(或Lexing)。 Lexer就是将字符串转换为解析器使用的预定义符号列表,我们的语言使用的任何符号(或标记)都应由Lexer标识。 在我们的情况下,我们需要捕获以下令牌:
- 关键字:
module
,go
,require
,replace
。 - 字符串如:
github.com/some/dependency1
,1.13
。 - 版本字符串,例如:
v1.2.3
,v1.4.21-haijkjasd9ijasd
。 - 其他杂类符号,例如:
=> , // , ( , )
,空格,行终止符和制表符。
第二个将使这些标记具有语法含义,您可以看到go.mod
文件遵循一些基本规则:
- 您必须使用
module
指令声明自己的模块名称。 - 您可以使用
go
指令声明go版本。 - 您可以使用
require
指令列出您的依赖项。 - 您可以使用
replace
指令替换某些依赖项。 - 您可以使用
exclude
指令排除某些依赖项。
这些规则通常称为语言(go.mod语言)的语法,并且有一种更正式的表示方法,其中一种是EBNF标准:
grammar GoMod;// A source file should be structured in the following way:
sourceFile : moduleDecl stmt* EOF;
stmt : (versionDecl | reqDecl | repDecl | excDecl);
// The module directive:
moduleDecl : 'module' STRING;
// The version directive:
versionDecl : 'go' STRING;
// The require directive, it can be either a single line:
// require github.com/some/dep1 OR multi line:
// require (
// github.com/some/dep1
// github.com/some/dep2
// )
reqDecl : 'require' dependency
| 'require (' dependency* ')'
;
// The replace directive:
repDecl : 'replace' STRING '=>' dependency;
// The exclude directive:
excDecl : 'exclude' dependency;
dependency : STRING version comment?;
version : VERSION | 'latest' ;
comment : '//' STRING;
VERSION : [v]STRING+;
STRING : [a-zA-Z0- 9 _+.@/-]+;
WS : [ \n\r\t\u000C]+ -> skip;
现在回到定义,解析器将获取令牌流(例如,文本文件的令牌化),并尝试将每个令牌与语法进行匹配,如果我们的文件不遵循语法,则会出现解析错误,但是,如果我们的文件确实遵循语法,那么我们可以确定输入文件是有效的(就语法而言),并且可以获取解析树,我们可以遍历所有匹配的语法规则。
解析的全部目的在于实现这种魔力……获取令牌流并从中构建解析树(如果语法与语法相匹配)。
有什么不同?
在本文开头,我曾说过一个库是创建这个宠物项目的火花,为什么呢?
很好的解析树并不是从中提取数据的最舒适的数据结构,在解析树上“行走”可能会造成混乱。 当今可用的大多数工具都使用代码生成技术从语法文件生成词法分析器和解析器,并且为了尽可能通用,它们中的大多数都为您提供了一个侦听器接口,该接口将在解析器进入或退出每个语法规则时得到通知。 。
参与者使用了不同的方法,这对于Go开发人员来说更常见。 它使用Go的结构系统表示语法(或更准确地说,是我们要从语法中捕获的部分),还使用“结构标签”来定义语法规则。
哇! 那是一口气,让我们边做边学。 首先,我们需要一些表示标准`go.mod`文件的结构:
// The root level object that represents a go.mod file
type GoModFile struct {
Module string
Statements []Statement
}
type Statement struct {
GoVersion * string
Requirements []Dependency
Replacements []Replacement
Excludes []Dependency
}
// A struct that represents a go.mod dependency
type Dependency struct {
ModuleName string
Version string
Comment * string
}
// A struct that represents a replace directive
type Replacement struct {
FromModule string
ToModule Dependency
}
为了编写解析器,我们需要令牌流。 给分词给了我们一个基于正则表达式的词法分析器(他们的文档说他们一直在研究一种EBNF风格的词法分析器,因为这是分词法IMO中最繁琐的部分,因此可能非常有用)。 让我们看看实际情况:
import (
"github.com/alecthomas/participle/lexer"
)
// The lexer uses named regex groups to tokenize the input:
var iniLexer = lexer.Must(lexer.Regexp(
/* We want to ignore these characters, so no name needed */
`([\s\n\r\t]+)` +
/* Parentheses [(,)]*/
`|(?P<Parentheses>[\(\)])` +
/* Arrow [=>]*/
`|(?P<Arrow>(=>))` +
/* Version [v STRING] */
`|(?P<Version>[v][a-zA-Z0-9_\+\.@\-\/]+)` +
/* String [a-z,A-Z,0-9,_,+,.,@,-,/]+ */
`|(?P<String>[a-zA-Z0-9_\+\.@\-\/]+)` ,
))
现在我们可以继续定义语法,将其视为EBNF文件和Go结构表示形式之间的组合。 这是分词必须提供的一些基本语法运算符:
- 例如,使用引号将符号匹配:
"module"
将尝试与符号module
匹配。 - 使用
@
捕获表达式(即在我们的词法分析器中定义的命名组),例如@String
将匹配String
符号并将其提取到struct字段中。 - 使用
@@
允许底层结构与输入匹配,这在您有多个规则替代项时很有用。 - 使用公共正则表达式运算符捕获组:
*
匹配零个或多个,+
匹配至少一个,?
匹配零或一,以此类推...
// The root level object that represents a go.mod file
type GoModFile struct {
Module string `"module" @String`
Statements []Statement `@@*`
}
type Statement struct {
GoVersion * string `( "go" @String )`
Requirements []Dependency `| (("require" "(" @@* ")") | ("require" @@))`
Replacements []Replacement `| (("replace" "(" @@* ")") | ("replace" @@))`
Excludes []Dependency `| (("exclude" "(" @@* ")") | ("exclude" @@))`
}
// A struct that represents a go.mod dependency
type Dependency struct {
ModuleName string `@String`
Version string `(@Version | @"latest")`
Comment * string `("//" @String)?`
}
// A struct that represents a replace directive
type Replacement struct {
FromModule string `@String "=>"`
ToModule Dependency `@@`
}
现在我们要做的就是根据语法构建解析器:
func Parse (source string ) (*GoModFile, error) {
p, err := participle.Build(&GoModFile{},
participle.Lexer(iniLexer),
)
if err != nil {
return nil , err
}
ast := &GoModFile{}
err = p.ParseString(source, ast)
return ast, err
}
至此,我们可以立即解析`go.mod`文件了! 让我们编写一个简单的测试以确保一切都按预期进行:
func TestParse_WithMultipleRequirements (t *testing.T) {
file, err := Parse( `
module github.com/matang28/go-latest
go 1.12
require (
github.com/bla1/bla1 v1.23.1 // indirect
github.com/bla2/bla2 v2.25.8-20190701-fuasdjhasd8
)
` )
assert.Nil(t, err)
assert.NotNil(t, file)
assert.EqualValues(t, "github.com/matang28/go-latest" , file.Module)
assert.EqualValues(t, "1.12" , *file.GoVersion)
assert.EqualValues(t, 2 , len (file.Requirements))
assert.EqualValues(t, "github.com/bla1/bla1" , file.Requirements[ 0 ].ModuleName)
assert.EqualValues(t, "v1.23.1" , file.Requirements[ 0 ].Version)
assert.EqualValues(t, "indirect" , *file.Requirements[ 0 ].Comment)
assert.EqualValues(t, "github.com/bla2/bla2" , file.Requirements[ 1 ].ModuleName)
assert.EqualValues(t, "v2.25.8-20190701-fuasdjhasd8" , file.Requirements[ 1 ].Version)
assert.Nil(t, file.Requirements[ 1 ].Comment)
assert.Nil(t, file.Replacements)
}
宠物项目在哪里?
为示例选择go.mod
文件并非偶然,我决定构建的工具是一个简单的自动化工具。 如果在组织中使用Go,您可能知道手动编辑go.mod
文件可能很繁琐。
go-latest
救援! go-latest
将递归扫描go.mod
文件,将与您的查询匹配的每个依赖项都修补为latest。
例如,对于此文件树:
.
├── go.mod
├── subModule1
│ └── go.mod
└── subModule2
└── go.mod
如果我想将与我的组织名称匹配的依赖关系修补到最新版本,请输入:
$> go-latest ”organization.com” .
看到?! 我告诉过你,宠物项目很有趣:),我不在乎我们可以通过使用简单的正则表达式来解决这个问题,因为这是宠物项目的本质!
和往常一样,感谢您的阅读...
图片积分(按其出现的顺序):
- Avi Richards在Unsplash上的照片
- Camylla Battani摄于Unsplash
- Caleb Woods在Unsplash上拍摄的照片
- 图片由Curology在Unsplash拍摄
- Josh Rakower在Unsplash上拍摄的照片
宠物商店项目