Search code examples
batch-filecmdcommand-linedelayedvariableexpansion

How to echo current variable value with statement on same line as set statement when in Windows command prompt?


In batch scripting I can do something like

echo off
setlocal EnableDelayedExpansion
set /a jjj = 0

echo Want _1:  &  set /a jjj+=1  &  echo _%jjj%
REM batch output    _0          bad
REM CLI output      _0          bad
echo Checking:  jjj is %jjj%

echo Want _2:  &  set /a jjj+=1  &  echo _!jjj!
REM batch output    _2          good
REM CLI output      _!jjj!      bad
echo Checking:  jjj is %jjj%

echo Want _3:  &  set /a jjj+=1  &  call echo _%jjj%
REM batch output    _2          bad
REM CLI output      _2          bad
echo Checking:  jjj is %jjj%

echo Want _4:  &  set /a jjj+=1  &  call echo _!jjj!
REM batch output    _4          good
REM CLI output      _!jjj!      bad
echo Checking:  jjj is %jjj%

echo Want _5:  &  set /a jjj+=1  &  call echo _%%jjj%%
REM batch output    _5          good
REM CLI output      _%4%        bad
echo Checking:  jjj is %jjj%

echo Want _6:  &  set /a jjj+=1  &  call echo _!!jjj!!
REM batch output    _6          good
REM CLI output      _!!jjj!!    bad
echo Checking:  jjj is %jjj%

endlocal

The echo Checking: jjj is %jjj% statements produce the correct output in every instance (1, 2, 3, 4, 5 and 6) when run as a batch file, and also when run at the command prompt.

Now notice the set and echo commands paired together on the same line in each case. These are the problem.

Having set and echo on a single line within the batch file seems to mean that they are treated like a block arising within a for loop or an if statement, so for a batch file this can be handled with either delayed expansion (using ! instead of %) or an explicit call statement (see ss64 and Reddit) ... or even both of those together (albeit redundantly).

However, when run directly in the command prompt (CLI) under Windows 10, I cannot find any way to access the most recent value of the variable if the set and echo are on the same line, separated by an ampersand (&).

Is there a way to do this, or is it a known limitation?


Solution

  • Solution 1

    On the CLI, the setlocal command has no effect, but you can enable delayed expansion by using cmd /V:ON

    echo Want _7:  &  set /a jjj+=1 > NUL &  cmd /v:on /c echo _!jjj!
    

    The cmd /v:on creates a new cmd.exe instance, therefore it can't modify variables of the parent process. It's a bit like the setlocal EnableDelayedExpansion command.

    Solution 2

    Mentioned by Aacini in his comment:
    You can still use CALL, but you have to add some carets in the variable name, because you cannot escape a percent sign on the CLI.
    Even the carets don't escape anything, but they create an undefined variable name which is not expanded.

    echo Want _8:  &  set /a jjj+=1 > NUL &  call echo %^jj^j%
    

    Solution 3

    Use the set /a output itself.
    On the CLI, set /a always outputs the result without newline, to avoid this use redirect to >NUL.

    echo Want _9:  &  set /a jjj+=1 > NUL &  set /a jjj & echo:
    

    Solution 4

    set /a var+=1 > NUL & for /F "tokens=1-31" %%= in ("-") do @call echo var is %%=var%%= 
    

    This is an absolutely safe variant and cannot be broken by any crafted variable.
    It creates 31 for-meta-variables, the first is %%, but the 25th is %= and contains nothing.
    The trick is how %%=VAR%%= expands.
    In the first percent expansion phase %%= just stays as it.
    In the FOR meta variable expansion phase the %= expands to nothing, after this phase the line looks like var is %var%.
    In the end the CALL can expand the %var% without problems.

    And because every percent sign is followed by an equal sign, and because variable names can't start with an equal sign, it's safe.