SomeoneLost
SomeoneLost

Reputation: 226

Difficulty using set/endlocal in Batch across files

I'm trying to call one bat file from another, while keeping the target variables local to itself(mostly). The code below is my failed attempt, the var i want is missing, but the local is still around. I've found some info on using setlocal here, which is how I think I have it. Also the info I'm using to push a var past setlocal is here. It's very possible I'm missing something from these. Any help is appreciated!

The calling bat:

@SET errmsg=Old Message
@CALL Target.bat TEST_JOB errmsg
@ECHO.jobname = %jobname%, but should be undefined
@IF DEFINED errmsg ECHO.Error occurred: %ERRORLEVEL% %errmsg%

The target bat:

@SETLOCAL
@ECHO OFF

:START
SET jobname=%~1
SET errmsgvar=%~2

:MAIN
CALL:ERROR "New Error Message"
ECHO.Should have never returned here
ENDLOCAL

:EXIT
@ECHO ON
@EXIT /b %ERRORLEVEL%

:ERROR "errorMesage"
ENDLOCAL & SET "%errmsgvar%=%~1"
ECHO.%errmsg% ^<-- Testing only, we don't know what the actual var will be
(GOTO) 2>NUL & GOTO:EXIT

The result:

C:\Projects\Batch>Caller.bat
New Error Message <-- Testing only, we don't know what the actual var will be
jobname = TEST_JOB, but should be undefined
Error occurred: 0 Old Message

Edit for clarity:

I'm trying to accomplish several things here...

It seems like my endlocal is being ignored, and its just doing endlocal at the end of the file, which breaks things

Upvotes: 2

Views: 669

Answers (4)

dbenham
dbenham

Reputation: 130829

As Magoo said, your TEST_JOB value is cleared properly. Your test must have been looking at a holdover result from a prior run. Best to explicitly clear the value in your calling bat, prior to the CALL, so you can never get a false result..

Your errmsg is not being set because of a flaw in your logic. More on that after I provide a little introduction to (goto) 2>nul.

You are attempting to use a relatively new (goto) 2>nul technique that is not widely known. It is effectively an exit /b except additional concatenated commands still execute, but in the context of the caller. I believe the first discovery was published at http://forum.script-coding.com/viewtopic.php?id=9050 (which I cannot read), and the first known English posting is at http://www.dostips.com/forum/viewtopic.php?f=3&t=6491.

Since that DosTips post, many useful tools have resulted:

Within your code, you try to ENDLOCAL in your error routine, but that only affects SETLOCAL that were issued while in your routine. You need to use the (goto) 2>nul trick prior to issuing the ENDLOCAL so that ENDLOCAL works properly on the parent context.

:error "errormesage"
(
  (goto) 2>nul
  endlocal
  set "%errmsgvar%=%~1"
  goto :exit
)

I'm worried about your returned ERRORLEVEL. I don't think it is returning the value you want when there is an error.

You must remember that all external commands always set the ERRORLEVEL whether there was an error or not. Almost all internal commands set the ERRORLEVEL to non-zero upon error, and some internal commands set the ERRORLEVEL to 0 upon success (but some preserve the existing ERRORLEVEL if there was no error).

It is safest to explicitly pass your desired error number into your :error routine, and then explicitly set the value upon exit. Here is one way to do it:

:error "errMsg" errNum
(
  (goto) 2>nul
  endlocal
  set "%errmsgvar%=%~1"
  cmd /c exit %2
  goto :exit
)

But I would modify things so that you always exit the same way regardless whether there was an error or not. The :exit routine below is called with the error message and error number if there was an error, and the values are set appropriately. Upon normal exit, neither value is passed in, so the errmsg variable gets cleared, and the routine returns with the current ERRORLEVEL (presumably 0).

Calling script

@setlocal
@set errmsg=Old Message
@set "jobname="
@call target.bat TEST_JOB errmsg
@if defined jobname (
  echo ERROR - jobname = %jobname%
) else (
  echo SUCCESS - jobname is undefined
)
@if defined errmsg (
  echo.Error occurred: %errorlevel% %errmsg%
) else (
  echo No error occurred
)

Target.bat

@echo off
setlocal enableDelayedExpansion

:start
set jobname=%~1
set errmsgvar=%~2

:main
set /a "1/(%random% %% 3)" || call :exit !errorlevel! "Random error"
echo Success
call :exit


:exit  [errNum]  ["errMsg"]
(
  (goto) 2>nul
  endlocal
  set "%errmsgvar%=%~2"
  echo on
  exit /b %1
)

The :main code randomly raises an error 33% of the time when it attempts to divide by zero.

Here is some sample output, showing both possible outcomes:

C:\test>test
Success
SUCCESS - jobname is undefined
No error occurred

C:\test>test
Divide by zero error.
SUCCESS - jobname is undefined
Error occurred: 1073750993 Random error

Upvotes: 3

Magoo
Magoo

Reputation: 80023

Worked perfectly for me.

I believe your test set jobname at some stage and hence your setlocal/endlocal frames simply restore the original environment.

I'd suggest you execute

set "jobname="

as the first line of your calling bat to force jobname to be clear before the action starts.

Upvotes: 1

RGuggisberg
RGuggisberg

Reputation: 4750

Change 2nd last line to

ECHO.%errmsgvar%.....

to match the 3rd to last line

By way of explanation... ENDLOCAL & SET "errmsgvar=%~1" 1. The entire line loaded and %~1 is expanded. 2. ENDLOCAL is executed and ends localization. 3. SET "errmsgvar=%~1" is executed as... SET "errmsgvar=WhateverWasIn%~1" This technique is tunneling and is a way to pass variables across locales.

Upvotes: 0

Roberts126
Roberts126

Reputation: 249

Change your second script to the following. If accounts for the ENDLOCAL at the end of the script. I believe the ENDLOCAL in the :ERROR block is for that block only and not the entire file.

@SETLOCAL
@ECHO OFF

:START
SET jobname=%~1
SET errmsgvar=%~2

:MAIN
CALL:ERROR "New Error Message"
ECHO.Should have never returned here
ENDLOCAL

:EXIT
REM case insensitive test for errmsgvar.
IF DEFINED errmsgvar (
    IF /I NOT "%errmsgvar%" == "" (
        ENDLOCAL & SET "errmsg=%errmsg%"
    )
)
@ECHO ON
@EXIT /b %ERRORLEVEL%

:ERROR "errorMesage"
ENDLOCAL & SET "errmsgvar=%~1"
SET "errmsg=%errmsgvar%"
ECHO.%errmsg% ^<-- Testing only, we don't know what the actual var will be
(GOTO) 2>NUL & GOTO:EXIT

Output becomes (extra line breaks for clarity):

Caller.bat

New Error Message <-- Testing only, we don't know what the actual var will be

jobname = TEST_JOB

Error occurred: 0 New Error Message

Upvotes: 0

Related Questions