Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 13688] ファイルの概要

このコミットは、Go言語の標準ライブラリである text/template パッケージにおける重要な機能追加に関するものです。具体的には、テンプレート内でパイプラインのグループ化を括弧 () を使用して行えるようにする変更が導入されました。これにより、特に論理演算子を使用する際のテンプレートの記述がより柔軟かつ読みやすくなります。

変更された主なファイルとその役割は以下の通りです。

  • src/pkg/text/template/doc.go: text/template パッケージの公式ドキュメント。括弧によるグループ化の新しい構文と使用例が追記されました。
  • src/pkg/text/template/exec.go: テンプレートの実行エンジン。括弧でグループ化されたパイプライン(PipeNode)を引数として評価できるように、evalArg および evalEmptyInterface 関数が更新されました。
  • src/pkg/text/template/exec_test.go: 実行エンジンのテストファイル。括弧を使用したパイプラインの新しいテストケースが追加され、add 関数がテスト用に導入されました。
  • src/pkg/text/template/parse/lex.go: テンプレートの字句解析器(lexer)。入力文字列をトークンに分割する際に、() を新しいトークンタイプ (itemLeftParen, itemRightParen) として認識し、括弧のネスト深度を管理するロジックが追加されました。
  • src/pkg/text/template/parse/lex_test.go: 字句解析器のテストファイル。括弧の認識、ネスト、および不正な括弧の使用に関する新しいテストケースが追加されました。
  • src/pkg/text/template/parse/node.go: 抽象構文木(AST)のノード定義。CommandNodeString() メソッドが、括弧で囲まれた PipeNode 引数を正しく文字列化するように更新されました。
  • src/pkg/text/template/parse/parse.go: テンプレートの構文解析器(parser)。字句解析器が生成したトークン列からASTを構築する際に、括弧で囲まれたパイプラインを正しく解析し、ネストされたパイプラインをサポートするようにロジックが変更されました。
  • src/pkg/text/template/parse/parse_test.go: 構文解析器のテストファイル。ネストされたパイプラインの新しいテストケースが追加されました。

コミット

commit cc842c738ea9a64570e306cbab37c3e3cf9a35dd
Author: Rob Pike <r@golang.org>
Date:   Fri Aug 24 12:37:23 2012 -0700

    text/template: allow grouping of pipelines using parentheses
    
    Based on work by Russ Cox. From his CL:
    
            This is generally useful but especially helpful when trying
            to use the built-in boolean operators.  It lets you write:
    
            {{if not (f 1)}} foo {{end}}
            {{if and (f 1) (g 2)}} bar {{end}}
            {{if or (f 1) (g 2)}} quux {{end}}
    
            instead of
    
            {{if f 1 | not}} foo {{end}}
            {{if f 1}}{{if g 2}} bar {{end}}{{end}}
            {{$do := 0}}{{if f 1}}{{$do := 1}}{{else if g 2}}{{$do := 1}}{{end}}{{if $do}} quux {{end}}
    
    The result can be a bit LISPy but the benefit in expressiveness and readability
    for such a small change justifies it.
    
    I believe no changes are required to html/template.
    
    Fixes #3276.
    
    R=golang-dev, adg, rogpeppe, minux.ma
    CC=golang-dev
    https://golang.org/cl/6482056

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/cc842c738ea9a64570e306cbab37c3e3cf9a35dd

元コミット内容

text/template: allow grouping of pipelines using parentheses

Based on work by Russ Cox. From his CL:

        This is generally useful but especially helpful when trying
        to use the built-in boolean operators.  It lets you write:

        {{if not (f 1)}} foo {{end}}
        {{if and (f 1) (g 2)}} bar {{end}}
        {{if or (f 1) (g 2)}} quux {{end}}

        instead of

        {{if f 1 | not}} foo {{end}}
        {{if f 1}}{{if g 2}} bar {{end}}{{end}}
        {{$do := 0}}{{if f 1}}{{$do := 1}}{{else if g 2}{{$do := 1}}{{end}}{{if $do}} quux {{end}}

The result can be a bit LISPy but the benefit in expressiveness and readability
for such a small change justifies it.

I believe no changes are required to html/template.

Fixes #3276.

R=golang-dev, adg, rogpeppe, minux.ma
CC=golang-dev
https://golang.org/cl/6482056

変更の背景

この変更の主な背景は、Goの text/template パッケージにおけるテンプレート記述の表現力と可読性の向上です。特に、テンプレート内で組み込みの真偽値演算子(not, and, or)を使用する際に、従来の構文では複雑な条件式を簡潔に記述することが困難でした。

コミットメッセージに示されているように、括弧によるパイプラインのグループ化が導入される前は、以下のような冗長な記述が必要でした。

  • {{if not (f 1)}} の代わりに {{if f 1 | not}}
  • {{if and (f 1) (g 2)}} の代わりに {{if f 1}}{{if g 2}} bar {{end}}{{end}}
  • {{if or (f 1) (g 2)}} の代わりに {{$do := 0}}{{if f 1}}{{$do := 1}}{{else if g 2}}{{$do := 1}}{{end}}{{if $do}} quux {{end}}

これらの例からわかるように、特に andor のような論理演算を表現する場合、複数の if ブロックをネストしたり、一時変数 ($do) を導入したりする必要があり、テンプレートの可読性が著しく低下していました。

この変更は、GitHub Issue #3276 で報告された問題に対応するものであり、Russ Cox氏の先行作業に基づいています。括弧によるグループ化を導入することで、これらの複雑な条件式をより自然で直感的な方法で記述できるようになり、テンプレートのメンテナンス性と理解度が向上しました。コミットメッセージでは「結果は少しLISP的になるかもしれないが、表現力と可読性における利点は、このような小さな変更を正当化する」と述べられており、この機能がもたらすメリットが強調されています。

また、html/template パッケージには変更が不要であると明記されており、これは text/templatehtml/template が共通のパーシングロジックを共有していることを示唆しています。

前提知識の解説

このコミットの変更内容を理解するためには、以下の前提知識が役立ちます。

1. Go言語の text/template パッケージ

text/template パッケージは、Go言語に組み込まれているテキストベースのテンプレートエンジンです。ユーザーが定義したデータ構造に基づいて、動的にテキストコンテンツを生成するために使用されます。ウェブページ、設定ファイル、コード生成など、様々な用途で利用されます。

  • テンプレートの構文: テンプレートは、通常のテキストと、{{...}} で囲まれた「アクション」と呼ばれる特殊な要素で構成されます。アクションは、変数、関数呼び出し、条件分岐 (if)、繰り返し (range) などを表現します。
  • パイプライン (Pipelines): Goテンプレートの強力な機能の一つで、Unixのパイプラインに似ています。複数のコマンド(関数呼び出しやフィールドアクセスなど)を | 記号で連結し、前のコマンドの出力が次のコマンドの入力として渡されます。例えば、{{.Name | upper | printf "%q"}} は、.Name の値を大文字にし、その結果を引用符で囲んで出力します。

2. 字句解析 (Lexing) と構文解析 (Parsing)

コンパイラやインタプリタがソースコードを処理する際の基本的な2つのフェーズです。

  • 字句解析 (Lexical Analysis / Lexing): ソースコードの文字列を読み込み、意味のある最小単位である「トークン (Token)」のストリームに変換するプロセスです。例えば、if はキーワードトークン、123 は数値トークン、+ は演算子トークンといった具合です。このプロセスは「字句解析器 (Lexer)」または「スキャナ (Scanner)」によって行われます。
  • 構文解析 (Syntactic Analysis / Parsing): 字句解析器が生成したトークンのストリームを読み込み、その言語の文法規則に従って構文的に正しいかどうかを検証し、通常は「抽象構文木 (Abstract Syntax Tree: AST)」と呼ばれるツリー構造を構築するプロセスです。このプロセスは「構文解析器 (Parser)」によって行われます。ASTは、プログラムの構造を抽象的に表現したもので、後続の処理(コード生成や実行)に利用されます。

3. 抽象構文木 (Abstract Syntax Tree: AST)

ASTは、ソースコードの抽象的な構文構造を表現するツリーデータ構造です。各ノードは、ソースコード内の構成要素(式、文、宣言など)を表します。ASTは、コンパイラやインタプリタがプログラムの意味を理解し、最適化やコード生成を行うための重要な中間表現です。

このコミットでは、字句解析器が新しいトークン(括弧)を認識し、構文解析器がそのトークンを使ってASTに新しい構造(括弧でグループ化されたパイプライン)を構築し、最終的に実行エンジンがそのASTを評価するという一連の流れが変更されています。

技術的詳細

このコミットは、text/template パッケージの字句解析、構文解析、および実行の各フェーズにわたる変更を伴います。

1. 字句解析器 (src/pkg/text/template/parse/lex.go) の変更

字句解析器は、テンプレート文字列をトークンに分解する役割を担います。この変更では、以下の点が重要です。

  • 新しいトークンタイプの導入: itemLeftParen (() と itemRightParen ()) という2つの新しいトークンタイプが定義されました。
  • 括弧の認識: lexInsideAction 関数内で、() が検出された際に、それぞれ itemLeftParenitemRightParen としてトークンを発行するロジックが追加されました。
  • ネスト深度の管理: lexer 構造体に parenDepth フィールドが追加されました。これは、現在解析中の括弧のネスト深度を追跡するために使用されます。
    • itemLeftDelim (アクションの開始 {{) が検出された際に parenDepth0 にリセットされます。
    • ( が検出されるたびに parenDepth がインクリメントされます。
    • ) が検出されるたびに parenDepth がデクリメントされます。
  • エラーハンドリング:
    • itemRightDelim (アクションの終了 }}) が検出された際に parenDepth0 でない場合(つまり、閉じられていない括弧がある場合)はエラー (unclosed left paren) を発生させます。
    • ) が検出された際に parenDepth0 未満になる場合(つまり、対応する開き括弧がない場合)はエラー (unexpected right paren) を発生させます。
  • ターミネータの更新: atTerminator 関数が、() もターミネータ(トークンの区切り文字)として認識するように更新されました。

2. 構文解析器 (src/pkg/text/template/parse/parse.go) の変更

構文解析器は、字句解析器が生成したトークン列から抽象構文木(AST)を構築します。

  • pipeline 関数の変更:
    • パイプラインの終了条件に itemRightParen が追加されました。これにより、括弧で囲まれたパイプラインが正しく終了できるようになります。
    • itemRightParen でパイプラインが終了した場合、そのトークンをバックアップ(消費しない)することで、外側の構文解析がその閉じ括弧を処理できるようにします。
  • command 関数の変更:
    • itemLeftParen が検出された場合、新しいパイプライン (p := t.pipeline("parenthesized expression")) を再帰的に解析します。
    • 解析されたパイプラインの直後に itemRightParen がない場合はエラー (missing right paren in parenthesized expression) を発生させます。
    • 正しく解析された括弧付きパイプラインは、現在のコマンドの引数として追加されます (cmd.append(p))。これにより、(.Y .Z) のような形式が CommandNode の引数として扱われるようになります。

これらの変更により、構文解析器は {{printf "%q" (print "out" "put")}} のような、括弧で囲まれたパイプラインをコマンドの引数として正しく解釈し、AST内に PipeNode として表現できるようになります。

3. ASTノード (src/pkg/text/template/parse/node.go) の変更

ASTノードは、テンプレートの構造を表現します。

  • CommandNode.String() の変更:
    • CommandNodeString() メソッドは、デバッグや表示のためにノードを文字列化する際に使用されます。
    • この変更では、コマンドの引数が *PipeNode 型である場合、そのパイプラインを括弧で囲んで文字列化するように修正されました。これにより、(.Y .Z) のような元の構文が正しく再現されます。

4. 実行エンジン (src/pkg/text/template/exec.go) の変更

実行エンジンは、構築されたASTを評価し、最終的なテキストを生成します。

  • evalArg および evalEmptyInterface の変更:
    • これらの関数は、コマンドの引数を評価する役割を担います。
    • 変更により、引数ノードが *parse.PipeNode 型である場合、s.evalPipeline(dot, arg) を呼び出してそのパイプラインを評価するように修正されました。
    • これにより、printf "%q" (print "out" "put")(print "out" "put") の部分が、実行時に正しく評価されるようになります。

これらの変更が連携することで、text/template は括弧によるパイプラインのグループ化を完全にサポートし、より複雑で読みやすいテンプレートの記述を可能にしました。

コアとなるコードの変更箇所

src/pkg/text/template/parse/lex.go (字句解析器)

括弧のトークンタイプ定義と、parenDepth の追加、そして lexInsideAction での括弧の処理ロジック。

// 新しいトークンタイプの追加
const (
	// ...
	itemLeftParen  // '(' inside action
	// ...
	itemRightParen // ')' inside action
	// ...
)

// lexer構造体にparenDepthを追加
type lexer struct {
	// ...
	parenDepth int       // nesting depth of ( ) exprs
}

// lexInsideAction関数での括弧処理
func lexInsideAction(l *lexer) stateFn {
	// ...
	if strings.HasPrefix(l.input[l.pos:], l.rightDelim) {
		if l.parenDepth == 0 { // 閉じられていない括弧がないかチェック
			return lexRightDelim
		}
		return l.errorf("unclosed left paren") // 閉じられていない括弧があればエラー
	}
	switch r := l.next(); {
	// ...
	case r == '(': // 開き括弧の処理
		l.emit(itemLeftParen)
		l.parenDepth++ // 深度をインクリメント
		return lexInsideAction
	case r == ')': // 閉じ括弧の処理
		l.emit(itemRightParen)
		l.parenDepth-- // 深度をデクリメント
		if l.parenDepth < 0 { // 対応する開き括弧がないかチェック
			return l.errorf("unexpected right paren %#U", r) // エラー
		}
		return lexInsideAction
	// ...
	}
}

// atTerminator関数でのターミネータ追加
func (l *lexer) atTerminator() bool {
	// ...
	switch r {
	case eof, ',', '|', ':', ')', '(': // 括弧もターミネータとして追加
		return true
	}
	// ...
}

src/pkg/text/template/parse/parse.go (構文解析器)

command 関数内で括弧で囲まれたパイプラインを解析するロジック。

func (t *Tree) command() *CommandNode {
	cmd := newCommand(t.lex.lineNumber())
Loop:
	for {
		switch token := t.next(); token.typ {
		case itemRightDelim, itemRightParen: // 閉じ括弧もコマンドの終了条件に
			t.backup()
			break Loop
		case itemPipe:
			break Loop
		case itemLeftParen: // 開き括弧が検出された場合
			p := t.pipeline("parenthesized expression") // パイプラインを再帰的に解析
			if t.next().typ != itemRightParen { // 閉じ括弧がない場合はエラー
				t.errorf("missing right paren in parenthesized expression")
			}
			cmd.append(p) // 解析されたパイプラインをコマンドの引数として追加
		case itemError:
			t.errorf("%s", token.val)
		case itemIdentifier:
			// ...
		}
	}
	return cmd
}

src/pkg/text/template/exec.go (実行エンジン)

evalArg 関数で PipeNode を評価するロジック。

func (s *state) evalArg(dot reflect.Value, typ reflect.Type, n parse.Node) reflect.Value {
	switch arg := n.(type) {
	// ...
	case *parse.PipeNode: // 引数がPipeNodeの場合
		return s.validateType(s.evalPipeline(dot, arg), typ) // パイプラインを評価
	}
	// ...
}

func (s *state) evalEmptyInterface(dot reflect.Value, n parse.Node) reflect.Value {
	switch n := n.(type) {
	// ...
	case *parse.PipeNode: // 引数がPipeNodeの場合
		return s.evalPipeline(dot, n) // パイプラインを評価
	}
	// ...
}

src/pkg/text/template/parse/node.go (ASTノード)

CommandNode.String()PipeNode 引数を括弧で囲んで文字列化するロジック。

func (c *CommandNode) String() string {
	s := ""
	for i, arg := range c.Args {
		if i > 0 {
			s += " "
		}
		if arg, ok := arg.(*PipeNode); ok { // 引数がPipeNodeの場合
			s += "(" + arg.String() + ")" // 括弧で囲んで文字列化
			continue
		}
		s += arg.String()
	}
	return s
}

コアとなるコードの解説

src/pkg/text/template/parse/lex.go

  • itemLeftParen, itemRightParen: これらは、字句解析器が入力ストリームから () を識別したときに生成する新しいトークンタイプです。これにより、パーサーはこれらの記号を構文的に意味のある要素として扱えるようになります。
  • parenDepth: このフィールドは、字句解析中に現在処理している括弧のネストの深さを追跡します。( が現れるとインクリメントされ、) が現れるとデクリメントされます。これにより、字句解析器は括弧の不一致(閉じられていない括弧や、対応する開き括弧がない閉じ括弧)を検出し、適切なエラーを報告できます。例えば、{{(3}} のように閉じ括弧がない場合、}} が現れたときに parenDepth が0でないためエラーになります。また、{{3)}} のように余分な閉じ括弧がある場合、) が現れたときに parenDepth が負になるためエラーになります。
  • lexInsideAction: この関数は、{{}} の間のアクションブロック内の字句解析を処理します。ここで () が検出され、それぞれ itemLeftParenitemRightParen として発行されます。また、l.rightDelim (}}) が検出された際に parenDepth が0でない場合、つまりアクションブロックが終了するにもかかわらず括弧が閉じられていない場合にエラーを発生させるロジックが追加されました。
  • atTerminator: この関数は、現在のトークンが式の終端であるかどうかを判断します。() が追加されたことで、これらの記号もトークンの区切りとして機能するようになりました。

src/pkg/text/template/parse/parse.go

  • command() 関数内の itemLeftParen 処理:
    • command() 関数は、テンプレート内の個々のコマンド(例: printf "%q".X)を解析します。
    • itemLeftParen が検出されると、これは括弧で囲まれた新しいパイプラインの開始を示します。
    • t.pipeline("parenthesized expression") を呼び出すことで、パーサーは再帰的にこの新しいパイプラインを解析します。これにより、パイプラインがネストされた構造を持つことができるようになります。
    • パイプラインの解析後、直後に itemRightParen が続くことを期待します。もし存在しない場合は、構文エラー (missing right paren) を報告します。
    • 正しく解析されたパイプライン (p) は、現在の CommandNode の引数リストに cmd.append(p) を介して追加されます。これにより、ASTは (.Y .Z) のような構造を正しく表現できます。

src/pkg/text/template/exec.go

  • evalArg および evalEmptyInterface 関数内の *parse.PipeNode 処理:
    • これらの関数は、テンプレート実行時にコマンドの引数を評価する役割を担います。
    • 変更前は、引数として PipeNode が直接渡されることは想定されていませんでした。
    • この変更により、引数として渡されたノードが *parse.PipeNode 型である場合、s.evalPipeline(dot, arg) を呼び出してそのパイプライン全体を評価するようにロジックが追加されました。
    • これにより、{{printf "%q" (print "out" "put")}}(print "out" "put") の部分が、実行時に print 関数が呼び出され、その結果が printf 関数に渡されるという正しい動作を実現します。

src/pkg/text/template/parse/node.go

  • CommandNode.String() 関数内の *PipeNode 処理:
    • CommandNode.String() は、ASTノードをデバッグや表示のために文字列形式に変換するメソッドです。
    • この変更により、コマンドの引数が *PipeNode 型である場合、そのパイプラインの文字列表現を括弧で囲んで出力するように修正されました。
    • 例えば、{{.X (.Y .Z)}} のようなテンプレートが解析された後、ASTを文字列化すると、元の括弧の構造が (.Y .Z) のように正しく再現されます。これは、デバッグやテンプレートの構造を理解する上で非常に役立ちます。

これらの変更は、字句解析、構文解析、AST表現、そして実行というテンプレートエンジンの主要なコンポーネント全体にわたって協調して機能し、括弧によるパイプラインのグループ化という新しい構文をシームレスにサポートしています。

関連リンク

参考にした情報源リンク

  • Go言語公式ドキュメント - text/template: https://pkg.go.dev/text/template
  • Go言語公式ドキュメント - text/template/parse: https://pkg.go.dev/text/template/parse
  • コンパイラの基本(字句解析、構文解析、AST)に関する一般的な情報源:
    • Aho, Alfred V., Monica S. Lam, Ravi Sethi, and Jeffrey D. Ullman. Compilers: Principles, Techniques, and Tools. Addison-Wesley, 2007. (通称「ドラゴンブック」)
    • オンラインのコンピュータサイエンス教育リソース(例: Coursera, edX のコンパイラコースなど)I have generated the detailed explanation. I will now output it.