Macro to return multiple variables across endlocal barriers

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

Post Reply
Message
Author
Liviu
Expert
Posts: 470
Joined: 13 Jan 2012 21:24

Macro to return multiple variables across endlocal barriers

#1 Post by Liviu » 27 Mar 2014 00:45

...LF-free variables, actually, but couldn't fit that in the title within the max length the board allows. I didn't find much prior art on the topic, and - honest - didn't fully follow Ed's http://www.dostips.com/forum/viewtopic.php?p=30315#p30315. So here is my shot at such a macro. It doesn't do LFs, but other than that it should work with the rest of all zoo characters - poison, control, Unicode - and is fully self contained without any external calls.

Code: Select all

@echo off & rem if defined retLocals goto :eof (remove 'rem' to block re-loads)

if "!" equ "" (
  >&2 echo *** 'retLocals' macro must be loaded under 'disableDelayedExpansion'
  exit /b 1
)

@rem single linefeed char 0x0A (two blank lines required below)
set LF=^


@rem newline macro (linefeed + line continuation)
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^"

exit /b 0

For a made-up and completely useless test case, the following

Code: Select all

@echo off & setlocal disableDelayedExpansion

@rem load 'retLocals' macro
call retLocals

@rem run silly :login
set "user="
set "pass="
call :login user pass
setlocal enableDelayedExpansion
echo(
echo user '!user!' / pass '!pass!'
endlocal & endlocal & goto :eof

@rem random code using 'retLocals'
:login
setlocal enableDelayedExpansion
echo(
set/p name="enter name: "
set/p %~2="enter pass: "
(%retLocals% %~1=name, %~2) & goto :eof
gives

Code: Select all

C:\tmp>retLocals.test.login

enter name: Λi√ıū
enter pass: (%!"^,""^^;&>>^

user 'Λi√ıū' / pass '(%!"^,""^^;&>>^'

C:\tmp>

I believe it's technically possible to write a similar macro with LF support, too, probably at the expense of an external batch call. That said, I don't have the dedication (or much interest) to follow that through myself. Feel free to run with it ;-)

Liviu

Post Reply