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

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

このコミットは、Go言語のコンパイラ(gc)において、raw string literal(バッククォートで囲まれた文字列)内のキャリッジリターン(\r)文字の扱いを実装し、テストを追加するものです。具体的には、raw string literal内で \r が出現した場合に、それを無視するようにコンパイラの字句解析器が変更されています。これは、異なるOS環境(特にWindowsのCRLF改行コード)で作成されたソースコードの互換性を高めるための重要な修正です。

コミット

commit 17264df11223436a3b05f47f58a233961b43c3f6
Author: Russ Cox <rsc@golang.org>
Date:   Thu Dec 15 10:47:09 2011 -0500

    gc: implement and test \r in raw strings
    
    For issue 680.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/5492046

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

https://github.com/golang/go/commit/17264df11223436a3b05f47f58a233961b43c3f6

元コミット内容

gc: implement and test \r in raw strings

For issue 680.

R=ken2
CC=golang-dev
https://golang.org/cl/5492046

変更の背景

この変更は、Go言語のIssue 680「deal with files using \r\n or \r line endings」(\r\n または \r の行末を持つファイルの処理)に関連しています。

Go言語のソースコードは、通常Unix系のシステムで一般的なLF(Line Feed, \n)を改行コードとして使用することを想定しています。しかし、Windows環境ではCRLF(Carriage Return + Line Feed, \r\n)が標準的な改行コードとして用いられます。また、古いMac OSではCR(Carriage Return, \r)のみが使われることもありました。

Goのraw string literal(バッククォート ` で囲まれた文字列)は、エスケープシーケンスを解釈せず、バッククォートとバッククォートの間の文字をそのまま文字列の内容として扱います。この特性は、正規表現やHTML、JSONなどの複数行にわたるテキストを記述する際に非常に便利です。

しかし、raw string literal内に \r が含まれている場合、特にCRLF改行コードを持つファイルから読み込まれた際に問題が発生する可能性がありました。Goのコンパイラが \r を文字として解釈してしまうと、意図しない文字が文字列に含まれてしまい、プログラムの挙動が変わってしまう恐れがありました。

このコミットは、raw string literalの内部で \r が出現した場合に、それを無視することで、異なるOS環境で作成されたソースコードの互換性を確保し、開発者が改行コードの違いを意識することなくraw string literalを使用できるようにすることを目的としています。これにより、Goのソースコードがよりポータブルになります。

前提知識の解説

Go言語の文字列リテラル

Go言語には主に2種類の文字列リテラルがあります。

  1. 解釈済み文字列リテラル (Interpreted String Literal):

    • ダブルクォート " で囲まれます。
    • バックスラッシュ \ で始まるエスケープシーケンス(例: \n (改行), \t (タブ), \" (ダブルクォート), \\ (バックスラッシュ) など)が解釈されます。
    • 例: "Hello\nWorld" は "Hello" の後に改行が入り "World" が続く文字列になります。
  2. Raw文字列リテラル (Raw String Literal):

    • バッククォート ` で囲まれます。
    • エスケープシーケンスは一切解釈されず、バッククォートとバッククォートの間の文字がそのまま文字列の内容となります。
    • 改行もそのまま文字列に含まれます。
    • 例: `Hello\nWorld` は "Hello"、バックスラッシュ、"n"、"World" という文字がそのまま並んだ文字列になります。
    • この特性から、複数行のテキストや、正規表現のようにバックスラッシュを多用する文字列の記述に適しています。

改行コード

テキストファイルにおける改行の表現方法は、オペレーティングシステムによって異なります。

  • LF (Line Feed, \n, ASCII 10): Unix、Linux、macOS(OS X以降)で主に使用されます。
  • CRLF (Carriage Return + Line Feed, \r\n, ASCII 13 + ASCII 10): Windowsで主に使用されます。タイプライターのキャリッジリターン(行頭に戻る)とラインフィード(次の行に進む)に由来します。
  • CR (Carriage Return, \r, ASCII 13): 古いMac OS(OS 9以前)で主に使用されました。

Goコンパイラは、ソースファイルを読み込む際にこれらの改行コードを適切に処理する必要があります。特にraw string literalにおいては、エスケープシーケンスとして解釈されないため、\r が文字列の内容として含まれてしまうと問題になる可能性がありました。

字句解析 (Lexical Analysis)

コンパイラは、ソースコードを機械が理解できる形式に変換するソフトウェアです。その最初の段階が「字句解析」または「スキャン」と呼ばれます。 字句解析器(lexerまたはscanner)は、ソースコードの文字ストリームを読み込み、意味のある最小単位である「トークン」(キーワード、識別子、演算子、リテラルなど)に分割します。

このコミットでは、Goコンパイラの字句解析器の一部である src/cmd/gc/lex.c が変更されています。特に、文字列リテラルを解析する部分で、入力ストリームから文字を読み込む際に \r を特別に扱うように修正が加えられています。

技術的詳細

このコミットの技術的な核心は、Goコンパイラの字句解析器がraw string literalを処理する際に、キャリッジリターン(\r)文字を無視するように変更された点にあります。

変更は src/cmd/gc/lex.c ファイルの l0 ラベルが付いたセクション、具体的には文字列リテラルを読み込むループ内で行われています。

元のコードでは、getr() 関数(おそらく入力ストリームから次の文字を読み込む関数)から読み込んだ文字 c をそのまま文字列の内容として追加していました。しかし、この変更により、getr() から読み込んだ文字 c\r である場合、その文字をスキップして次の文字の読み込みに進む continue ステートメントが追加されました。

			c = getr();
			if(c == '\r')
				continue;
			if(c == EOF) {
				yyerror("eof in string");
				break;

この修正により、raw string literalが \r\n の改行コードを含むファイルから読み込まれた場合でも、\r は文字列の内容として含まれず、\n のみが改行として扱われるようになります。これにより、Windows環境で作成されたGoのソースファイルが、Unix/Linux環境で期待通りに動作するようになります。

また、この変更を検証するために test/crlf.go という新しいテストファイルが追加されました。このテストファイルは、raw string literal内に \r\r\n が含まれる様々なケースを定義し、それらが最終的に \n のみを含む期待される文字列("hello\n world")と一致するかどうかを検証しています。

テストファイル crlf.go の主要な部分は以下の通りです。

  • prog 変数に、CR\r に置換される)や BQ(バッククォートに置換される)を含むテンプレート文字列が定義されています。
  • main 関数内で、これらのプレースホルダーが実際の文字に置換され、fmt.Print(prog) によって出力されます。
  • s, t, u という3つのraw string literalが定義されており、それぞれ異なる方法で \r や改行を含んでいます。
  • golden 変数には、期待される最終的な文字列 "hello\n world" が定義されています。
  • main 関数内の if ステートメントで、s, t, ugolden と一致するかどうかを検証しています。これにより、\r が正しく無視され、\n のみが改行として認識されていることが確認されます。

このテストは、コンパイラの変更が意図した通りに機能し、raw string literal内の \r が正しく処理されることを保証します。

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

src/cmd/gc/lex.c の差分

--- a/src/cmd/gc/lex.c
+++ b/src/cmd/gc/lex.c
@@ -840,6 +840,8 @@ l0:
 			ncp += ncp;
 		}
 		c = getr();
+		if(c == '\r')
+			continue;
 		if(c == EOF) {
 			yyerror("eof in string");
 			break;

test/crlf.go の新規ファイル

// $G $D/$F.go && $L $F.$A && ./$A.out >tmp.go &&
// $G tmp.go && $L tmp.$A && ./$A.out
// rm -f tmp.go

// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Test source files and strings containing \r and \r\n.

package main

import (
	"fmt"
	"strings"
)

func main() {
	prog = strings.Replace(prog, "BQ", "`", -1)
	prog = strings.Replace(prog, "CR", "\r", -1)
	fmt.Print(prog)
}

var prog = `
package main
CR

import "fmt"

var CR s = "hello\n" + CR
	" world"CR

var t = BQhelloCR
+ worldBQ

var u = BQhCReCRlCRlCRoCR
+ worldBQ

var golden = "hello\n world"

func main() {
	if s != golden {
		fmt.Printf("s=%q, want %q", s, golden)
	}
	if t != golden {
		fmt.Printf("t=%q, want %q", t, golden)
	}
	if u != golden {
		fmt.Printf("u=%q, want %q", u, golden)
	}
}
`

コアとなるコードの解説

src/cmd/gc/lex.c の変更点

 		c = getr();
+		if(c == '\r')
+			continue;
 		if(c == EOF) {
 			yyerror("eof in string");
 			break;
  • c = getr();: この行は、入力ストリームから次の文字を読み込み、変数 c に格納します。getr() は、Goコンパイラの字句解析器がソースコードを読み進めるための内部関数です。
  • if(c == '\r'): 読み込んだ文字 c がキャリッジリターン(\r)であるかどうかをチェックします。
  • continue;: もし c\r であった場合、この continue ステートメントが実行されます。これは、現在のループの残りの処理をスキップし、次のイテレーション(つまり、次の文字の読み込み)に進むことを意味します。これにより、\r 文字は文字列の内容として追加されることなく無視されます。
  • if(c == EOF) { ... }: \r のチェックの後に、ファイルの終端(EOF)に達したかどうかのチェックが続きます。これは、文字列リテラルが閉じられる前にファイルが終了してしまった場合の構文エラーを検出するためのものです。

この変更により、raw string literalの解析中に \r が検出されても、それは文字列の一部として扱われず、単にスキップされるようになりました。結果として、"foo\r\nbar" のようなraw string literalは、"foo\nbar" と同じ内容を持つことになります。

test/crlf.go の解説

このテストファイルは、\r の処理が正しく行われることを検証するためのものです。

  • テストスクリプトのヘッダ:

    // $G $D/$F.go && $L $F.$A && ./$A.out >tmp.go &&
    // $G tmp.go && $L tmp.$A && ./$A.out
    // rm -f tmp.go
    

    これはGoのテストシステムが使用するディレクティブです。

    1. $G $D/$F.go: crlf.go をコンパイルします。
    2. $L $F.$A && ./$A.out >tmp.go: コンパイルされたバイナリを実行し、その出力を tmp.go にリダイレクトします。crlf.gomain 関数は、prog 変数を処理してGoのソースコードを標準出力に出力します。
    3. $G tmp.go && $L tmp.$A && ./$A.out: tmp.go をコンパイルし、実行します。この tmp.go が、\r が正しく処理されたGoのソースコードとして生成されているかを検証します。
    4. rm -f tmp.go: 一時ファイルを削除します。
  • main 関数内の置換処理:

    func main() {
    	prog = strings.Replace(prog, "BQ", "`", -1)
    	prog = strings.Replace(prog, "CR", "\r", -1)
    	fmt.Print(prog)
    }
    

    prog 変数に定義されたテンプレート文字列内のプレースホルダー BQ をバッククォート に、CRを実際のキャリッジリターン\rに置換しています。そして、その結果を標準出力に出力しています。この出力がtmp.go として保存され、再度コンパイル・実行されることで、\r` の処理が検証されます。

  • prog 変数内のテストケース: prog 変数内には、Goのソースコードとして解釈されるべき文字列が定義されています。この中に、\r\r\n を含むraw string literalのテストケースが含まれています。

    • var CR s = "hello\n" + CR " world"CR: これは、\r が文字列リテラルの途中に挿入された場合のテストです。CR\r に置換されるため、"hello\n" の後に \r が入り、その後に " world" が続く形になります。コンパイラが \r を無視すれば、s"hello\n world" となるはずです。

    • var t = BQhelloCR + worldBQ: これは、raw string literal で囲まれた文字列内に\r が含まれる場合のテストです。BQ はバッククォートに、CR\r に置換されます。tは ``hello\r\n world`` のような形になります。コンパイラが\r を無視すれば、t"hello\n world"` となるはずです。

    • var u = BQhCReCRlCRlCRoCR + worldBQ: これは、raw string literal内で複数の \r が連続して出現する場合のテストです。同様に、u"hello\n world" となるはずです。

  • 検証ロジック:

    var golden = "hello\n world"
    
    func main() {
    	if s != golden {
    		fmt.Printf("s=%q, want %q", s, golden)
    	}
    	if t != golden {
    		fmt.Printf("t=%q, want %q", t, golden)
    	}
    	if u != golden {
    		fmt.Printf("u=%q, want %q", u, golden)
    	}
    }
    

    golden 変数には、期待される最終的な文字列 "hello\n world" が格納されています。main 関数内で、s, t, u の各変数が golden と一致するかどうかを比較しています。もし一致しない場合は、エラーメッセージが出力されます。これにより、\r が正しく無視され、\n のみが改行として認識されていることが確認されます。

このテストは、コンパイラの変更が意図した通りに機能し、raw string literal内の \r が正しく処理されることを保証するための包括的な検証を提供しています。

関連リンク

参考にした情報源リンク