Go言語では同じディレクトリで同じパッケージ(package main等)であれば、別ファイルに分割することができる。
AtCoderやCodinGameでテキストとして提出するときに一つのファイルにまとめる必要があるため、結合する方法を考える。
TL;DR
てっとり早く使いたい方は以下を参照
go-combine-multi-source
を以下コマンドでインストール
$ go install github.com/kajikentaro/playground/go-combine-multi-source@latest
実行するとPlease enter target directory:
と聞かれるので、mainパッケージが存在するディレクトリを指定する
$ go-combine-multi-source
Please enter target directory: ./
コマンドを実行した場所にresult.go.tmp
が生成される
ソース
https://github.com/kajikentaro/playground/blob/master/go-combine-multi-source/main.go
解説
まず標準入力でディレクトリ名を受け取る。
その後はAST木に分解するため、parserライブラリを用いる。このとき不要なテストコードを読み込まないよう、filter関数(isNotTestFile)を使ってはじいている。
func isNotTestFile(fi fs.FileInfo) bool {
filename := fi.Name()
r := regexp.MustCompile(`_test.go$`)
isTestFile := r.MatchString(filename)
return !isTestFile
}
func main() {
// 名前を標準入力から受け取って開く
var dirname string
fmt.Print("Please enter target directory: ")
fmt.Scan(&dirname)
fset := token.NewFileSet()
f, err := parser.ParseDir(fset, dirname, isNotTestFile, parser.Mode(0))
if err != nil {
log.Fatal(err)
}
次に各ノードを全て探索していく。
Goのソースコードを結合するときに障害になるのは、import文。なぜならimport先が重複してしまうとコンパイルエラーになるから。
ast.Inspectは第一引数にNodeインターフェースを受け取り、そこから生えている全てのノードを検索する。f["main"]
となっているがここを好きなパッケージ名にすれば、main以外でも動くようになるはず。
import文が書かれているNodeの型は*ast.ImportSpec型であるのでそこで条件分岐。
stringを一意にするためにmap構造を用いて保存する。
// importの重複しない一覧を作成する
importMap := map[string]bool{}
ast.Inspect(f["main"], func(n ast.Node) bool {
if v, isOk := n.(*ast.ImportSpec); isOk {
importMap[v.Path.Value] = true
}
return true
})
ここで先に最終目標であるast.File型を確認しておく。
ast.FileのDeclsメンバにはソースコード上で一番上の宣言(top-level declarations)を配列で格納する。
つまりここにimport文のDeclとimport文以外のDeclを配置すれば良い
type File struct {
Doc *CommentGroup // associated documentation; or nil
Package token.Pos // position of "package" keyword
Name *Ident // package name
Decls []Decl // top-level declarations; or nil
Scope *Scope // package scope (this file only)
Imports []*ImportSpec // imports in this file
Unresolved []*Ident // unresolved identifiers in this file
Comments []*CommentGroup // list of all comments in the source file
}
先にimport文のDeclを作成する
ネスト表現がややこしいのでドキュメントを確認しながら行った。
// importのDeclを作成
importDecl := ast.GenDecl{
Tok: token.IMPORT,
Specs: []ast.Spec{},
}
for k := range importMap {
importDecl.Specs = append(importDecl.Specs, &ast.ImportSpec{
Path: &ast.BasicLit{
Kind: token.STRING,
Value: k,
},
})
}
次にimport文以外のDeclを作成する
// import以外のDeclのリストを作成
theOtherDecls := []ast.Decl{}
for _, file := range f["main"].Files {
for _, decl := range file.Decls {
if v, isOk := decl.(*ast.GenDecl); isOk {
if v.Tok == token.IMPORT {
continue
}
}
theOtherDecls = append(theOtherDecls, decl)
}
}
これらをくっつけてFile型を生成
// 両方をくっつけて新しいfileにする
newFile := &ast.File{
Name: ast.NewIdent("main"),
Decls: append([]ast.Decl{&importDecl}, theOtherDecls...),
}
あとはnewFileからテキストデータに戻すだけ
// result.go.tmp を作成
file, err := os.OpenFile("result.go.tmp", os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
log.Fatal(err)
}
defer file.Close()
format.Node(file, token.NewFileSet(), newFile)
}