TheStigma
TheStigma

Reputation: 11

escaping "!" in a for-loop with delayed variable expansion

I need to escape "!" (and other special chars) in a for-loop where delayed variable expansion is enabled

I've tried manually escaping the loop-varible with string substitution ^^! in the loop-variable, %%a but with no luck at all. Is it already too late after the for has read them? If so, how the heck can I even accomplish this?

Below is a short function. The only relevant part here is the for-loop and the echo statement. That is printing out whole lines from a file every X'th line, and those lines are file-paths. They (sometimes) contain characters like "!" and other troublesome special characters. I just want echo here to pass it without interpreting it at all - but instead it ends up deleting my "!" chars. For my use they need to be exactly correct or they are useless as they must correlate to actual files later on in what I use them for.

setlocal EnableDelayedExpansion

:SplitList
if [%3] == [] goto :SplitListUsage
set inputfile=%~1
set outputfile=%~2
set splitnumber=%~3
set skipLines=0
set skipLines=%~4

if %skipLines% GTR 0 (
  set skip=skip=%skipLines%
) else (
  set skip=
)

@echo off > %outputfile%

set lineNumber=0
for /f "tokens=* %skip% delims= " %%a in (%inputfile%) do (
    set /a modulo="!lineNumber! %% !splitnumber!"
    if "!modulo!" equ "0" (
        echo %%a >> !outputfile!
    )
    set /a lineNumber+=1
)
exit /B 0

Upvotes: 1

Views: 269

Answers (1)

sst
sst

Reputation: 1463

Quick solution:

 if !modulo! equ 0 (
     setlocal DisableDelayedExpansion
     (echo %%a)>> "%outputfile%"
     endlocal
 )


Better solution:

Based on your code sample, there is no need to have delayed expansion enabled for the entire code,
in fact you should keep it disabled to not mess with the file names or input strings which may contain !, and enabled it when necessary:

setlocal DisableDelayedExpansion

REM Rest of the code...

for /f "tokens=* %skip% delims= " %%a in (%inputfile%) do (
    set /a "modulo=lineNumber %% splitnumber"
    setlocal EnableDelayedExpansion
    for %%m in (!modulo!) do (
        endlocal
        REM %%m is now have the value of modulo
        if %%m equ 0 (
            (echo %%a)>> "%outputfile%"
        )
    )
    set /a lineNumber+=1
)


Side notes:

There are some other issues with your code which as you might have noticed, some of them are corrected in the above solutions. But to not distract you from main issue you had, I covered them here separately.

There is also room for improving the performance of the code when writing to the outputfile.

Here is the re-written code which covers the rest:

@echo off
setlocal DisableDelayedExpansion

:SplitList
if "%~3"=="" goto :SplitListUsage
set "inputfile=%~1"
set "outputfile=%~2"
set "splitnumber=%~3"
set "skipLines=0"
set /a "skipLines=%~4 + 0" 2>nul

if %skipLines% GTR 0 (
  set "skip=skip=%skipLines%"
) else (
  set "skip="
)

set "lineNumber=0"
(
    for /f "usebackq tokens=* %skip% delims= " %%a in ("%inputfile%") do (
        set /a "modulo=lineNumber %% splitnumber"
        setlocal EnableDelayedExpansion
        for %%m in (!modulo!) do (
            endlocal
            REM %%m is now have the value of modulo
            if %%m equ 0 echo(%%a
        )
        set /a lineNumber+=1
    )
)>"%outputfile%"
  • You didn't protect the variable assignments with double qoutes " e.g. set inputfile=%~1. If the now naked batch parameter %~1 contains spaces or special characters like & your batch files fails, either fatally with a syntax error or at execution time with incorrect data. The recommended syntax is to use set "var=value" which does not assign the quotes to the variable value but provides protection against special characters. It also protects the assignment against the accidental trailing spaces.
  • The %inputfile% may contain special characters or spaces, so it should be protected by double quoting it when using in the FOR /F's IN clause. When double quoting the file name in FOR /F the usebackq parameter must also be used.
  • With SET /A there is no need to expand the variable values: set /a modulo="!lineNumber! %% !splitnumber!". The variable names can be used directly, and it will work correctly with or without delayed expansion.
  • Using (echo %%a) >> "%outputfile%" inside a FOR loop introduces a severe performance penalty specially with a large number of iterations, because at each iteration the output file will be opened, written to and then closed. To improve the performance The whole FOR loop can redirected once. Any new data will be appended to the already opened file.
  • The odd looking echo( is to protect against the empty variable values, or when the variable value is /?. Using echo %%a may print the `ECHO is on/off' message if the variable is empty or may print the echo usage help.
  • In the main solutions, The reason I've used (echo %%a)>> "%outputfile%" instead of echo %%a >> "%outputfile%" is to prevent outputting the extra space between %%a and >>. Now you know the reason for using echo(, it is easy to understand the safer alternative: (echo(%%a)>> "%outputfile%"

Upvotes: 2

Related Questions