[インデックス 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種類の文字列リテラルがあります。
-
解釈済み文字列リテラル (Interpreted String Literal):
- ダブルクォート
"で囲まれます。 - バックスラッシュ
\で始まるエスケープシーケンス(例:\n(改行),\t(タブ),\"(ダブルクォート),\\(バックスラッシュ) など)が解釈されます。 - 例:
"Hello\nWorld"は "Hello" の後に改行が入り "World" が続く文字列になります。
- ダブルクォート
-
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,uがgoldenと一致するかどうかを検証しています。これにより、\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のテストシステムが使用するディレクティブです。
$G $D/$F.go:crlf.goをコンパイルします。$L $F.$A && ./$A.out >tmp.go: コンパイルされたバイナリを実行し、その出力をtmp.goにリダイレクトします。crlf.goのmain関数は、prog変数を処理してGoのソースコードを標準出力に出力します。$G tmp.go && $L tmp.$A && ./$A.out:tmp.goをコンパイルし、実行します。このtmp.goが、\rが正しく処理されたGoのソースコードとして生成されているかを検証します。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 が正しく処理されることを保証するための包括的な検証を提供しています。
関連リンク
- Go Issue 680: https://github.com/golang/go/issues/680
- Go言語仕様 - 文字列リテラル: https://go.dev/ref/spec#String_literals (英語)
参考にした情報源リンク
- Web search results for "Go issue 680":
- Go言語の文字列リテラルに関する一般的な知識
- 改行コードに関する一般的な知識
- コンパイラの字句解析に関する一般的な知識