Batch recursing through folders & populating an array - arrays

I am looking to recurse through folders/subfolders/etc. & populate an array with the folder paths dynamically.
Example: I have a folder called "A" which has 2 subfolders "B" & "C". "C" has a sub folder "D". So the array would be:
Folder[01]=A
Folder[02]=A/B
Folder[03]=A/C
Folder[04]=A/C/D
Would FOR/ D command work with what I need? If so, how do I take what the loop gets & add it to an array? Has to be in batch unfortunately. Thank you!

How do I populate an array with the folder paths dynamically.
Use the following batch file (MakeFolderArray.cmd):
#echo off
setlocal enabledelayedexpansion
rem get length of %cd% (the current directory)
call :strlen cd _length
set /a _index=1
for /d /r %%a in (*) do (
set _name=%%a
rem remove everything from the drive root up to the current directory,
rem which is _length chars
call set _name=!!_name:~%_length%!!
rem replace \ with /
set _name=!_name:\=/!
set Folder[0!_index!]=!_name!
set /a _index+=1
)
set /a _index-=1
for /l %%i in (1,1,%_index%) do (
echo Folder[0%%i]=!Folder[0%%i]!
)
endlocal
goto :eof
:strLen strVar [rtnVar]
setlocal disableDelayedExpansion
set len=0
if defined %~1 for /f "delims=:" %%N in (
'"(cmd /v:on /c echo(!%~1!&echo()|findstr /o ^^"'
) do set /a "len=%%N-3"
endlocal & if "%~2" neq "" (set %~2=%len%) else echo %len%
exit /b
Example:
F:\test>MakeFolderArray
Folder[01]=/A
Folder[02]=/A/B
Folder[03]=/A/C
Folder[04]=/A/C/D
Credits:
Thanks to dbenham for the strlen code from this answer (which works if the string contains \ characters).
Further Reading
An A-Z Index of the Windows CMD command line - An excellent reference for all things Windows cmd line related.
dir - Display a list of files and subfolders.
for /l - Conditionally perform a command for a range of numbers.
for /d - Conditionally perform a command on several Directories/Folders.
set - Display, set, or remove CMD environment variables. Changes made with SET will remain only for the duration of the current CMD session.
variable edit/replace - Edit and replace the characters assigned to a string variable.
variables - Extract part of a variable (substring).

Related

Batch For Loop not separating string at specified delimiter

I am using a program called "Easy Context Menu" which allows me to create customized context menus when you right click in File Explorer. I wanted to create my own Context Menu item that allows me to copy a file and it's parenting folder structure into another directory, excluding all of the files in the parented folders.
Example:
target file path: C:\the\big\foo\bar.txt
destination path: D:\cow
I want the file 'bar.txt' and it's parenting folders up until 'big' to be copied into the directory 'cow' without the other files in 'big' or 'foo'. The end result should look like:
D:\cow\big\foo\bar.txt
'big' should only contain 'foo' and 'foo' should only contain 'bar.txt'. The program allows me to send the file as a parameter to a file of my choice, in this case a batch file.
I have gotten stuck at the for loop in the ':main" subroutine. It will only print the whole path of the selected file and ignores the delimiter. 'countA' is returned as '1' after the loop and it would have printed the whole path minus drive letter. I do not understand why the for loop is ignoring the delimiter and not separating the path into separate folder names. The reason I am tying to do this is so the user can choose at which point the parented folders should be copied over. This is my first time writing actual code in batch so I still don't completely understand 'setlocal' and a few other things.
I have tried changing the tokens and I am able to choose the individual sections in between the slashes, but I am never able to capture the entire string as separate chunks. I have also changed the delimiter and got the same issue.
#echo off
setlocal
set _target=""
set _dest=""
if not exist %1 goto:error_no_path
set _target=%~pnx1
echo %_target%
goto :dest_selct
:dest_selct
echo select destination folder
pause
set "psCommand="(new-object -COM 'Shell.Application')^
.BrowseForFolder(0,'Please choose destination folder.',0,0).self.path""
for /f "usebackq delims=" %%I in (`powershell %psCommand%`) do set "folder=%%I"
setlocal enabledelayedexpansion
echo You chose %folder%
choice /c ync /n /m "Is this correct? ([Y]es, [N]o, [C]ancel)"
if %ERRORLEVEL% EQU 3 goto:eof
if %ERRORLEVEL% EQU 2 goto:dest_selct
if %ERRORLEVEL% EQU 1 (set _dest="%folder%")&&(goto:main)
:error_no_path
ECHO No file path/name
pause
GOTO:eof
:main
echo main
set _countA=0
echo count starts at %_countA%
for /f "tokens=* delims=\" %%F in ("%_target%") do (
echo token %_countA% is %%F
echo count is %_countA%
set var!_countA!=%%F
set /a countA+=1
)
echo var1: %var1%
echo count is now %countA%
pause
I should have more than one variable at the end, each being the name of a folder that parents the target file and the count should be however many parent folders there are, plus the file itself. What I am actually getting is one variable and it contains the whole target path still.
The for loop does not delimit as you have tokens=*,
which gets all tokens as one token. Leading delimiters are stripped.
Only var0 has a value, not the echoed var1.
To split _target by path segments, use:
for %%F in ("%_target:\=" "%") do echo %%~F
Another issue you have is if you choose n at the choice prompt,
the goto:dest_selct will cause setlocal enabledelayedexpansion
to execute again. Since you are not using endlocal, then the
local environment is recursing without ending. Sometimes endlocal
is implied, so you may not need to use it, though not in this case.
Try this fix:
#echo off
setlocal
set "_target="
set "_dest="
if not exist "%~1" goto:error_no_path
set "_target=%~pnx1"
echo %_target%
goto :dest_selct
:dest_selct
echo select destination folder
pause
set "psCommand="(new-object -COM 'Shell.Application')^
.BrowseForFolder(0,'Please choose destination folder.',0,0).self.path""
for /f "usebackq delims=" %%I in (`powershell %psCommand%`) do set "folder=%%I"
echo You chose %folder%
choice /c ync /n /m "Is this correct? ([Y]es, [N]o, [C]ancel)"
if %ERRORLEVEL% EQU 3 goto:eof
if %ERRORLEVEL% EQU 2 goto:dest_selct
if %ERRORLEVEL% EQU 1 (set "_dest=%folder%") && goto:main
:error_no_path
ECHO No file path\name
pause
GOTO:eof
:main
setlocal enabledelayedexpansion
echo main
set "countA=0"
echo count starts at %countA%
rem Remove leading backslash if exist.
if "%_target:~,1%" == "\" set "_target=%_target:~1%"
rem Split into path segments.
for %%F in ("%_target:\=" "%") do (
echo token !countA! is %%~F
echo count is !countA!
set "var!countA!=%%~F"
set /a countA+=1
)
echo var1: %var1%
rem Echo all variables starting with var and their values.
set var
echo count is now %countA%
endlocal
pause
Adjusted double quoting.
Changed instances of variable name of _countA to countA to match.
set var to show all variables starting with var for debugging.
for loop now spilts on path segments.
Moved setlocal enabledelayedexpansion out of label loop.
Fix 1st argument check if not defined.
Refer to set /? about variable substitution i.e. "%_target:\=" "%", which replaces \ with " ".

batch for loop in file

I have a Batch script :
#echo off
setlocal enableDelayedExpansion
SET /P UserInput=Please Enter a Number:
SET /A number=UserInput
ECHO number=%number%
for %%i in (*.jpeg) do call :JPG %%~ni %%i
goto :end
:JPG
set str=%1
set /a str2=%str:_color=%
set /a newnamej=%str2%+%number%
echo %1 ==> I can see the problem with it
set lastnamej=%newnamej%_color.jpeg
ren %2 %lastnamej%
goto :eof
:end
The goal of this script is to take all file in a folder. They are all named after a number (1_color.jpeg, 2_color.jpeg, 3_color.jpeg,..) and I want to rename them with an additionnal number (if user input is 5, 1_color.jpeg will become 6_color.jpeg, and so on).
I have a problem with this script.
if I use a number such as 555, the first file will pass in the for loop 2 times.
(little example : 1_color.jpeg and 2_color.jpeg,
I use my script with 5 so 1_color.jpeg => 6_color.jpeg and 2_color.jpeg => 7_color.jpeg but then, 6_color.jpeg will be read again once, and will become 11_color.jpeg, so my result will be 11_color.jpeg and 7_color.jpeg).
Do someone know how to fix this issue?
Thanks for all!
The problem have two parts: the for %%i in (*.jpeg) ... command may be dinamically affected by the position that a renamed file will occupy in the whole file list, so some files may be renamed twice and, in certain particular cases with many files, up to three times.
The solution is to use a for /F %%i in ('dir /B *.jpeg') ... command instead, that first get the list of all files, and then start the renaming process.
Also, the rename must be done from last file to first one order, to avoid duplicate numbers.
However, in this case the use of for /F combined with "tokens=1* delims=_" option also allows to process the first underscore-separated number in the file names in a simpler way:
#echo off
setlocal EnableDelayedExpansion
SET /P number=Please Enter a Number:
ECHO number=%number%
for /F "tokens=1* delims=_" %%a in ('dir /O:-N /B *.jpeg') do (
set /A newNum=%%a+number
ren "%%a_%%b" "!newNum!_%%b"
)
User Aacini provided a nice solution in his answer, pointing out both issues at hand, namely the fact that for does not fully enumerate the directory in advance (see this thread: At which point does for or for /R enumerate the directory (tree)?) and the flaw in the logic concerning the sort order of the processed files.
However, there is still a problem derived from the purely (reverse-)alphabetic sort order of dir /B /O:-N *.jpeg, which can still cause collisions, as the following example illustrates:
9_color.jpeg
8_color.jpeg
7_color.jpeg
6_color.jpeg
5_color.jpeg
4_color.jpeg
3_color.jpeg
2_color.jpeg
10_color.jpeg
1_color.jpeg
So if the entered number was 1, file 9_color.jpeg is tried to be renamed to 10_color.jpeg, which fails because that file already exists as it has not yet been processed (hence renamed to 11_color.jpeg).
To overcome this problem, you need to correctly sort the items in reverse alpha-numeric order. This can be achieved by left-zero-padding the numbers before sorting them, because then, alphabetic and alpha-numeric sort orders match. Here is a possible implementation:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
rem // Define constants here:
set "_LOCATION=." & rem // (directory containing the files to rename)
set "_PATTERN=*_*.jpeg" & rem // (search pattern for the files to rename)
set "_REGEX1=^[0-9][0-9]*_[^_].*\.jpeg$" & rem // (`findstr` filter expression)
set "_TEMPFILE=%TEMP%\%~n0_%RANDOM%.tmp" & rem // (path to temporary file)
rem // Retrieve numeric user input:
set "NUMBER="
set /P NUMBER="Please Enter a number: "
set /A "NUMBER+=0"
if %NUMBER% GTR 0 (set "ORDER=/R") else if %NUMBER% LSS 0 (set "ORDER=") else exit /B
rem /* Write `|`-separated list of left-zero-padded file prefixes, original and new
rem file names into temporary file: */
> "%_TEMPFILE%" (
for /F "tokens=1* delims=_" %%E in ('
dir /B "%_LOCATION%\%_PATTERN%" ^| findstr /I /R /C:"%_REGEX1%"
') do (
set "NAME=%%F"
setlocal EnableDelayedExpansion
set "PADDED=0000000000%%E"
set /A "NUMBER+=%%E"
echo !PADDED:~-10!^|%%E_!NAME!^|!NUMBER!_!NAME!
endlocal
)
)
rem /* Read `|`-separated list from temporary file, sort it by the left-zero-padded
rem prefixes, extract original and new file names and perform actual renaming: */
< "%_TEMPFILE%" (
for /F "tokens=2* delims=|" %%K in ('sort %ORDER%') do (
ECHO ren "%%K" "%%L"
)
)
rem // Clean up temporary file:
del "%_TEMPFILE%"
endlocal
exit /B
After having successfully verified the correct output of the script, to not forget to remove the upper-case ECHO command in front of the ren command line.
The script uses a temporary file that receives a |-separated table with the padded numeric prefix in the first, the original file name in the second and the new file name in the third column, like this:
0000000010|10_color.jpeg|11_color.jpeg
0000000001|1_color.jpeg|2_color.jpeg
0000000002|2_color.jpeg|3_color.jpeg
0000000003|3_color.jpeg|4_color.jpeg
0000000004|4_color.jpeg|5_color.jpeg
0000000005|5_color.jpeg|6_color.jpeg
0000000006|6_color.jpeg|7_color.jpeg
0000000007|7_color.jpeg|8_color.jpeg
0000000008|8_color.jpeg|9_color.jpeg
0000000009|9_color.jpeg|10_color.jpeg
The temporary file is read and sorted by the sort command. The strings from the second and third columns are extracted and passed over to the ren command.

In a batch file ran from Windows, select latest application version using directory name

I use a portable application that have updates quite often. The problem is that each version of the application has a folder named "processing-x.y.z". Each time I install a new version, I need to associate the files with the new version which is in a different folder. So to workaround this annoyance, I want to associate the "*.pde" file type to a batch file.
The folder names go as follow
processing-3.2.1
processing-3.2.2
etc.
I have created this small batch script that get the executable from the latest version.
#echo off
for /f "delims=" %%D in ('dir processing* /a:d /b /o-n') do (
set currentFolder=%%~fD
:: Check if environment variable already set
if not %currentFolder%==%processing% (
:: Set environment variable processing
setx processing %currentFolder%
)
%currentFolder%/processing.exe %1
goto :eof
)
It works when launching it from the command-line, but not within Windows. Is there a specific reason? Also, is there a way to optimize this code?
Thanks
Supposing the version numbers always consist of a single digit each, I would do it the following way:
#echo off
rem // Reset variable:
set "currentFolder="
rem /* Loop through the folders in ascending order, overwrite the variable
rem in each iteration, so it holds the highest version finally: */
for /f "delims=" %%D in ('dir /B /A:D /O:N "processing-*.*.*"') do (
set "currentFolder=%%~fD"
)
rem // Check if environment variable is already set:
if not "%processing%"=="%currentFolder%" (
rem // Set environment variable `processing`:
setx processing "%currentFolder%"
)
rem // Execute `processing.exe`:
"%currentFolder%/processing.exe" "%~1"
If the individual version numbers can consist of more than one digit (four at most here), use this:
#echo off
setlocal EnableExtensions EnableDelayedExpansion
rem /* Assign each found folder to a variable called `$ARRAY_X_Y_Z`, where `X`, `Y`, `Z`
rem are zero-padded variants of the original numbers `x`, `y`, `z`, so for instance,
rem a folder called `processing-4.7.12` is stored in variable `$ARRAY_0004_0007_0012`: */
for /F "tokens=1-4 delims=-. eol=." %%A in ('
dir /B /A:D "processing-*.*.*" ^| ^
findstr /R /I "^processing-[0-9][0-9]*.[0-9][0-9]*.[0-9][0-9]*$"
') do (
rem // Perform the left-side zero-padding here:
set "MAJ=0000%%B" & set "MIN=0000%%C" & set "REV=0000%%D"
set "$ARRAY_!MAJ:~-4!_!MIN:~-4!_!REV:~-4!=%%A-%%B.%%C.%%D"
)
rem // Reset variable:
set "currentFolder="
rem /* Loop through the output of `set "$ARRAY_"`, which returns all variables beginning
rem with `$ARRAY_` in ascending alphabetic order; because of the zero-padding, where
rem alphabetic and alpha-numeric orders become equivalent, the item with the greatest
rem version number item is iterated lastly, therefore the latest version is returned: */
for /F "tokens=1,* delims==" %%E in ('set "$ARRAY_"') do (
set "currentFolder=%%F"
)
endlocal & set "currentFolder=%currentFolder%"
rem // The rest of the script os the same as above...
You can also find similar approaches here:
How to get latest version number using batch (this approach also relies on the sorting featurre of the set command)
How to sort lines of a text file containing version numbers in format major.minor.build.revision numerical? (this uses the sort command upon a temporary file or on piped (|) data)
not tested (edited: should handle the cases when minor versions have more digits):
#echo off
setlocal enableDelayedExpansion
set /a latest_n1=0
set /a latest_n2=0
set /a latest_n3=0
for /f "tokens=2,3* delims=-." %%a in ('dir processing* /a:d /b /o-n') do (
set "current_v=%%a.%%b.%%c"
set /a "current_n1=%%a"
set /a "current_n2=%%b"
set /a "current_n3=%%c"
if !current_n1! GTR !latest_n1! (
set /a latest_n1=!current_n1!
set /a latest_n2=!current_n2!
set /a latest_n3=!current_n3!
set "latest_v=!current_v!"
) else if !current_n1! EQU !latest_n1! if !current_n2! GTR !latest_n2! (
set /a latest_n1=!current_n1!
set /a latest_n2=!current_n2!
set /a latest_n3=!current_n3!
set "latest_v=!current_v!"
) else if !current_n1! EQU !latest_n1! if !current_n2! EQU !latest_n2! if !current_n3! GTR !latest_n3! (
set /a latest_n1=!current_n1!
set /a latest_n2=!current_n2!
set /a latest_n3=!current_n3!
set "latest_v=!current_v!"
)
)
echo latest version=processing-%latest_v%
#echo off
for /f "delims=" %%D in ('dir processing* /a:d /b /o-n') do (
if "%processing%" neq "%cd%\%%F" setx processing "%cd%\%%F" >nul
.\%%F\processing.exe %1
goto :eof
)
is equivalent code - almost.
First problem - the :: form of comments should not be used within a code block (parenthesised series of statements) as :: is in fact a label that itself stars with a colon. Since it is a label, it terminates the block.
Next problem - within a code block, %var% refers to the value ofvar**as it stood when thefor` was encountered** - not as it changes within the loop.
Next problem - as noted by others, /o-n produces a by-name sequence, so 10 is likely to sort after 9 (since the sort is reversed). I've not changed this in the replacement code, but /o-d to sort by reverse-date may be better suited to your application.
Now to how your code works.
First, processing is set to whatever the last run established. If that is different from the value calculated from this run, then setx the new value. Bizarrely, setx does not set the value in the current cmd instance, only for instances created in the future.
You then attempt to execute your process and then exit the batch with the goto :eof. Only problem is that %currentfolder% is not the value as changed by the loop, because it's supposed to be in the code block. It appears to change because the ::-comments have broken the block and where currentfolder is used in your code, it is outside of the block.
Instead, use .\%%F\executablename which means "from the current directoryname (.);subdirectory %%F;filenametoexecute" - note that "\" is a directory-separator in windows, '/' indicates a switch.

batch pick random file that contains utf-8 characters in its name

I want to open a random file in a directory and its subdirectorys with batch. And I know there are enough questions on stackoverflow who give the code for that but none of which I found were with utf-8 character support.
I use the following code which I found in stackoverflow.
#echo off
setlocal
:: Create numbered list of files in a temporary file
set "tempFile=%temp%\%~nx0_fileList_%time::=.%.txt"
dir /b /s /a-d %1 | findstr /n "^" >"%tempFile%"
:: Count the files
for /f %%N in ('type "%tempFile%" ^| find /c /v ""') do set cnt=%%N
call :openRandomFile
:: Delete the temp file
del "%tempFile%"
exit /b
:openRandomFile
set /a "randomNum=(%random% %% cnt) + 1"
for /f "tokens=1* delims=:" %%A in (
'findstr "^%randomNum%:" "%tempFile%"'
) do start "" "%%B"
cmd /k
It works fine that way, until it picks a file like "blablabla_空色デイズ.mp3", in that case it gives an error like "file blablabla_?????.mp3 could not be found." and I have dozens of these files.
I have tried using chcp 65001 on the start of the file for using utf-8 and if I did so, the teporary created .txt list shows the correct names of japanese files, but the pick up itself does not work anymore after that, so I took away #echo off and cmd prints an error on set /a "randomNum=(%random% %% cnt) + 1": Error: division by zero.
And at this point, I dont understand anymore whats going on, because the file is working great without chcp 65001.
I don't know batch, please, does anyone have an idea how to make it run?
I would be really happy!
You will have a lot of problems with findstr and command output when utf8/unicode characters are involved. In this type of scenarios it is safer (but slower) to avoid them
#echo off
setlocal enableextensions disabledelayedexpansion
rem %1 = folder where to start to list files
rem if empty, current active directory is used
rem %2 = file mask
rem if empty * is used
rem What to search
set "fileMask=%~2" & if not defined fileMask set "fileMask=*"
rem Retrieve the number of matching files
set "nFiles=0" & for /r "%~f1." %%a in ("%fileMask%") do set /a "nFiles+=1"
if %nFiles% lss 1 exit /b
rem Select a random file
set /a "n=(%random% %% nFiles) + 1"
echo Selected file = %n% / %nFiles%
rem Count up to the selected file and start it
2>nul (
for /r "%~f1." %%a in ("%fileMask%") do (
set /a "n-=1", "1/n" || ( start "" "%%~fa" & goto :done )
)
)
:done
That is, count the number of files, select one of them and then start iterating over the list of files decrementing the number of the selected file. When it is 0 (we have reached the selected file), the 1/n division will fail and the conditional execution operator will execute the start command.
I'd try
) do start "" "%%~sB"
in the :openRandomFile procedure, which should open the file using its short name.
Sorry - can't test it as I don't have any UTF-8 filenames (that I know about)

How to get the relative path for each file in a directory tree based on specified folder path?

I want to loop recursively through a directory and get the file names with full file path and additionally with path relative to the base folder path. For example, a file located in D:\foo\bar\a\b\file.txt has the relative path bar\a\b\.
I'm using this code right now which does not work as expected:
FOR /R D:\Download\758_DATA\ %%F IN (*) DO (
SET B = %%~pF
Set B=%B:~-6% ::take substring of the path
ECHO.%B%
ECHO %%F
ECHO.
)
To get relative paths you can use variable substitution together with delayed expansion, like this:
setlocal enabledelayedexpansion
for /R . %%f in (*) do (
set B=%%f
echo Relative !B:%CD%\=!
)
I'm using current directory (%CD%) as root directory here.
I'm not sure what you want to accomplish, but at least this will give you a good starting point:
#echo off & setlocal enabledelayedexpansion
set rootdir=D:\download
for /R %rootdir% %%F in (*) do (
set "B=%%~pF"
set "B=!B:~10!"
echo Full : %%F
echo Partial: !B!
echo(
)
endlocal
Since you're modifying a variable within a loop, you need to tell the command interpreter that you want to allow variables to be "expanded with delay". This is what the setlocal enabledelayedexpansion is for. Then you can use ! as a variable delimiter instead of %, and variables written as such will be expanded at runtime. This is necessary because the for loop will be called like it is one single call. (You can see this when you leave out the echo off.)
Edit: Revised example which includes automatic cut off:
#echo off
setlocal enableextensions enabledelayedexpansion
set "rootdir=%~f1"
if not defined rootdir set "rootdir=%CD%"
set "rootdir=%rootdir:/=\%"
if "%rootdir:~-1%" == "\" set "rootdir=%rootdir:~0,-1%"
set "foo=%rootdir%"
set cut=
:loop
if defined foo (
set /A cut+=1
set "foo=!foo:~1!"
goto :loop
)
echo Root dir: %rootdir%
echo strlen : %cut%
rem also remove leading /
set /A cut+=1
for /R "%rootdir%" %%F in (*) do (
set "B=%%~fF"
rem take substring of the path
set "B=!B:~%cut%!"
echo Full : %%F
echo Partial : !B!
echo(
)
endlocal

Resources