0
@echo off
setlocal
set "myString=abcdef!%%^^()^!"
call :strlen result myString
echo %result%
goto :eof

:strlen <resultVar> <stringVar>
(   
setlocal EnableDelayedExpansion
set "s=!%~2!#"
set "len=0"
for %%P in (4096 2048 1024 512 256 128 64 32 16 8 4 2 1) do (
if "!s:~%%P,1!" NEQ "" ( 
set /a "len+=%%P"
set "s=!s:~%%P!"
)
)
)
( 
endlocal
set "%~1=%len%"
exit /b
)

I got this code from someone else but cannot understand how it works. This wouldn't be a problem except that I tried to make this batch file applicable for use to be called from another batch, but ended up running into errors. If I could understand how exactly this works to get the length of the string I then will be able to tailor it so I can use it.

You don't necessarily need to tell me what every command does, (I do have some experience), but if you could tell me which commands are used in what order I could look up the ones I don't yet know.

%%P? 

!%~2!#? 

Just looks like rubbish to me.

Tom Burris
  • 380
  • 2
  • 19
  • As far as I can tell, the `!` is just a dummy placeholder and means nothing. It's just a way of forcing a character into a variable value to stop it being empty.... I think. `%~2` means take variable `%2` and "expand it removing quotes". Variable `%2` is the second parameter passed into the batch file. Perhaps you should show how you are calling it and what the error is. Also take a look at this for some clues: http://stackoverflow.com/questions/5034076/what-does-dp0-mean-and-how-does-it-work – Nick.Mc Aug 10 '15 at 05:52
  • If you count *abcdef!%%^^()^!*, it's 15 chars long. But if you run in batch, `%1` returns 14. The only character skipped the count is `%`, it won't co-op with each other `%` in a string.. – Happy Face Aug 10 '15 at 06:09
  • 1
    @Nick, the `!` character is the delayed expansion variant of `%`. – paxdiablo Aug 10 '15 at 06:28
  • Oh thanks for the clarification. – Nick.Mc Aug 10 '15 at 06:55
  • I was having issues carrying the variables from my original bat file, (that calls this one), and back. It would be quite a bit more convenient than what I could accomplish, which was having this file get the string from a text file made by the original then having this one save the length in the text file to be retrieved form the original. Solution? – Tom Burris Aug 11 '15 at 04:34

2 Answers2

2

A lot of this would become obvious if you sat down for hours and read the output of set /? but, assuming you don't have the time for that (and, let's face it, who on Earth really wants to read documentation?), I'll try and explain.

Specifically, there's a bit in that help text that states %x:S,L% (or the equivalent with delayed expansion, !x:S,L!) means the substring of the x variable, starting at S (zero-based) and L characters long.

The pseudo-code of that function is basically:

def strlen (byref RESULT, byval STRING):
    set str to STRING, appending "#" as well
    set len = 0
    for pos = 4096 to 1 inclusive, halving each time:
        while str has a character at position pos:
            add pos to len
            remove pos characters from start of str
        end
    end
    RESULT = len

The initial string has a character added so as to not be adversely affected by the substring with its first index being zero rather than one. The length of the string will then be the position of that final character.

The rest is relatively straightforward. The len variable is initialised to zero then, starting pos at 4096, if the string has a character at that position (!s:~%%P,1!" NEQ ""), increase len by that much and remove that many characters from the start.

Once the length of the adjusted string gets below 4096, you start working on 2048 and so on until there is only the # left in the string. The length has then been found and you return it (by actually setting the passed in first variable to that value).


So, for the string ABCDEFGHIJK (turned into ABCDEFGHIJK#), nothing would be added to len until pos reached eight. That's because the all the substrings beyond eleven would be empty.

Once pos reached eight however, the one-character substring at that position is I so you add eight to the length (to get eight) and removes the initial eight characters, giving you IJK#. The string is now to short to have another character at position eight, so pos becomes four.

Then no character is found at that offset (the four characters are at offsets zero, one, two and three) so you go immediately to two. There is a character K at that position so the length has two added (to become ten), the string becomes K# and, because there's now no character at offset two, pos becomes one.

With pos at one, there's a character # at that position so the length has one added (to become eleven), and the string becomes #. Now there's no character at position one so the loop exits, and the final length is returned to the caller.


If you have a hard time following that, use the following code which explains it better (with added debug statements):

@echo off
setlocal
set myString=ABCDEFGHIJK
call :strlen result myString
echo %result%
goto :eof

:strlen
    setlocal enabledelayedexpansion
    set "s=!%~2!#"
    echo.String is [!s!]
    set len=0
    for %%p in (4096 2048 1024 512 256 128 64 32 16 8 4 2 1) do (
        echo.   Position is [%%p], string is [!s!], character is [!s:~%%p,1!]
        if "!s:~%%p,1!" NEQ "" ( 
            set /a "len+=%%p"
            set s=!s:~%%p!
            echo.      Found char, len is [!len!], string is now [!s!]
        )
    )
    endlocal && set %~1=%len%

The output you see from that mirrors my explanation above:

String is [ABCDEFGHIJK#]
   Position is [4096], string is [ABCDEFGHIJK#], character is []
   Position is [2048], string is [ABCDEFGHIJK#], character is []
   Position is [1024], string is [ABCDEFGHIJK#], character is []
   Position is [512], string is [ABCDEFGHIJK#], character is []
   Position is [256], string is [ABCDEFGHIJK#], character is []
   Position is [128], string is [ABCDEFGHIJK#], character is []
   Position is [64], string is [ABCDEFGHIJK#], character is []
   Position is [32], string is [ABCDEFGHIJK#], character is []
   Position is [16], string is [ABCDEFGHIJK#], character is []
   Position is [8], string is [ABCDEFGHIJK#], character is [I]
      Found char, len is [8], string is now [IJK#]
   Position is [4], string is [IJK#], character is []
   Position is [2], string is [IJK#], character is [K]
      Found char, len is [10], string is now [K#]
   Position is [1], string is [K#], character is [#]
      Found char, len is [11], string is now [#]
11
paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953
1

paxdiablo did a great job explaining how that highly optimized batch script manages to compute the length of a string.

The original algorithm was developed at http://www.dostips.com/forum/viewtopic.php?p=5385, where you can follow how it was developed. There are many variations of the basic algorithm.

It is a laudable goal to understand how it works, but there is no need to understand the algorithm if all you want to do is develop a script that can be used by other scripts to compute the length of a string. However, you do need to understand the basics of how to develop and call batch "functions". There is a great tutorial at http://www.dostips.com/forum/viewtopic.php?p=5385.

I would reverse the order of the arguments, and make the return variable optional. If not specified, then the function can print the value to the screen (stdout). This makes it very convenient to use from the command line.

You can put the following script in a file named STRLEN.BAT, and place that file in a folder that is included in your PATH environment variable. I use c:\utils as a repository for various batch utilities.

STRLEN.BAT

:strlen  StrVar  [RtnVar]
::
:: Compute the length of the string in variable StrVar
:: and return the result in variable RtnVar.
:: If RtnVar is not specified, then print the result to stdout.
::
:: This code is a derivative of code developed at
:: http://www.dostips.com/forum/viewtopic.php?p=5385
::
@echo off 
setlocal EnableDelayedExpansion
set "s=!%~1!#"
set "len=0"
for %%P in (4096 2048 1024 512 256 128 64 32 16 8 4 2 1) do (
  if "!s:~%%P!" NEQ "" ( 
    set /a "len+=%%P"
    set "s=!s:~%%P!"
  )
)
endlocal & if "%~2" equ "" (echo %len%) else set "%~2=%len%"
exit /b

Usage is trivial.

To print out the length of the PATH variable from the command line:

D:\test>strlen path
1330

If you want to use the utility in a batch script, then of course you must use CALL.

@echo off
:: Print the length of PATH to the screen
call strlen path

echo(

:: Store the length of PATH in variable LEN, then show the result
call strlen path len
echo length of PATH = %len%

It is fine to develop a library of utility scripts that can be used by other scripts for your own use. But if you are distributing scripts to be used by others, it is common practice to embed the routines within a single script to make distribution simpler and more reliable.

Community
  • 1
  • 1
dbenham
  • 127,446
  • 28
  • 251
  • 390