Python Batchfile Wrapper, Redux
Batchfile Wrapper
I've made some significant 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 redirection 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 predecessor.
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 other\example.cmd
will run d:\some\path or other\example.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 sufficiently cryptic that it merits some explanation.
The first set of nested loops attempts to find
python.cmd
, python.bat
, and python.exe
, respectively,
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 SetPythonExe
subroutine simply sets %PythonExe%
to %1
if and only if %PythonExe%
doesn't already have a value and
%1
is not empty:
We can't set %PythonExe%
directly in the loop.
As explained at for loops and variable expansion,
environment 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 surrounding double quotes.
(ss64.com has details on parameter syntax.)
The square brackets and double quotes are necessary to make it all work
if either %PythonExe%
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 SetPythonExe
.
At this point, %PythonExe%
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 "%PythonExe%"
will
fail, setting %errorlevel%
to 9009:
"%PythonExe%" %PythonExeFlags% "%~dpn0.py" %*
goto :EOF
%PythonExeFlags%
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 transliterating \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 :SetPythonExe
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 %PythonExe%
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 interpreter's interactive 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.