Page 1 of 1

:lTrim bug and improved function template

Posted: 01 Apr 2011 12:44
by dbenham
There is a bug in the current :lTrim function that prevents it from supporting strings containing !

test0.bat - Test with existing code:

Code: Select all

@echo off
setlocal disableDelayedExpansion
set kind=cruel
set test=     Hello !kind! world! Goodbye! and Hello again! and so on...
set test
call :lTrim test
set test
exit /b

:lTrim string char -- strips white spaces (or other characters) from the beginning of a string
::                 -- string [in,out] - string variable to be trimmed
::                 -- char   [in,opt] - character to be trimmed, default is space
:$created 20060101 :$changed 20080227 :$categories StringManipulation
:$source http://www.dostips.com
:::
::: Should not be using EnabledDelayedExpansion - Causes probles when ! in string
:::
SETLOCAL ENABLEDELAYEDEXPANSION
call set "string=%%%~1%%"
set "charlist=%~2"
if not defined charlist set "charlist= "
for /f "tokens=* delims=%charlist%" %%a in ("%string%") do set "string=%%a"
( ENDLOCAL & REM RETURN VALUES
    IF "%~1" NEQ "" SET "%~1=%string%"
)
EXIT /b


Results:

Code: Select all

D:\utils>test0
test1=     Hello !kind! world! Goodbye! and Hello again! and so on...
test1=Hello cruel world and Hello again and so on...


The :lTrim body is using EnabledDelayedExpansion yet it never attempts to use the feature.

This results in the following when variable test is expanded within the function:
1) "!kind!" is expanded to "cruel"
2) "! Goodbye!" is expanded to nothing because the variable does not exist
3) "again!" simply becomes "again" and the rest is preserved because there is no matching !


I believe the intended behavior is achieved by simply changing the SETLOCAL to use DisableDelayedExpansion. But this only works when :lTrim is called with DisableDelayedExpansion.

test1.bat - Test with fixed code:

Code: Select all

@echo off
setlocal disableDelayedExpansion
set kind=cruel
set disabled=     Hello !kind! world! Goodbye! and Hello again! and so on...
set enabled=%disabled%

echo:
set disabled
call :lTrim disabled
set disabled

setlocal enableDelayedExpansion
echo:
set enabled
call :lTrim enabled
set enabled
exit /b

:lTrim string char -- strips white spaces (or other characters) from the beginning of a string
::                 -- string [in,out] - string variable to be trimmed
::                 -- char   [in,opt] - character to be trimmed, default is space
:::
::: fixed enabled delayed expansion bug in body
::: But only works with ! in string if called with disabled delayed expansion
:::
setlocal disabledelayedexpansion
call set "string=%%%~1%%"
set "charlist=%~2"
if not defined charlist set "charlist= "
for /f "tokens=* delims=%charlist%" %%a in ("%string%") do set "string=%%a"
( endlocal & rem return values
    if "%~1" neq "" set "%~1=%string%"
)
exit /b


Results:

Code: Select all

D:\utils>test1

disabled=     Hello !kind! world! Goodbye! and Hello again! and so on...
disabled=Hello !kind! world! Goodbye! and Hello again! and so on...

enabled=     Hello !kind! world! Goodbye! and Hello again! and so on...
enabled=Hello cruel world and Hello again and so on...


This is a general problem with the current function template.

To handle function calls with delayed expansion we need to escape any ! in the return. But if we do that when delayed expansion is disabled we introduce unwanted ^ in the result.

To build a function that can support EnableDelayedExpansion and DisableDelayedExpansion we must:
1) detect whether the function was called with delayed expansion
2) if it was, then escape any ! in the return value before the return

test2.bat - Test with support for enabled and disabled delayed expansion:

Code: Select all

@echo off
setlocal disableDelayedExpansion
set kind=cruel
set disabled=     Hello !kind! world! Goodbye! and Hello again! and so on...
set enabled=%disabled%

echo:
set disabled
call :lTrim disabled
set disabled

setlocal enableDelayedExpansion
echo:
set enabled
call :lTrim enabled
set enabled
exit /b

:lTrim string char -- strips white spaces (or other characters) from the beginning of a string
::                 -- string [in,out] - string variable to be trimmed
::                 -- char   [in,opt] - character to be trimmed, default is space
:::
::: Now can call with delayed expansion enabled or disabled and still works with ! in string
:::
if !errorlevel!==%errorlevel% (
  setlocal disableDelayedExpansion
  set delay=true
) else (
  setlocal disableDelayedExpansion
  set delay=
)
call set "string=%%%~1%%"
set "charlist=%~2"
if not defined charlist set "charlist= "
for /f "tokens=* delims=%charlist%" %%a in ("%string%") do set "string=%%a"
if defined delay set string=%string:!=^^!%
(endlocal & rem return values
  if "%~1" neq "" set "%~1=%string%"
)
exit /b


Results:

Code: Select all

D:\utils>test2

disabled=     Hello !kind! world! Goodbye! and Hello again! and so on...
disabled=Hello !kind! world! Goodbye! and Hello again! and so on...

enabled=     Hello !kind! world! Goodbye! and Hello again! and so on...
enabled=Hello !kind! world! Goodbye! and Hello again! and so on...



Based on the above, I propose the following modified function template:

Code: Select all

:myFunctionName    -- function description here
::                 -- %~1: argument description here
::
:: Use this version if function body requires DisabledDelayedExpansion
::
IF !ERRORLEVEL!==%ERRORLEVEL% (
  SETLOCAL DISABLEDELAYEDEXPANSION
  SET delay=true
) ELSE (
  SETLOCAL DISABLEDELAYEDEXPANSION
  SET delay=
)
REM.--function body here
SET LocalVar1=...
SET LocalVar2=...
IF DEFINED delay (
  set LocalVar1=%LocalVar1:!=^^!%
  set LocalVar2=%LocalVar1:!=^^!%
)
(ENDLOCAL & REM -- RETURN VALUES
    IF "%~1" NEQ "" SET %~1=%LocalVar1%
    IF "%~2" NEQ "" SET %~2=%LocalVar2%
)
GOTO:EOF

:myFunctionName    -- function description here
::                 -- %~1: argument description here
::
:: Use this version if function body requires EnabledDelayedExpansion (untested, but should work)
::
IF !ERRORLEVEL!==%ERRORLEVEL% (
  SETLOCAL ENABLEDELAYEDEXPANSION
  SET "delay=ENDLOCAL&"
) ELSE (
  SETLOCAL ENABLEDELAYEDEXPANSION
  SET delay=
)
REM.--function body here
SET LocalVar1=...
SET LocalVar2=...
IF DEFINED delay (
  SETLOCAL DISABLEDELAYEDEXPANSION
  SET LocalVar1=%LocalVar1:!=^^!%
  SET LocalVar2=%LocalVar1:!=^^!%
)
(%delay%ENDLOCAL & REM -- RETURN VALUES
    IF "%~1" NEQ "" SET %~1=%LocalVar1%
    IF "%~2" NEQ "" SET %~2=%LocalVar2%
)
GOTO:EOF



Dave Benham

Re: :lTrim bug and improved function template

Posted: 01 Apr 2011 14:34
by jeb
Hello dbenham,

good idea, but there are some more pitfalls.

Even with a simple string like

Code: Select all

set "var=  hello ! ^^"

will loose one caret, as not only the ! is a problem in delayed expansion mode, also the caret.

Code: Select all

@echo off
setlocal DisableDelayedExpansion
set "var=YOU^^! ^^^^ "^^^^" Hello! "
set "var2=YOU^^# ^^^^ "^^^^" Hello# "
set var
echo Disabled %var%
setlocal EnableDelayedExpansion
echo Enabled  %var%
echo Enabled2 %var2%
call echo %var%
call call call call call echo %var%

----- OUTPUT ----
var=YOU^^! ^^^^ "^^" Hello!
var2=YOU^^# ^^^^ "^^" Hello#
Disabled YOU^! ^^ "^^" Hello!
Enabled  YOU! ^ "^" Hello
Enabled2 YOU^# ^^ "^^" Hello#
YOU! ^ "^^" Hello
YOU! ^ "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" Hello


So the carets have to be doubled too, but only sometimes :o

And now I want to trim my string

Code: Select all

set var=###"&"^&


But it fails with an error :cry:

So I would think, to solve this with percent expansion could be a challenge, but it's easier to use delayed expansion.

jeb

Re: :lTrim bug and improved function template

Posted: 01 Apr 2011 17:30
by dbenham
Thanks Jeb for pointing out the flaw.

I humbly withdraw my suggestion for the modified function template. I don't see a practical way to create a function that supports calls while delayed expansion is enabled. :cry:

I think the :lTrim function should still be fixed though (switch from enabled to disabled delayed expansion in the body)

Dave