Search code examples
batch-filesetmethod-call

EnableDelayedExpansion or call set?


In a code like this:

set /p "PROGNAME=enter the name of the prog: "
set /p "VERSION=enter the version of the prog: "
setlocal enabledelayedexpansion
if "%PROGNAME%"=="foo" (
    set "OPTIONS=(someParams)"
    set "PROGPATH=C:\MyPath\%PROGNAME%%OPTIONS%_%VERSION%.exe"
) else if "%PROGNAME%"=="bar" (
    set "OPTIONS=(otherParams)"
    set "PROGPATH=C:\MyPath\%PROGNAME%%OPTIONS%_%VERSION%.exe"
)
set "PROGDLL=!PROGPATH:%PROGNAME%=%PROGNAME%dll!"
endlocal

I have learned that I need enabledelayedexpansion both for the PROGPATH variable, because it's set in the same loop than the OPTIONS variable it needs, and for the PROGDLL variable, because of the substitution.

However, in this particular case, I do not want the variables to be local. Like, not at all; I wanna access them even after the end of the script. So enabledelayedexpansion can't be used.

I then made something like this:

set /p "PROGNAME=enter the name of the prog: "
set /p "VERSION=enter the version of the prog: "
if "%PROGNAME%"=="foo" (
    set "OPTIONS=(someParams)"
    call set "PROGPATH=C:\MyPath\%PROGNAME%%OPTIONS%_%VERSION%.exe"
    call set "PROGDLL=%%PROGPATH:foo=foodll%%"
) else if "%PROGNAME%"=="bar" (
    set "OPTIONS=(otherParams)"
    call set "PROGPATH=C:\MyPath\%PROGNAME%%%OPTIONS%%_%VERSION%.exe"
    call set "PROGDLL=%%PROGPATH:bar=bardll%%"
)

And while this works perfectly, I am now quite lost: if all it needed was a call set, what's the point of enabledelayedexpansion ?? I mean, I've been using enabledelayedexpansion in my script each time I needed to set some dependent variables in a loop, should I replace all of those by call set and delete the enabledelayedexpansion ?

I would like to understand when I should use enabledelayedexpansion and when I should use call set, in general.

(and also, why the double %% with call set ?)


Solution

  • Well, your code should actually look like this:

    rem // At this point, the default state should apply, hence no delayed expansion:
    set /P "PROGNAME=enter the name of the prog: "
    set /P "VERSION=enter the version of the prog: "
    setlocal EnableDelayedExpansion
    rem /* At this point, delayed expansion should be applied for all possible variables,
    rem    because otherwise, you may run into problems with exclamation marks: */
    if "!PROGNAME!"=="foo" (
        set "OPTIONS=(someParams)"
        rem /* You need `!OPTIONS!` here, of course, since it is set in the same block;
        rem    but you should also use `!PROGNAME!` and `!VERSION!` herein: */
        set "PROGPATH=C:\MyPath\!PROGNAME!!OPTIONS!_!VERSION!.exe"
    ) else if "!PROGNAME!"=="bar" (
        set "OPTIONS=(otherParams)"
        set "PROGPATH=C:\MyPath\!PROGNAME!!OPTIONS!_!VERSION!.exe"
    )
    rem /* For the sub-string substitution, you should avoid immediate expansion,
    rem    because you may get in trouble with unbalanced quotes, if applicable;
    rem    you could use a `for /F` loop to delay expansion of search/replace strings;
    rem    as you can see, delayed expansion is used as much as possible, and
    rem    the whole replacement expression is transferred to the `for` meta-variable: */
    for /F "delims=" %%S in ("!PROGNAME!=!PROGNAME!dll") do set "PROGDLL=!PROGPATH:%%I!"
    rem /* To avoid loss of set variables due to end of the environment localisation,
    rem    use another `for /F` loop; delayed expansion is used as much as possible, and
    rem    the whole assignment expression is transferred to the `for` meta-variable: */
    for /F "delims=" %%E in ("PROGPATH=!PROGPATH!") do endlocal & set "%%E"
    

    Note, that the set variables are only available in the same cmd.exe instance which the batch script runs in.


    If you want to use call rather than delayed expansion, the code should be this:

    rem // At this point, the default state should apply, hence no delayed expansion:
    set /P "PROGNAME=enter the name of the prog: "
    set /P "VERSION=enter the version of the prog: "
    setlocal EnableDelayedExpansion
    if "%PROGNAME%"=="foo" (
        set "OPTIONS=(someParams)"
        rem // You need `call` here, of course, since `OPTIONS` is set in the same block:
        call set "PROGPATH=C:\MyPath\%PROGNAME%%%OPTIONS%%_%VERSION%.exe"
        rem /* Here is an alternative way using `%%` for all variables, which may avoid
        rem    problems with unbalanced quotes, but you may get unwanted `^`-doubling: */
        rem call set "PROGPATH=C:\MyPath\%%PROGNAME%%%%OPTIONS%%_%%VERSION%%.exe"
    ) else if "!PROGNAME!"=="bar" (
        set "OPTIONS=(otherParams)"
        call set "PROGPATH=C:\MyPath\%PROGNAME%%%OPTIONS%%_%VERSION%.exe"
        rem call set "PROGPATH=C:\MyPath\%%PROGNAME%%%%OPTIONS%%_%%VERSION%%.exe"
    )
    rem // Of course you will need `call` for the sub-string substitution:
    call set "PROGDLL=%%PROGPATH:%PROGNAME%=%PROGNAME%dll%%"
    

    But regard, that this approach is slower, and you may get troubles with unwanted doubling of caret symbols ^. Furthermore, the %% could cause unintended expansion of for meta-variables, when, for instance, the code is placed inside a section where %%P is applicable, call set "PROGDLL=%%PROGPATH:…%%" will lead to unexpected results, because the %%P portion becomes expanded.