ions == nil || len(c.addOptions) == 0) &&
!c.clear &&
!c.clearOption &&
(c.removeOptions == nil || len(c.removeOptions) == 0) &&
(c.remove == nil || len(c.remove) == 0) {
return errors.New("one of " +
"[-add-tags, -add-options, -remove-tags, -remove-options, -clear-tags, -clear-options]" +
" should be defined")
}
return nil
}
将验证部分放置在一个单独的函数中,以便测试。
现在我们了解了如何获取配置并进行验证,我们继续解析文件:
我们已经开始讨论如何解析文件了。这里的解析是 config
结构体的一个方法。实际上,所有的方法都是 config
结构体的一部分:
func main() {
cfg := config{}
node, err := cfg.parse()
if err != nil {
return err
}
// continue find struct selection ...
}
func (c *config) parse() (ast.Node, error) {
c.fset = token.NewFileSet()
var contents interface{}
if c.modified != nil {
archive, err := buildutil.ParseOverlayArchive(c.modified)
if err != nil {
return nil, fmt.Errorf("failed to parse -modified archive: %v", err)
}
fc, ok := archive[c.file]
if !ok {
return nil, fmt.Errorf("couldn't find %s in archive", c.file)
}
contents = fc
}
return parser.ParseFile(c.fset, c.file, contents, parser.ParseComments)
}
parse 函数只做一件事:解析源代码并返回一个 ast.Node
。如果我们传入的是文件,那就非常简单了,在这种情况下,我们使用 parser.ParseFile() 函数。需要注意的是 token.NewFileSet()
,它创建一个 *token.FileSet
类型。我们将它存储在 c.fset
中,同时也传给了 parser.ParseFile()
函数。为什么呢?
因为 fileset 用于为每个文件独立存储每个节点的位置信息。这在以后非常有用,可以用于获得 ast.Node
的确切位置(请注意,ast.Node
使用了一个压缩了的位置信息 token.Pos
。要获取更多的信息,它需要通过 token.FileSet.Position()
函数来获取一个 token.Position
,其包含更多的信息)
让我们继续。如果通过 stdin 传递源文件,那么这更加有趣。config.modified
字段是一个易于测试的 io.Reader
,但实际上我们传递的是 stdin。我们如何检测是否需要从 stdin 读取呢?
我们询问用户是否想通过 stdin 传递内容。这种情况下,工具用户需要传递 --modified
标志(这是一个布尔标志)。如果用户了传递它,我们只需将 stdin 分配给 c.modified
:
flagModified = flag.Bool("modified", false,
"read an archive of modified files from standard input")
if *flagModified {
cfg.modified = os.Stdin
}
如果再次检查上面的 config.parse()
函数,您将发现我们检查是否已分配了 .modified
字段。因为 stdin 是一个任意的数据流,我们需要能够根据给定的协议进行解析。在这种情况下,我们假定存档包含以下内容:
- 文件名,后接一行新行
- 文件大小(十进制),后接一行新行
- 文件的内容
因为我们知道文件大小,可以无障碍地解析文件内容。任何超出给定文件大小的部分,我们仅仅停止解析。
此方法也被其他几个工具所使用(如 guru、gogetdoc 等),对编辑器来说非常有用。 因为这样可以让编辑器传递修改后的文件内容,而不会保存到文件系统中。因此命名为 modified。
现在我们有了自己的节点,让我们继续 “搜索结构体” 这一步:
在 main 函数中,我们将使用从上一步解析得到的 ast.Node
调用 findSelection()
函数:
func main() {
// ... parse file and get ast.Node
start, end, err := cfg.findSelection(node)
if err != nil {
return err
}
// continue rewriting the node with the start&end position
}
cfg.findSelection()
函数根据配置返回结构体的开始位置和结束位置以告知我们如何选择一个结构体。它迭代给定节点,然后返回开始位置/结束位置(如上配置部分中所述):
但是怎么做呢?记住有三种模式。分别是行选择、偏移量和结构体名称:
// findSelection returns the start and end position of the fields that are
// suspect to change. It depends on the line, struct or offset selection.
func (c *config) findSelection(node ast.Node) (int, int, error) {
if c.line != "" {
return c.lineSelection(node)
} else if c.offset != 0 {
return c.offsetSelection(node)
} else if c.structName != "" {
return c.structSelection(node)
} else {
return 0, 0, errors.New("-line, -offset or -struct is not passed")
}
}
行选择是最简单的部分。这里我们只返回标志值本身。因此如果用户传入 --line 3,50
标志,函数将返回(3, 50, nil)
。 它所做的就是拆分标志值并将其转换为整数(同样执行验证):
func (c *config) lineSelection(file ast.Node) (int, int, error) {
var err error
splitted := strings.Split(c.line, ",")
start, er