Bypass ENDLOCAL barrier

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

Message
Author
SilverHawk
Posts: 17
Joined: 04 Mar 2015 01:42

Bypass ENDLOCAL barrier

#1 Post by SilverHawk » 04 Mar 2015 02:43

Hi there,
I kindly need your support to solve a matter that is driving me crazy since a couple of days...
I can't export the variables (that handle the last error) outside of every function in my library.

Code: Select all

@echo off
%~d0
cd %~dp0

SET "Library.TRUE=TRUE"
SET "Library.Error.Occurred="
SET "Library.Error.Code="
SET "Library.Error.Message="
SET "Library.Error.Date="
SET "Library.Error.Time="

SETLOCAL EnableDelayedExpansion

ECHO Calling [ReceiveError]...
CALL:ReceiveError

:: ### STEP 3 ### - Here I should receive the error
ECHO ErrOccurred [%Library.Error.Occurred%]
ECHO ErrMessage [%Library.Error.Message%]
GOTO:EOF

:ReceiveError
SETLOCAL
IF 0 EQU 0 (
  :: ### STEP 1 ## - Generate an example error
  CALL:GenerateError
  IF "!Library.Error.Occurred!" EQU "%Library.TRUE%" (
    GOTO MyExit
  )
)
REM Do some stuff
REM Do some stuff
REM Do some stuff
:MyExit
(
  ENDLOCAL
  :: ### STEP 2 ### - Export the error variables outside, bypassing ENDLOCAL barrier
  SET "Library.Error.Occurred=%Library.Error.Occurred%"
  SET "Library.Error.Code=%Library.Error.Code%"
  SET "Library.Error.Message=%Library.Error.Message%"
  SET "Library.Error.Date=%Library.Error.Date%"
  SET "Library.Error.Time=%Library.Error.Time%"
  SET "O_01=0"
)
GOTO:EOF

:GenerateError
SETLOCAL
IF 0 NEQ 1 (
  (
    ENDLOCAL
    CALL:Library.Error.Handle Y, 1, "This is an error example"
    GOTO:EOF
  )
)
GOTO:EOF

:Library.Error.Handle
:: Description:
::   Handle error from data provided
:: IN: ARGUMENTS
::  %~1: Show to Screen
::  %~2: Error Code
::  %~3: Error Message
SET "Library.Error.Occurred=%Library.TRUE%"
SET "Library.Error.Code=%~2"
SET "Library.Error.Message=%~3"
SET "Library.Error.Date=%DATE%"
SET "Library.Error.Time=%TIME%"
IF "%~1" EQU "Y" (
  ECHO ### ERROR ###: Error occurred
  ECHO ### ERROR ###:   Message: [%Library.Error.Message%]
  ECHO ### ERROR ###:   Code: [%Library.Error.Code%]
  ECHO ### ERROR ###:   Date and Time: [%Library.Error.Date% %Library.Error.Time%]
)
GOTO:EOF


What I need is to have a better method to export all the variables I need.
In ### STEP 2 ### I wish to replace:

Code: Select all

  SET "Library.Error.Occurred=%Library.Error.Occurred%"
  SET "Library.Error.Code=%Library.Error.Code%"
  SET "Library.Error.Message=%Library.Error.Message%"
  SET "Library.Error.Date=%Library.Error.Date%"
  SET "Library.Error.Time=%Library.Error.Time%"
  SET "O_01=0"

with a macro / function / something else:

Code: Select all

  %SomethingThatExportEveryErrorVariables%
  SET "O_01=0"


I prefer to get an "Object" method, calling some routine or macro, because IMHO it would be better for at least these reasons:
1) To have only one instruction, to avoid copy/paste errors or type mistakes (maybe 1 line wrong out of 5);
2) To have a more manageable flow: any modify inside routine / macro in the library will take effect everywhere with zero modifies outside of the library;
3) Improves readability.

What do you suggest?
What's wrong in this code?

Any hint is really appreciated
Thanks in advance

SilverHawk

EDIT: I modified routine ReceiveError and some below code to explain better what's my request. I hope it helps.
Last edited by SilverHawk on 06 Mar 2015 03:20, edited 1 time in total.

foxidrive
Expert
Posts: 6031
Joined: 10 Feb 2012 02:20

Re: Bypass ENDLOCAL barrier

#2 Post by foxidrive » 04 Mar 2015 05:35

You might like to explain in words which aspect is causing you problems and show how it is happening.

jeb
Expert
Posts: 1055
Joined: 30 Aug 2007 08:05
Location: Germany, Bochum

Re: Bypass ENDLOCAL barrier

#3 Post by jeb » 04 Mar 2015 06:08

Hi SilverHawk,

you can't use a function call to export variables over the endlocal barrier, but you can use a macro.

You could read return over an endlocal barrier with a macro

Squashman
Expert
Posts: 4486
Joined: 23 Dec 2011 13:59

Re: Bypass ENDLOCAL barrier

#4 Post by Squashman » 06 Mar 2015 07:14

You edited your original post and you were still in the moderation group where all your posts have to be approved. DO NOT EDIT YOUR POSTS AFTER PEOPLE HAVE ALREADY REPLIED TOO THEM!

SilverHawk
Posts: 17
Joined: 04 Mar 2015 01:42

Re: Bypass ENDLOCAL barrier

#5 Post by SilverHawk » 06 Mar 2015 08:40

Sorry for this misunderstanding, in other sites is preferable to edit previous posts instead of doing whole copy / paste of the original, but modified, question.
I did in this way hoping it would be better to read the post. Only for readibility reasons.

Regaring the question, how do you suggest I can proceed?
Thanks again

SilverHawk

foxidrive
Expert
Posts: 6031
Joined: 10 Feb 2012 02:20

Re: Bypass ENDLOCAL barrier

#6 Post by foxidrive » 07 Mar 2015 01:16

SilverHawk wrote:Sorry for this misunderstanding, in other sites is preferable to edit previous posts instead of doing whole copy / paste of the original, but modified, question.

Regulars may use the "read new posts" option and so they will never see the edits in your previous posts.
Regaring the question, how do you suggest I can proceed?


Explain in words which aspect is causing you problems and show how it is happening.

SilverHawk
Posts: 17
Joined: 04 Mar 2015 01:42

Re: Bypass ENDLOCAL barrier

#7 Post by SilverHawk » 09 Mar 2015 08:05

@foxidrive: you're right. I use to modify the main question and, after, add a new post with "I modified the question in these points... Check it out". A new user has to read only the first post, to understand the question, instead to read many of them. Anyway no matter, I'll follow this rule, thanks for your kind reply.

This is what I wish: to replace this code

Code: Select all

  SET "Library.Error.Occurred=%Library.Error.Occurred%"
  SET "Library.Error.Code=%Library.Error.Code%"
  SET "Library.Error.Message=%Library.Error.Message%"
  SET "Library.Error.Date=%Library.Error.Date%"
  SET "Library.Error.Time=%Library.Error.Time%"
  SET "O_01=0"

with a macro / function / something else:

Code: Select all

  %SomethingThatExportEveryErrorVariables%
  SET "O_01=0"

Also, removing the

Code: Select all

GOTO MyExit
could be better.
This macro / function will be in the main library and called everytime I need.

Calling the jeb's macro, as suggested, doesn't move the problem: I've to write at least 5 lines per routine, exactly as now.
Or am I missing something?

SilverHawk

Ed Dyreen
Expert
Posts: 1569
Joined: 16 May 2011 08:21
Location: Flanders(Belgium)
Contact:

Re: Bypass ENDLOCAL barrier

#8 Post by Ed Dyreen » 09 Mar 2015 17:13

I believe you are missing something, jeb's macro technique allows you to do this;

Code: Select all

setlocal
:: (
       set A=alpha
       set B=beta
       set C=ceta
:: )
( %enlocal_% A, B, C )
I suggest to study jeb's link.

I suggest;

Code: Select all

set errorVars=error.A, error.B, error.C
setlocal
:: (
       set error.A=alpha
       set error.B=beta
       set error.C=ceta
:: )
( %enlocal_% %errorVars% )
I think it's a bad idea to create a macro that will export only error variables, macro's have a devastating effect on memory.

Aacini
Expert
Posts: 1914
Joined: 06 Dec 2011 22:15
Location: México City, México
Contact:

Re: Bypass ENDLOCAL barrier

#9 Post by Aacini » 09 Mar 2015 17:46

Excuse me. When I read your post the first time I just didn't understood what you want. You inserted a series of parentheses in places that don't require them, and placed SETLOCAL/ENDLOCAL commands in very unusual places. After read your last modified code I finally understood that the series of SET commands below "STEP 2" have the purpose of "bypass ENDLOCAL barrier", that is, return values to the environment of the caller program. However, if you understand how this mechanism work, you should realize that your further questions just don't apply here.

These SET statements can not be included in a subroutine invoked via CALL, because the previous ENDLOCAL command can not be included before invoking the subroutine, and if it is included in the subroutine, then the mechanism don't works. When you question how to replace these 5 SET commands:

Code: Select all

  SET "Library.Error.Occurred=%Library.Error.Occurred%"
  SET "Library.Error.Code=%Library.Error.Code%"
  SET "Library.Error.Message=%Library.Error.Message%"
  SET "Library.Error.Date=%Library.Error.Date%"
  SET "Library.Error.Time=%Library.Error.Time%"

... with something else, shorter code, the answer is simple:

Code: Select all

for %%a in (Occured Code Message Date Time) do SET "Library.Error.%%a=!Library.Error.%%a!"

However, you forgot that this mechanism is not comprised of just the SET commands, but also by the parentheses and the ENDLOCAL command before them. In order for this mechanism to work, the complete variable names enclosed in percent signs must be explicitly written, for example:

Code: Select all

(
   ENDLOCAL
   for %%a in ( "Library.Error.Occurred=%Library.Error.Occurred%"
                "Library.Error.Code=%Library.Error.Code%"
                "Library.Error.Message=%Library.Error.Message%"
                "Library.Error.Date=%Library.Error.Date%"
                "Library.Error.Time=%Library.Error.Time%" ) do SET %%a
)

Previous code have "just" one FOR command instead of 5 set commands! However, I am afraid it is not exactly what you are looking for...

Although previous method is not what you want, it suggest a different approach that may work:

Code: Select all

for /F "delims=" %%a in ('set Library.Error.') do ENDLOCAL & SET "%%a"

Previous code execute an ENDLOCAL and a SET command with each one of the desired variables. Because this code is executed inside a subroutine that have just one SETLOCAL command at beginning, the additional ENDLOCAL commands have not any effect...

You may even define a macro with the previous command:

Code: Select all

set ExportErrorVariables=for /F "delims=" %%a in ('set Library.Error.') do ENDLOCAL ^& SET "%%a"

... and then use it as you originally wanted:

Code: Select all

%ExportErrorVariables%
SET "O_01=0"

Antonio

SilverHawk
Posts: 17
Joined: 04 Mar 2015 01:42

Re: Bypass ENDLOCAL barrier

#10 Post by SilverHawk » 10 Mar 2015 08:33

Thanks everyone for your quick reply.

I already followed the jeb's link, but for what I read actually the macro is able to return only one variable. Anyway I used the macro, with initialization part that now I omit, that begin with:

Code: Select all

set ^"jebReturn=for %%# in (1 2) do if %%#==2 (%\n%
   setlocal EnableDelayedExpansion%\n%
   set safeReturn_count=0%\n%
   for %%C in (!args!) do set "safeReturn[!safeReturn_count!]=%%~C" ^& set /a safeReturn_count+=1%\n%
   if not defined safeReturn[2] set "safeReturn[2]=1"%\n%
   ...

Is it the right code?
In the example provided by Ed, it's called the macro:

Code: Select all

%enlocal_%

where can I find this macro? Is it the jeb's one renamed?

Also, following the Ed's and Antonio's example, I tried to rework a macro to obtain what I need but anyway I'm not able to pass more than the first variable (due to the ENDLOCAL call, I suppose):

Code: Select all

set Library.Generic.ReturnVariables=for /L %%a in (1 1 2) do if %%a==2 (%\n%
      for /L %%b in (1 1 26) do (%\n%
        IF DEFINED argv ( %\n%
          for /F "tokens=1-26 delims=," %%c in ("!argv!") do (%\n%
             SET "argv=%%d" %\n%
             for /F "delims=" %%z in ('SET %%c') do (%\n%
               ENDLOCAL %\n%
               ECHO SET "%%z" %\n%
               SET "%%z" %\n%
             ) %\n%
          ) %\n%
        ) %\n%
      ) %\n%
) ELSE set argv=Library.Error.Occurred, Library.Error.Code, Library.Error.Message, Library.Error.Date, Library.Error.Time,

so when I execute it with:

Code: Select all

SET "O_01=blabla"
%Library.Generic.ReturnVariables% O_01

I expect to return not only all errors variables, but also the O_01.
This is the actual result:

Code: Select all

ErrOccurred [TRUE]
ErrMessage []
O_01 []


What I wish to obtain is exactly what is suggested by Ed, but slightly modified:

Code: Select all

set errorVars=error.A, error.B, error.C
setlocal
:: (
       set error.A=alpha
       set error.B=beta
       set error.C=ceta
:: )
( %enlocal_% %errorVars%, O_01, O_02)


Last but not least, thanks for your precious support.

SilverHawk

jeb
Expert
Posts: 1055
Joined: 30 Aug 2007 08:05
Location: Germany, Bochum

Re: Bypass ENDLOCAL barrier

#11 Post by jeb » 10 Mar 2015 10:29

I changed the macro a bit, so it can handle more than one variable to tranfer it over a endlocal barrier.
And I added comments for better understanding the obvious code :)

So now you only need to call it

Code: Select all

%endlocal% Library.Error.Occurred Library.Error.Code Library.Error.Message Library.Error.Date Library.Error.Time


The macro can't handle neigther newline nor carriage return in variable contents anymore, I currently spend no time into handle these too.

The macro itselt looks now

Code: Select all

setlocal DisableDelayedExpansion
set LF=^


set ^"\n=^^^%LF%%LF%^%LF%%LF%^^"
%=   I use EDE for EnableDelayeExpansion and DDE for DisableDelayedExpansion =%

set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n%
   setlocal EnableDelayedExpansion%\n%
 %=       Take all variable names into the varName array       =%%\n%
   set varName_count=0%\n%
   for %%C in (!args!) do set "varName[!varName_count!]=%%~C" ^& set /a varName_count+=1%\n%
 %= Build one variable with a list of set statements for each variable delimited by newlines =%%\n%
 %= The lists looks like --> set result1=myContent\n"set result1=myContent1"\nset result2=content2\nset result2=content2\n     =%%\n%
 %= Each result exists two times, the first for the case returning to DDE, the second for EDE =%%\n%
 %= The correct line will be detected by the (missing) enclosing quotes  =%%\n%
   set "retContent=1!LF!"%\n%
   for /L %%n in (0 1 !varName_count!) do (%\n%
      for /F "delims=" %%C in ("!varName[%%n]!") DO (%\n%
         set "content=!%%C!"%\n%
         set "retContent=!retContent!"set !varName[%%n]!=!content!"!LF!"%\n%
 %=      This complex block is only for replacing '!' with '^!'      =%%\n%
 %=    First replacing   '"'->'""q'   '^'->'^^' =%%\n%
         set ^"content_EDE=!content:"=""q!"%\n%
         set "content_EDE=!content_EDE:^=^^!"%\n%
 %= Now it's poosible to use CALL SET and replace '!'->'""e!' =%%\n%
         call set "content_EDE=%%content_EDE:^!=""e^!%%"%\n%
         %= Now it's possible to replace '""e' to '^', this is effectivly '!' -> '^!'  =%%\n%
         set "content_EDE=!content_EDE:""e=^!"%\n%
         %= Now restore the quotes  =%%\n%
         set ^"content_EDE=!content_EDE:""q="!"%\n%
         set "retContent=!retContent!set "!varName[%%n]!=!content_EDE!"!LF!"%\n%
      )%\n%
   )%\n%
 %= Now return all variables from retContent over the barrier =%%\n%
   for /F "delims=" %%V in ("!retContent!") DO (%\n%
 %= Only the first line can contain a single 1 =%%\n%
      if "%%V"=="1" (%\n%
 %= We need to call endlocal twice, as there is one more setlocal in the macro itself =%%\n%
         endlocal%\n%
         endlocal%\n%
      ) ELSE (%\n%
 %= This is true in EDE             =%%\n%
         if "!"=="" (%\n%
            if %%V==%%~V (%\n%
               %%V !%\n%
            )%\n%
         ) ELSE IF not %%V==%%~V (%\n%
            %%~V%\n%
         )%\n%
      )%\n%
   )%\n%
 ) else set args="

Aacini
Expert
Posts: 1914
Joined: 06 Dec 2011 22:15
Location: México City, México
Contact:

Re: Bypass ENDLOCAL barrier

#12 Post by Aacini » 10 Mar 2015 10:37

@SilverHawk:

Did you tested my solution? In your example just change these lines:

Code: Select all

:MyExit
(
  ENDLOCAL
  :: ### STEP 2 ### - Export the error variables outside, bypassing ENDLOCAL barrier
  SET "Library.Error.Occurred=%Library.Error.Occurred%"
  SET "Library.Error.Code=%Library.Error.Code%"
  SET "Library.Error.Message=%Library.Error.Message%"
  SET "Library.Error.Date=%Library.Error.Date%"
  SET "Library.Error.Time=%Library.Error.Time%"
  SET "O_01=0"
)
GOTO:EOF

... by these ones:

Code: Select all

:MyExit
  :: ### STEP 2 ### - Export the error variables outside, bypassing ENDLOCAL barrier
  for /F "delims=" %%a in ('set Library.Error.') do ENDLOCAL & SET "%%a"
  SET "O_01=0"
GOTO:EOF

I just did it, and it works:

Code: Select all

C:\> test.bat
Calling [ReceiveError]...
### ERROR ###: Error occurred
### ERROR ###:   Message: [This is an error example]
### ERROR ###:   Code: [1]
### ERROR ###:   Date and Time: [10/03/2015 10:40:59.23]
ErrOccurred [TRUE]
ErrMessage [This is an error example]
O_01 [0]

Antonio

SilverHawk
Posts: 17
Joined: 04 Mar 2015 01:42

Re: Bypass ENDLOCAL barrier

#13 Post by SilverHawk » 10 Mar 2015 10:57

@jeb
Just two word: IT WORKS!!!!!!!!!!!!!!!!!!
Very good job.

@Aacini
Sure that works, but only IF I assign O_01 at the end (just before the GOTO:EOF).
I want to export also the previous assigned variables (like O_01): yes, you're right, the example I wrote above isn't very clear. Bad dog.

Thanks to everyone
Have a nice day

SilverHawk

SilverHawk
Posts: 17
Joined: 04 Mar 2015 01:42

Re: Bypass ENDLOCAL barrier

#14 Post by SilverHawk » 10 Mar 2015 11:02

@jeb
I was going to forget: thanks for the comments, the macro is more clear now (not fully, but it's a start...)

SilverHawk
Posts: 17
Joined: 04 Mar 2015 01:42

Re: Bypass ENDLOCAL barrier

#15 Post by SilverHawk » 11 Mar 2015 07:47

@jeb
I "improved" your macro to skip variables that are not defined, otherwise I get a wrong value.

For example:

Code: Select all

SET "Var1=a"
SET "Var2="

%endlocal% Var1, Var2

ECHO Var1: [%Var1%]
ECHO Var2: [%Var1%]


With actual macro I get:

Code: Select all

Var1: [a]
Var2: ["="]


With modified macro I get:

Code: Select all

Var1: [a]
Var2: []


There is the modified macro:

Code: Select all

set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n%
   setlocal EnableDelayedExpansion%\n%
 %=       Take all variable names into the varName array       =%%\n%
   set varName_count=0%\n%
   for %%C in (!args!) do set "varName[!varName_count!]=%%~C" ^& set /a varName_count+=1%\n%
 %= Build one variable with a list of set statements for each variable delimited by newlines =%%\n%
 %= The lists looks like --> set result1=myContent\n"set result1=myContent1"\nset result2=content2\nset result2=content2\n     =%%\n%
 %= Each result exists two times, the first for the case returning to DDE, the second for EDE =%%\n%
 %= The correct line will be detected by the (missing) enclosing quotes  =%%\n%
   set "retContent=1!LF!"%\n%
   for /L %%n in (0 1 !varName_count!) do (%\n%
      for /F "delims=" %%C in ("!varName[%%n]!") DO (%\n%
         set "content=!%%C!"%\n%
       if defined content ( %\n%
           set "retContent=!retContent!"set !varName[%%n]!=!content!"!LF!"%\n%
 %=      This complex block is only for replacing '!' with '^!'      =%%\n%
 %=      First replacing   '"'->'""q'   '^'->'^^' =%%\n%
           set ^"content_EDE=!content:"=""q!"%\n%
           set "content_EDE=!content_EDE:^=^^!"%\n%
 %= Now it's poosible to use CALL SET and replace '!'->'""e!' =%%\n%
           call set "content_EDE=%%content_EDE:^!=""e^!%%"%\n%
         %= Now it's possible to replace '""e' to '^', this is effectivly '!' -> '^!'  =%%\n%
           set "content_EDE=!content_EDE:""e=^!"%\n%
         %= Now restore the quotes  =%%\n%
           set ^"content_EDE=!content_EDE:""q="!"%\n%
           set "retContent=!retContent!set "!varName[%%n]!=!content_EDE!"!LF!"%\n%
       )%\n%
      )%\n%
   )%\n%
 %= Now return all variables from retContent over the barrier =%%\n%
   for /F "delims=" %%V in ("!retContent!") DO (%\n%
 %= Only the first line can contain a single 1 =%%\n%
      if "%%V"=="1" (%\n%
 %= We need to call endlocal twice, as there is one more setlocal in the macro itself =%%\n%
         endlocal%\n%
         endlocal%\n%
      ) ELSE (%\n%
 %= This is true in EDE             =%%\n%
         if "!"=="" (%\n%
            if %%V==%%~V (%\n%
               %%V !%\n%
            )%\n%
         ) ELSE IF not %%V==%%~V (%\n%
            %%~V%\n%
         )%\n%
      )%\n%
   )%\n%
 ) else set args=


SilverHawk

Post Reply