Just made batch functions functions Deconcatenate,GetLastElement and GetNthElement

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

Post Reply
Message
Author
shodan
Posts: 46
Joined: 01 May 2023 01:49

Just made batch functions functions Deconcatenate,GetLastElement and GetNthElement

#1 Post by shodan » 01 May 2023 02:04

I was having a hard time making this work in a FOR loop
So I made a character-by-character version

But then I also cracked using the FOR loop

I think they both have their uses, however the version using the FOR loop are about 10x faster

I would like to see these functions get broken

Are there inputs that will make them choke that shouldn't ?
Do they work with those maxed out 8191 character strings ?

I haven't made a "batch torture string generator" yet to try and break them but maybe you've already got something like this ?

Code: Select all

@echo off
:setup
set debug=true
:main

GoTo :DeconcatenateDEMO
GoTo :END

REM This demonstrates functions Deconcatenate,GetLastElement and GetNthElement
:DeconcatenateDEMO
REM set debug=true

set TestVar=HKEY_CURRENT_USER\EUDC\666\myteststring
set TestVar=HKEY_CURRENT_USER/EUDC/666/myteststring
set EchoArrayPrefix=Output[%%index%%]=

echo. & echo Deconcatenate to array %date% %time%
Call :deconcatenate "/" %TestVar% Output
Call :EchoArray Output
Call :ClearArray Output

echo. & echo DeconcatenateFast to array %date% %time%
Call :deconcatenateFast "/" %TestVar% Output
Call :EchoArray Output
Call :ClearArray Output

echo. & echo GetLastElement from  array %date% %time%
Call :GetLastElement / %TestVar% Output
set Output=& echo last element %output%

echo. & echo GetLastElementFast from  array %date% %time%
Call :GetLastElementFast / %TestVar% Output
set Output=&echo last element %output%

echo. & echo GetNthElement from  array %date% %time%
set Output=&Call :GetNthElement / %TestVar% Output 0
set Output=&echo 0th element %output%
set Output=&Call :GetNthElement / %TestVar% Output 1
set Output=&echo 1st element %output%
set Output=&Call :GetNthElement / %TestVar% Output 2
set Output=&echo 2nd element %output%
set Output=&Call :GetNthElement / %TestVar% Output 3
set Output=&echo 3rd element %output%
set Output=&Call :GetNthElement / %TestVar% Output 4
set Output=&echo 4th element %output%
set Output=&Call :GetNthElement / %TestVar% Output 5
set Output=&echo 5th element %output%

echo. & echo GetNthElementFast from  array %date% %time%
set Output=&Call :GetNthElementFast / %TestVar% Output 0
set Output=&echo 0th element %output%
set Output=&Call :GetNthElementFast / %TestVar% Output 1
set Output=&echo 1st element %output%
set Output=&Call :GetNthElementFast / %TestVar% Output 2
set Output=&echo 2nd element %output%
set Output=&Call :GetNthElementFast / %TestVar% Output 3
set Output=&echo 3rd element %output%
set Output=&Call :GetNthElementFast / %TestVar% Output 4
set Output=&echo 4th element %output%
set Output=&Call :GetNthElementFast / %TestVar% Output 5
set Output=&echo 5th element %output%

echo. & echo Demo finished %date% %time%

GoTo :EOF

:END
GoTo :EOF
REM Function library



:ReadFile

GoTo :EOF

:ReadFolder

GoTo :EOF

:ReadCommand

GoTo :EOF

::Usage Call :Deconcatenate Delimiter Input Output 'Returns array starting at [0], see .lbound and .ubound
:deconcatenate
set "_delimiter=%~1" & set /a _delimiter.len=0
set "_input=%~2" & set /a _input.len=0
call :len _delimiter _delimiter.len
call :len _input _input.len
set /a _cursor=0 & set /a _token=0 & set "_char=" & set "_buffer="
set /a %3.lbound=%_cursor%
REM if "[%debug%]"=="[true]" echo input [%_input%] len %_input.len% delimiter [%_delimiter%] len %_delimiter.len%
:deconcatenate_input_loop
REM if "[%debug%]"=="[true]" pause
call set _char=%%_input:~%_cursor%,1%%
REM if "[%debug%]"=="[true]" echo char %_char%
set /a _cursor+=1
set /a _cursor2=0 & set "_IsDelimiter=false"  
:deconcatenate_delimiter_loop
call set _charDelimiter=%%_delimiter:~%_cursor2%,1%%
if "[%_char%]"=="[%_charDelimiter%]" set "_IsDelimiter=true"
if not "[%_IsDelimiter%]"=="[true]" set "_buffer=%_buffer%%_char%"
REM if "[%debug%]"=="[true]" echo _charDelimiter [%_charDelimiter%] _IsDelimiter [%_IsDelimiter%] _cursor %_cursor% _cursor2 %_cursor2% _buffer %_buffer%
set /a _cursor2+=1
if %_cursor2% LSS %_delimiter.len% goto :deconcatenate_delimiter_loop
REM if "[%debug%]"=="[true]" if "[%_IsDelimiter%]"=="[true]" echo writing set %~3[%_token%] = %_buffer% 
if "[%_IsDelimiter%]"=="[true]" call set "%~3[%%_token%%]=%_buffer%" & set "_buffer=" & set /a _token+=1 
REM if "[%debug%]"=="[true]" if "[%_IsDelimiter%]"=="[true]" pause
if %_cursor% LSS %_input.len% goto :deconcatenate_input_loop
if %_cursor% EQU %_input.len% call set "%~3[%%_token%%]=%_buffer%" & set "_buffer=" & set /a %3.ubound=%_token%
GoTo :EOF

::Usage Call :Deconcatenate Delimiter Input Output 'Returns array starting at [0], see .lbound and .ubound
:DeconcatenateFast 
Set "_delimiter=%~1"
Set "_input=%~2"
set /a index=0
set /a %3.lbound=%index%
call set _input="%%_input:%_delimiter%=" "%%"
for %%w in (%_input%) do (
	REM echo %%w
	call set "%~3[%%index%%]=%%w"
	set /a "index+=1"
)
set /a "index-=1"
set %3.ubound=%index%
GoTo :EOF

:: Requires :len (maybe support %ReturnEmpty% for special cases ?)
::Usage Call :GetLastElement Delimiter Input Output 'Returns the last element in string based on specified delimiter
:GetLastElement
set "_delimiter=%~1" & set /a _delimiter.len=0
set "_input=%~2" & set /a _input.len=0
call :len _delimiter _delimiter.len
call :len _input _input.len
set /a _cursor=%_input.len%-1 & set "_char=" & set "_buffer="
REM if "[%debug%]"=="[true]" echo input [%_input%] len %_input.len% delimiter [%_delimiter%] len %_delimiter.len%
:GetLastElement_input_loop
REM if "[%debug%]"=="[true]" pause
call set _char=%%_input:~%_cursor%,1%%
REM if "[%debug%]"=="[true]" echo char %_char%
set /a _cursor-=1
set /a _cursor2=0 & set "_IsDelimiter=false"  
:GetLastElement_delimiter_loop
call set _charDelimiter=%%_delimiter:~%_cursor2%,1%%
if "[%_char%]"=="[%_charDelimiter%]" set "_IsDelimiter=true"
if not "[%_IsDelimiter%]"=="[true]" set "_buffer=%_char%%_buffer%"
REM if "[%debug%]"=="[true]" echo _charDelimiter [%_charDelimiter%] _IsDelimiter [%_IsDelimiter%] _cursor %_cursor% _cursor2 %_cursor2% _buffer %_buffer%
set /a _cursor2+=1
if %_cursor2% LSS %_delimiter.len% goto :GetLastElement_delimiter_loop
REM if "[%debug%]"=="[true]" if "[%_IsDelimiter%]"=="[true]" if not "[%_buffer%]"=="[]" echo is a delimiter and buffer not empty %_buffer% 
if "[%_IsDelimiter%]"=="[true]" if not "[%_buffer%]"=="[]" set "%~3=%_buffer%" & GoTo :EOF
if %_cursor% LSS %_input.len% goto :GetLastElement_input_loop
GoTo :EOF

::Usage Call :Deconcatenate Delimiter Input Output 'Returns the last element in string based on specified delimiter
:GetLastElementFast 
Set "_delimiter=%~1"
Set "_input=%~2"
call set _input="%%_input:%_delimiter%=" "%%"
for %%w in (%_input%) do (
	REM echo %%w
	call set "%3=%%~w"
)
GoTo :EOF

::Usage Call :GetNthElement Delimiter Input Output NthCount 'Returns Nth element from string using specified delimiter
:GetNthElement
set "_delimiter=%~1" & set /a _delimiter.len=0
set "_input=%~2" & set /a _input.len=0
call :len _delimiter _delimiter.len
call :len _input _input.len
set /a _cursor=0 & set /a _token=0 & set "_char=" & set "_buffer="
::set /a %3.lbound=%_cursor%
REM if "[%debug%]"=="[true]" echo input [%_input%] len %_input.len% delimiter [%_delimiter%] len %_delimiter.len%
:GetNthElement_input_loop
REM if "[%debug%]"=="[true]" pause
call set _char=%%_input:~%_cursor%,1%%
REM if "[%debug%]"=="[true]" echo char %_char%
set /a _cursor+=1
set /a _cursor2=0 & set "_IsDelimiter=false"  
:GetNthElement_delimiter_loop
call set _charDelimiter=%%_delimiter:~%_cursor2%,1%%
if "[%_char%]"=="[%_charDelimiter%]" set "_IsDelimiter=true"
if not "[%_IsDelimiter%]"=="[true]" set "_buffer=%_buffer%%_char%"
REM if "[%debug%]"=="[true]" echo _charDelimiter [%_charDelimiter%] _IsDelimiter [%_IsDelimiter%] _cursor %_cursor% _cursor2 %_cursor2% _buffer %_buffer%
set /a _cursor2+=1
if %_cursor2% LSS %_delimiter.len% goto :GetNthElement_delimiter_loop
REM if "[%debug%]"=="[true]" if "[%_IsDelimiter%]"=="[true]" echo if %_token% EQU %4 
if "[%_IsDelimiter%]"=="[true]" if %_token% EQU %4 set "%~3=%_buffer%" & GoTo :EOF 
if "[%_IsDelimiter%]"=="[true]" set "_buffer=" && set /a _token+=1 
REM if "[%debug%]"=="[true]" if "[%_IsDelimiter%]"=="[true]" pause
if %_cursor% LSS %_input.len% goto :GetNthElement_input_loop
::if %_cursor% EQU %_input.len% call set "%~3[%%_token%%]=%_buffer%" & set "_buffer=" & set /a %3.ubound=%_token%
if %_token% EQU %4 set "%~3=%_buffer%"
GoTo :EOF

::Usage Call :GetNthElementFast Delimiter Input Output NthCount 'Returns Nth element from string using specified delimiter
:GetNthElementFast 
Set "_delimiter=%~1"
Set "_input=%~2"
set "_buffer="
set /a index=0
call set _input="%%_input:%_delimiter%=" "%%"
setlocal enableDelayedExpansion
for %%w in (%_input%) do (
	REM echo %%w
	call set "_buffer=%%~w"
	if !index! EQU %4 GoTo :GetNthElementFast_ExitFor
	set /a "index+=1"
)
REM clearing buffer, index did not reach count
 set "_buffer="
:GetNthElementFast_ExitFor
endlocal & set "%3=%_buffer%"
GoTo :EOF

:: EchoArray InputArray optional start optional end
:EchoArray
set /a index=0 
set /a limit=2147483647
if not ["%~2"]==[""] set /a index=%~2
if not ["%~3"]==[""] set /a limit=%~3
call set "lbound=%%%~1.lbound%%" 
call set "ubound=%%%~1.ubound%%"
call set "len=%%%~1.len%%"
call set "next=%%%~1.next%%"
call set "previous=%%%~1.previous%%"
if not ["%lbound%"]==[""] call echo %~1.lbound %%%~1.lbound%%
if not ["%ubound%"]==[""] call echo %~1.ubound %%%~1.ubound%%
if not ["%len%"]==[""] call echo %~1.len %%%~1.len%%
if not ["%next%"]==[""] call echo %~1.next %%%~1.next%%
if not ["%previous%"]==[""] call echo %~1.previous %%%~1.previous%%
:EchoArray-internal-loop
if not defined %1[%index%] GoTo :EOF
call echo %EchoArrayPrefix%%%%1[%index%]%%%EchoArraySuffix%
set /a index+=1
if %index% GTR %limit% goto :EOF
GoTo :EchoArray-internal-loop

:: ClearArray InputArray optional start optional end
:ClearArray
set /a index=0 
set /a limit=2147483647
if not ["%~2"]==[""] set /a index=%~2
if not ["%~3"]==[""] set /a limit=%~3
if not ["%~1.lbound"]==[""] set %~1.lbound=
if not ["%~1.ubound"]==[""] set %~1.ubound=
if not ["%~1.count"]==[""] set %~1.count=
if not ["%~1.next"]==[""] set %~1.next=
if not ["%~1.previous"]==[""] set %~1.previous=
:ClearArray-internal-loop
if not defined %~1[%index%] GoTo :EOF
call set %%%~1[%index%]%=
set /a index+=1
if %index% GTR %limit% goto :EOF
GoTo :ClearArray-internal-loop
 
REM ********* function *****************************
:len <resultVar> <stringVar>
(   
    setlocal EnableDelayedExpansion
    (set^ tmp=!%~1!)
    if defined tmp (
        set "len=1"
        for %%P in (4096 2048 1024 512 256 128 64 32 16 8 4 2 1) do (
            if "!tmp:~%%P,1!" NEQ "" ( 
                set /a "len+=%%P"
                set "tmp=!tmp:~%%P!"
            )
        )
    ) ELSE (
        set len=0
    )
)
( 
    endlocal
    set "%~2=%len%"
    exit /b
)
REM https://stackoverflow.com/a/5841587

penpen
Expert
Posts: 1991
Joined: 23 Jun 2013 06:15
Location: Germany

Re: Just made batch functions functions Deconcatenate,GetLastElement and GetNthElement

#2 Post by penpen » 03 May 2023 17:51

shodan wrote:
01 May 2023 02:04
Are there inputs that will make them choke that shouldn't ?
I currently don't have much time, so i only done a quick test of typical poison characters "&()[]{}^=;!'+,`~".

Work:

Code: Select all

set "TestVar=HKEY_CURRENT_USER/EUDC/666/myteststring"
set "TestVar=HKEY_CURRENT_USER(EUDC/666/myteststring"
set "TestVar=HKEY_CURRENT_USER)EUDC/666/myteststring"
set "TestVar=HKEY_CURRENT_USER[EUDC/666/myteststring"
set "TestVar=HKEY_CURRENT_USER]EUDC/666/myteststring"
set "TestVar=HKEY_CURRENT_USER{EUDC/666/myteststring"
set "TestVar=HKEY_CURRENT_USER}EUDC/666/myteststring"
set "TestVar=HKEY_CURRENT_USER^^EUDC/666/myteststring"
set "TestVar=HKEY_CURRENT_USER'EUDC/666/myteststring"
set "TestVar=HKEY_CURRENT_USER+EUDC/666/myteststring"
set "TestVar=HKEY_CURRENT_USER`EUDC/666/myteststring"
set "TestVar=HKEY_CURRENT_USER~EUDC/666/myteststring"
Don't work:

Code: Select all

set "TestVar=HKEY_CURRENT_USER&EUDC/666/myteststring"
set "TestVar=HKEY_CURRENT_USER=EUDC/666/myteststring"
set "TestVar=HKEY_CURRENT_USER;EUDC/666/myteststring"
set "TestVar=HKEY_CURRENT_USER^^^!EUDC&666/myteststring"
set "TestVar=HKEY_CURRENT_USER,EUDC/666/myteststring"
shodan wrote:
01 May 2023 02:04
Do they work with those maxed out 8191 character strings ?
On my win10 machine it cracks on your own input example:

Code: Select all

set TestVar=HKEY_CURRENT_USER\EUDC\666\myteststring
Works:

Code: Select all

set TestVar=HKEY_CURRENT_USER\EUDC\666\myteststring/a
Doesn't work:

Code: Select all

set "TestVar=H"
So it seems it doesn't to be a length issue, but recognizing the end of the first token goes wrong, when there is no delimiter present.


penpen

shodan
Posts: 46
Joined: 01 May 2023 01:49

Re: Just made batch functions functions Deconcatenate,GetLastElement and GetNthElement

#3 Post by shodan » 20 Aug 2023 02:20

Thanks

I wrote that it feel like so long ago.

I spent all summer writing batch files, this is one of the first function I wrote.

I still have a lot to learn with regards to handling poisonous characters and very long strings.

When I figure those out I will rewrite these functions.

I would like to write multiple versions

One that is very readable, that works in most cases.

One that works in extreme cases with any and all poison characters and very long strings

and lastly a version that is specifically written for speed while maintaining more flexibility than a for loop.

What is special about those functions specifically, is that they resemble in their structure many other functions. They have the potential to be "good practices templates" for writing other functions. As I was learning to code batch, I often referred to these to remind myself how to build a function with a general loop.

Just for fun, here is one of my more recent function (written about 6 weeks ago)

Code: Select all

:: Default configuration 
:: Will escape space, quotes, exclamation, percent, special characters (& < > ^ |) and delimiters ()
:: but not backets (( ) [ ] { }) or extra delimiters (' + ` ~ @)
::Usage Call :AddEscapeCharacters byref InputString optional OutputString
:AddEscapeCharacters
set "_AddEscapeCharacters_prefix=_AEC"
Call :SetIfNotDefined "%~1" _AEC_input "%~2" _AEC_output
Call :SetIfNotDefined "%_AEC_input%" _AEC_output
set "_AEC_special_esclist=^& ^< ^> ^^ ^|"
set "_AEC_delimiter_esclist1=^= ^; ^,"
set "_AEC_regular_escape_char=^^^"
set "_AEC_space_escape_char=^^^"
set "_AEC_quotes_escape_char=^^^"
set "_AEC_percent_escape_char=%%"
set "_AEC_exclamation_escape_char=^^^"
:AddEscapeCharacters-arguments
if "[%~3]" EQU "[NOLEN]" set "_AEC_nolen=true"
if "[%~3]" EQU "[NOTSPACE]" set "_AEC_space_escape_char=^"
REM if "[%~3]" EQU "[NOTSPACE]" set "_AEC_space_escape_char="
if "[%~3]" EQU "[NOTQUOTES]" set "_AEC_quotes_escape_char=^"
if "[%~3]" EQU "[NOTEXCLAMATION]" set "_AEC_exclamation_escape_char=^^"
if "[%~3]" EQU "[NOTPERCENT]" set "_AEC_percent_escape_char=%"
if "[%~3]" EQU "[NOTSPECIAL]" set "_AEC_special_esclist="
if "[%~3]" EQU "[BRACKETS]" set "_AEC_bracket_esclist=^( ^) ^[ ^] ^{ ^}"
if "[%~3]" EQU "[NOTDELIMITERS]" set "_AEC_delimiter_esclist="
if "[%~3]" EQU "[EXTDELIMITERS]" set "_AEC_extdelimiter_esclist=^' ^+ ^` ^~ ^@"
if "[%~3]" EQU "[NOTLASTDIGIT]" set "_AEC_notlastdigit=true"
if "[%~4]" NEQ "[]" ( shift & GoTo :AddEscapeCharacters-arguments )
set "_AEC_escape_list=%_AEC_special_esclist% %_AEC_bracket_esclist% %_AEC_delimiter_esclist% %_AEC_extdelimiter_esclist%"
set /a "_AEC_input.index=0" & set /a "_AEC_output.escapechars=0" & set /a "_AEC_output.totalchars=0"
setlocal enabledelayedexpansion
:AddEscapeCharacters-loop
set "_AEC_escapechar=" & set "_AEC_input_char=!%_AEC_input%:~%_AEC_input.index%,1!"
for %%a in ( %_AEC_escape_list% ) do ( if ^!_AEC_input_char!==%%a ( set "_AEC_escapechar=!_AEC_regular_escape_char!" ) )
if "!_AEC_input_char!"==" " set "_AEC_escapechar=!_AEC_space_escape_char!"
if "!_AEC_input_char!"==^"^"^" set "_AEC_escapechar=!_AEC_quotes_escape_char!"
if "!_AEC_input_char!"=="^!" set "_AEC_escapechar=!_AEC_exclamation_escape_char!"
if "!_AEC_input_char!"=="%%" set "_AEC_escapechar=!_AEC_percent_escape_char!"
set _AEC_intermediate=!_AEC_intermediate!!_AEC_escapechar!!_AEC_input_char!
set /a "_AEC_input.index+=1"
if "[!_AEC_escapechar!]" NEQ "[]" set /a "_AEC_output.escapechars+=1"
set /a "_AEC_output.totalchars=!_AEC_input.index!+!_AEC_output.escapechars!"
if !_AEC_output.totalchars! LSS 8030 if "!%_AEC_input%:~%_AEC_input.index%,1!" NEQ "" GoTo :AddEscapeCharacters-loop
set "_AEC_last_char=!_AEC_intermediate:~-1!"
if "[%_AEC_notlastdigit%]" NEQ ["true"] for %%a in (0 1 2 3 4 5 6 7 8 9) do ( if "[!_AEC_last_char!]"=="[%%a]" ( set /a "_AEC_input.escapechars+=1" & set /a "_AEC_output.totalchars+=1" & set _AEC_intermediate=!_AEC_intermediate:~,-1!^^^%%a) )
REM set _AEC_intermediate
endlocal & set /a "%_AEC_output%.len=%_AEC_input.index%" & set /a "%_AEC_output%.totallen=%_AEC_output.totalchars%" & set /a "%_AEC_output%.lenesc=%_AEC_output.escapechars%" & set %_AEC_output%=%_AEC_intermediate%
if "[%_AEC_nolen%]" EQU "[true]" set "%_AEC_output%.len=" & set "%_AEC_output%.totallen=" & set "%_AEC_output%.lenesc="
Call :ClearVariablesByPrefix %_AddEscapeCharacters_prefix% _AddEscapeCharacters
GoTo :EOF

Post Reply