Batch For Loop not separating string at specified delimiter - batch-file

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 " ".

Related

If a variable gets set to the name of a variable, then can it become that variable? (Batch)

All the P(numbers) are paths. For example, %P1% might be D:\ . Then, I get them to say a number. If they choose 1, I combine that with a P. That gives P1, the first variable. Can I set A to the variable P1? It would make the code way more optimized. I want to try and fit everything in a ridiculously tiny amount of space.
Download: https://mega.nz/file/QYZA3RbQ#Uvr9acXEBssYdXSHkdas1OjFt0X6xdmyl1WLH9HbmP8
echo 1. %P1%
echo 2. %P2%
echo 3. %P3%
echo 4. %P4%
echo 5. %P5%
set /p Pc=
set "a=p%pc%"
echo %a%
pause```
Short answer, based on the initial code:
Using delayedexpansion
set "a=!P%Pc%!"
echo %a%
or use call
call set "a=%%P%Pc%%%"
echo %a%
However. to do this right, You will need to change the way your config file works.
For now, your config has a major flaw, you explicitly want to ignore certain line numbers, which if someone removes or add a line, it will break your code. You need to use some for loops and then skip lines that starts with REM
So the config needs to look something like this (I made it smaller for the sake of a shorter answer):
REM Please set this to 1, or the program will NOT run!
1
REM ------------------------------------------------------------------------------------------
REM Please put paths to the folder your [PWADS] are in. Not the pwads themselves.
REM ------------------------------------------------------------------------------------------
pwads=C:\Some path
pwads=D:\Another
REM ------------------------------------------------------------------------------------------
REM Please put the path to your [IWADS]. Only ten though! Make sure it's the iwads themselves.
REM ------------------------------------------------------------------------------------------
iwads=D:\iwads1
iwads=D:\some path2
REM ------------------------------------------------------------------------------------------
REM Please put the path to your [Source Port] exe's. You must be doing something weird if you have more than ten.
REM ------------------------------------------------------------------------------------------
sp=D:\
sp=E:\
REM ------------------------------------------------------------------------------------------
REM Now, please leave this alone. It's so I know everything works.
REM ------------------------------------------------------------------------------------------
1
Now the code. portion.
#echo off
setlocal enabledelayedexpansion
set pwnum=0
set iwnum=0
set spnum=0
for /f "tokens=1,* delims==" %%i in ('type launcherconfiguration.txt ^| findstr /v "REM "') do (
if "%%i" equ "0" (
echo not configured
goto :eof
)
if "%%~i" == "pwads" (
set /a pwnum+=1
set "PWADlocat!pwnum!=%%~j"
) else (
if "%%~i" == "iwads" (
set /a iwnum+=1
set "IWADlocat!iwnum!=%%~j"
) else (
if "%%~i" == "sp" (
set /a spnum+=1
set "SPlocat!spnum!=%%~j"
)
)
)
)
echo PWADLocation:
for /l %%a in (1,1,%pwnum%) do echo %%a. !PWADLocat%%a!
set /p pw=Choose:
call echo you chose %%PWADlocat!pw!%%
call set "PW=%%PWADlocat!sp!%%"
echo you chose %PW%
pushd "%PW%" rem This will goto the dir
rem do other things
popd
echo IWADLocation:
for /l %%a in (1,1,%iwnum%) do echo %%a. !IWADLocat%%a!
set /p pw=Choose:
call set "IW=%%IWADlocat!sp!%%"
echo you chose %IW%
pushd "%IW%" rem This will goto chosen the dir
rem do other things
popd
echo SPLocation:
for /l %%a in (1,1,%spnum%) do echo %%a. !SPLocat%%a!
set /p sp=Choose:
call set "SP=%%SPlocat!sp!%%"
echo you chose %SP%
pushd "%SP%" rem This will goto the chosen SP dir
rem do other things
popd
So let me explain it briefly. We set a number per datatype, we simply increase the count as more lines matched are found and then we use another for /l loop to just use the count again.
To test this, you need to copy this code into a new batch file, place it in the same location as your config file and test it. There are alot more to your code, but I have only focused on what you asked.
Note, I would still prefer to use choice but because of the beeping you mentioned, I retained the set /p option in this code.

Remove duplicate files in a directory

I have a working script, (below), which compares files, by size only, and if one or more of the files have different size, it shows on the .bat screen.
#echo off
setlocal EnableDelayedExpansion
rem Group all file names by size
for /R %%a in (*.*) do (
set "size[%%~Za]=!size[%%~Za]!,%%~Fa"
)
rem Show groups that have more than one element
for /F "tokens=2,3* delims=[]=," %%a in ('set size[') do (
if "%%c" neq "" echo [%%a]: %%b,%%c
)
pause
But I need more!
I also need the script delete duplicate files, keeping just one. For example, if there are 3 files of the same size but with different names, the script should choose one to keep, deleting the other 2.
#LotPings, i tried with 2 .pdf files, and appeared this message in the pic below:
#LotPings
Now it recognized fine! But it doesn't deleting the duplicate, just recognizing.... Look the image below:
I think treating files with same size as same content is a bit naive.
If in doubt better do a binary compare with fc.
Your way of stuffing %%~Fa into a string without quoting every single name possibly containing spaces can be problematic in further processing.
If the files are identical you could create links.
You know that for /r will recurse into subdirs?
The following batch will process the size[] array differently passing the size and content to a subroutine which keeps the first file and (only echos) the del command.
:: Q:\Test\2018\06\06\SO_50723488.cmd
#echo off & setlocal
rem Group all file names by size
for /R %%a in (*.*) do call set size[%%~Za]=%%size[%%~Za]%%,"%%~Fa"
rem Process groups
for /F "tokens=2* delims=[]=," %%a in ('set size[') do Call :Sub %%a %%b
pause
Goto :Eof
:Sub
If "%~3"=="" (Set "size[%1]="&goto :EOf)
Echo ========================================
Echo processing %*
Echo Keep %2
Shift&shift
:loop
Echo Del %1
if not "%~2"=="" (shift&goto :loop)
Sample output:
> Q:\Test\2018\06\06\SO_50723488.cmd
========================================
processing 0 "Q:\Test\2018\05\03\Text3.txt","Q:\Test\2018\05\27\log.html"
Keep "Q:\Test\2018\05\03\Text3.txt"
Del "Q:\Test\2018\05\27\log.html"
========================================
processing 14 "Q:\Test\2018\05\03\Text1.txt","Q:\Test\2018\05\15\HP-G1610-20180515.json"
Keep "Q:\Test\2018\05\03\Text1.txt"
Del "Q:\Test\2018\05\15\HP-G1610-20180515.json"
========================================
processing 21 "Q:\Test\2018\05\12\text1.txt","Q:\Test\2018\05\12\text2.txt"
Keep "Q:\Test\2018\05\12\text1.txt"
Del "Q:\Test\2018\05\12\text2.txt"
========================================
processing 22 "Q:\Test\2018\05\20\2.txt","Q:\Test\2018\05\22\version.ps1"
Keep "Q:\Test\2018\05\20\2.txt"
Del "Q:\Test\2018\05\22\version.ps1"
========================================
processing 288 "Q:\Test\2018\05\11\RawData.txt","Q:\Test\2018\05\23\SO_50478080.ps1"
Keep "Q:\Test\2018\05\11\RawData.txt"
Del "Q:\Test\2018\05\23\SO_50478080.ps1"
The following script removes duplicate files in the current directory. It leaves one file, and removes rest of its duplicates. I developed the script for Windows 10, so it might not work for other Windows versions.
The advantage of this script is it not only checks the last modified date and time, but also the file size. Let's say you have a text file with *** symbols? you can turn them all ### symbols, and still occupy the same space. The script checks the last modified date, and time along with its size to ensure both the files are identical, otherwise it doesn't remove any file.
setlocal enabledelayedexpansion
echo(
echo "This program removes duplicate files in the current directory."
echo "Warning: Take a backup of the current directory to avoid accidents"
PAUSE
echo(
set path=%cd%
set /A count=0
for %%a in (*.*) do (
for %%b in (*.*) do (
if exist "%%b" if %%a neq %%b (
if "%%~Ta %%~Za" equ "%%~Tb %%~Zb" (
echo "%%a File is deleted"
set /A count+=1
del "%%a"
)
)
)
)
echo(
echo "---------"
echo "!count! Duplicate Files were deleted in the current directory"
echo "---------"
echo(
endlocal
PAUSE
If you have any problems report me at
https://github.com/donqq/Duplicate-File-Remover/

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.

Batch recursing through folders & populating an array

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).

Batch script, file drag&drop, change extension and reload file

I have a batch script which accepts >=1 files via drag & drop. The full paths are kept in an array and later fed to another program as input. The file name+extension is kept in another array which is then shown to the user.
I am trying to check the file extension and if it is not .bin, automatically rename the file I dragged & dropped to .bin and reload it to be added to the two arrays. How can I do that? I have tried some if commands or %%~xi but usually it doesn't properly detect if it's .bin or not and of course the path is not updated at the array.
#echo off
pushd %~dp0
setlocal enabledelayedexpansion
set /a Count = 0
:START
set file="%~1"
if %file%=="" goto RUN
for %%i in (%file%) do set name=%%~nxi
set /a Count += 1
set [FilePath.!Count!]=%file%
set [FileName.!Count!]=%name%
shift
goto START
:RUN
for /l %%i in (1, 1, %Count%) do (
program.exe -command "![FilePath.%%i]!"
echo.
echo File Name: ![FileName.%%i]!
echo.
pause
cls
if exist file.log del file.log
)
You could just rename it during your array population. There are a few other potential problems with your script (such as pushd to an unquoted directory, delayedexpansion where you don't need it where it will potentially clobber filenames containing exclamation marks, and if %file%=="" should have "%file%" quoted). And there's really no reason to maintain an array of filenames or loop twice. Your script is much more complicated than it needs to be.
#echo off
setlocal
pushd "%~dp0"
for %%I in (%*) do (
if /I not "%%~xI"==".bin" move "%%~fI" "%%~dpnI.bin" >NUL
program.exe -command "%%~dpnI.bin"
echo;
echo File Name: "%%~nxI"
echo;
rem If you want to rename the file back the way it was after running
rem program.exe on it, uncomment the following line:
rem if /I not "%%~xI"==".bin" move "%%~dpnI.bin" "%%~fI" >NUL
pause
cls
if exist file.log del file.log
)

Resources