Arrays, linked lists and other data structures in cmd.exe (batch) script

ArraysWindowsBatch FileCmdDelayedvariableexpansion

Arrays Problem Overview


I was playing with cmd.exe, but in its help I didn't find any info, how to define arrays.

I have found, how to define simple variables:

set a=10
echo %a%

But, I want to create arrays, linked list etc...

So, does it able in cmd.exe ( I mean: does in cmd.exe exist any array keywords? )

I want to realize some algorithms as:

  • bubble sort
  • quick sort
  • gnome sort

etc...

So, I also want to know, does Cmd.exe have references or instances, structs etc?

Cause its help not full in: /?

Could Cmd.exe be defined as full by Turing-Machine definition? ( Turing-Complete )

Arrays Solutions


Solution 1 - Arrays

Ok. I'll try to be as clear as possible to not be misunderstood...

In Windows Batch files a variable name should begin with a letter and may include any valid character, where valid characters are: #$'()*+,-.?@[]_`{}~ besides letters and digits.

This means that from the cmd.exe point of view, SET NORMAL_NAME=123 is exactly the same as SET A#$'()*+,-.?@[\]_{}~=123 and also the same as SET VECTOR[1]=123; all three are normal variables. This way, it is up to you to write variable names in the form of array elements:

set elem[1]=First element
set elem[2]=Second one
set elem[3]=The third one

This way, echo %elem[2]% will show Second one.

If you want to use another variable as index, you must know that the replacement of variables enclosed in percent symbols by their values is parsed from left to right; this means that:

set i=2
echo %elem[%i%]%

doesn't give the desired result because it means: show the value of the elem[ variable, followed by i, followed by the value of the ] variable.

To solve this problem you must use Delayed Expansion, that is, insert setlocal EnableDelayedExpansion command at the beginning, enclose index variables in percent symbols, and enclose the array elements in exclamation marks:

setlocal EnableDelayedExpansion
set elem[1]=First element
set elem[2]=Second one
set elem[3]=The third one
set i=2
echo !elem[%i%]!

You may also use parameters of FOR commands as indexes: for /L %%i in (1,1,3) do echo !elem[%%i]!. You must use !index! to store values in array elements when the index is changed inside a FOR or IF: set elem[!index!]=New value. To get the value of an element when the index changes inside FOR/IF, enclose the element in double percent symbols and precede the command with call. For example, to move a range of array elements four places to the left:

for /L %%i in (%start%,1,%end%) do (
   set /A j=%%i + 4
   call set elem[%%i]=%%elem[!j!]%%
)

Another way to achieve the previous process is to use an additional FOR command to change the delayed expansion of the index by an equivalent replaceable parameter, and then use the delayed expansion for the array element. This method runs faster than previous CALL:

for /L %%i in (%start%,1,%end%) do (
   set /A j=%%i + 4
   for %%j in (!j!) do set elem[%%i]=!elem[%%j]!
)

This way, the Batch file behaves like it manages arrays. I think the important point here is not to discuss if Batch manages arrays or not, but the fact that you may manage arrays in Batch files in an equivalent way of other programming languages.

@echo off
setlocal EnableDelayedExpansion

rem Create vector with names of days
set i=0
for %%d in (Sunday Monday Tuesday Wednesday Thrusday Friday Saturday) do (
   set /A i=i+1
   set day[!i!]=%%d
)

rem Get current date and calculate DayOfWeek
for /F "tokens=1-3 delims=/" %%a in ("%date%") do (
   set /A mm=10%%a %% 100, dd=10%%b %% 100, yy=%%c
)
if %mm% lss 3 set /A mm=mm+12, yy=yy-1
set /A a=yy/100, b=a/4, c=2-a+b, e=36525*(yy+4716)/100, f=306*(mm+1)/10, jdn=c+dd+e+f-1523, dow=jdn %% 7 + 1
echo Today is !day[%dow%]!, %date%

Note that index values are not limited to numbers, but they may be any string that contain valid characters; this point allows to define what in other programming languages are called associative arrays. At this answer there is a detailed explanation of the method used to solve a problem using an associative array. Note also that the space is a valid character in variable names, so you must pay attention to not insert spaces in variable names that may go unnoticed.

I elaborated on the reasons I have to use array notation in Batch files at this post.

In this post there is a Batch file that reads a text file and stores the indexes of the lines in a vector, then does a Buble Sort of vector elements based on line contents; the equivalent result is a sort over file contents.

In this post there is a basic Relational Data Base application in Batch based on indexes stored in files.

In this post there is a complete multiple linked-list application in Batch that assembles a large data structure taken from a subdirectory and displays it in the form of TREE command.

Solution 2 - Arrays

Windows shell scripting really isn't designed to work with arrays, let alone complex data structures. For the most part, everything's a string in the windows shell, but, there are some things you can do to "work with" arrays, like declaring n variables VAR_1, VAR_2, VAR_3... using a loop and filtering on the prefix VAR_, or creating a delimited string and then using the FOR construct that iterates over a delimited string.

Similarly, you can use the same basic idea to create a struct-like set of variables like ITEM_NAME, ITEM_DATA or w/e. I even found this link that talks about simulating an associative array in CMD.

It is all terribly hackish and inconvenient when it comes down to it. The command-line shell just wasn't designed for heavy programming. I agree with @MatteoItalia -- if you need serious scripting, use a real scripting language.

Solution 3 - Arrays

I made a bubble sort implementation in batch using pseudo-arrays a while ago. Not sure why you'd use it (although I will admit to doing so in another batch file) as it gets pretty slow as the list size increases. It was more to set myself a little challenge. Someone might find this useful.

:: Bubblesort
:: Horribly inefficient for large lists
:: Dave Johnson implementation 05/04/2013
@echo off
setlocal enabledelayedexpansion
:: Number of entries to populate and sort
set maxvalue=50
:: Fill a list of vars with Random numbers and print them
for /l %%a in (1,1,%maxvalue%) do (
	set /a tosort%%a=!random!
)
:: echo them
set tosort
:: Commence bubble sort
Echo Sorting...
set /a maxvalue-=1
set iterations=0
for /l %%a in (%maxvalue%,-1,1) do ( REM Decrease by 1 the number of checks each time as the top value will always float to the end
	set hasswapped=0
		for /l %%b in (1,1,%%a) do (
			set /a next=%%b+1
			set next=tosort!next!
			set next=!next!
			call :grabvalues tosort%%b !next!
			rem echo comparing tosort%%b = !tosortvalue! and !next! = !nextvalue!
			if !nextvalue! LSS !tosortvalue! (
			rem	set /a num_of_swaps+=1
			rem	echo Swapping !num_of_swaps!
				set !next!=!tosortvalue!
				set tosort%%b=!nextvalue!
				set /a hasswapped+=1
			)
		)
	set /a iterations+=1
	if !hasswapped!==0 goto sorted
)
goto:eof
:grabvalues
set tosortvalue=!%1!
set nextvalue=!%2!
goto:eof
:sorted
::nice one our kid
set tosortvalue=
echo Iterations required: %iterations%
set tosort
endlocal

Solution 4 - Arrays

Concerning this statement*:

> I have found, how to define simple variables: > > set a = 10 > echo %a%

This is simply wrong! Variable a will remain empty (supposing it was empty initially) and echo %a% will return ECHO is on. A variable called aSPACE will actually be set to the value SPACE10.

So for the code to work, you must get rid of the SPACEs around the equal-to sign:

set a=10
echo %a%

To make the assignment safe against all characters, use the quoted syntax (supposing you have the command extensions enabled, which is the default for the Windows command prompt anyway):

set "a=1&0"
echo(%a%

For all the rest of your question I recommend to read Aacini's great and comprehensive answer.


*) This statement has meanwhile been edited out.

Solution 5 - Arrays

Seriously speaking: I never heard that batch has arrays, maybe you can emulate them with some strange trick, but I wouldn't call it a good idea.

References/instances/structs are stuff for a real language, cmd scripting is just a bunch of extensions that grew over the very primitive interpreter that was command.com, you can do some basic scripting, but anything more complicated than a bunch of calls to other commands is doomed to become ugly and incomprehensible.

The only "advanced" construct is the do-it-all weirdo for loop, which, mixed with the strange "rules" of variable substitution (%var%, %%var, !var!, are different stuff because of the idiotic parser), makes writing even trivial algorithms a collection of strange hacks (see e.g. here for an implementation of quicksort).

My tip is, if you want to do your scripting in a sane way, use a real scripting language, and leave batch for simple, quick hacks and for backwards compatibility.

Solution 6 - Arrays

The following program simulates vectors (arrays) operations in cmd. The subroutines presented in it were initially designed for some special cases like storing the program parameters in an array or looping through filenames in a "for" loop and storing them in an array. In these cases, in an enabled delayed expansion block, the "!" characters - if present in values of the parameters or in the "for" loop variable's value - would get interpreted. That's why, in these cases, the subroutines must be used inside a disabled delayed expansion block:

@echo off

rem The subroutines presented bellow implement vectors (arrays) operations in CMD

rem Definition of a vector <v>:
rem      v_0 - variable that stores the number of elements of the vector;
rem      v_1..v_n, where n=v_0 - variables that store the values of the vector elements.


rem :::MAIN START:::

setlocal disabledelayedexpansion

	rem Getting all the parameters passed to the program in the vector 'params':
	rem Delayed expansion is left disabled in order not to interpret "!" in the program parameters' values (%1, %2, ... );
	rem If a program parameter is not quoted, special characters in it (like "^", "&", "|") get interpreted at program launch.
:loop1
	set "param=%~1"
	if defined param (
		call :VectorAddElementNext params param
		shift
		goto :loop1
	)
	rem Printing the vector 'params':
	call :VectorPrint params

	pause&echo.

	rem After the vector variables are set, delayed expansion can be enabled and "!" are not interpreted in the vector variables's values:
	echo Printing the elements of the vector 'params':
	setlocal enabledelayedexpansion
		if defined params_0 (
			for /l %%i in (1,1,!params_0!) do (
				echo params_%%i="!params_%%i!"
			)
		)
	endlocal

	pause&echo.

	rem Setting the vector 'filenames' with the list of filenames in the current directory:
	rem Delayed expansion is left disabled in order not to interpret "!" in the %%i variable's value;
	for %%i in (*) do (
		set "current_filename=%%~i"
		call :VectorAddElementNext filenames current_filename
	)
	rem Printing the vector 'filenames':
	call :VectorPrint filenames

	pause&echo.

	rem After the vector variables are set, delayed expansion can be enabled and "!" are not interpreted in the vector variables's values:
	echo Printing the elements of the vector 'filenames':
	setlocal enabledelayedexpansion
		if defined filenames_0 (
			for /l %%i in (1,1,!filenames_0!) do (
				echo filenames_%%i="!filenames_%%i!"
			)
		)
	endlocal

	pause&echo.

endlocal
pause

rem :::MAIN END:::
goto :eof


:VectorAddElementNext
rem Vector Add Element Next
rem adds the string contained in variable %2 in the next element position (vector length + 1) in vector %1
(
	setlocal enabledelayedexpansion
		set "elem_value=!%2!"
		set /a vector_length=%1_0
		if not defined %1_0 set /a vector_length=0
		set /a vector_length+=1
		set elem_name=%1_!vector_length!
)
(
	endlocal
	set "%elem_name%=%elem_value%"
	set %1_0=%vector_length%
	goto :eof
)

:VectorAddElementDVNext
rem Vector Add Element Direct Value Next
rem adds the string %2 in the next element position (vector length + 1) in vector %1
(
	setlocal enabledelayedexpansion
		set "elem_value=%~2"
		set /a vector_length=%1_0
		if not defined %1_0 set /a vector_length=0
		set /a vector_length+=1
		set elem_name=%1_!vector_length!
)
(
	endlocal
	set "%elem_name%=%elem_value%"
	set %1_0=%vector_length%
	goto :eof
)

:VectorAddElement
rem Vector Add Element
rem adds the string contained in the variable %3 in the position contained in %2 (variable or direct value) in the vector %1
(
	setlocal enabledelayedexpansion
		set "elem_value=!%3!"
		set /a elem_position=%2
		set /a vector_length=%1_0
		if not defined %1_0 set /a vector_length=0
		if !elem_position! geq !vector_length! (
			set /a vector_length=elem_position
		)
		set elem_name=%1_!elem_position!
)
(
	endlocal
	set "%elem_name%=%elem_value%"
	if not "%elem_position%"=="0" set %1_0=%vector_length%
	goto :eof
)

:VectorAddElementDV
rem Vector Add Element Direct Value
rem adds the string %3 in the position contained in %2 (variable or direct value) in the vector %1
(
	setlocal enabledelayedexpansion
		set "elem_value=%~3"
		set /a elem_position=%2
		set /a vector_length=%1_0
		if not defined %1_0 set /a vector_length=0
		if !elem_position! geq !vector_length! (
			set /a vector_length=elem_position
		)
		set elem_name=%1_!elem_position!
)
(
	endlocal
	set "%elem_name%=%elem_value%"
	if not "%elem_position%"=="0" set %1_0=%vector_length%
	goto :eof
)

:VectorPrint
rem Vector Print
rem Prints all the elements names and values of the vector %1 on sepparate lines
(
	setlocal enabledelayedexpansion
		set /a vector_length=%1_0
		if !vector_length! == 0 (
			echo Vector "%1" is empty!
		) else (
			echo Vector "%1":
			for /l %%i in (1,1,!vector_length!) do (
				echo [%%i]: "!%1_%%i!"
			)
		)
)
(
	endlocal
	goto :eof
)

:VectorDestroy
rem Vector Destroy
rem Empties all the elements values of the vector %1
(
	setlocal enabledelayedexpansion
		set /a vector_length=%1_0
)
(
	endlocal
	if not %vector_length% == 0 (
		for /l %%i in (1,1,%vector_length%) do (
			set "%1_%%i="
		)
		set "%1_0="
	)
	goto :eof
)

It is also possible to store the program parameters in an "array" or loop through the filenames in a directory using a "for" loop and store them in an "array" (without interpreting "!" in their values) without using the presented subroutines in the program above:

@echo off

setlocal disabledelayedexpansion

	rem Getting all the parameters passed to the program in the array 'params':
	rem Delayed expansion is left disabled in order not to interpret "!" in the program parameters' values (%1, %2, ... );
	rem If a program parameter is not quoted, special characters in it (like "^", "&", "|") get interpreted at program launch.
	set /a count=1
:loop1
	set "param=%~1"
	if defined param (
		set "params_%count%=%param%"
		set /a count+=1
		shift
		goto :loop1
	)
	set /a params_0=count-1

	echo.

	rem After the array variables are set, delayed expansion can be enabled and "!" are not interpreted in the array variables's values:
	rem Printing the array 'params':
	echo Printing the elements of the array 'params':
	setlocal enabledelayedexpansion
		if defined params_0 (
			for /l %%i in (1,1,!params_0!) do (
				echo params_%%i="!params_%%i!"
			)
		)
	endlocal

	pause&echo.

	rem Setting the array 'filenames' with the list of filenames in the current directory:
	rem Delayed expansion is left disabled in order not to interpret "!" in the %%i variable's value;
	set /a count=0
	for %%i in (*) do (
		set "current_filename=%%~i"
		set /a count+=1
		call set "filenames_%%count%%=%%current_filename%%"
	)
	set /a filenames_0=count

	rem After the array variables are set, delayed expansion can be enabled and "!" are not interpreted in the array variables's values:
	rem Printing the array 'filenames':
	echo Printing the elements of the array 'filenames':
	setlocal enabledelayedexpansion
		if defined filenames_0 (
			for /l %%i in (1,1,!filenames_0!) do (
				echo filenames_%%i="!filenames_%%i!"
			)
		)
	endlocal

endlocal
pause

goto :eof

Solution 7 - Arrays

TLDR:

I hit upon the Idea of using a "For" loop and the "set" command to allow the parsing of variables, allowing me to create Pseudo Arrays, both ordered and linked-list style, and more importantly, Pseudo Objects akin to structures.

A typical batch Pseudo Array, and how to parse:

SET "_Arr.Names="Name 1" "Name 2" ... "Name N""

FOR %A IN (%_Arr.Names%) DO @( Echo.%~A )

REM Results:

REM Name 1
REM Name 2
REM ...
REM Name N

Below we make some Dumb Pseudo Arrays and a manual ordered Pseudo Array, plus create an Ordered Pseudo Array catching the output of a DIR Command.

We also take the Dumb Pseudo Arrays and convert them into Ordered arrays (removing the original Dumb Pseudo Array variables after).

We then update all of the ordered Arrays to contain more elements manually.

Finally we Dynamically report some of the values from the Array by doing a pre-defined For L Loop for values 7 to 9, and Generating a Random value to print the 4th example value of the array.

Note:

I create a variable to hold the method for adding members to make adding them simpler.

I point this out as it should make it easy to see how we make the minor jump from ordered arrays to Pseudo objects.

@(
 SETLOCAL ENABLEDELAYEDEXPANSION
 ECHO OFF
 
 REM Manually Create a shortcut method to add more elements to a specific ordered array
 SET "_Arr.Songs.Add=SET /A "_Arr.Songs.0+=1"&&CALL SET "_Arr.Songs.%%_Arr.Songs.0%%"
 
 REM Define some 'dumb' Pseudo arrays
 SET "_Arr.Names="Name 1" "Name 2" "Name 3" "Name 4" "Name 5" "Name 6" "Name 7" "Name 8""
 SET "_Arr.States="AL" "AK" "AZ" "AR" "CA" "CO" "CT" "DE" "FL" "GA" "HI" "ID" "IL" "IN" "IA" "KS" "KY" "LA" "ME" "MD" "MA" "MI" "MN" "MS" "MO" "MT" "NE" "NV" "NH" "NJ" "NM" "NY" "NC" "ND" "OH" "OK" "OR" "PA" "RI" "SC" "SD" "TN" "TX" "UT" "VT" "VA" "WA" "WV" "WI" "WY""
 
)

REM Manually Create One Ordered Array
%_Arr.Songs.Add%=Hey Jude"
%_Arr.Songs.Add%=The Bartman"
%_Arr.Songs.Add%=Teenage Dirtbag"
%_Arr.Songs.Add%=Roundabout"
%_Arr.Songs.Add%=The Sound of Silence"
%_Arr.Songs.Add%=Jack and Diane"
%_Arr.Songs.Add%=One Angry Dwarf and 200 Solumn Faces"

REM Turn All Pre-Existing Normal Pseudo Arrays into Element Arrays
REM Since Ordered Arrays use Index 0, we can skip any manually created Ordered Arrays:
FOR /F "Tokens=2 Delims==." %%A IN ('SET _Arr. ^| FIND /V ".0=" ^| SORT') DO (
 IF /I "%%~A" NEQ "!_TmpArrName!" (
  SET "_TmpArrName=%%~A"
  IF NOT DEFINED _Arr.!_TmpArrName!.Add (
   REM Create a shortcut method to add more members to the array
   SET "_Arr.!_TmpArrName!.Add=SET /A "_Arr.!_TmpArrName!.0+=1"&&CALL SET "_Arr.!_TmpArrName!.%%_Arr.!_TmpArrName!.0%%"
  )
  FOR %%a IN (!_Arr.%%~A!) DO (
   CALL SET /A "_Arr.!_TmpArrName!.0+=1"
   CALL SET "_Arr.!_TmpArrName!.%%_Arr.!_TmpArrName!.0%%=%%~a"
  )
 )
 IF DEFINED _Arr.!_TmpArrName! (
  REM Remove Unneeded Dumb Psuedo Array "_Arr.!_TmpArrName!"
  SET "_Arr.!_TmpArrName!="
 )
)

REM Create New Array of unknown Length from Command Output, and Store it as an Ordered Array
 SET "_TmpArrName=WinDir"
 FOR /F "Tokens=* Delims==." %%A IN ('Dir /B /A:D "C:\Windows"') DO (
  IF NOT DEFINED _Arr.!_TmpArrName!.Add (
   SET "_Arr.!_TmpArrName!.Add=SET /A "_Arr.!_TmpArrName!.0+=1"&&CALL SET "_Arr.!_TmpArrName!.%%_Arr.!_TmpArrName!.0%%"
  )
  CALL SET /A "_Arr.!_TmpArrName!.0+=1"
  CALL SET "_Arr.!_TmpArrName!.%%_Arr.!_TmpArrName!.0%%=%%~A"
 )
)

REM Manually Add additional Elements to the Ordered Arrays:
%_Arr.Names.Add%=Manual Name 1"
%_Arr.Names.Add%=Manual Name 2"
%_Arr.Names.Add%=Manual Name 3"

%_Arr.States.Add%=51st State"
%_Arr.States.Add%=52nd State"
%_Arr.States.Add%=53rd State"

%_Arr.Songs.Add%=Live and Let Die"
%_Arr.Songs.Add%=Baby Shark"
%_Arr.Songs.Add%=Safety Dance"

%_Arr.WinDir.Add%=Fake_Folder 1"
%_Arr.WinDir.Add%=Fake_Folder 2"
%_Arr.WinDir.Add%=Fake_Folder 3"

REM Test Output:

REM Use a For Loop to List Values 7 to 9 of each array and A Psuedo Rnadom 4th value
REM We are only interested in Ordered Arrays, so the .0 works nicely to locate those exclusively.
FOR /F "Tokens=2,4 Delims==." %%A IN ('SET _Arr. ^| FIND ".0=" ^| SORT') DO (
 CALL :Get-Rnd %%~B
 ECHO.
 ECHO.%%~A 7 to 9, Plus !_Rnd#! - Psuedo Randomly Selected
 FOR /L %%L IN (7,1,9) DO (
  CALL Echo. * Element [%%L] of %%~A Pseudo Array = "%%_Arr.%%~A.%%L%%"
 )
 CALL Echo. * Random Element [!_Rnd#!] of %%~A Pseudo Array = "%%_Arr.%%~A.!_Rnd#!%%"
)
ENDLOCAL 
GOTO :EOF

:Get-Rnd
 SET /A "_RandMax=(32767 - ( ( ( 32767 %% %~1 ) + 1 ) %% %~1) )", "_Rnd#=!Random!"
 IF /I !_Rnd#! GTR !_RandMax! ( GOTO :Get_Rnd# )
 SET /A "_Rnd#%%=%~1"
GOTO :EOF

Example Results:

Results:

Names 7 to 9, Plus 5 - Psuedo Randomly Selected
 * Element [7] of Names Pseudo Array = "Name 7"
 * Element [8] of Names Pseudo Array = "Name 8"
 * Element [9] of Names Pseudo Array = "Manual Name 1"
 * Random Element [5] of Names Pseudo Array = "Name 5"

Songs 7 to 9, Plus 5 - Psuedo Randomly Selected
 * Element [7] of Songs Pseudo Array = "One Angry Dwarf and 200 Solumn Faces"
 * Element [8] of Songs Pseudo Array = "Live and Let Die"
 * Element [9] of Songs Pseudo Array = "Baby Shark"
 * Random Element [5] of Songs Pseudo Array = "The Sound of Silence"

States 7 to 9, Plus 9 - Psuedo Randomly Selected
 * Element [7] of States Pseudo Array = "CT"
 * Element [8] of States Pseudo Array = "DE"
 * Element [9] of States Pseudo Array = "FL"
 * Random Element [9] of States Pseudo Array = "FL"

WinDir 7 to 9, Plus 26 - Psuedo Randomly Selected
 * Element [7] of WinDir Pseudo Array = "assembly"
 * Element [8] of WinDir Pseudo Array = "AUInstallAgent"
 * Element [9] of WinDir Pseudo Array = "Boot"
 * Random Element [26] of WinDir Pseudo Array = "Fonts"

Initially I would do things similar to Aacini, a simple line of variables with an incremental counter, manually, or assigning them through a simple loop from a quick list of variables.

This was fine for small 2-D Arrays.

However I find it a pain for long arrays of data, especially when I need multi-value content.

To say nothing of when I need to match and populate content in those multi-dimensional arrays dynamically, where the simple usage there breaks down.

I found that it became hard when you ended up needing multiple arrays of information which you needed to update or add features to across the board.

As such an array is essentially a list of sub-strings you need to exports as variables, and adding or changing their ordering means changing your code.

Take for instance a scenario where you need to log into multiple FTP servers, delete files older than X days from certain paths.

Initially you might create simple arrays of substrings I'll define like this:

Site.##=[Array (String)] [Array (String)] @(
       IP=[SubSting],
       Username=[SubString],
       Password[SubString])

Or as shown in in this example code.

(
  SETOCAL
  ECHO OFF

  REM Manage Sites:
  SET "Sites=13"
  SET "MaxAge=28"

  SET "Site.1="[IP]" "[User Name]" "[Password]" "[Path]""
  SET "Site.2="[IP]" "[User Name]" "[Password]" "[Path]""
  SET "Site.3="[IP]" "[User Name]" "[Password]" "[Path]""
  REM  ...
  SET "Site.11="[IP]" "[User Name]" "[Password]" "[Path]""
  SET "Site.12="[IP]" "[User Name]" "[Password]" "[Path]""
  SET "Site.13="[IP]" "[User Name]" "[Password]" "[Path]""
)

FOR /L %%L IN (1,1,%Sites%) DO (
   FOR /F "Tokens=*" %%A IN ('CALL ECHO %%Site.%%L%%') DO (
      Echo. Pulled this example from a more complex example of my actual code, so the example variables may not need this loop, but it won't hurt to have if they don't need the extra expansion.
     Call :Log
     CALL :DeleteFTP %%~A
   )
)

GOTO :EOF
:DeleteFTP
   REM Simple ftp command for cygwin to delete the files found older than X days.
   SET "FTPCMD="%~dp0lftp" %~1 -u %~2,%~3 -e "rm -rf %~4%MaxAge% "
   FOR /F "Tokens=*" %%F IN ('"%FTPCMD% 2^>^&1"') DO @(
     ECHO.%%~F
   )
GOTO :EOF

Now, 13 sites, this isn't all that bad, I'm sure you're saying. right? You can just add one at the end and then put in the info and done.

Then you need to add the names of the sites in for reporting, so you add another term to each string at place 5 so you don't have to change your function..

::...
SET "Site.1="[IP]" "[User Name]" "[Password]" "[Path]" "[Site Name]""
::...

Then you realise you'll need to keep them in order by their site names (or IPs, but the names are easier for most people to remember and you need to be able to let other people have a look) so you change the order in all 13 spots, the call to expand the variables, and the function.

::...
SET "Site.1="[Site Name]" "[IP]" "[User Name]" "[Password]" "[Path]""
::...
FOR /F "Tokens=*" %%A IN ('CALL ECHO %%Site.%%L%%')
::...
SET "FTPCMD="%~dp0lftp" %~2 -u %~3,%~4 -e "rm -rf %~5%MaxAge% "
::...

Then it just keeps getting worse:

  • The number of directories you have to check, using different users, at the same site begins increasing.

  • You realise you need to have different retention times per site, and later, per directory.

  • You end up having 30, 40,50 of these and it's hard to remember which is which by looking at the end of a long string and copying them around, etc.

  • You stopped adding more paths, but sometime you have to remove old ones or it causes problems when they are gone, and if you forget to update the total number of site sin the list you might miss running the script on some.

  • when a directory is added or removed you have to add it/remove it fro each site making it harder to use the ordering, and easier to miss sites as they aren;t easy to ID.

Just, what a pain, and this isn't even when you need to have a dynamic set of objects, this is all manual.

So what can you do? Well, here's what I did:

I ended up resorting to implementing a sort of poor-mans Structure or Object-array (of strings) in my cmd scripts where the need fits.

IE the structure would be a "Site Object" which would have multiple properties, which might be objects with sub properties themselves. Since CMD is not actually Object oriented, its a bit of a kludge, just as arrays are.

Since the example I started with ended up being the first place I tried these you can see this intermediate amalgam step I'll define like this:

eg: Site.[ID].[Object Property]=[Value, or array of values]

   Site
     .ID=[int]
      .Name=[string]
      .Path=[String]
      .MaxAge=[Int]
      .Details=[Array (String)] @(
       IP=[SubSting],
       Username=[SubString],
       Password[SubString])

To combat the issue with needing to re-order sets of Data on the fly I considered using a form of linked lists I toyed with, but since I wanted to easily add items to each grouping of sites while retaining order between sites I settled on a simple method.

Here is another code example of this step in usage:

@(
    SETLOCAL ENABLEDELAYEDEXPANSION
    ECHO OFF
    
    SET "_SiteCount=0"
    SET "_SiteID=0"
    
    SET /A "_SiteID= !_SiteID! + 1"
    SET "Site.!_SiteID!.MaxAge=Day5Ago"
    SET "Site.!_SiteID!.Name=[SITE NAME HEADER FOR EMAIL]"
    SET "Site.!_SiteID!.Detail="[IP]" "[UserName]" "[Password]" "[Path]""
    
    REM ...

    SET /A "_SiteID= !_SiteID! + 1"
    SET "Site.!_SiteID!.MaxAge=Day15Ago"
    SET "Site.!_SiteID!.Name=[SITE NAME HEADER FOR EMAIL]"
    SET "Site.!_SiteID!.Detail="[IP]" "[UserName]" "[Password]" "[Path]""
)

CALL :Main

(
    ENDLOCAL
    Exit /b %eLvl%
)

:Main
   REM In some forms of these the order isn't meaningful, but in others you need to follows the order and so we just count he number of site objects by counting one of their properties.
   FOR /F %%A IN ('SET ^| FIND /I "Site." ^| FIND /I ".Name="') DO ( CALL SET /A "_SiteCount+=1" )
    FOR /L %%L IN (1,1,34) DO (
        CALL :PSGetDate_DaysAgo %%L
    )
    FOR /L %%L IN (1,1,%_SiteCount%) DO (
        SET "Site.%%L.Create=NONE"
    )
    FOR /L %%L IN (1,1,%_SiteCount%) DO (
        FOR /F "Tokens=*" %%A IN ('CALL ECHO ""%%Site.%%L.Name%%" %%Site.%%L.Detail%% "Site.%%L" "%%%%Site.%%L.MaxAge%%%%""') DO (
            CALL ECHO CALL :DeleteFTP %%~A
            CALL :DeleteFTP %%~A
        )
    )
    CALL :SendMail "%EMLog%" "%_EMSubject%"

GOTO :EOF

:DeleteFTP
    REM ECHO.IF "%~7" EQU "%skip%" (
    IF "%~7" EQU "%skip%" (
        GOTO :EOF
    )
    SET "FTPCMD="%~dp0lftp" %~2 -u %~3,%~4 -e "rm -rf %~5%~7 "
    SET "FTPCMD=%FTPCMD%; bye""
    FOR /F "Tokens=*" %%F IN ('"%FTPCMD% 2^>^&1"') DO @(
        ECHO."%%F"
        ECHO."%%~F"
        REM CALL :Output "%Temp%\%~2_%~7.log" "%%F"
        %OP% "%Temp%\%~2_%~7.log"
        SET "FTPOut=%%~F"
    )
GOTO :EOF

As you can probably see, these structures work very well where you have sets of forking hierarchical data that you need to apply manually and show data in a specific sequential order.

Although, to be sure today I usually make the base of the structures the name of the script, as I find this is more useful, and may or may not use ordered arrays depending on need.

SET "_GUID=^%Time^%_^%Random:~-1^%^%Random:~-1^%^%Random:~-1^%^%Random:~-1^%^%Random:~-1^%^%Random:~-1^%^%Random:~-1^%^%Random:~-1^%^%Random:~-1^%"

eg: %~n0.[ObjectName].[Object Property].[Object Sub Property]=[Value, or array of values]

       [Script Name]
         .[Object Name](May Hold Count of Names)=[int]
          .Name=[string]
          .Paths(May Hold Count of IDs)=[INT]
            .GUID=%_GUID%
             .Path=String
             .MaxAge=[Int]
          .Details=[Array (String)] @(
           IP=[SubSting],
           Username=[SubString],
           Password[SubString])

But what about where you might have to collect large sets of dynamically generated data, and group it into pre-made categories and then mix that up to report it.

Well here again these can be useful too, and you can build them on the fly in your code adding more properties as needed.

In a similar script to the FTP delete, we need to check the sizes of multiple directories, I am going to dumb tis one down quite a bit and look at just one check:

@(
    SETLOCAL ENABLEDELAYEDEXPANSION
    ECHO OFF

    SET /A "_SiteID= !_SiteID! + 1"
    SET "SiteName=SiteA"
    SET "%~n0.!SiteName!=%%_SiteID%%
    SET "%~n0.!SiteName!.SiteID=!_SiteID!
    SET "%~n0.!SiteName!.Paths="PathA" "PathB" "PathC" "PathD" "PathE""
)

CALL :CheckFTP [FTP Login variables from source object including Site ID]

:CheckFTP
 REM Not necessary to assign Variables, doing this for exposition only:
 CALL SET "TempSiteName=%~6"
 CALL SET "TempPaths=%%%~n0.%~1.Paths%%"
 REM Clear the site Temp KB variables
 FOR \F "Tokens=2* Delims== " %%H IN (%TempPaths% "Total" "Temp") DO (
  CALL SET /A "%%%~n0.%~1.Paths.%%~H.KB=0"
 )
 FOR %%J IN (%TempPaths%) DO (
   FOR /F "Tokens=1-2" %%F IN ('[FTP Command using source object options]') DO @(
     CALL :SumSite "%~6" "%%~F" "%%~G"
     FOR /F "Tokens=1,2,* delims=/" %%f IN ("%%~G") DO (
       CALL :ConvertFolder "%~6" "%%~F" "%%~g" "%%~h" "%~6_%%~g_%%~h"
     )
   )
 )

FOR /F "Tokens=3,4,7 Delims==_." %%g IN ('SET ^| FIND /I "%~6_" ^| FIND /I ".KB" ^| FIND /I /V "_."') DO (
    CALL :WriteFolder "%%g/%%~h" "%TmpFile%" "%~6_%%~g_%%~h"
    REM echo.CALL :WriteFolder "%%g/%%~h" "%TmpFile%" "%~6_%%~g_%%~h"
)
CALL :ConvertSite "%~1"
CALL :WriteTotalFolder "%~7" "%TmpFile%" "%~6"
CALL :SendMail "%TmpFile%" "Backup_%~1"
GOTO :EOF

:SumSite
  CALL SET "TSumPaths=%%%~n0.%~1.Paths%% "Total""
   FOR %%H IN (%TSumPaths%) DO (
    CALL SET /A "%~n0.%~1.Paths.%%~H.KB=%%%~n0.%~1.Paths.%%~H.KB%%+%~2"
  )

:SumSite
  CALL SET "TSumPaths=%%%~n0.%~1.Paths%% "Total""
   FOR %%H IN (%TSumPaths%) DO (
    CALL SET /A "%~n0.%~1.Paths.%%~H.KB=%%%~n0.%~1.Paths.%%~H.KB%%+%~2"
  )
GOTO :EOF

:ConvertFolder
    REM Convert's Folder values to MB and GB
    SET /A "%~1.Temp.KB=%~2"
    CALL SET /A "%~1.Temp.MB=%%%~1.Temp.KB%%/1024"
    CALL SET /A "%~1.Temp.GB=(%%%~1.Temp.KB%%/1024)/1024"
    CALL SET /A "%~5.Temp.KB=%%%~5.Temp.KB%%+%~2"
    CALL SET /A "%~5.Temp.MB=%%%~5.Temp.KB%%/1024"
    CALL SET /A "%~5.Temp.GB=(%%%~5.Temp.KB%%/1024)/1024"
GOTO :EOF

:WriteFolder

    CALL :PickGMKBytes "%~1" "%~2" "G" "M" "K" "%%%~3.Temp.GB%%" "%%%~3.Temp.MB%%" "%%%~3.Temp.KB%%"

GOTO :EOF

:PickGMKBytes

    IF /I "%~6" NEQ "" (
        IF /I "%~6"=="0" (
            CALL :PickGMKBytes "%~1" "%~2" "%~4" "%~5" "%~6" "%~7" "%~8"
        ) ELSE (
            CALL :Output "%~2" "%~6%~3  %~1"
        )
    ) ELSE (
        CALL :Output "%~2" "0B  %~1"
    )

GOTO :EOF


:ConvertSite
 CALL SET "TempPaths=%%%~n0.%~1.Paths%%"
    FOR %%V IN (%TempPaths% "Total") DO (
        CALL SET /A "%~1.%%~V.MB=%%%~1.%%~V.KB%%/1024"
        CALL SET /A "%~1.%%~V.GB=(%%%~1.%%~V.KB%%/1024)/1024"
    )

GOTO :EOF

To be fair, this script example may not be very explicit in showing what is happening, and I had to make changes on the fly to fix a new object style, but essentially: It creates connection objects, and then dynamically extends them to include sub folders, and maintain running totals for each subfolder and site in KB, MB, and GB, and pics which of the values to report after summing up all of the directories for a given folder etc dynamically.

While i had to edit it a bit because this is also an earlier version of these as well, I thought it was one of the instances where it might best show the benefits. If I find a better example in one of my other scripts I might update it there as well.

Solution 8 - Arrays

All purpose Array handling script

@ECHO OFF
Set "UseErr=Echo/&Echo/Usage Error - Ensure command extensions and Delayed Expansion are enabled with: &Echo/Setlocal EnableExtensions EnableDelayedExpansion&Echo/ or from the command line:&Echo/CMD /V:On /K&Exit /B 1"
If Not "!Comspec!"=="%Comspec%" (%UseErr%)
(Set "GRPNm="&Set "TAB=	"&Set "S_Offset="&Set "mode="&Set "#STDOut="&Set "nGRPNm="&Set "#Order="&Set "#Help="&Set "Inset="&Set "Usage=Echo/###&Exit /B 1") > Nul 2> Nul
(Set "SwParam="&Set "SwFParam="&Set "#ORP#=0"&Set "#FP#=0"&Set "Inset="&Set "#STDOut=0"&Set "GRPNm="&Set "!GRPNm!="&Set "SubEl="&Set "FlNm=%~n0"& Set "Mode="&Set "FindV=") > Nul 2> Nul
If "%~1"=="" (
    Echo/&Echo/Modes:&Echo/ [Def]!TAB!!TAB!!TAB!Define, modify or clear an array.
    Echo/ [Def]!TAB!!TAB!!TAB!Switches:!TAB![/A:Groupname] [/F:Filepath.ext] [/D] [/O:Index#Arg] [/E:Element Sub value] [[element0] ~ [element#]]
    Echo/ [Sort-int]!TAB!!TAB!Sorts array by lowest or highest value using /L or /H switches
    Echo/ [Sort-int]!TAB!!TAB!Switches:!TAB![/A:Groupname] [/N:New Groupname] [/L^|/H] [/D]
    Echo/ [Sort-str]!TAB!!TAB!Sorts an array or text files string values using alphanumerical order of sort: [0-9][a-z]
    Echo/ [Sort-str]!TAB!!TAB!Switches:!TAB![/A:Groupname] [/F:Filepath.ext] [/D]
    Echo/ [Find]    !TAB!!TAB!Searches an array for the string value supplied.
    Echo/ [Find] [searchstring]!TAB!Switches: [/A:Groupname]&Echo/
    %Usage:###=/M:Mode required&Echo/[Def][Sort-int^|str][Find-Value]%
   ) Else Call :GetArgs %*
If Errorlevel 1 Exit /B 1
If "!Mode!"=="" (%Usage:###=/M:Mode  required&Echo/[Def][Sort-int^|str][Find-Value]%)
Call :!Mode! %* 2> Nul || (%Usage:###=Invalid Mode or switch error for /M:!Mode!&Echo/[Def][Sort-int^|str][Find-Value]%)
Exit /B 0
:str
 Set "Usage=Echo/###&Echo/Call !FlNm! ["/F:filepath.ext" ^| "/A:Array Group Name"] & Exit /B 1"
 Set "#!GRPNm!=0"
 If "!#FP#!"=="1" (
  (For /F "UseBackQ Delims=" %%G in (`Type "!FilePath!" ^| Sort`)Do (
   For %%x in ("!GRPNm![!#%GRPNm%!]") Do (
    Setlocal DisableDelayedExpansion
    Endlocal & Set "%%~x=%%~G"
    If "!#STDOut!"=="1" Echo/%%~x=%%~G
   )
   Set /A "#!GRPNm!+=1"
  )) 2> Nul || (%Usage:###:=Echo/Invalid Filepath:"!FilePath!"%)
  Exit /B 0
 )
 If Not "!#FP#!"=="1" (For /F "Tokens=1,2 Delims==" %%G in ('Set !GRPNm![')Do Echo/%%H)>"%TEMP%\__Sort.txt"
 (For /F "UseBackQ Delims=" %%G in (`Type "%TEMP%\__Sort.txt" ^| Sort`)Do (
   For %%x in ("!GRPNm![!#%GRPNm%!]") Do (
    Setlocal DisableDelayedExpansion
    Endlocal & Set "%%~x=%%~G"
    If "!#STDOut!"=="1" Echo/%%~x=%%~G
   )
    Set /A "#!GRPNm!+=1"
  )
 )
 Del /Q "%TEMP%\__Sort.txt"
Exit /B 0
:Find
 Set "Usage=Echo/###&Echo/Call !FlNm! [/M:Find-Searchstring] [/A:Group Name]&Exit /B 1"
 If "!FindV!"=="" (%Usage:###=/M:Find-Value Required%)
 (For /F "Tokens=1,2 Delims==" %%i in ('Set !GRPNm![') Do Echo/"%%j"|"%__AppDir__%findstr.exe"/LIC:"!FindV!" > Nul 2> Nul && (Echo/!FindV! found:&Echo/%%~i=%%~j))
Exit /B 0
:Int
Set "Usage=Echo/###&Echo/Call !FlNm! [/M:Sort-Int] [/A:Group Name] [/N:New Group Name] [Sort-Int] [/H^|/L]&Echo/Call %~n0 [/M:Sort-Int] [/A:Groupname] [Sort-Int] [/H^|/L]&Exit /B 1"
If "!#Help!"=="1" (%Usage:###=/M:Sort-Int Usage:%)
If "!nGRPNm!"=="" Set "nGRPNm=!GRPNm!"
If Not "%#Order%"=="" (Call :Sort%#Order% !nGRPNm! #!nGRPNm! !Inset!) Else (%Usage:###=Sort Order Required /H or /L%)
Exit /B 0
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: Subroutines for Population of Arrays with numeric values in sorted order.
:sortL <Element_VarName> <Element_Index_VarName> <Variable Names containing the values to be Sorted and Populated to the Array>
 Set "%2=0"
 FOR %%P In (%*) DO If Not "%%P"=="%1" If Not "%%P"=="%2" If Not "%%P"=="" (
  Set "%1[!%2!]=!%%P!"
  Set /A "%2+=1"
 )
 For /L %%a In (1,1,!%2!)Do (
  Set /A "S_Offset=%%a - 1"
  For /L %%b IN (0,1,%%a)Do (
   If not %%b==%%a For %%c in (!S_Offset!)Do (
    IF !%1[%%c]! LEQ !%1[%%b]! (
     Set "tmpV=!%1[%%c]!"
     Set "%1[%%c]=!%1[%%b]!"
     Set "%1[%%b]=!tmpV!"
 ))))
 Set /A %2-=1
 If "!#STDOut!"=="1" For /L %%G in (0 1 !%2!)Do Echo/%1[%%G]=!%1[%%G]!
Exit /B 0
:sortH <Element_VarName> <Element_Index_VarName> <Variable Names containing the values to be Sorted and Populated to the Array>
 Set "%2=0"
 FOR %%P In (%*) DO If Not "%%~P"=="%~1" If Not "%%~P"=="%2" If Not "%%P"=="" (
  Set "%1[!%2!]=!%%~P!"
  Set /A "%2+=1"
 )
 For /L %%a In (1,1,!%2!)Do (
  Set /A "S_Offset=%%a - 1"
  For /L %%b IN (0,1,%%a)Do (
   If not %%b==%%a For %%c in (!S_Offset!)Do (
    If Not !%1[%%c]! LSS !%1[%%b]! (
     Set "tmpV=!%1[%%c]!"
     Set "%1[%%c]=!%1[%%b]!"
     Set "%1[%%b]=!tmpV!"
 ))))
 Set /A %2-=1
 If "!#STDOut!"=="1" For /L %%G in (0 1 !%2!)Do Echo/%1[%%G]=!%1[%%G]!
Exit /B 0
:Def
Set "Usage=Echo/###&Echo/Call !FlNm! [/M:Def] [/A:Groupname] ["element0" ~ "element#"] [/F:Filepath.ext] [/E:"Element sub value"]&Echo/ - Assign each line in the given filepath plus element parameters to the Array&Echo/Call %~n0 [/M:Def] [/A:Groupname] REM : Clears the Array for the given Group Name&Echo/Call %~n0 [/M:Def] [/A:Groupname] [element] [element] [/O:Index#Arg] REM : Overides Elements from the index supplied&Exit /B 0"
 If "!#ORP#!"=="1" Echo/!SwParam!|"%__AppDir__%findstr.exe" /RX [0-9]* > Nul 2> Nul
 If not "!SwParam!"=="" If Errorlevel 1 (%Usage:###=O:!SwParam! #Arg invalid. Only Integers accepted.%)
 If "!GRPNm!"=="" (%Usage:###=/A:Groupname Required%)
 If "!#ORP#!"=="1" Set "#!GRPNm!=0"
 If "!#%GRPNm%!"=="" Set "#!GRPNm!=0"
 If "%#FP#%"=="1" (
  If exist "!FilePath!" (
   For /F "Delims=" %%G in (!FilePath!)Do If Not "%%~G"=="" (
    For %%x in ("!GRPNm![!#%GRPNm%!]")Do (
     Setlocal DisableDelayedExpansion
     If "%#STDOut%"=="1" Echo/%%~x=%%~G
     Endlocal & Set "%%~x=%%G"
    )
    Set /A "#!GRPNm!+=1" > Nul
   )
  ) Else (%Usage:###=/F:!FilePath! Invalid path%)
 )
 If not "!Inset!"=="" (
  For %%G in (!Inset!)Do (
   For %%x in ("%GRPNm%[!#%GRPNm%!]")Do (
    Setlocal DisableDelayedExpansion
    If "%#STDOut%"=="1" Echo/%%~x=%%~G
    Endlocal & Set "%%~x=%%~G"
   )
   If Not "!SubEL!"=="" Set "%%~G=!SubEl!"
   Set /A "#!GRPNm!+=1" > Nul
  )
 ) Else (
  If Not "%#FP#%"=="1" (
   For /F "Tokens=1,2 Delims==" %%I in ('Set %GRPNm%')Do Set "%%~I=" > Nul 2> Nul
   Set "#!GRPNm!=" > Nul 2> Nul
  )
 )
Exit /B 0
:GetArgs
 If Not "!#Help!"=="1" If "%~1" == "" (
  If /I "!Mode!"=="int" If "!GRPNm!"=="" (Echo/Call %~n0 [/M:Sort-int] [/A:GroupName] [/H^|/L] [/D]&%Usage:###=/A:Groupname Required%)Else If /I "!Mode!"=="int" (For /F "Tokens=1,2 Delims==" %%G in ('Set !GRPNm![')Do Set "Inset=!Inset! %%G") > Nul 2> Nul || (%Usage:###=Usage Error - /A:!GRPNm! is not defined%)
  If /I "!Mode!"=="str" If "!GRPNm!"=="" (Echo/Call %~n0 [/M:Sort-str] [/A:GroupName] [/N:New Groupname] [/F:Filepath.ext] [/D]&%Usage:###=/A:Groupname Required%)Else If /I "!Mode!"=="str" (For /F "Tokens=1,2 Delims==" %%G in ('Set !GRPNm![')Do Set "Inset=!Inset! %%G") > Nul 2> Nul || (%Usage:###=Usage Error - /A:!GRPNm! is not defined%)
  Exit /B 0
 ) Else If "%~1" == "" Exit /B 0
 Set "Param=%~1"
 Echo/"!Param!"|"%__AppDir__%findstr.exe" /LIC:"Find-" > Nul 2> Nul && ((Set "FindV=!Param:/M:Find-=!"&Set "Mode=Find")&Shift&Goto :GetArgs)
 Echo/"!Param!"|"%__AppDir__%findstr.exe" /LIC:"/M:" > Nul 2> Nul && (
  Set "MODE=!Param:*/M:=!"& Echo/"!Mode!"|"%__AppDir__%findstr.exe" /LIC:"Sort-" > Nul 2> Nul && (Set "Mode=!Mode:*Sort-=!")
  If "!Param:*/M:=!"=="" (
   Echo/&Echo/Modes:&Echo/ [Def]!TAB!!TAB!Define, modify or clear an array.
   Echo/ [Sort-int]!TAB!Sorts array by lowest or highest value using /L or /H switches
   Echo/ [Sort-str]!TAB!Sorts an array or text files string values using alphanumerical order of sort: [0-9][a-z]
   Echo/ [Find:Value]!TAB!Searches an array for the string value supplied.&Echo/
   %Usage:###=/M:Mode required&Echo/[Def][Sort-int^|str][Find-Value]%
  )
  Shift&Goto :GetArgs
 )
 Echo/"%~1"|"%__AppDir__%findstr.exe" /LIC:"/H"  > Nul 2> Nul && (Set "#Order=H"&Shift&Goto :GetArgs)
 Echo/"%~1"|"%__AppDir__%findstr.exe" /LIC:"/L"  > Nul 2> Nul && (Set "#Order=L"&Shift&Goto :GetArgs)
 Echo/"%~1"|"%__AppDir__%findstr.exe" /LIC:"/D"  > Nul 2> Nul && (Set "#STDOut=1"&Shift&Goto :GetArgs)
 Echo/"%~1"|"%__AppDir__%findstr.exe" /LIC:"/F:" > Nul 2> Nul && ((If Not "!Param:/F:=!"=="" (Set "#FP#=1"&Set "FilePath=!Param:/F:=!")Else %Usage:###=/F:Filepath.ext not Supplied%)&Shift&Goto :GetArgs)
 Echo/"%~1"|"%__AppDir__%findstr.exe" /LIC:"/N:" > Nul 2> Nul && (Set "nGRPNm=!Param:*/N:=!"&(If "!Param:*/N:=!"=="" %Usage:###=/N:New Group Name required%)&Shift&Goto :GetArgs)
 Echo/"%~1"|"%__AppDir__%findstr.exe" /LIC:"/A:" > Nul 2> Nul && (Set "GRPNm=!Param:*/A:=!"&(If "!Param:*/A:=!"=="" %Usage:###=/A:Group Name required%)&Shift&Goto :GetArgs)
 Echo/"%~1"|"%__AppDir__%findstr.exe" /LIC:"/O:" > Nul 2> Nul && (Set "SwParam=!Param:*/O:=!"&(If Not "!Param:/O:=!"=="" (Set "#ORP#=1")Else %Usage:###=/O:#Arg not Supplied%)&Shift&Goto :GetArgs)
 Echo/"%~1"|"%__AppDir__%findstr.exe" /LIC:"/E:" > Nul 2> Nul && (Set "SubEl=!Param:*/E:=!"&(If "!Param:/S:=!"=="" %Usage:###=/E:Sub Element not Supplied%)&Shift&Goto :GetArgs)
 Set Inset=!Inset! %1
 Shift&Goto :GetArgs
  • Modes:
  • [Def] Define, modify or clear an array.
  • [Def] Switches: [/A:Groupname] [/F:Filepath.ext] [/D] [/O:Index#Arg] [/E:Element Sub value] "element0" ~ "element#"
  • [Sort-int] Sorts array by lowest or highest value using /L or /H switches
  • [Sort-int] Switches:
    [/A:Groupname] [/N:New Groupname] [/L|/H] [/D]
  • [Sort-str]
    Sorts an array or text files string values using alphanumerical order of sort: [0-9][a-z]
  • [Sort-str] Switches:
    [/A:Groupname] [/F:Filepath.ext] [/D]
  • [Find-searchstring]
    Searches an array for the string value supplied.
  • [Find-searchstring] Switches: [/A:Groupname]

Solution 9 - Arrays

On the subject of "Turing Completeness in Batch programming"

Yes, Batch is Turing complete according to best of my knowledge (and if you ignore the "unlimited" features like unlimited memory and computational time; so one could argue that Batch is only "theoretically Turing equivalent").

There are all basic boolean and arithmetic operators as well as loops (for) and branching (if). Also there is a goto functionality which allows modeling loops (while/do while/for) and sub routines. Nesting of blocks is possible. Variables can be named, stored, deleted/cleared, displayed/written to file. A halt condition can be reached with exit (or goto eof).
As a sidenote: It is possible to write a batch file from inside a batch program, write it to disk and run it (allows self modification/customization/sub routines/state saving and restoring).

However there is no unlimited memory storage. Only 32-Bit arithmetics can be used in computations. And obviously the computer running the batch file also has hardware and physical limits (only finite time, speed or space).

It should be noted that all "higher level" concepts you mentioned are not part of the "batch programming language". There is no concept of classes, objects, records/structs, arrays, linked lists, stacks, queues, etc integrated. Nor are there any default algorithms like sorting, etc. provided (except maybe if sort or findStr, more etc. with pipes are taken into consideration). Randomizing is also very basic with the %RANDOM% variable.
If you need those concepts, you need to model them with the given basic language elements I mentioned above on your own (or use some library/third-party-batchfiles for that matter).
Of course it is possible to call not only batch files but any supplemental program on the computer and return to the batch execution afterwards (communicating via file, standard I/O streams or exit/errorlevel codes). Those programs could have been written in higher level languages that provides those sorts of things in a more convenient way.

From my point of view Bash (Linux) and Powershell (Windows/Linux) are far more advanced in those fields.

Solution 10 - Arrays

One approach I've used before is using files as arrays and folders as dictionaries of arrays.

Now hear me out - it might stupid at first to you, but it has some merit to it.

The idea is that a file can be treated as an array, and even support native, easy-to-use array iteration with the FOR command.

> array.txt > > txt > these > are > items > in > an > array >


For a 2d array you can use a folder of files like the one above. (with names like 0.txt through 100.txt). Keep in mind that you might need to have a separate file to index these as a directory of arrays isn't necessarily sorted the way you'd expect in a for loop, and is really more like a hash map where it's string -> [string].

Alternatively, I'm sure it wouldn't be too hard to parse csv (keep in mind commas and tabs inside of string-values! ;) )


For a mixed array (where some items are other arrays, and some are strings) you could have a file formatted something like this:

> complex-array.txt > > > "value > "1 > "2 > \path.txt > \path2.txt >

and a folder like this:

complex-array\path.txt
complex-array\path2.txt

where if a line begins with one character it's a value, and another, it's a path (possibly relative to this file). Of course, this could be recursive.


There's one big gotcha though. The script leaves behind files that (probably) need to be cleaned up before each run. (I say before because it's not safe to assume that the computer won't be unplugged while this script is running).

I'm unsure about the performance downsides to this, and batch is pretty slow anyway so perhaps it won't matter. (I'm fairly certain that the variable-name mangling tactic is faster as the values would remain in memory longer)

Solution 11 - Arrays

@echo off

set array=

setlocal ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION

set nl=^&echo(
    
set array=auto blue ^!nl!^
  bycicle green ^!nl!^
  buggy   red

echo convert the String in indexed arrays

set /a index=0

for /F "tokens=1,2,3*" %%a in ( 'echo(!array!' ) do (

 echo(vehicle[!index!]=%%a color[!index!]=%%b 
 set vehicle[!index!]=%%a
 set color[!index!]=%%b
 set /a index=!index!+1   

)

echo use the arrays

echo(%vehicle[1]% %color[1]%
echo oder

set index=1
echo(!vehicle[%index%]! !color[%index%]!

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionuser1131997View Question on Stackoverflow
Solution 1 - ArraysAaciniView Answer on Stackoverflow
Solution 2 - ArraystruthealityView Answer on Stackoverflow
Solution 3 - ArraysDave_JView Answer on Stackoverflow
Solution 4 - ArraysaschipflView Answer on Stackoverflow
Solution 5 - ArraysMatteo ItaliaView Answer on Stackoverflow
Solution 6 - Arraysuser5280669View Answer on Stackoverflow
Solution 7 - ArraysBen PersonickView Answer on Stackoverflow
Solution 8 - ArraysT3RR0RView Answer on Stackoverflow
Solution 9 - ArraysAntaresView Answer on Stackoverflow
Solution 10 - ArraysLazerbeak12345View Answer on Stackoverflow
Solution 11 - ArraystuxView Answer on Stackoverflow