James Ko
James Ko

Reputation: 34569

Windows Batch: findstr not setting ERRORLEVEL within a for loop

Can anyone explain to me why the following snippet prints 0:

@echo off
setlocal

for /f %%i in ('cmd /c echo blah') do (
    echo %%i | findstr bin > NUL
    echo %ERRORLEVEL%
)

While adding another, equivalent statement outside of the for-loop makes it print 1 1:

@echo off
setlocal

echo blah | findstr bin > NUL
echo %ERRORLEVEL%

for /f %%i in ('cmd /c echo blah') do (
    echo %%i | findstr bin > NUL
    echo %ERRORLEVEL%
)

I'm a bit of a newbie to Batch, so this is kinda mysterious to me since the two statements seem to be unrelated. Any help with this would be appreciated, thanks!

Upvotes: 1

Views: 4602

Answers (4)

DanH
DanH

Reputation: 1

@echo off
setlocal enabledelayedexpansion

:::This part is not exactly necessary but it creates the file dictionary.txt and the var - this
echo.word>dictionary.txt
set this=notaword

:::So instead of checking for errorlevel just check for a space in the output
:middle
for /f "tokens=*" %%a in ('findstr /n /b /c:%this% dictionary.txt') do (
  set "output=%%a"
  goto :yeah
)
:yeah
if exist %output% (""," "
    goto :oops
) else (
    goto :nice
)
:oops
echo.%this% is NOT a word
pause & set this=word & goto :middle
:nice
echo.%output%
pause

Upvotes: 0

Magoo
Magoo

Reputation: 80113

The issue is that within a code block (parenthesised series of statements) any %var%will be replaced by the actual value of the variable at parse time.

Hin your first example, %errorlevel% 0 and echoed as such. In second example, it is 1 when the for is encountered, hence it it replaced by 1.

If you want to display a value of an environment variable that may be changed within a loop, then you need to do one of three things:

  1. Invoke setlocal enabledelayedexpansion and echo !var! instead of %var% - noting that the number of nested setlocal instructions you can have active is limited.

  2. Call a subroutine

  3. Employ a syntax-exploit.

There are many, many articles about delayedexpansion on SO.

Crudely, you could simply use (note - case is largely irrelevant in batch, except for the case of the loop-control variable (metavariable - %%i in this instance)

@echo off
setlocal ENABLEDELAYEDEXPANSION

echo blah | findstr bin > NUL
echo %ERRORLEVEL% or !errorlevel!

for /f %%i in ('cmd /c echo blah') do (
    echo %%i | findstr bin > NUL
    echo %ERRORLEVEL% or !errorlevel!
)

Another way is to dynamically invoke setlocal

@echo off
setlocal

echo blah | findstr bin > NUL
echo %ERRORLEVEL% or !errorlevel!
for /f %%i in ('cmd /c echo blah') do (
    echo %%i | findstr bin > NUL
    setlocal ENABLEDELAYEDEXPANSION
    echo %ERRORLEVEL% or !errorlevel!
    endlocal    
)

The disadvantage of this is that the endlocal backs out any changes made to the environment since that last setlocal. Also note that if delayedexpansion is not in effect, ! is no longer a special character.

Or, you can use errorlevel in its traditional manner:

@echo off
setlocal

echo blah | findstr bin > NUL
echo %ERRORLEVEL% or !errorlevel!

for /f %%i in ('cmd /c echo blah') do (
    echo %%i | findstr bin > NUL
    if errorlevel 1 (echo errorlevel is 1 or greater
    ) else (echo errorlevel is 0
    )
)

Note that this looks at the run-time value of errorlevel and if errorlevel n means "if errorlevel is n or greater than n"

Or - call a subroutine:

@echo off
setlocal

echo blah | findstr bin > NUL
echo %ERRORLEVEL% or !errorlevel!
for /f %%i in ('cmd /c echo blah') do (
    echo %%i | findstr bin > NUL
    call :show
)
goto :eof

:show
echo %ERRORLEVEL%
goto :eof

Note here that goto :eof (here the colon is important - this means "go to the physical end-of-file"

Or, a special version of using a subroutine - a syntax-exploit

@echo off
setlocal

echo blah | findstr bin > NUL
echo %ERRORLEVEL% or !errorlevel!
for /f %%i in ('cmd /c echo blah') do (
    echo %%i | findstr bin > NUL
    call echo %%errorlevel%%
)

Upvotes: 6

AyrA
AyrA

Reputation: 863

@echo off
setlocal ENABLEDELAYEDEXPANSION

for /f %%i in ('cmd /c echo blah') do (
    echo %%i | findstr bin > NUL
    echo !ERRORLEVEL!
)

this should work.

A for loop in cmd is technically one line, so the variable gets expanded only once. Using exclamation marks instead of percent and "ENABLEDELAYEDEXPANSION" should fix it.

Upvotes: 0

kodybrown
kodybrown

Reputation: 2603

UPDATED:

If you put your echo %ERRORLEVEL% statement after the closing ) of the for statement it works as expected.

for /f %%i in ('cmd /c echo blah') do (
    echo %%i | findstr bin > NUL
)
echo %ERRORLEVEL%

I'm kind of guessing here, for the exact 'definitive' reason, but setlocal is definitely causing it. I'm assuming the value of the ERRORLEVEL within the for loop is the result of the cmd /c ... statement. It seems like a strange behavior, but I've seen worse in batch files.

If you remove the setlocal at the beginning it works as one would normally expect it to..

ORIGINAL:

The findstr sets the errorlevel when it runs. So, echo %errorlevel% will always output 0.

For example the following findstr statement is successful, so it outputs the line with the match (the only line passed to it):

C:\>echo blahbin | findstr bin
blahbin
C:\>echo %errorlevel%
0            ; success / found it..

While this statement is not successful (findstr doesn't output anything) and the errorlevel is set to 1:

C:\>echo blah_in | findstr bin
C:\>echo %errorlevel%
1            ; failed

Upvotes: 0

Related Questions