MSWindows Programming : Propogating child process exit values out of .bat scripts

“God dammit. Why won’t you just DO what I WANT you hopeless pile of crap!”

So goes the refrain. I think you know where I’m coming from. Yet again, I have ended up learning far more about crappy DOS scripting than I ever wanted to know.

So I’m writing a program to automate some small task on Windows. One of the jobs of this tool is to modify the current environment. But I don’t know how a child process in Windows can modify the environment of it’s parent (namely the command-line shell that invoked it.) Can it be done?

So I hack a ghastly workaround: Wrap the script in a .bat file. A .bat file is invoked from the command-line in the same process as the shell, so any change it makes to the environment are made to the environment of the invoking shell itself. This also has the advantage that the tool can now be invoked by typing ‘toolname’, just like on other platforms, as opposed to ‘toolname.py’ or even ‘python toolname.py’. So I wrap my Python script ‘toolname.py’ with a new file, ‘toolname.bat’, living in the same directory:

:: first run our tool
python "%~dp0%~n0.py" %*

:: then make any changes to the environment
set THIS=THAT

The %~dp0 and %~n0 gobbledygook is a batch file way of referencing the same drive, path and filename (minus extension) as the current script, to which I add ‘.py’ to run toolname.py. Easy enough.

There’s a minor problem: The environment changes that need to be made depend on what goes on inside toolname.py. So I have that Python write a new batch file to the temp directory, containing all the ‘set’ commands which will replace the hardcoded ‘set THIS=THAT’ in the above script. Then we call that new temporary bat file from here:

:: first run our tool
python "%~dp0%~n0.py" %*

:: then make any changes to the environment
call %Temp%\%~n0-setvar.bat

Which is good enough. Presumably it will barf all over the place if run concurrently. But there’s a more pressing problem. I need the exit value of this tool to be equal to the exit value from toolname.py. Currently, the exit value of this .bat script is always zero, because the ‘call’ command at the end is always successful.

One solution I’ve seen used is to remember the exit value from toolname.py, and then use the DOS exit command to propagate this value out to our caller:

:: first run our tool
python "%~dp0%~n0.py" %*
set EXITVAL=%ERRORLEVEL%

:: then make any changes to the environment
call %Temp%\%~n0-setvar.bat
 
exit %EXITVAL%

The problem with this is that ‘exit’ doesn’t do what you think it does. It doesn’t just stop interpreting the current script, rather it terminates the current interpreter, ie. the shell that is running the script. If you run this from a command-line, since Windows doesn’t differentiate between a console and a shell, your window disappears. Sigh.

The exit command has a fix for this: It takes a switch ‘/B’, that causes it to just end the current script, rather than killing the shell. But now, it ignores any %EXITVAL% parameter you try to feed it, so the exit value of your batch file is always zero.

This is what I get for developing software on Windows. Nothing ever works the way it ought to. It’s as though everything were designed to oppose simple engineering idioms, like composing systems out of small, interchangeable parts.

So here’s what I finally did. The exit value of running a batch script can be set without using the hopelessly brain-dead ‘exit’ command. It is equal to the exit value of the last process the script invokes. So instead of exit, simply find a process that will exit with the value you need, and invoke it as the final command in your batch script:

:: first run our tool
python "%~dp0%~n0.py"; %*
set EXITVAL=%ERRORLEVEL%

:: then make any changes to the environment
call %Temp%\%~n0-setvar.bat

:: and propagate the exit value to our invoker
python -c "import sys; sys.exit(%EXITVAL%)"

Bingo. I now have a Python process that can modify the environment of its invoking shell, and propagates the correct exit value out of the wrapping DOS script. [Short bow. Applause. Roses. etc.]

9 thoughts on “MSWindows Programming : Propogating child process exit values out of .bat scripts

  1. Pingback: Python:Interface for modifying Windows environment variables from Python – IT Sprite

  2. No worries, chap. I enjoy reading your blog, and I’m glad I can give something back occasionally :-).

  3. Hey Owen. Thanks for that. I didn’t know that either. I was looking at setting registry settings to enable extensions.

    If it were just me typing it, I could live with it, but I can’t ask my users to remember to type that whenever they use my program.

    I’m glad someone like you knows all these tricks though – no doubt they will come in handy for me one day. Many thanks!

  4. Well, it’s possible to do “CMD /E:ON /C whatever.bat”, which forces command extensions on while running “whatever.bat”, but I think your approach does the job, to be honest.

  5. Owen,
    Thanks heaps for that last response. Yes, you are absolutely right, I didn’t have command extensions enabled. Enabling them fixes it.

    However, I can’t rely on that as a solution, because this script is intended to be distributed publically. So I can’t count on them being enabled on the client machine.

    Unless I can enable them in the middle of my script? It would probably be rude for me to do this permenantly, so then I need to undo whatever I did. And then I need to exit with the correct exit value. But I need to use ‘exit’ before I perform the undo. Ohdear, cascading idiocy detected…

  6. To check, I created a batch file just as you described. Here’s the results:

    C:\Temp>echo %ERRORLEVEL%

    0

    C:\Temp>t.bat

    C:\Temp>exit /b 42

    C:\Temp>echo %ERRORLEVEL%

    42

    That’s pretty conclusive for me. I note that %ERRORLEVEL% only returns a value if Command Extensions are enabled — is it possible they are disabled on your system?

  7. Intruiging Owen, interesting to know that.

    I’m also on XP. I just created a test script right now to verify that I’m really seeing the behaviour I describe, and I still am.

    If I create a script, tryme.bat, which contains just the line:

    exit /B 42

    then running this script from the command line, just by typing its name, results in an %ERRORLEVEL% (or $? in Cygwin) of zero, not forty-two. Have I misunderstood something? Are you doing anything different?

  8. That’s great stuff, but one thing doesn’t fit my experience: “EXIT /B ERRORCODE” works as advertised for me, and I make use of that in various build scripts. We’re running XP here, don’t know how much that affects things.

Leave a Reply