5

Trying to preserve exclamation marks in variables. A simplified, illustrative script below:

    echo off
    set testvar="C:\Windows\TestOfIllegals[!]"
    echo Pre-EnableDE: %testvar%

    setlocal enableextensions enabledelayedexpansion
    echo Post-EnableDE: %testvar%

    Setlocal DisableDelayedExpansion
    echo ssetlocal sub-instance...
    echo TestVar after re-disableDE: %testvar%
    set modTestVar=%testvar%
    echo TestVar to new var, modTestVar: %modTestVar%
    endlocal & set "RetVar2=%modTestVar%"

    echo modTestVar back in main script: %RetVar2%

    Setlocal DisableDelayedExpansion
    echo modTestVar, main script in another setlocal diasbleDE instance: %RetVar2%
    endlocal

    pause
    exit /b

this produces output:

    Pre-EnableDE: "C:\Windows\TestOfIllegals[!]"
    Post-EnableDE: "C:\Windows\TestOfIllegals[]"
    ssetlocal sub-instance...
    TestVar after re-disableDE: "C:\Windows\TestOfIllegals[!]"
    TestVar to new var, modTestVar: "C:\Windows\TestOfIllegals[!]"
    modTestVar back in main script: "C:\Windows\TestOfIllegals[]"
    modTestVar, main script in another setlocal diasbleDE instance: "C:\Windows\TestOfIllegals[]"

Why isn't the exclamation mark being preserved in modTestVar? Is there any way to do this?

(I know people have said before "post the full script" - but rather long and this represents the core issue. However, happy to post if helpful).

Thanks

stigzler
  • 793
  • 2
  • 12
  • 29

1 Answers1

11

It's preserved in the variable, but you need to use delayed expansion.

When you used percent expansion in delayed expansion mode the variable will be expanded and the content, especially the exclamation marks will be parsed will be parsed also later, and a single exclamation mark will simply removed.

echo off
set testvar="C:\Windows\TestOfIllegals[!]"
echo Pre-EnableDE: %testvar%

setlocal enableextensions enabledelayedexpansion
echo Post-EnableDE: !testvar!

Setlocal DisableDelayedExpansion
echo ssetlocal sub-instance...
echo TestVar after re-disableDE: %testvar%
set modTestVar=%testvar%
echo TestVar to new var, modTestVar: %modTestVar%
endlocal & set "RetVar2=%modTestVar%"

echo modTestVar back in main script: !RetVar2!

Setlocal DisableDelayedExpansion
echo modTestVar, main script in another setlocal diasbleDE instance: %RetVar2%
endlocal

pause

The other/only problem is when you try to transfer a variable over an endlocal barrier (like endlocal & set "RetVar2=%modTestVar%").
This is not trivial.

This is a batch macro for this, used like this %endlocal% modTestVar

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%
         if defined content (%\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%
         ) ELSE set "content_EDE="%\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="
jeb
  • 78,592
  • 17
  • 171
  • 225
  • You, sir, are a life saver! Gulped at first when saw that script, but just copied and pasted at top of mine and called as per your instruction. For others: you can also pass other variables - e.g. `%endlocal% modTestVar modTestVar2` Just to check - this will act as an `endlocal` in itself, keeping the locals paired up (if that makes sense)?? – stigzler Apr 25 '15 at 19:44
  • Is there any way to stop undefined variables being set to `"="`? For example, if B is undefined within setlocal and `%endlocal% A B C` is used, B seems to get set to the value `"="`?? – stigzler Apr 25 '15 at 20:50
  • 2
    I modified the code to support also empty variables – jeb Apr 27 '15 at 08:27
  • What a star. A fantastic macro. – stigzler Apr 27 '15 at 19:32
  • 3
    And I thought C++ was... complicated enough. – alecov Aug 02 '16 at 16:47