George V. Reilly

Python Batchfile Wrapper, Redux

Python Batchfile Wrapper

Batchfile Wrapper

I’ve made some sig­nif­i­cant changes to my Python Batchfile Wrapper. The main virtue of this wrapper is that it finds python.exe and invokes it on the associated Python script, ensuring that input redi­rec­tion works.

I’ve also adapted py2bat to work with my wrapper. I’m calling my version py2cmd.

Here’s my latest batch file, which is shorter than its pre­de­ces­sor.

To use it, place it in the same directory as the Python script you want to run and give it the same basename; i.e., d:\some\path or oth­er\ex­am­ple.cmd will run d:\some\path or oth­er\ex­am­ple.py.

@echo off
setlocal
set PythonExe=
set PythonExeFlags=-u

for %%i in (cmd bat exe) do (
    for %%j in (python.%%i) do (
        call :SetPythonExe "%%~$PATH:j"
    )
)
for /f "tokens=2 delims==" %%i in ('assoc .py') do (
    for /f "tokens=2 delims==" %%j in ('ftype %%i') do (
        for /f "tokens=1" %%k in ("%%j") do (
            call :SetPythonExe %%k
        )
    )
)
"%PythonExe%" %PythonExeFlags% "%~dpn0.py" %*
goto :EOF

:SetPythonExe
if not [%1]==[""] (
    if ["%PythonExe%"]==[""] (
        set PythonExe=%~1
    )
)
goto :EOF

This is suf­fi­cient­ly cryptic that it merits some ex­pla­na­tion.

The first set of nested loops attempts to find python.cmd, python.bat, and python.exe, re­spec­tive­ly, along your PATH:

for %%i in (cmd bat exe) do (
    for %%j in (python.%%i) do (
        call :SetPythonExe "%%~$PATH:j"
    )
)

The %%~$PATH:j expression searches the PATH for %%j (i.e., python.cmd, etc). If it’s found, the expression evaluates to the full path to %%j. Otherwise, it evaluates to the empty string. I’ve bracketed the expression with double quotes in order to handle spaces in directory names.

The Set­PythonExe subroutine simply sets %PythonEx­e% to %1 if and only if %PythonEx­e% doesn’t already have a value and %1 is not empty:

System Message: WARNING/2 (<string>, line 86)

Literal block expected; none found.

We can’t set %PythonEx­e% directly in the loop. As explained at for loops and variable expansion, en­vi­ron­ment variables in the body of the loop are evaluated once before the loop starts and won’t change until after the loop terminates:

:SetPythonExe
if not [%1]==[""] (
    if ["%PythonExe%"]==[""] (
        set PythonExe=%~1
    )
)
goto :EOF

Note: the %~1 notation strips off any sur­round­ing double quotes. (ss64.com has details on parameter syntax.)

The square brackets and double quotes are necessary to make it all work if either %PythonEx­e% or %1 contains spaces. Getting this right was one of the hardest parts of the whole exercise.

The second set of nested loops are scarier:

for /f "tokens=2 delims==" %%i in ('assoc .py') do (
    for /f "tokens=2 delims==" %%j in ('ftype %%i') do (
        for /f "tokens=1" %%k in ("%%j") do (
            call :SetPythonExe %%k
        )
    )
)

The outer loop runs once: assoc .py yields .py=Python.File and %%i is set to Python.File. Running ftype Python.File yields Python.File="C:\Python24\python.exe" "%1" %* (on my machine).

The second loop also runs once: %%j is set to everything on the right-hand side of the =.

The third loop also runs once: %%k is set to the first token in %%j, "C:\Python24\python.exe", which is passed in to Set­PythonExe.

At this point, %PythonEx­e% will have a value if python.cmd (or python.bat or python.exe) existed on your path, or the .py extension was registered.

If it doesn’t have a value, then the invocation of "%PythonEx­e%" will fail, setting %er­ror­level% to 9009:

"%PythonExe%" %PythonExeFlags% "%~dpn0.py" %*
goto :EOF

%PythonEx­e­Flags% was set to -u at the beginning of the script. As explained in my Python Batchfile Wrapper post, this treats stdin, stdout, and stderr as raw streams, instead of translit­er­at­ing \r\n into \n. If you want cooked input, simply remove the -u.

The "%~dpn0.py" notation yields the absolute path to the Python script with the .py extension sitting beside this batch file: another example of parameter syntax.

Finally, goto :EOF ends execution of the batchfile, skipping the :Set­PythonExe subroutine.

Whew!

py2cmd

You can have a batchfile sitting alongside a Python script as above, or you can have a self-contained batchfile cum Python script.

py2bat has been kicking around for years. It takes a Python script and turns it into a batchfile, by relying on a couple of tricks.

I’ve adapted py2bat into a new script, py2cmd. In essence, the generated batchfile looks like this:

@echo off
REM="""
... set PythonExe as above ...
"%PythonExe%" -x %0
goto :EOF
"""

# python code starts here
# ...

When this file is executed by cmd.exe, the control flow should be obvious. Disable echoing to the screen, a funny-looking REM, set %PythonEx­e% as before (not shown), invoke python.exe with the -x flag on the current batchfile, and finally skip past the rest of the file.

When Python is invoked with the -x flag, it skips the first line of the script (@echo off). The second line sets the variable REM to the multiline string which continues down to the closing """ below the goto :EOF. Everything after that is the original Python script. All the batchfile nonsense is wrapped up inside the REM variable.

Download py2cmd.

Other Wrappers

Fredrik Lundh’s ExeMaker generates a stub executable to launch a Python script with the same basename. It requires that Python already be installed on the target machine. I couldn’t get ExeMaker to work properly. The stub executable leaves me at the Python in­ter­preter’s in­ter­ac­tive prompt.

py2exe takes a Python script and bundles up all the Python support files to make it run on a machine that doesn’t have Python installed. Works fine for me, but you get 4MB+ of associated runtime. Massive overkill if the target machine is known to have Python installed.

blog comments powered by Disqus
Bush's surge speech » « Review: The Wrong Kind of Blood