Repository: pyenv-win/pyenv-win (language: python)

Issue:
bug: no argument "pyenv which" or "pyenv whence" raises VBScript error
**Describe the bug**
Prior to commit 7f911351c5ac4b4dbe98b018dc94fceec30ad366 a command `pyenv which` or `pyenv whence` (without any argument) would default to `pyenv which --help` or `pyenv whence --help`, respectively. The linked commit changed the if-then logic such that that same call now produces a VBScript error by trying to access a non-existent `arg(1)`.

https://github.com/pyenv-win/pyenv-win/blob/8051923769e322d6387a5579ea3fd79f21d659a6/pyenv-win/libexec/pyenv.vbs#L125-L129

At least that is my best understanding; I'm interested to know if others have the same issue.

**To Reproduce**
Steps to reproduce the behavior:
1. Install `pyenv` as usual.
2. Run `pyenv which` or `pyenv whence` (no argument).
3. See error.

**Expected behavior**
I assume that the VBScript error is not the intended behavior and that a default to `--help` is desired instead.


**Desktop (please complete the following information):**
 - OS: Windows 10
 - Tools used: git bash
 - Version: 2.64.6.1


Discussion / hints:

Fix patch:
diff --git a/pyenv-win/bin/pyenv.bat b/pyenv-win/bin/pyenv.bat
index cbe5ab28..1b84bd4c 100644
--- a/pyenv-win/bin/pyenv.bat
+++ b/pyenv-win/bin/pyenv.bat
@@ -4,58 +4,70 @@ chcp 1250 >nul
 
 set "pyenv=cscript //nologo "%~dp0..\libexec\pyenv.vbs""
 
-rem if 'pyenv' called alone, then run pyenv.vbs
+:: if 'pyenv' called alone, then run pyenv.vbs
 if [%1]==[] (
   %pyenv%
   exit /b
 )
 
-rem use pyenv.vbs to aid resolving absolute path of "active" version into 'bindir'
+:: use pyenv.vbs to aid resolving absolute path of "active" version into 'bindir'
 set "bindir="
-for /f %%i in ('%pyenv% version') do call :normalizepath "%~dp0..\versions\%%i" bindir
+for /f %%i in ('%pyenv% vname') do call :normalizepath "%~dp0..\versions\%%i" bindir
 
-rem all help implemented as plugin
+:: all help implemented as plugin
 if /i [%2]==[--help] goto :plugin
+if /i [%1]==[--help] (
+  call :plugin %2 %1
+  exit /b
+)
+if /i [%1]==[help] (
+  if [%2]==[] call :plugin help --help
+  if not [%2]==[] call :plugin %2 --help
+  exit /b
+)
 
-rem let pyenv.vbs handle these
+:: let pyenv.vbs handle these
 set commands=rehash global local version vname version-name versions commands shims which whence help --help
 for %%a in (%commands%) do (
   if /i [%1]==[%%a] (
+    rem endlocal not really needed here since above commands do not set any variable
+    rem endlocal closed automatically with exit
+    rem no need to update PATH either
     %pyenv% %*
     exit /b
   )
 )
 
-rem jump to plugin or fall to exec
+:: jump to plugin or fall to exec
 if /i not [%1]==[exec] goto :plugin
-
-
-rem ====================================================================================
+:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 :exec
 
 if not exist "%bindir%" (
-  echo No global python version has been set yet. Please set the global version.
+  echo No global python version has been set yet. Please set the global version by typing:
+  echo pyenv global 3.7.2
   exit /b
 )
 
-shift
-
-for /f "tokens=1,2 delims=/" %%i in ("%1") do set "exepath=%%i" & set "exe=%%j"
-if [%exe%]==[] (set "exe=%exepath%") else (set "exe=%bindir%\%exepath%\%exe%")
-set "exe="%exe%""
-goto :run
+rem TODO test unicode
 
-
-
-
-rem ====================================================================================
+set cmdline=%*
+set cmdline=%cmdline:~5%
+:: update PATH to active version and run command
+:: endlocal needed only if cmdline sets a variable: SET FOO=BAR
+set cmddir=
+if exist %bindir%\%2 set cmddir=%bindir%\
+if exist %bindir%\%2.exe set cmddir=%bindir%\
+endlocal && set "path=%bindir%;%bindir%\Scripts;%path%" && %cmddir%%cmdline%
+exit /b
+:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
 :plugin
-
 set "exe=%~dp0..\libexec\pyenv-%1"
+rem TODO needed?
 call :normalizepath %exe% exe
 
 if exist "%exe%.bat" (
-  set "exe=endlocal & call "%exe%.bat""
+  set "exe=call "%exe%.bat""
 
 ) else if exist "%exe.cmd%" (
   set "exe=call "%exe%.cmd""
@@ -65,39 +77,30 @@ if exist "%exe%.bat" (
 
 ) else (
   echo pyenv: no such command '%1'
-  exit /b
+  exit /b 1
 )
 
-
-
-rem ====================================================================================
-:run
-
-rem update PATH to active version
-set "path=%bindir%;%bindir%\Scripts;%path%"
-
-rem copy params to program precisely, preserving double-quotes and percents.
-rem this is the main fix for how 'exec' processing in pyenv.vbs did not
-rem correctly handle (get passed) params with percent chars in them
-set "params="
-:paramloop
-if not [%2]==[] (
-  set "params=%params% %2"
-  shift
-  goto paramloop
-)
-
-rem run exec or plugin
-%exe% %params%
+:: replace first arg with %exe%
+set cmdline=%*
+set cmdline=%cmdline:^=^^%
+set cmdline=%cmdline:!=^!%
+set arg1=%1
+set len=1
+:loop_len
+set /a len=%len%+1
+set "arg1=%arg1:~1%"
+if not [%arg1%]==[] goto :loop_len
+
+setlocal enabledelayedexpansion
+set "cmdline=!exe! !cmdline:~%len%!"
+:: run command (no need to update PATH for plugins)
+:: endlocal needed to ensure exit will not automatically close setlocal
+:: otherwise PYTHON_VERSION will be lost
+endlocal && endlocal && %cmdline%
 exit /b
-
-
-
-
-rem ====================================================================================
-rem convert path which may have relative nodes (.. or .)
-rem to its absolute value so can be used in PATH
+:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+:: convert path which may have relative nodes (.. or .)
+:: to its absolute value so can be used in PATH
 :normalizepath
 set "%~2=%~dpf1"
-exit /b
-
+goto :eof
diff --git a/pyenv-win/libexec/libs/pyenv-lib.vbs b/pyenv-win/libexec/libs/pyenv-lib.vbs
index 0da7a69c..7a533e4f 100644
--- a/pyenv-win/libexec/libs/pyenv-lib.vbs
+++ b/pyenv-win/libexec/libs/pyenv-lib.vbs
@@ -260,9 +260,8 @@ Sub WriteWinScript(baseName, strDirBin)
     If Not objfs.FileExists(filespec) Then
         With objfs.CreateTextFile(filespec)
             .WriteLine("@echo off")
-            .WriteLine("setlocal")
             .WriteLine("chcp 1250 > NUL")
-            .WriteLine("pyenv exec "&strDirBin&"%~n0 %*")
+            .WriteLine("call pyenv exec "&strDirBin&"%~n0 %*")
             .Close
         End With
     End If
diff --git a/pyenv-win/libexec/pyenv-shell.bat b/pyenv-win/libexec/pyenv-shell.bat
index 83721159..6e1ec887 100644
--- a/pyenv-win/libexec/pyenv-shell.bat
+++ b/pyenv-win/libexec/pyenv-shell.bat
@@ -11,7 +11,6 @@ if "%1" == "--help" (
   EXIT /B
 )
 
-
 if [%1]==[] (
   if [%PYENV_VERSION%]==[] (
     echo no shell-specific version configured
@@ -29,4 +28,3 @@ if [%1]==[] (
   echo pyenv specific python requisite didn't meet. Project is using different version of python.
   echo Install python '%1' by typing: 'pyenv install %1'
 )
-
diff --git a/pyenv-win/libexec/pyenv-vname.bat b/pyenv-win/libexec/pyenv-vname.bat
index 4db1fd5e..81de234b 100644
--- a/pyenv-win/libexec/pyenv-vname.bat
+++ b/pyenv-win/libexec/pyenv-vname.bat
@@ -2,7 +2,7 @@
 setlocal
 
 if "%1" == "--help" (
-echo Usage: pyenv version-name
+echo Usage: pyenv vname
 echo.
 echo Shows the currently selected Python version.
 EXIT /B
diff --git a/pyenv-win/libexec/pyenv.vbs b/pyenv-win/libexec/pyenv.vbs
index 87059797..a4ce650c 100644
--- a/pyenv-win/libexec/pyenv.vbs
+++ b/pyenv-win/libexec/pyenv.vbs
@@ -59,31 +59,6 @@ Sub PrintHelp(cmd, exitCode)
     WScript.Quit exitCode
 End Sub
 
-Sub ExecCommand(str)
-    ' WScript.echo "kkotari: pyenv.vbs exec command..!"
-    Dim utfStream
-    Dim outStream
-    Set utfStream = CreateObject("ADODB.Stream")
-    Set outStream = CreateObject("ADODB.Stream")
-    With utfStream
-        .CharSet = "utf-8"
-        .Mode = 3 ' adModeReadWrite
-        .Open
-        .WriteText("chcp 1250 > NUL" & vbCrLf)
-        .WriteText(str & vbCrLf)
-        .Position = 3
-    End With
-    With outStream
-        .Type = 1 ' adTypeBinary
-        .Mode = 3 ' adModeReadWrite
-        .Open
-        utfStream.CopyTo outStream
-        .SaveToFile strPyenvHome & "\exec.bat", 2
-        .Close
-    End With
-    utfStream.Close
-End Sub
-
 Function getCommandOutput(theCommand)
     ' WScript.echo "kkotari: pyenv.vbs get command output..!"
     Set objCmdExec = objws.exec(thecommand)
@@ -124,7 +99,9 @@ End Sub
 
 Sub CommandWhich(arg)
     ' WScript.echo "kkotari: pyenv.vbs command which..!"
-    If arg.Count < 2 or arg(1) = "" Then
+    If arg.Count < 2 Then
+        PrintHelp "pyenv-which", 1
+    ElseIf arg(1) = "" Then
         PrintHelp "pyenv-which", 1
     End If
 
@@ -189,7 +166,9 @@ End Sub
 
 Sub CommandWhence(arg)
     ' WScript.echo "kkotari: pyenv.vbs command whence..!"
-    If arg.Count < 2 or arg(1) = "" Then
+    If arg.Count < 2 Then
+        PrintHelp "pyenv-whence", 1
+    ElseIf arg(1) = "" Then
         PrintHelp "pyenv-whence", 1
     End If
 
@@ -495,6 +474,7 @@ Sub main(arg)
            Case "whence"       CommandWhence(arg)
            Case "help"         CommandHelp(arg)
            Case "--help"       CommandHelp(arg)
+           ' Case Else           WScript.Echo "main Case Else"
         End Select
     End If
 End Sub


Test patch:
diff --git a/tests/test_pyenv.py b/tests/test_pyenv.py
index a90b2c55..3ea48bf2 100644
--- a/tests/test_pyenv.py
+++ b/tests/test_pyenv.py
@@ -3,6 +3,7 @@
 import subprocess
 from pathlib import Path
 
+
 class TestPyenvBase:
     @pytest.fixture
     def setup(self):
diff --git a/tests/test_pyenv_feature_exec.py b/tests/test_pyenv_feature_exec.py
index 3523a068..c9a69156 100644
--- a/tests/test_pyenv_feature_exec.py
+++ b/tests/test_pyenv_feature_exec.py
@@ -1,6 +1,124 @@
+import os
+import subprocess
+import sys
+import pytest
+from pathlib import Path
+from tempenv import TemporaryEnvironment
 from test_pyenv import TestPyenvBase
+from test_pyenv_helpers import TempPyEnv
 
 
 class TestPyenvFeatureExec(TestPyenvBase):
-    pass
+    _temp_env = None
+    _temp_path = None
+    ctx = None
 
+    """
+    We do not have any python version installed.
+    But we prepend the path with sys.executable dir.
+    And we remote fake python.exe (empty file generated) to ensure sys.executable is found and used.
+    This method allows us to execute python.exe.
+    But it cannot be used to play with many python versions.
+    """
+    @classmethod
+    def setup_class(cls):
+        settings = {'versions': ['3.7.7'], 'global_ver': '3.7.7'}
+        cls._temp_env = TempPyEnv(settings)
+        cls.ctx = cls._temp_env.context
+        cls._temp_path = TemporaryEnvironment({"PATH": f"{os.path.dirname(sys.executable)};"
+                                                       f"{str(Path(cls.ctx.pyenv_path, 'bin'))};"
+                                                       f"{str(Path(cls.ctx.pyenv_path, 'shims'))};"
+                                                       f"{os.environ['PATH']}"})
+        cls._temp_path.__enter__()
+        cls.ctx.pyenv('rehash')
+        os.unlink(str(Path(cls.ctx.pyenv_path, r'versions\3.7.7\python.exe')))
+
+    @classmethod
+    def teardown_class(cls):
+        cls._temp_path.__exit__(None)
+        cls._temp_env.cleanup()
+
+    @pytest.mark.parametrize(
+        "command",
+        [
+            lambda path: [str(path / "bin" / "pyenv.bat"), "exec", "python"],
+            lambda path: [str(path / "shims" / "python.bat")],
+        ],
+        ids=["pyenv exec", "python shim"],
+    )
+    @pytest.mark.parametrize(
+        "arg",
+        [
+            "Hello",
+            "Hello World",
+            "Hello 'World'",
+            'Hello "World"',  # " is escaped as \" by python
+            "Hello %World%",
+            # "Hello %22World%22",
+            "Hello !World!",
+            "Hello #World#",
+            "Hello World'",
+            'Hello World"',
+            "Hello ''World'",
+            'Hello ""World"',
+        ],
+        ids=[
+            "One Word",
+            "Two Words",
+            "Single Quote",
+            "Double Quote",
+            "Percentage",
+            # "Escaped",
+            "Exclamation Mark",
+            "Pound",
+            "One Single Quote",
+            "One Double Quote",
+            "Imbalance Single Quote",
+            "Imbalance Double Quote",
+        ]
+    )
+    def test_exec_arg(self, setup, command, arg):
+        with TemporaryEnvironment({'World': 'Earth'}):
+            args = [*command(self.__class__.ctx.pyenv_path), "-c", "import sys; print(sys.argv[1])", arg]
+            result = subprocess.run(args, capture_output=True, encoding="utf8")
+            # \x0c: generated by cls in cmd AutoRun
+            stdout = result.stdout.strip("\r\n\x0c")
+            # " is escaped as \" by python, so we unescape it here
+            stdout = stdout.replace('\\', '"')
+            stderr = result.stderr.strip()
+            if stderr != "":
+                print(f'stderr: {stderr}')
+            assert stdout == arg.replace('%World%', 'Earth')
+
+    def test_exec_set_var(self, setup):
+        with TemporaryEnvironment({'VARIABLE': 'OLD'}):
+            pyenv = str(self.__class__.ctx.pyenv_path / "bin" / "pyenv.bat")
+            args = [pyenv, "exec", "SET", "VARIABLE=NEW", "&&", "SET", "VARIABLE"]
+            result = subprocess.run(args, capture_output=True, encoding="utf8")
+            # \x0c: generated by cls in cmd AutoRun
+            stdout = result.stdout.strip("\r\n\x0c")
+            assert stdout == "VARIABLE=NEW"
+
+    @pytest.mark.parametrize(
+        "args",
+        [
+            ["--help", "exec"],
+            ["help", "exec"],
+            ["exec", "--help"],
+        ],
+        ids=[
+            "--help exec",
+            "help exec",
+            "exec --help",
+        ]
+    )
+    def test_exec_help(self, setup, args):
+        args = [str(self.__class__.ctx.pyenv_path / "bin" / "pyenv.bat"), *args]
+        result = subprocess.run(args, capture_output=True, encoding="utf8")
+        # \x0c: generated by cls in cmd AutoRun
+        stdout = result.stdout.strip("\r\n\x0c")
+        assert "\r\n".join(stdout.splitlines()[:1]) == pyenv_exec_help()
+
+
+def pyenv_exec_help():
+    return "Usage: pyenv exec <command> [arg1 arg2...]"
diff --git a/tests/test_pyenv_feature_global.py b/tests/test_pyenv_feature_global.py
index ecaa5ec7..d4ff0b0b 100644
--- a/tests/test_pyenv_feature_global.py
+++ b/tests/test_pyenv_feature_global.py
@@ -2,8 +2,23 @@
 from test_pyenv_helpers import not_installed_output, run_pyenv_test
 
 
+def pyenv_global_help():
+    return (f"Usage: pyenv global <version>\r\n"
+            f"       pyenv global --unset")
+
+
 class TestPyenvFeatureGlobal(TestPyenvBase):
-    def test_no_global_version(self, setup):
+    def test_global_help(self, setup):
+        def commands(ctx):
+            for args in [
+                ["--help", "global"],
+                ["help", "global"],
+                ["global", "--help"],
+            ]:
+                assert "\r\n".join(ctx.pyenv(args).splitlines()[:2]) == pyenv_global_help()
+        run_pyenv_test({}, commands)
+
+    def test_global_no_version(self, setup):
         def commands(ctx):
             assert ctx.pyenv("global") == "no global version configured"
         run_pyenv_test({}, commands)
diff --git a/tests/test_pyenv_feature_help.py b/tests/test_pyenv_feature_help.py
new file mode 100644
index 00000000..cb5517be
--- /dev/null
+++ b/tests/test_pyenv_feature_help.py
@@ -0,0 +1,10 @@
+from test_pyenv import TestPyenvBase
+from test_pyenv_helpers import run_pyenv_test
+
+
+class TestPyenvFeatureGlobal(TestPyenvBase):
+    def test_help(self, setup):
+        def commands(ctx):
+            stdout = "\r\n".join(ctx.pyenv("help").splitlines()[:2])
+            assert stdout.strip() == "Usage: pyenv <command> [<args>]"
+        run_pyenv_test({}, commands)
diff --git a/tests/test_pyenv_feature_shell.py b/tests/test_pyenv_feature_shell.py
index 4b691ebc..7b07890e 100644
--- a/tests/test_pyenv_feature_shell.py
+++ b/tests/test_pyenv_feature_shell.py
@@ -5,7 +5,22 @@
 from test_pyenv_helpers import not_installed_output, run_pyenv_test
 
 
+def pyenv_shell_help():
+    return (f"Usage: pyenv shell <version>\r\n"
+            f"       pyenv shell --unset")
+
+
 class TestPyenvFeatureShell(TestPyenvBase):
+    def test_shell_help(self, setup):
+        def commands(ctx):
+            for args in [
+                ["--help", "shell"],
+                ["help", "shell"],
+                ["shell", "--help"],
+            ]:
+                assert "\r\n".join(ctx.pyenv(args).splitlines()[:2]) == pyenv_shell_help()
+        run_pyenv_test({}, commands)
+
     def test_no_shell_version(self, setup):
         def commands(ctx):
             assert ctx.pyenv("shell") == "no shell-specific version configured"
diff --git a/tests/test_pyenv_feature_version.py b/tests/test_pyenv_feature_version.py
index 53fe092a..73a39c91 100644
--- a/tests/test_pyenv_feature_version.py
+++ b/tests/test_pyenv_feature_version.py
@@ -3,7 +3,21 @@
 from test_pyenv_helpers import run_pyenv_test
 
 
+def pyenv_version_help():
+    return "Usage: pyenv version"
+
+
 class TestPyenvFeatureVersion(TestPyenvBase):
+    def test_version_help(self, setup):
+        def commands(ctx):
+            for args in [
+                ["--help", "version"],
+                ["help", "version"],
+                ["version", "--help"],
+            ]:
+                assert "\r\n".join(ctx.pyenv(args).splitlines()[:2]).strip() == pyenv_version_help()
+        run_pyenv_test({}, commands)
+
     def test_no_version(self, setup):
         def commands(ctx):
             assert ctx.pyenv("version") == ("No global python version has been set yet. "
diff --git a/tests/test_pyenv_feature_version_name.py b/tests/test_pyenv_feature_version_name.py
index 328a91f6..59a20b0c 100644
--- a/tests/test_pyenv_feature_version_name.py
+++ b/tests/test_pyenv_feature_version_name.py
@@ -3,6 +3,30 @@
 from test_pyenv_helpers import run_pyenv_test
 
 
+def test_version_name_help():
+    def commands(ctx):
+        for args in [
+            ["--help", "version-name"],
+            ["help", "version-name"],
+            ["version-name", "--help"],
+        ]:
+            stdout = "\r\n".join(ctx.pyenv(args).splitlines()[:2]).strip()
+            assert stdout == "Usage: pyenv version-name"
+    run_pyenv_test({}, commands)
+
+
+def test_vname_help():
+    def commands(ctx):
+        for args in [
+            ["--help", "vname"],
+            ["help", "vname"],
+            ["vname", "--help"],
+        ]:
+            stdout = "\r\n".join(ctx.pyenv(args).splitlines()[:2]).strip()
+            assert stdout == "Usage: pyenv vname"
+    run_pyenv_test({}, commands)
+
+
 @pytest.mark.parametrize("command", ['version-name', 'vname'])
 def test_no_version(command):
     def commands(ctx):
diff --git a/tests/test_pyenv_feature_whence.py b/tests/test_pyenv_feature_whence.py
index 97f436be..dd2bcb72 100644
--- a/tests/test_pyenv_feature_whence.py
+++ b/tests/test_pyenv_feature_whence.py
@@ -2,7 +2,22 @@
 from test_pyenv_helpers import run_pyenv_test
 
 
+def pyenv_whence_usage():
+    return (f"Usage: pyenv whence [--path] <command>\r\n"
+            f"\r\n"
+            f"Shows the currently given executable contains path\r\n"
+            f"selected. To obtain python version of executable, use `pyenv whence pip'.")
+
+
 class TestPyenvFeatureWhence(TestPyenvBase):
+    def test_whence_no_arg(self, setup):
+        def commands(ctx):
+            assert ctx.pyenv("whence") == pyenv_whence_usage()
+            assert ctx.pyenv(["whence", "--help"]) == pyenv_whence_usage()
+            assert ctx.pyenv(["--help", "whence"]) == pyenv_whence_usage()
+            assert ctx.pyenv(["help", "whence"]) == pyenv_whence_usage()
+        run_pyenv_test({'versions': ['3.7.7']}, commands)
+
     def test_whence_major(self, setup):
         def commands(ctx):
             for name in ['python', 'python3', 'pip3']:
diff --git a/tests/test_pyenv_feature_which.py b/tests/test_pyenv_feature_which.py
index 1e937bdf..129eac88 100644
--- a/tests/test_pyenv_feature_which.py
+++ b/tests/test_pyenv_feature_which.py
@@ -7,7 +7,22 @@ def assert_paths_equal(actual, expected):
     assert actual.lower() == expected.lower()
 
 
+def pyenv_which_usage():
+    return (f"Usage: pyenv which <command>\r\n"
+            f"\r\n"
+            f"Shows the full path of the executable\r\n"
+            f"selected. To obtain the full path, use `pyenv which pip'.")
+
+
 class TestPyenvFeatureWhich(TestPyenvBase):
+    def test_which_no_arg(self, setup):
+        def commands(ctx):
+            assert ctx.pyenv("which") == pyenv_which_usage()
+            assert ctx.pyenv(["which", "--help"]) == pyenv_which_usage()
+            assert ctx.pyenv(["--help", "which"]) == pyenv_which_usage()
+            assert ctx.pyenv(["help", "which"]) == pyenv_which_usage()
+        run_pyenv_test({'versions': ['3.7.7']}, commands)
+
     def test_which_exists_is_global(self, setup):
         def commands(ctx):
             for name in ['python', 'python3', 'python38', 'pip3', 'pip3.8']:
diff --git a/tests/test_pyenv_helpers.py b/tests/test_pyenv_helpers.py
index 53006981..33562f8a 100644
--- a/tests/test_pyenv_helpers.py
+++ b/tests/test_pyenv_helpers.py
@@ -47,8 +47,10 @@ def pyenv_setup(settings):
     dirs = [r'bin', r'libexec\libs', r'shims', r'versions']
     for d in dirs:
         os.makedirs(Path(pyenv_path, d))
+    _, _, libexec_files = next(os.walk(src_path.joinpath('libexec')))
+    for f in libexec_files:
+        shutil.copy(Path(src_path, 'libexec', f), Path(pyenv_path, 'libexec', f))
     files = [r'bin\pyenv.bat',
-             r'libexec\pyenv.vbs',
              r'libexec\pyenv-shell.bat',
              r'libexec\libs\pyenv-install-lib.vbs',
              r'libexec\libs\pyenv-lib.vbs']
@@ -90,6 +92,45 @@ def __init__(self, pyenv, pyenv_path, local_path):
         self.local_path = local_path
 
 
+class TempPyEnv:
+    def __init__(self, settings):
+        self.tmp_path = tempfile.TemporaryDirectory()
+        settings['pyenv_path'] = pyenv_path = Path(self.tmp_path.name, 'pyenv')
+        settings['local_path'] = local_path = Path(self.tmp_path.name, 'local')
+        os.mkdir(pyenv_path)
+        os.mkdir(local_path)
+        pyenv_setup(settings)
+        self.prev_cwd = os.getcwd()
+        os.chdir(local_path)
+        self.bat = Path(pyenv_path, r'bin\pyenv.bat')
+        self.context = PyenvContext(self.pyenv, pyenv_path, local_path)
+
+    def pyenv(self, pyenv_args=None):
+        args = ['cmd', '/d', '/c', 'call', str(self.bat)]
+        if pyenv_args is not None:
+            if isinstance(pyenv_args, list):
+                args = args + pyenv_args
+            else:
+                args.append(pyenv_args)
+        result = subprocess.run(args, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        stderr = str(result.stderr, "utf-8").strip()
+        if stderr != "":
+            print(f'stderr: {stderr}')
+        # \x0c: generated by cls in cmd AutoRun
+        stdout = str(result.stdout, "utf-8").strip("\r\n\x0c")
+        return stdout
+
+    def cleanup(self):
+        os.chdir(self.prev_cwd)
+        self.tmp_path.cleanup()
+
+    def __enter__(self):
+        return self.context
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.cleanup()
+
+
 def run_pyenv_test(settings, commands):
     with tempfile.TemporaryDirectory() as tmp_path:
         settings['pyenv_path'] = pyenv_path = Path(tmp_path, 'pyenv')
@@ -101,7 +142,7 @@ def run_pyenv_test(settings, commands):
             bat = Path(pyenv_path, r'bin\pyenv.bat')
 
             def pyenv(pyenv_args=None):
-                args = ['cmd', '/d', '/c', f'call {bat}']
+                args = ['cmd', '/d', '/c', 'call', str(bat)]
                 if pyenv_args is not None:
                     if isinstance(pyenv_args, list):
                         args = args + pyenv_args