1

I am trying to utilize the different %rtnLocal% and %endlocal% return macros in order to utilize in my batch to carry-over 1000's of filepaths & names that contain "!" as posted in preserving exclamation marks in variable between setlocals batch.

I choose macros as I've also seen dbendham, jeb, Aacini, and others discuss how macros are generally a faster option; as opposed to Call functions or Call files.

Here is my sample and I just can't get them to function properly, I am not sure what I am doing wrong if perhaps a win10 since then has slightly modified that causes this to stop working. Setlocal(2) is the contains 1 of the 'return macros', after which are simulated setlocal levels for testing, and no matter what I change the level to rtnLocal*2 , *4, etc.. (except for more than setlocals are set; it then sets outside the batch at the prompt level) it does not populate outside the existing setlocal.

Thank you for any clarification

Edit: Corrected Setlocal imbalanace.

@echo OFF
Setlocal EnableExtensions DisableDelayedExpansion
SETlocal EnableExtensions DisableDelayedExpansion
        ::Return V2.0 - 2016 -jeb
    :: https://stackoverflow.com/questions/29869394/preserving-exclamation-marks-in-variable-between-setlocals-batch/29869518#29869518
    @rem The other/only problem is when you try to transfer a variable over an endlocal barrier (like endlocal & set "RetVar2=%modTestVar%").
    @rem This is not trivial.
    @rem 
    @rem This is a batch macro for this, used like this %endlocal% modTestVar
if "!" equ "" (
  >&2 echo *** 'retLocals' macro must be loaded under 'disableDelayedExpansion'
  exit /b 1
)

set LF=^


set ^"\n=^^^%LF%%LF%^%LF%%LF%^^"
@rem single carriage-return char 0x0D (usable as !CR! under edx, only)
for /F "usebackq delims= " %%C in (`copy /z "%~f0" nul`) do set "CR=%%C"

:: macro to return multiple LF-free variables across endlocal barriers
::
:: syntax:  %retLocals%[*cnt] out1[=in1] [, out2[=in2] [, out3[=in3]] ...]
:: - 'out#' (#=1,2,3,...) are variables being returned/set in the outer context
:: - 'in#' are local/inner context variables whose values are returned
:: - optional [=in#] defaults to 'out#=out#'
:: - optional [*cnt] is the nesting depth, default '*1' for one 'endlocal' call
:: - spaces around ',' commas and '=' equal signs are optional
::
:: e.g. %retLocals% var               copies inner 'var' across 1 endlocal
::                                    to namesake 'var' in the outer context
::
::      %retLocals%*2 out1=in1, out2  copies 'in1', 'out2' across 2 endlocal's
::                                    to 'out1', 'out2' in the outer context
::
::      %retLocals%*0 out=in          copies 'in' to 'out' within same context
::                                    like 'set out=%in%' but safe against
::                                    embedded quotes and funny chars except LF
::
:: ! names of variables 'out#', 'in#' are expected to be plain quote-less
::   well behaved strings, with no funny (<|>^%!=,;) characters
:: ! 'in#' names must not be 'cd' since that's used by %retLocals% internally
:: ! outer environment other than 'out#' is not modified
::   except for '%retLocals%*0' with '*0' de-nesting, which clears 'cd'
:: ! errorlevel is reset to '0' but the caller can preserve the inner value
::   if needed via '(%retLocals% ...) & set outErr=%errorlevel%'

set ^"retLocals=for %%# in (1 2) do if %%#==2 (%\n%
  setlocal enableDelayedExpansion%\n%
  set x=%\n%
  for %%L in ("!LF!") do for %%R in ("!CR!") do ^
  for /f "tokens=1,*" %%U in ("!cd!") do ^
  for /f "tokens=2 delims=*" %%U in ("%%U*%%U") do (%\n%
    set v=%%~V%\n%
    for /f "delims=" %%V in ("!v:,=%%~L!") do ^
    for /f "tokens=1,2 delims== " %%V in ("%%~V=%%~V") do (%\n%
      set w=!%%~W!%\n%
      if defined w (%\n%
        set w=!w:^"=""q!%\n%
        set w=!w:%%~R=""r!%\n%
        set "path=" ^& set "pathExt=;"%\n%
        set "w=!w:^=^^^^!"%\n%
        call set "w=%%w:^!=^^^!%%"%\n%
        set "w=!w:^^=^!")%\n%
      if defined x set x=!x!!LF!%\n%
      set x=!x!"%%~V=!%%~W!"!LF!"%%~V=!w!")%\n%
    for /f "delims=" %%X in ("!x!") do (%\n%
      if "!cd:~0,1!"=="*" (%\n%
        for /l %%U in (0 1 %%U) do endlocal%\n%
        set "cd=" ^& call;)%\n%
      if errorlevel 1 ((if "!"=="" (%\n%
        set "%%~X"!%\n%
        for /f "delims==" %%V in ("%%~X") do (%\n%
          if defined %%~V (%\n%
            set %%~V=!%%~V:""r=%%~R!%\n%
            set %%~V=!%%~V:^""q="!%\n%
        )))) ^& call;%\n%
      ) else (if not "!"=="" set "%%~X") ^& call%\n%
))) else set cd=*1^"
SETlocal EnableExtensions DisableDelayedExpansion
SETlocal EnableExtensions DisableDelayedExpansion
    SETlocal EnableExtensions DisableDelayedExpansion
        SETlocal EnableExtensions DisableDelayedExpansion
            SETlocal EnableExtensions EnableDelayedExpansion
            ECHO -----------------------------
            SET /a "ID2=2"
            %retLocals%*3 ID5=ID2
            Echo This0: %ID2%,%ID5%
            EndLocal
        Echo This1: %ID2%,%ID5%
        EndLocal
    Echo This2: %ID2%,%ID5%
    EndLocal
Echo This3: %ID2%,%ID5%
ECHO *******************************
EndLocal
Echo This4: %ID2%,%ID5%
EndLocal
Echo This5: %ID2%,%ID5%
EndLocal
EndLocal
hsgg4
  • 31
  • 1
  • 5
  • I'm not sure that you should necessarily expect to get a speed increase by using macros. _BTW, your posted code has a `setlocal`/`endlocal` imbalance, (six to eight respectively). – Compo Feb 26 '21 at 22:52

1 Answers1

0

I suppose your expectation is wrong.

The macro exits 4 levels of endlocals, that works, you can see that id5 is set.
But you can't expect, that the variable still exists after you execute another endlocal.
Or you have to use the macro again to endlocal with variable preservation.

Btw. To debug setlocal/endlocal behaviour you could add some debug code:

@echo OFF
set level=0
set "DEBUG_SETLOCAL= & set /a level+=1 & <nul set /p ".=setlocal: " & set "level""
set "DEBUG_ENDLOCAL= & <nul set /p ".=endlocal: " & set "level""
setlocal DisableDelayedExpansion %DEBUG_SETLOCAL%
setlocal DisableDelayedExpansion %DEBUG_SETLOCAL%
...
setlocal DisableDelayedExpansion %DEBUG_SETLOCAL%
setlocal DisableDelayedExpansion %DEBUG_SETLOCAL%
    setlocal DisableDelayedExpansion %DEBUG_SETLOCAL%
        setlocal DisableDelayedExpansion %DEBUG_SETLOCAL%
            setlocal EnableDelayedExpansion %DEBUG_SETLOCAL%
            ECHO -----------------------------
            SET /a "ID2=2"
            <nul set /p ".=current: " & set "level"
            %retLocals%*3 ID5=ID2
            Echo This0: id2=%ID2% id5=%ID5%
            EndLocal %DEBUG_ENDLOCAL%
        Echo This1: %ID2%,%ID5%
        EndLocal %DEBUG_ENDLOCAL%
    Echo This2: %ID2%,%ID5%
...

The output is then

setlocal: level=1
setlocal: level=2
setlocal: level=3
setlocal: level=4
setlocal: level=5
setlocal: level=6
setlocal: level=7
-----------------------------
current: level=7
This0: id2= id5=2
endlocal: level=3
This1: ,
endlocal: level=2
This2: ,
...

After current: level=7 you use your macro and it executes 4 times endlocal internally.
This0: id2= id5=2 shows the expected output.

Btw.

  • You should avoid the useless EnableExtension everywhere.
  • Don't use reserved variable names like cd
  • debug your macro with set statements, like set "## here i am ##" %\n% or set "myvar" %\n% to show the variable content
jeb
  • 78,592
  • 17
  • 171
  • 225