My External Storage

Sep 1, 2019 - 4 minute read - Comments - go

[Go] gofmtコマンドの-rオプションの使い方

Goでは標準ツールとして公式からgofmtコマンドというフォーマッタが提供されている。
このコマンドはコードのインデントなどをフォーマットしてくれるほかに、-rオプションでASTベースの置換も行える。
実装ベースから使い方を追ってみたのでメモする。

TL;DR

  • gofmtコマンドはGoをインストールするとgoコマンドと一緒にインストールされる公式ツール
  • コードのフォーマットを整えてくれる他にASTベースの置換が行える
  • -rオプションと置換ルールを渡せばよい。
    • 置換前のエクスプレッション -> 置換後のエクスプレッション
    • 小文字一文字を使うことで置換ルールにワイルドカード指定もできる
  • -sオプションを使うと、スライス操作などのリファクタリングもすることができる
  • 単純にリネームしたいだけならgorenameコマンドのほうがよい

なお、執筆時のGoのバージョンは以下の通り。

$ go version
go version go1.12.9 darwin/amd64

gofmtコマンドのオプションについて

gofmtコマンドはGoの公式コードフォーマッタだ。gofmtコマンドはGoをソースコードからコンパイルするとgoコマンドと一緒にビルドされる。
インデントや改行などを規律に則って実施してくれる。gofmtコマンドにはただフォーマットを整える以外に置換する機能も備わっている。

$ gofmt -h
usage: gofmt [flags] [path ...]
  -cpuprofile string
    	write cpu profile to this file
  -d	display diffs instead of rewriting files
  -e	report all errors (not just the first 10 on different lines)
  -l	list files whose formatting differs from gofmt's
  -r string
    	rewrite rule (e.g., 'a[b:len(a)] -> a[b:]')
  -s	simplify code
  -w	write result to (source) file instead of stdout

このオプションの中で-rオプションが置換を実行するためのオプションだ。

-rオプションを使ったコードの置換ルール

上記のコマンドラインのヘルプを見るだけではよくわからないが、-rオプションの利用方法の詳細はGoDocに記載されている。

置換ルールは以下のルールに基づいて書く。

  • pattern -> replacementという形で書く
  • patternreplacementはGoのエクスプレッションとして正しい文字列を渡す
  • エクスプレッション中の小文字一文字はワイルドカードとして解釈される
    • α[β:len(α)]だったらαβがワイルドカード扱いになる。

実際にコードを確認してみる。gofmtコマンドの-rオプションの実装は以下にある。

initRewrite関数をみると、たしかに->で文字列を分割し、それぞれがExpressionとして評価できるか確認している。

func initRewrite() {
	if *rewriteRule == "" {
		rewrite = nil // disable any previous rewrite
		return
	}
	f := strings.Split(*rewriteRule, "->")
	if len(f) != 2 {
		fmt.Fprintf(os.Stderr, "rewrite rule must be of the form 'pattern -> replacement'\n")
		os.Exit(2)
	}
	pattern := parseExpr(f[0], "pattern")
	replace := parseExpr(f[1], "replacement")
	rewrite = func(p *ast.File) *ast.File { return rewriteFile(pattern, replace, p) }
}

また、isWildcard関数で小文字1文字か確認していた。

func isWildcard(s string) bool {
	rune, size := utf8.DecodeRuneInString(s)
	return size == len(s) && unicode.IsLower(rune)
}

実際の実行例

実際にgofomtコマンドでソースコードを置換してみる。置換前のコードは以下になる。

package main

import "fmt"

// Foo is simple function.
func Foo() int {
	return 10
}

// Hoge is sample Struct.
type Hoge struct {
	Foo    string
	foovar int
}

func main() {
	foovar := "same name"
	hoge := Hoge{}
	phoge := &Hoge{}
	hoge.Foo = "Foo"
	phoge.foovar = Foo()

	fmt.Println(hoge.Foo)
	fmt.Println(phoge.foovar)
	fmt.Println(foovar)
}

構造体やフィールド名の置換を試みたところ、以下のように編集することができた。

$ gofmt -r "Foo -> Bar" main.go
package main

import "fmt"

// Foo is simple function.
func Bar() int {
	return 10
}

// Hoge is sample Struct.
type Hoge struct {
	Bar    string
	foovar int
}

func main() {
	foovar := "same name"
	hoge := Hoge{}
	phoge := &Hoge{}
	hoge.Bar = "Foo"
	phoge.foovar = Bar()

	fmt.Println(hoge.Bar)
	fmt.Println(phoge.foovar)
	fmt.Println(foovar)
}

あくまで構造体やフィールドの置換で、"Foo"のような文字列に関しては一緒に置換されないようだ。
また、構造体コメントも変更はしてくれなかった。

$ gofmt -r "Hoge -> Bar" main.go
package main

import "fmt"

// Foo is simple function.
func Foo() int {
	return 10
}

// Hoge is sample Struct.
type Bar struct {
	Foo    string
	foovar int
}

func main() {
	foovar := "same name"
	hoge := Bar{}
	phoge := &Bar{}
	hoge.Foo = "Foo"
	phoge.foovar = Foo()

	fmt.Println(hoge.Foo)
	fmt.Println(phoge.foovar)
	fmt.Println(foovar)
}

終わりに

今回は横浜Go読書会で改訂版みんなのGo言語を輪読している際に知ったgofmtコマンドのオプションについて調べた。
本当はもっと実装を読み解きたかったのだが、再帰部分(applymatch関数)をちゃんと理解できなかった。いくつか作りたい静的解析ツールがあるので、時間があるときに読み直して内容を理解したい。

なお、本記事では-rオプションしか触れなかったが、-sオプションもコードの簡単なリファクタリングをしてくれる。
実用途してはこちらのほうが扱いやすそうだ。

-sオプションは以下のような冗長なコードをリファクタリングしてくれる。

An array, slice, or map composite literal of the form:
	[]T{T{}, T{}}
will be simplified to:
	[]T{{}, {}}

A slice expression of the form:
	s[a:len(s)]
will be simplified to:
	s[a:]

A range of the form:
	for x, _ = range v {...}
will be simplified to:
	for x = range v {...}

A range of the form:
	for _ = range v {...}
will be simplified to:
	for range v {...}

参考

関連記事