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

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

このコミットは、Go言語の標準ライブラリ os/exec パッケージにおけるコマンド実行時のパス解決ロジックの改善に関するものです。具体的には、実行ファイル名がパスセパレータを含むかどうかを判定する内部関数 containsPathSeparator を廃止し、より堅牢な filepath.Base 関数を利用するように変更しています。これにより、特にWindows環境における多様なパス形式(例: d:hello.txt のようなドライブ相対パス)の取り扱いが正確になり、コマンドの検索(LookPath)の挙動が改善されました。

コミット

commit aac872e11806b7a66ab51f5efab7496a36e4f3da
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Fri Feb 7 12:30:30 2014 +1100

    os/exec: use filepath.Base in Command
    
    filepath.Base covers all scenarios
    (for example paths like d:hello.txt)
    on windows
    
    LGTM=iant, bradfitz
    R=golang-codereviews, iant, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/59740050

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

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

元コミット内容

os/exec: use filepath.Base in Command

このコミットは、os/exec パッケージの Command 関数において、実行ファイル名がパスセパレータを含むかどうかの判定に filepath.Base を使用するように変更します。これにより、特にWindows環境における d:hello.txt のようなパスを含む、あらゆるシナリオが適切に処理されるようになります。

変更の背景

Goの os/exec パッケージは、外部コマンドを実行するための機能を提供します。exec.Command(name string, arg ...string) 関数は、与えられた name が単なるコマンド名(例: ls, notepad.exe)なのか、それとも実行ファイルへの完全なパス(例: /bin/ls, C:\Windows\System32\notepad.exe)なのかを区別する必要があります。

もし name が単なるコマンド名であれば、システムパス(PATH 環境変数)を検索して実行ファイルを見つける必要があります(これは LookPath 関数が行います)。しかし、name が既にパスを含んでいる場合、LookPath を呼び出す必要はなく、指定されたパスを直接使用すべきです。

この区別を行うために、以前は containsPathSeparator という内部関数が使用されていました。この関数は、文字列内にOS固有のパスセパレータ(Unix系では /、Windowsでは \ または /)が含まれているかを単純にチェックしていました。しかし、Windows環境では d:hello.txt のようなドライブ相対パスが存在します。これはパスセパレータを含まないものの、実際にはファイル名だけでなくドライブ指定を含む「パス」として扱われるべきものです。containsPathSeparator はこのようなケースを適切に処理できず、誤って LookPath を呼び出してしまう可能性がありました。

この問題を解決し、より堅牢なパス判定ロジックを導入するために、filepath.Base 関数への置き換えが決定されました。filepath.Base は、与えられたパスの最後の要素(ファイル名またはディレクトリ名)を返します。もし filepath.Base(name) の結果が元の name と同じであれば、それはパスセパレータを含まない(または Base がパスセパレータと認識しない)単純なファイル名であると判断できます。

前提知識の解説

  1. os/exec パッケージ: Go言語で外部コマンドを実行するための機能を提供する標準ライブラリです。exec.Command("command_name", "arg1", "arg2") のように使用し、指定されたコマンドを新しいプロセスとして起動します。

  2. exec.Commandname 引数: exec.Command の最初の引数 name は、実行するコマンドの名前またはパスです。

    • コマンド名の場合: 例: ls, go, notepad.exe。この場合、os/exec はシステムの PATH 環境変数を参照して実行ファイルを探します。この検索は LookPath 関数によって行われます。
    • パスの場合: 例: /bin/ls, C:\Windows\System32\notepad.exe, ./my_script.sh。この場合、os/exec は指定されたパスを直接使用します。
  3. exec.LookPath 関数: LookPath(file string) は、指定された file が実行可能ファイルであるかどうかを PATH 環境変数に従って検索し、見つかった場合はその絶対パスを返します。見つからない場合はエラーを返します。

  4. path/filepath パッケージ: ファイルパスを操作するためのユーティリティ関数を提供する標準ライブラリです。OS固有のパスセパレータ(os.PathSeparator)を考慮し、クロスプラットフォームで動作するように設計されています。

  5. filepath.Base 関数: filepath.Base(path string) は、パスの最後の要素を返します。これは通常、ファイル名またはディレクトリ名です。

    • 例: filepath.Base("/foo/bar/baz.txt")"baz.txt" を返します。
    • 例: filepath.Base("/foo/bar/")"bar" を返します。
    • 例: filepath.Base("baz.txt")"baz.txt" を返します。
    • 例: filepath.Base("d:hello.txt")"hello.txt" を返します(Windowsの場合)。
  6. os.IsPathSeparator 関数: os.IsPathSeparator(c uint8) は、指定されたバイト c がOS固有のパスセパレータである場合に true を返します。

  7. Windowsのパス形式: Windowsでは、Unix系OSとは異なるパスの表記規則がいくつか存在します。

    • ドライブレター: C:\, D:\ など。
    • バックスラッシュ: パスセパレータとして \ が主に使用されますが、/ も多くの場合で許容されます。
    • ドライブ相対パス: d:hello.txt のように、ドライブレターの後にパスセパレータなしでファイル名が続く形式。これはカレントディレクトリが D: ドライブにある場合に D:\current_dir\hello.txt を意味します。

技術的詳細

このコミットの核心は、os/exec.Command 関数が name 引数をどのように解釈するかという点にあります。

変更前は、Command 関数内で containsPathSeparator(name) というヘルパー関数が呼び出されていました。この関数は、name 文字列を1文字ずつ走査し、os.IsPathSeparator を使ってパスセパレータ(Windowsでは \/)が含まれているかをチェックしていました。

// 変更前の containsPathSeparator 関数
func containsPathSeparator(s string) bool {
	for i := 0; i < len(s); i++ {
		if os.IsPathSeparator(s[i]) {
			return true
		}
	}
	return false
}

このロジックは、/path/to/commandC:\path\to\command.exe のような一般的なパス形式では正しく機能します。しかし、Windows特有の d:hello.txt のようなパス形式では問題が生じます。この形式は、ドライブ指定を含んでいますが、\/ といった明示的なパスセパレータを含んでいません。したがって、containsPathSeparatord:hello.txt を「パスセパレータを含まない」と誤って判断し、結果として LookPath を呼び出してシステムパスを検索しようとします。これは意図しない挙動であり、d:hello.txtPATH 上に存在しない場合、コマンドが見つからないというエラーにつながる可能性があります。

この問題を解決するために、filepath.Base(name) == name という条件が導入されました。 filepath.Base(name) は、name がパスセパレータを含む場合、その最後の要素(ファイル名)を返します。例えば、filepath.Base("/bin/ls")"ls" を返します。 一方、name がパスセパレータを含まない単純なファイル名である場合、filepath.Base(name)name そのものを返します。例えば、filepath.Base("ls")"ls" を返します。 そして重要な点として、filepath.Base("d:hello.txt")"hello.txt" を返します。これは、filepath.Base がWindowsのドライブ相対パスを適切に解釈し、: をパスの一部としてではなく、ドライブ指定の一部として扱うためです。

したがって、filepath.Base(name) == name という条件は以下のように機能します。

  • name/bin/ls の場合: filepath.Base("/bin/ls")"ls""ls" == "/bin/ls"false。これはパスなので LookPath は呼ばれない。
  • namels の場合: filepath.Base("ls")"ls""ls" == "ls"true。これは単純なコマンド名なので LookPath が呼ばれる。
  • named:hello.txt の場合: filepath.Base("d:hello.txt")"hello.txt""hello.txt" == "d:hello.txt"false。これはパスなので LookPath は呼ばれない。

このように、filepath.Base(name) == name という条件は、name がシステムパスを検索して見つけるべき単純なコマンド名であるか、それとも既にパス情報を含んでいるかを、containsPathSeparator よりも正確かつ堅牢に判定することができます。特にWindowsの特殊なパス形式を考慮に入れている点が重要です。

この変更に伴い、os/exec パッケージは path/filepath パッケージに依存するようになったため、src/pkg/go/build/deps_test.go 内の依存関係リストも更新されています。

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

このコミットでは、主に以下の2つのファイルが変更されています。

  1. src/pkg/go/build/deps_test.go: os/exec パッケージの依存関係に path/filepath が追加されました。

    --- a/src/pkg/go/build/deps_test.go
    +++ b/src/pkg/go/build/deps_test.go
    @@ -125,7 +125,7 @@ var pkgDeps = map[string][]string{
     	"os":            {"L1", "os", "syscall", "time"},
     	"path/filepath": {"L2", "os", "syscall"},
     	"io/ioutil":     {"L2", "os", "path/filepath", "time"},
    -	"os/exec":       {"L2", "os", "syscall"},
    +	"os/exec":       {"L2", "os", "path/filepath", "syscall"},
     	"os/signal":     {"L2", "os", "syscall"},
     
     	// OS enables basic operating system functionality,
    
  2. src/pkg/os/exec/exec.go:

    • path/filepath パッケージがインポートされました。
    • Command 関数内の条件式が !containsPathSeparator(name) から filepath.Base(name) == name に変更されました。
    • containsPathSeparator 関数が完全に削除されました。
    --- a/src/pkg/os/exec/exec.go
    +++ b/src/pkg/os/exec/exec.go
    @@ -12,6 +12,7 @@ import (
     	"errors"
     	"io"
     	"os"
    +	"path/filepath"
     	"strconv"
     	"sync"
     	"syscall"
    @@ -111,7 +112,7 @@ func Command(name string, arg ...string) *Cmd {
     		Path: name,
     		Args: append([]string{name}, arg...),
     	}
    -	if !containsPathSeparator(name) {
    +	if filepath.Base(name) == name {
     		if lp, err := LookPath(name); err != nil {
     			cmd.lookPathErr = err
     		} else {
    @@ -121,15 +122,6 @@ func Command(name string, arg ...string) *Cmd {
     	return cmd
     }
     
    -func containsPathSeparator(s string) bool {
    -	for i := 0; i < len(s); i++ {
    -		if os.IsPathSeparator(s[i]) {
    -			return true
    -		}
    -	}
    -	return false
    -}
    -
     // interfaceEqual protects against panics from doing equality tests on
     // two interfaces with non-comparable underlying types.
     func interfaceEqual(a, b interface{}) bool {
    

コアとなるコードの解説

src/pkg/os/exec/exec.goCommand 関数内の変更がこのコミットの主要な部分です。

// 変更前
if !containsPathSeparator(name) {
    // ... LookPath を呼び出すロジック ...
}

// 変更後
if filepath.Base(name) == name {
    // ... LookPath を呼び出すロジック ...
}

この条件式は、name がシステムパスを検索して見つけるべき「単純なコマンド名」であるかどうかを判定しています。

  • 変更前 (!containsPathSeparator(name)): containsPathSeparator は、name 文字列内にOS固有のパスセパレータ(例: /\)が含まれているかをチェックしていました。もし含まれていなければ false を返し、! で反転されて true となり、LookPath が呼び出されていました。このロジックは、d:hello.txt のようなWindows特有のパス形式を正しく「パス」として認識できず、誤って LookPath を呼び出す原因となっていました。

  • 変更後 (filepath.Base(name) == name): filepath.Base(name) は、与えられたパスの最後の要素(ファイル名)を返します。

    • もし name/usr/bin/ls のようなパスであれば、filepath.Base("/usr/bin/ls")"ls" を返します。この場合、"ls" == "/usr/bin/ls"false となり、LookPath は呼び出されません。これは正しい挙動です。
    • もし namels のような単純なファイル名であれば、filepath.Base("ls")"ls" を返します。この場合、"ls" == "ls"true となり、LookPath が呼び出されます。これも正しい挙動です。
    • そして、重要な d:hello.txt のようなWindows特有のパスの場合、filepath.Base("d:hello.txt")"hello.txt" を返します。この場合、"hello.txt" == "d:hello.txt"false となり、LookPath は呼び出されません。これにより、d:hello.txt が正しく「パス」として扱われるようになります。

この変更により、os/exec.Command は、コマンド名がパスセパレータを含むかどうかをより正確に、特にWindows環境の多様なパス形式を考慮して判断できるようになりました。これにより、コマンドの検索ロジックが堅牢になり、予期せぬエラーが減少します。

また、containsPathSeparator 関数が不要になったため、コードベースから削除され、全体的なコードの簡素化にも貢献しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード
  • Windowsのパスに関する一般的な情報 (例: ドライブ相対パス)
  • filepath.Base の挙動に関するGoのドキュメントとテストケース
  • os.IsPathSeparator の挙動に関するGoのドキュメント