Below is an alternative approach, borrowing from prior art using delimited for/f loops and the tunneling trick. Basic idea is straightforward - replace the problem characters with %~1, %~2 etc, perform the substitution on the sanitized strings, then restore the characters replaced in the first step. One complication is that the initial replacement must be done in one pass, otherwise escaping % and ~ would step over each other.
The code below won't handle quotes and control characters, and is way slower than optimal. That aside, if you find other holes or failure cases, please chime in.
Code: Select all
@echo off & setlocal disableDelayedExpansion
call :repStr "%~1" "%~2" "%~3" outStr
( endlocal
if not "%~4"=="" (set "%~4=%outStr%") else if not "%~1"=="" (set "%~1=%outStr%")
) & goto :eof
:: replaces a substring with a (possibly empty) substitute string
:: to be called from 'setlocal disableDelayedExpansion' environment
::
:: %1 [in, required] - name of variable containing the input string
:: %2 [in, required] - name of variable containing the string being replaced
:: %3 [in, optional] - name of variable containing the replacement string
:: if missing or empty, occurences of string %2 are removed
:: %4 [out, optional] - name of variable to receive the output string
:: if missing or empty, the %1 value is updated in place
::
:: errorlevel 0 ok
:: errors 1 missing %1 input string variable, error
:: 2 missing %2 string to replace variable, error
:: warnings -1 empty %1 input string variable, output string set to empty
:: -2 empty %2 string to replace variable, input copied to output
:: -10 called from enableDelayedExpansion, returned output may be wrong
:repStr
if "%~1"=="" (exit /b 1) else if "%~2"=="" exit /b 2
setlocal disableDelayedExpansion
if not "%~4"=="" (set "outStrRef=%~4") else (set "outStrRef=%~1")
setlocal enableDelayedExpansion
set "inStr=!%~1!" & if not defined inStr endlocal & endlocal & set "%outStrRef%=" & exit /b -1
set "oldStr=!%~2!" & if not defined oldStr endlocal & endlocal & set "%outStrRef%=%inStr%" & exit /b -2
if not "%~3"=="" (set "newStr=!%~3!") else set "newStr="
endlocal & set "inStr=%inStr%" & set "oldStr=%oldStr%" & set "newStr=%newStr%"
call :rigStr inStr
call :rigStr oldStr
call :rigStr newStr
setlocal enableDelayedExpansion
set "outStr=!inStr:%oldStr%=%newStr%!"
endlocal & set "outStr=%outStr%"
for /f "tokens=1-5" %%1 in ("%% ~ * = !") do (
set "outStr=%outStr%"
)
endlocal & set "outStr=%outStr%"
if not "!"=="" (exit /b 0) else (exit /b -10)
:: replaces '%' = '%~1', '~' = '%~2', '*' = '%~3', '=' = '%~4', '!' = '%~5'
:rigStr %~*=!
setlocal disableDelayedExpansion
setlocal enableDelayedExpansion
set "tail=#!%~1!"
endlocal & set "tail=%tail%"
set "outStr="
:loop
for /F "delims=%%~*=!" %%A in ("%tail%") do (
set "next=%%A"
call :next
)
if defined tail goto :loop
endlocal & set "%~1=%outStr%"
goto :eof
:: used by rigStr - replaces one delim, updates 'outStr' and 'tail'
:next
setlocal enableDelayedExpansion
call :strlen next offset
if %offset% leq 1 (
if "!tail:~1,1!"=="%%" (
set "outStr=!outStr!%%~1"
) else if "!tail:~1,1!"=="~" (
set "outStr=!outStr!%%~2"
) else if "!tail:~1,1!"=="*" (
set "outStr=!outStr!%%~3"
) else if "!tail:~1,1!"=="=" (
set "outStr=!outStr!%%~4"
) else if "!tail:~1,1!"=="^!" (
set "outStr=!outStr!%%~5"
)
set "tail=!tail:~2!"
) else (
set "outStr=!outStr!!next:~1!"
set "tail=!tail:~%offset%!"
)
if defined tail (
set "tail=#!tail!"
)
endlocal & set "outStr=%outStr%" & set "tail=%tail%"
goto :eof
:: http://www.dostips.com/?t=Function.strLen
::
:strLen string len -- returns the length of a string
:: -- string [in] - variable name containing the string being measured for length
:: -- len [out] - variable to be used to return the string length
:: Many thanks to 'sowgtsoi', but also 'jeb' and 'amel27' dostips forum users helped making this short and efficient
:$created 20081122 :$changed 20101116 :$categories StringOperation
:$source http://www.dostips.com
( SETLOCAL ENABLEDELAYEDEXPANSION
set "str=A!%~1!"&rem keep the A up front to ensure we get the length and not the upper bound
rem it also avoids trouble in case of empty string
set "len=0"
for /L %%A in (12,-1,0) do (
set /a "len|=1<<%%A"
for %%B in (!len!) do if "!str:~%%B,1!"=="" set /a "len&=~1<<%%A"
)
)
( ENDLOCAL & REM RETURN VALUES
IF "%~2" NEQ "" SET /a %~2=%len%
)
EXIT /b
Liviu