5 years later, working on a routine for URL-encoding strings, I again faced the problem of replacing = characters in strings.
Which brought me back to this thread.
I reused the brillant routine posted long ago by @amel5 in comment #5
, but I quickly noticed several issues with that code:
- It lost the end of strings that did not end with an = character
- It failed miserably if other $_XXX variables existed beforehand
- The known issue when ! or LF characters are present
- It's limited to 99 = signs
The first issue is fixed by appending !$b! to the !$v! result, and then trimming the tail | appended in the beginning.
It took me a while, and several complex intermediate solutions, before understanding that the fix was that simple.
(Appending one tail | character like this is necessary to avoid error messages when the string ends with an =)
I'm surprised that this severe bug remained unnoticed by the author, or anybody who read that thread since then!
The second issue is straightforward to fix, with a simple loop in the beginning to delete all such variables.
I've not attempted to fix the third issue with the ! or LF characters, because there are no ! in the strings I need to URL-encode now.
The obvious fix would be to encode all ^ " ! LF beforehand to some code sequence TBD, then decode them in the end.
Performance would suffer though, another reason for _not_ doing it now...
... Unless someone knows a simple and fast trick for detecting the presence of ! characters in a string?
The 99 = limitation was probably not something I would encounter in real life, but I don't like arbitrary limitations, and this looked simple!
I first tried to fix it by increasing the number of loops to 8192 (The max Batch string size?), and putting the exit /b _inside_ the inner for loop.
But surprisingly, even for a string with a single =, and hence only two loops, the routine took almost twice as long. (251ms vs. 127ms).
I don't understand why, but this is clearly not the good way to go.
Then I tried removing the outside for /L loop altogether, and replacing it with a conditional goto back to the beginning. The %%i counter is replaced by a $i variable. This worked, and the execution time for the same test was back down to 157ms... Better than the first try, but still not as good as the original code.
And with a large number of = signs in the input string, the original code advantage kept growing.
OK, I'll live with the 99 = limitation.
Then there were a few things I think were needlessly complex in the initial version:
- There was a pair of (parentheses) around the whole routine except the final return. Why? Things work just as well without it.
- Also the code to return the result across the endlocal barrier looked overly complex.
As far as I can tell, the much simpler version I used is strictly equivalent to the initial one.
I suspect that the initial version was the remainder of an abandoned attempt to fix the 99 = limitation exactly as I did at first. (In which case it is indeed necessary to use the "for %%v in '!variable!' ..." trick.)
Finally one small improvement:
I added dbenham's = presence test, from comment #26
, during the initialization phase. In the absence of = signs, it returns immediately.
This significantly lowers the routine duration in the absence of = signs, from 89ms to 46ms.
If = signs are present, the increase is barely measurable. (Less than 1ms)
With all that, the = replacement routine becomes:
Code: Select all
:# Replace all = characters in a string
:# - Max 99 = characters
:# - The string must not contain ! or LF characters
:ReplaceEquals %1=STRING_VARNAME %2=REPLACEMENT_VARNAME
if not defined %1 exit /b &:# Avoid issues with empty strings
for /F "delims==" %%v in ('set $_ 2^>NUL') do set "%%v=" &:# Clear existing $_XXX variables
:# $_=input value $f=Termination flag $v=output value $r=replacement value
set "$_=!%~1!|" & set "$f=1" & set "$v=" & set "$r=!%~2!"
if /i "!$_:%$_%=%$_%!" equ "!$_!" endlocal & exit /b 0 &:# No = sign in $_. Return now to save time
for /L %%i in (0,1,99) do if defined $f (
for /F "delims==" %%a in ('set $_') do (
set "$a=%%a" & set "$b=!%%a!" &:# $a=$_variable name $b=its value=all that followed the first =
set "%%a=" & set "$_!$b!" 2>NUL || set "$f="
if %%i gtr 0 set "$v=!$v!!$a:~2!!$r!"
set "$v=!$v!!$b:~0,-1!" &:# The complete result, with the tail | removed in the end
endlocal & set "%~1=%$v%" & exit /b