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

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

このコミットは、Go言語の標準ライブラリであるpath/filepathパッケージのSplitList関数と、os/execパッケージのLookPath関数におけるWindows環境でのPATH環境変数の扱いを改善するものです。具体的には、WindowsのPATH要素に含まれる引用符(")と、セパレータであるセミコロン(;)の解釈に関する問題を修正し、より堅牢なパス解析を実現しています。

コミット

commit b4109f801a2b51978e1ddc1918a4558a8d8ba36c
Author: Péter Surányi <speter.go1@gmail.com>
Date:   Wed Feb 20 16:19:52 2013 +1100

    path/filepath, os/exec: unquote PATH elements on Windows
    
    On Windows, directory names in PATH can be fully or partially quoted
    in double quotes ('"'), but the path names as used by most APIs must
    be unquoted. In addition, quoted names can contain the semicolon
    (';') character, which is otherwise used as ListSeparator.
    
    This CL changes SplitList in path/filepath and LookPath in os/exec
    to only treat unquoted semicolons as separators, and to unquote the
    separated elements.
    
    (In addition, fix harmless test bug I introduced for LookPath on Unix.)
    
    Related discussion thread:
    https://groups.google.com/d/msg/golang-nuts/PXCr10DsRb4/sawZBM7scYgJ
    
    R=rsc, minux.ma, mccoyst, alex.brainman, iant
    CC=golang-dev
    https://golang.org/cl/7181047

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

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

元コミット内容

Windows環境において、PATH環境変数内のディレクトリ名が二重引用符(")で完全に、または部分的に囲まれている場合があるが、ほとんどのAPIで使用されるパス名は引用符なしでなければならないという問題がありました。さらに、引用符で囲まれたパス名には、通常リストセパレータとして使用されるセミコロン(;)文字が含まれる可能性がありました。

このコミットは、path/filepathパッケージのSplitList関数とos/execパッケージのLookPath関数を変更し、引用符で囲まれていないセミコロンのみをセパレータとして扱い、分離された要素から引用符を削除するように修正します。

(加えて、Unix版のLookPathテストで導入した無害なテストバグも修正しています。)

変更の背景

WindowsのPATH環境変数は、実行可能ファイルを探すためのディレクトリのリストをセミコロンで区切って保持しています。しかし、Windowsのパスにはスペースや特殊文字が含まれることがあり、これらを正しく扱うためにパス全体または一部を二重引用符で囲む慣習があります。

Go言語のpath/filepath.SplitList関数は、このようなパス文字列を個々のディレクトリパスに分割するために使用されます。また、os/exec.LookPath関数は、PATH環境変数を利用して実行可能ファイルのフルパスを検索します。

このコミット以前のGoのこれらの関数は、WindowsのPATHにおける引用符の扱いや、引用符内のセミコロンの解釈に関して不完全な挙動を示していました。具体的には、引用符で囲まれたパス内のセミコロンを誤ってセパレータとして認識してしまったり、引用符自体をパスの一部として扱ってしまったりする問題がありました。これにより、GoアプリケーションがWindows上で外部コマンドを正しく見つけられない、または予期しないパスを解決してしまう可能性がありました。

この問題は、Goコミュニティのメーリングリストで議論されており、特にhttps://groups.google.com/d/msg/golang-nuts/PXCr10DsRb4/sawZBM7scYgJの議論スレッドで詳細が述べられています。このコミットは、その議論の結果として、WindowsのPATH環境変数のより正確な解析を実現するために導入されました。

前提知識の解説

PATH環境変数

PATH環境変数は、オペレーティングシステムが実行可能ファイル(コマンド)を探す際に参照するディレクトリのリストです。ユーザーがコマンド名を入力した際、OSはこのPATHに指定されたディレクトリを順番に検索し、最初に見つかった実行可能ファイルを実行します。

  • Windows: PATH内のディレクトリはセミコロン(;)で区切られます。例: C:\Windows;C:\Windows\System32;C:\Program Files\Git\cmd
  • Unix/Linux/macOS: PATH内のディレクトリはコロン(:)で区切られます。例: /usr/local/bin:/usr/bin:/bin

Windowsにおけるパスの引用符

Windowsのファイルシステムでは、パスにスペースや特殊文字(例: C:\Program Files)が含まれることがよくあります。コマンドラインやスクリプトでこのようなパスを扱う場合、パス全体を二重引用符(")で囲むのが一般的です。

例: C:\Program Files\My AppC:\"Program Files"\My App"C:\Program Files\My App" のように表現されることがあります。

path/filepath.SplitList

Go言語のpath/filepathパッケージは、ファイルパスを操作するためのユーティリティを提供します。SplitList関数は、PATH環境変数のようなリスト形式のパス文字列を、個々のパス要素の文字列スライスに分割するために使用されます。

os/exec.LookPath

Go言語のos/execパッケージは、外部コマンドの実行をサポートします。LookPath関数は、与えられたコマンド名(例: gitnode)がPATH環境変数内のどのディレクトリに存在するかを検索し、そのコマンドのフルパスを返します。

問題点と解決策の概要

従来のGoの実装では、WindowsのPATH文字列を分割する際に、引用符の有無に関わらずセミコロンをセパレータとして扱っていました。これにより、例えば"C:\Program Files\My;App"のようなパスがあった場合、これをC:\Program Files\MyAppの2つの要素に誤って分割してしまう可能性がありました。また、引用符自体がパスの一部として残ってしまう問題もありました。

このコミットの目的は、以下の挙動を実現することです。

  1. 引用符で囲まれていないセミコロンのみをセパレータとして認識する。
  2. 分割された各パス要素から引用符を正しく除去する。

これにより、WindowsのPATH環境変数が持つ複雑な形式(引用符、引用符内のセミコロン)を正確に解析し、Goアプリケーションが外部コマンドをより確実に実行できるようになります。

技術的詳細

このコミットの主要な変更点は、Windows環境におけるPATH文字列の解析ロジックを改善するために、splitListという新しいヘルパー関数を導入し、それをos/exec.LookPathpath/filepath.SplitListの両方で利用するようにしたことです。

splitList関数の導入

以前は、path/filepath.SplitListは単純にstrings.Split(path, string(ListSeparator))を使用していました。しかし、これは引用符や引用符内のセミコロンを考慮しないため、WindowsのPATHの特殊な要件を満たせませんでした。

新しいsplitList関数(src/pkg/os/exec/lp_windows.gosrc/pkg/path/filepath/path_windows.goに実装)は、この問題を解決するために、パス文字列を文字ごとに走査し、引用符の状態を追跡するステートマシンを実装しています。

splitListのロジック概要:

  1. 初期化:

    • list := []string{}: 分割されたパス要素を格納するスライス。
    • start := 0: 現在のパス要素の開始インデックス。
    • quo := false: 現在、引用符の内部にいるかどうかを示すブールフラグ。
  2. 文字列の走査:

    • パス文字列を先頭から末尾まで1文字ずつ走査します。
    • 各文字cに対してswitch文を使用します。
  3. 引用符の処理:

    • case c == '"': 引用符が見つかった場合、quoフラグを反転させます。これにより、引用符の開始と終了を検出します。
  4. セパレータの処理:

    • case c == os.PathListSeparator && !quo: 現在の文字がパスリストセパレータ(Windowsでは;)であり、かつ引用符の内部にいない場合のみ、そのセミコロンをセパレータとして扱います。
      • list = append(list, path[start:i]): startから現在のインデックスiまでの部分文字列を1つのパス要素としてlistに追加します。
      • start = i + 1: 次のパス要素の開始インデックスを更新します。
  5. 最後の要素の追加:

    • ループが終了した後、残りの部分文字列(path[start:])を最後のパス要素としてlistに追加します。
  6. 引用符の除去:

    • list内の各パス要素に対して、strings.Contains(s, ")で引用符が含まれているかを確認し、strings.Replace(s, ", ``, -1)を使用してすべての引用符を空文字列に置換することで除去します。

os/exec.LookPathの変更

src/pkg/os/exec/lp_windows.go内のLookPath関数は、os.Getenv("PATH")で取得したPATH文字列を分割する際に、従来のstrings.Splitの代わりに新しく定義されたsplitList関数を使用するように変更されました。

// 変更前
// for _, dir := range strings.Split(pathenv, `;`) {

// 変更後
for _, dir := range splitList(pathenv) {

path/filepath.SplitListの変更

src/pkg/path/filepath/path.go内のSplitList関数も、Windows環境では新しく定義されたsplitList関数を呼び出すように変更されました。

// 変更前
// func SplitList(path string) []string {
// 	if path == "" {
// 		return []string{}
// 	}
// 	return strings.Split(path, string(ListSeparator))
// }

// 変更後
func SplitList(path string) []string {
	return splitList(path) // Windows固有のsplitListを呼び出す
}

プラットフォームごとの実装

このコミットでは、splitList関数の実装がWindows (src/pkg/path/filepath/path_windows.go, src/pkg/os/exec/lp_windows.go) とUnix/Plan9 (src/pkg/path/filepath/path_unix.go, src/pkg/path/filepath/path_plan9.go) で異なるようにしています。Unix/Plan9では、引用符の問題がないため、従来のstrings.Splitを使用するシンプルなsplitListが引き続き使われます。これにより、プラットフォーム固有の挙動を適切に分離しています。

テストの追加と修正

src/pkg/path/filepath/path_test.goには、Windows特有のPATH文字列の分割に関する新しいテストケースwinsplitlisttestsが追加されました。これには、引用符で囲まれたパス、部分的に引用符で囲まれたパス、引用符内のセミコロンを含むパスなど、様々なエッジケースが含まれています。

また、src/pkg/path/filepath/path_windows_test.goという新しいテストファイルが追加され、TestWinSplitListTestsAreValid関数が導入されました。このテストは、winsplitlisttestsで定義された各テストケースが、実際にWindowsのコマンドプロンプト(ComSpec)でPATHを設定して実行可能ファイルを検索した場合に期待される挙動と一致するかどうかを検証します。これにより、GoのSplitListLookPathがWindowsのネイティブなPATH解析と互換性を持つことを保証しています。

Unix版のLookPathテスト (src/pkg/os/exec/lp_unix_test.go) における無害なテストバグ(f.Close()のエラーチェック漏れ)も修正されています。

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

このコミットにおけるコアとなるコードの変更は、主に以下のファイルと関数に集中しています。

  1. src/pkg/os/exec/lp_windows.go:

    • LookPath関数内でos.Getenv("PATH")の分割にstrings.Splitの代わりに新しいsplitList関数を使用するように変更。
    • Windows固有のsplitListヘルパー関数が新しく追加されました。
  2. src/pkg/path/filepath/path.go:

    • SplitList関数が、プラットフォーム固有のsplitListヘルパー関数を呼び出すように変更。
  3. src/pkg/path/filepath/path_windows.go:

    • Windows固有のsplitListヘルパー関数が新しく追加されました。この実装はos/exec/lp_windows.goのものと同一です。
  4. src/pkg/path/filepath/path_test.go:

    • Windows特有のSplitListのテストケースを定義するwinsplitlisttests変数が追加されました。
    • TestSplitList関数が、Windows環境の場合にwinsplitlisttestsも実行するように変更されました。
  5. src/pkg/path/filepath/path_windows_test.go:

    • 新規追加されたテストファイル。
    • TestWinSplitListTestsAreValid関数が、winsplitlisttestsの各ケースがWindowsの実際のPATH挙動と一致するかを検証します。
  6. src/pkg/path/filepath/path_unix.go:

    • Unix固有のsplitListヘルパー関数が追加されました。これは従来のstrings.Splitのラッパーです。
  7. src/pkg/path/filepath/path_plan9.go:

    • Plan9固有のsplitListヘルパー関数が追加されました。これも従来のstrings.Splitのラッパーです。

コアとなるコードの解説

最も重要な変更は、Windows環境におけるsplitList関数の実装です。この関数は、PATH文字列を正確に解析するための中心的なロジックを含んでいます。

以下は、src/pkg/path/filepath/path_windows.go(およびsrc/pkg/os/exec/lp_windows.go)に追加されたsplitList関数の簡略化された解説です。

func splitList(path string) []string {
	if path == "" {
		return []string{} // 空文字列の場合は空のスライスを返す
	}

	list := []string{} // 結果のパス要素を格納するスライス
	start := 0         // 現在のパス要素の開始インデックス
	quo := false       // 引用符の内部にいるかどうかのフラグ

	// パス文字列を1文字ずつ走査
	for i := 0; i < len(path); i++ {
		switch c := path[i]; {
		case c == '"': // 引用符が見つかった場合
			quo = !quo // 引用符の状態を反転させる
		case c == ListSeparator && !quo: // セパレータ(;)が見つかり、かつ引用符の内部にいない場合
			list = append(list, path[start:i]) // 現在の要素をリストに追加
			start = i + 1                      // 次の要素の開始位置を更新
		}
	}
	list = append(list, path[start:]) // 最後の要素をリストに追加

	// 各要素から引用符を除去
	for i, s := range list {
		if strings.Contains(s, `"`) {
			list[i] = strings.Replace(s, `"`, ``, -1) // すべての引用符を削除
		}
	}

	return list
}

このsplitList関数は、WindowsのPATH環境変数の特殊性を考慮して設計されています。

  • 引用符の追跡: quoフラグによって、現在の走査位置が引用符の内部にあるかどうかが正確に追跡されます。これにより、引用符で囲まれた部分内のセミコロンがセパレータとして誤って解釈されることを防ぎます。
  • 条件付きセパレータ認識: c == ListSeparator && !quoの条件により、セミコロンがセパレータとして機能するのは、それが引用符の外部にある場合のみとなります。
  • 引用符の除去: 最後に、分割された各パス要素から不要な引用符がすべて削除されます。これにより、GoのAPIが期待する「引用符なし」のパス名が提供されます。

このロジックにより、例えばPATHC:\foo;"C:\Program Files\My App";C:\barのような場合でも、C:\fooC:\Program Files\My AppC:\barという3つの正しいパス要素に分割されるようになります。

関連リンク

参考にした情報源リンク

  • 上記の「関連リンク」セクションに記載されているGoの公式リポジトリのコミット情報、CL、およびGoコミュニティの議論スレッド。