I have a directory with many sub-directories that contain thousands of jpgs. What I want to do is create a batch script that will go through all of the sub-directories and delete every 2nd image i.e. keep the first, third, fifth image but delete the second, fourth, and six image etc per directory (ordered by filename).
I tried with the following but my knowledge of batch scripting is poor, and the syntax is clearly incorrect.
#echo off
set z = 0
for /f %%a in ('dir/b *.jpg')
do (
set z = z + 1
if z == 2 del %%a
)
The DO must be on the same line as FOR.
You must use SET /A if you want to do math
Your logic is wrong - Currently it will only delete the 2nd file, not every other one. You should take the mod 2 value (remainder devided by 2) and delete if the result is 0.
You must use %z% if you want to see the current value (except within a SET /A statement). But that will not work inside a code block that just set the value. In that case you need to enable delayed expansion and use !z! instead.
Expanding a FOR variable that contains ! (valid in file names) while delayed expansion is enabled will corrupt the value. So delayed expansion must be toggled on and off
You say you want to recurse sub-directories, but your code only looks at one folder.
Spaces are significant in the SET statement. Your code defines a variable z with a space at the end of the name. Not what you want.
Here is a debugged version:
#echo off
setlocal
for /r %%D in (.) do (
set "z=0"
for /f %%F in ('dir /b "%%D\*.jpg"') do (
set /a "z+=1, r=z%%2"
setlocal enableDelayedExpansion
if !r! equ 0 del "%%D\%%F"
endlocal
)
)
There are ways to solve this without delayed expansion. One is to simply alternate between defining and undefining a variable.
#echo off
setlocal
for /r %%D in (.) do (
set "del="
for /f %%F in ('dir /b "%%D\*.jpg"') do if defined del (
del "%%D\%%F"
set "del="
) else set "del=1"
)
Another is to intentionally divide by 0 when you want to delete, and delete only when there is an error. Error messages are hidden by 2>nul, and the || operator conditionally executes the following command only if the prior command failed.
#echo off
setlocal
for /r %%D in (.) do (
set "z=0"
for /f %%F in ('dir /b "%%D\*.jpg"') do 2>nul set /a "z+=1, 1/(z%%2)" || del "%%D\%%F"
)
try this and remove the echo if the output looks good:
#echo off &setlocal
for /f "tokens=1*delims=:" %%a in ('dir /b /s /a-d *.jpg^|findstr /n $') do (
echo %%a|findstr "[02468]$" >nul && echo del "%%~b"
)
Related
Ok, so I'm still pretty new to batch scripting and I have this problem with my code that I'm using setlocal enabledelayedexpansion in my code for the for loop, the For loop goes through folder names in a specific directory, but the problem is that some of the names may include "!"(exclamation marks) and if that is the case they are not counted for in the "!filename!" and when the code creates a new directory it does not include the "!". Also when the xcopy tries to copy the files from the original folder, the folder is not found, because the variable "!filename!" is not the same as the original(does not have the exclamation point).
So I found that for this I need to only add "setlocal enable delayed expansion" to some parts of the code and turn it off at other, but I cant seem to find the right places.
The code:
#ECHO OFF
setlocal enabledelayedexpansion
SET Location_Folder=V:
SET Destination_folder=V:\Nonimportable
SET Check_file_validation=V:\Nonimportable\result.txt
SET Excluded_folder=Nonimportable
set "lineNr=12"
For /f "tokens=*" %%O in ('dir /b /a:d "%Location_Folder%"') do (
set filename=%%O
call D:\somefolder\otherfolder\batscriptname.bat !filename!
set Validation=
echo !filename!| FINDSTR /i /c:"%Excluded_folder%" >NUL
IF ERRORLEVEL 1 (
for /F "skip=12 delims=" %%a in (%Check_file_validation%) do if not defined Validation (
set Validation=%%a
call :valid
)
) else (
echo This folder name is excluded: !filename!
)
)
goto Finish
:valid
echo !Validation!| FINDSTR /c:"1" >NUL
if ERRORLEVEL 1 (
set Folder_path=%Location_Folder%\!filename!
set New_Folder_path=%Destination_folder%\!filename!
mkdir "!New_Folder_path!"
echo D | xcopy /o /y /q /s /v "!Folder_path!" "!New_Folder_path!"
rmdir /s /q "!Folder_path!"
) else (
echo Folder is valid !filename!
goto Finish
)
:Finish
exit /b
The Call part calls another small (~5lines) batch file that checks the sqlplus server if the "!filename!" is valid
EDIT: The whole code works fine and does what it should, unless there is a "!" in the name of some folder.
The problem is the parameter expansion in set filename=%%O.
In %%O is still the exclamation mark, but when delayed expansion is enabled, the bangs are dropped.
The conclusion is simple, delayed expansion have to be disabled when you expand a FOR parameter.
But when you also need delayed expansion?
You simply toggle the mode.
setlocal DisableDelayedExpansion
For /f "tokens=*" %%O in ('dir /b /a:d "%Location_Folder%"') do (
set "filename=%%O" -- Here the DelayedExpansion have to be disabled
setlocal EnableDelayedExpansion
call D:\somefolder\otherfolder\batscriptname.bat filename
set "Validation="
...
endlocal
)
See also my modification of the CALL myBat.bat filename instead of CALL myBat.bat !filename!.
You shouldn't use content with CALL, better use a variable by reference and in your function take the content by
set "_filename=!%1!"
It's because CALL itself has some nasty behaviour with spaces, carets, etc
If you use a variable within a code block (parenthesised series of commands) then %var% will yield the value of the variable when the block is originally encountered (ie parse-time value) and !var! the value of the variable as it changes during the block (ie "run-time" value).
If you call a procedure - internal or external, then the values of the variables that the procedure sees are the run-time values from the parent. If these values are changed within the called procedure then the same rules apply, and the changed values are returned to the parent procedure.
However if you invoke setlocal then any value-variation made is reverted to its original value if you execute an endlocal instruction or reach end-of-file within the context of the setlocal.
OK - so that's how delayedexpansion works.
In your case, there is no need for delayedexpansion at all. In the loop in the mainline (%%O) you can use %%O in place of !filename!. In the :valid procedure, you can move the two set commands outside of the code block and then there's no need at all to use !vars! since no access is required to variables whose values change within blocks.
#ECHO OFF
setlocal
SET Location_Folder=V:
SET Destination_folder=V:\Nonimportable
SET Check_file_validation=V:\Nonimportable\result.txt
SET Excluded_folder=Nonimportable
set "lineNr=12"
For /f "tokens=*" %%O in ('dir /b /a:d "%Location_Folder%"') do (
set filename=%%O
call D:\somefolder\otherfolder\batscriptname.bat %%O
set Validation=
echo %%O| FINDSTR /i /c:"%Excluded_folder%" >NUL
IF ERRORLEVEL 1 (
for /F "skip=12 delims=" %%a in (%Check_file_validation%) do if not defined Validation (
set Validation=%%a
call :valid
)
) else (
echo This folder name is excluded: %%O
)
)
goto Finish
:valid
set Folder_path=%Location_Folder%\%filename%
set New_Folder_path=%Destination_folder%\%filename%
echo %Validation%| FINDSTR /c:"1" >NUL
if ERRORLEVEL 1 (
mkdir "%New_Folder_path%"
echo D | xcopy /o /y /q /s /v "%Folder_path%" "%New_Folder_path%"
rmdir /s /q "%Folder_path%"
) else (
echo Folder is valid %filename%
rem redundant instruction : goto Finish
)
:Finish
exit /b
#Echo off&SetLocal EnableExtensions EnableDelayedExpansion
cd "C:\Documents and Settings\John\Desktop\New\Interest\f2"
Pushd "C:\Documents and Settings\John\Desktop\New\Interest\f2"
Set Line#=26
Set /A LOfs=24 -1, Len=34 - LOfs
For %%A in (*.txt) do For /F "tokens=1* delims=:" %%B in (
'Findstr /N ".*" "%%A" ^|Findstr "^%Line#%:"'
) do if %errorlevel% == 0 Set "Line=%%C"&Ren "%%~fA" "!Line:~%LOfs%,%Len%! - %%A!""
Popd
In the above I am trying to change the filename of files in a directory with text in it at a certain position.
If line 26 is blank do nothing and do not change filename.
I have gone wrong somewhere and am going round in circles.
Can anyone help?
Thank you.
You don't state how your script fails, but I can see some potential problems. I also see possible simplifications.
You certainly don't need both CD and PUSHD
I got rid of the numeric variables and included the number literals in the actual code. You can revert back to variables if you want.
You don't need the outer FOR loop. FINDSTR can search multiple files when using wildcards in the file name, and then it will include the filename, followed by : in the output. So if you add the /N option, output will have the form filename:line#:text. You can then adjust the 2nd FINDSTR to return only the correct line numbers.
It is not enough to ignore blank lines. Your rename only works if there is at least one valid file name character after the 23rd character. Filenames cannot include :, *, ?, /, \, <, >, or |. (I may have missed some). I adjusted the FOR /F delims and the FINDSTR search to compensate.
FOR variable expansion like %%A will corrupt values if they contain ! and delayed expansion is enabled. ! is a valid character in file names. So the delayed expansion must be toggled on and off within the loop.
I believe the following will do what you want. The code below will simply echo the rename commands. Remove the ECHO before the ren once it gives the correct results.
#echo off
setlocal disableDelayedExpansion
pushd "C:\Documents and Settings\John\Desktop\New\Interest\f2"
for /f "tokens=1,3 delims=:*?\/<>|" %%A in (
'findstr /n "^" "*.txt" ^| findstr "^[^:]*:26:.......................[^:*?\\/<>|]"'
) do (
set "old=%%A"
set "line=%%B"
setlocal enableDelayedExpansion
ECHO ren "!old!" "!line:~23,11! - !old!"
endlocal
)
popd
An slightly different method to Daves:
#Echo Off
Set "SrcDir=%UserProfile%\Desktop\New\Interest\f2"
Set "Mask=*.txt"
Set "Line#=26"
Set "LOfs=23"
Set "Len=11"
If /I Not "%CD%"=="%SrcDir%" Pushd "%SrcDir%"2>Nul&&(Set _=T)||Exit/B
For /F "Tokens=1-2* Delims=:" %%A In ('FindStr/N "^" "%Mask%" 2^>Nul'
) Do If "%%B"=="%Line#%" If Not "%%~C"=="" (Set "Line=%%C"
SetLocal EnableDelayedExpansion
If Not "!Line:~%LOfs%,%Len%!"=="" (
If Not Exist "!Line:~%LOfs%,%Len%! - %%A" (
Ren "%%A" "!Line:~%LOfs%,%Len%! - %%A"))
EndLocal)
If "_"=="T" PopD
This method don't require findstr.exe nor toggle setlocal/endlocal, so it should run faster. Also, it avoids to re-process any already renamed file changing the plain for %%A by a for /F combined with dir command.
#Echo off
SetLocal EnableDelayedExpansion
cd "C:\Documents and Settings\John\Desktop\New\Interest\f2"
Set /A Line#=26, LOfs=24 -1, Len=34 - LOfs
For /F "delims=" %%A in ('dir /A-D /B *.txt') do (
rem Read the desired line from this file
(for /L %%i in (1,1,%Line#%) do set "Line=" & set /P "Line=") < "%%A"
if defined Line ECHO Ren "%%~fA" "!Line:~%LOfs%,%Len%! - %%A"
)
Note also that when this Batch file ends the current directory is automatically recovered to the current one when setlocal command was executed, so pushd/popd commands are not needed either.
I have some text files placed in the same folder as a batch file. I need the batch file to echo the filenames without its extention line by line with a numbering. It has to be in alphabetical order. Also, it needs to store the file name into a variable fruitx where x is the number of its numbering. For example...
If I have three files in the folder:
Banana.txt
Cherry.txt
Apple.txt
I need the batch file to echo:
1) Apple
2) Banana
3) Cherry
Then, I need the variable %fruit1% be "Apple", %fruit2% be "Banana" and %fruit3% be "Cherry".
set x=0
for /F "tokens=* delims=" %%I in ('dir /b *.txt') do (
set /a x=x+1&echo %x%^) %%~nI&set fruit%x%=%%~nI)
echo %x%
Here is my code. It doesn't work and I can't figure out why. It echos 0 for each of the numberings instead.
Sorry if this sounds confusing. Thanks in advance!
When the cmd parser reads a line or a block of lines (the code inside the parenthesis), all variable reads are replaced with the value inside the variable before starting to execute the code. If the execution of the code in the block changes the value of the variable, this value can not be seen from inside the same block, as the read operation on the variable does not exist, as it was replaced with the value in the variable.
This same behaviour is seen in lines where several commands are concatenated with &. The line is fully parsed and then executed. If the first commands change the value of a variable, the later commands can not use this changed value because the read operation replace.
To solve it, you need to enable delayed expansion, and, where needed, change the syntax from %var% to !var!, indicating to the parser that the read operation needs to be delayed until the execution of the command.
In your case, the problematic variable is x, that changes its value inside the loop and needs this value inside the same loop
#echo off
setlocal enableextensions enabledelayedexpansion
set "x=0"
for /f "delims=" %%a in ('dir /a-d /b /on *.txt 2^>nul') do (
set /a "x+=1"
echo !x!^) %%~na
set "fruit!x!=%%~na"
)
echo(---------------------------
set fruit
Also, an alternative approach without delayed expansion can be to filter the generated list of files with findstr to generate the number for each element
#echo off
setlocal enableextensions disabledelayedexpansion
for /f "tokens=1,2 delims=:" %%a in ('dir /a-d /b /on *.txt 2^>nul ^| findstr /n "^"') do (
echo %%a^) %%~nb
set "fruit%%a=%%~nb"
)
echo(---------------------------
set fruit
#echo off
setlocal enableDelayedExpansion
set counter=0
for /f %%a in ('dir /b /a:-d /o:n *.txt') do (
set /a counter=counter+1
echo !counter! ^) %%~na
set "fruit!counter!=%%~na"
)
echo listing fruits
set fruit
I want to loop recursively through a directory and have it's filenames echoed.
I ran into the 1 vs 01 name trouble.
Say, I have this:
D:\Downloads\prefixorder\order\1.txt
D:\Downloads\prefixorder\order\10.txt
D:\Downloads\prefixorder\order\2.txt
D:\Downloads\prefixorder\order\3.txt
D:\Downloads\prefixorder\order\1\new.txt
D:\Downloads\prefixorder\order\10\new.txt
D:\Downloads\prefixorder\order\2\new.txt
D:\Downloads\prefixorder\order\order\1.txt
D:\Downloads\prefixorder\order\order\10.txt
D:\Downloads\prefixorder\order\order\2.txt
D:\Downloads\prefixorder\order\order\20.txt
D:\Downloads\prefixorder\order\order\3.txt
D:\Downloads\prefixorder\order\order2\1.txt
D:\Downloads\prefixorder\order\order2\10.txt
D:\Downloads\prefixorder\order\order2\2.txt
D:\Downloads\prefixorder\order\order2\20.txt
D:\Downloads\prefixorder\order2copy\1.txt
D:\Downloads\prefixorder\order2copy\10.txt
D:\Downloads\prefixorder\order2copy\2.txt
D:\Downloads\prefixorder\order2copy\20.txt
D:\Downloads\prefixorder\order3\1.txt
D:\Downloads\prefixorder\order3\10.txt
D:\Downloads\prefixorder\order3\2.txt
D:\Downloads\prefixorder\order3\20.txt
D:\Downloads\prefixorder\1.txt
D:\Downloads\prefixorder\10.txt
D:\Downloads\prefixorder\2.txt
and I want to have 10 listed below 1 in each folder (and the folder being sorted like this, too).
Basically looking like this:
D:\Downloads\prefixorder\1.txt
D:\Downloads\prefixorder\2.txt
D:\Downloads\prefixorder\10.txt
D:\Downloads\prefixorder\order\1.txt
D:\Downloads\prefixorder\order\2.txt
D:\Downloads\prefixorder\order\3.txt
D:\Downloads\prefixorder\order\10.txt
D:\Downloads\prefixorder\order\1\new.txt
D:\Downloads\prefixorder\order\2\new.txt
D:\Downloads\prefixorder\order\10\new.txt
D:\Downloads\prefixorder\order\order\1.txt
D:\Downloads\prefixorder\order\order\2.txt
D:\Downloads\prefixorder\order\order\3.txt
D:\Downloads\prefixorder\order\order\10.txt
D:\Downloads\prefixorder\order\order\20.txt
D:\Downloads\prefixorder\order\order2\1.txt
D:\Downloads\prefixorder\order\order2\2.txt
D:\Downloads\prefixorder\order\order2\10.txt
D:\Downloads\prefixorder\order\order2\20.txt
D:\Downloads\prefixorder\order2copy\1.txt
D:\Downloads\prefixorder\order2copy\2.txt
D:\Downloads\prefixorder\order2copy\10.txt
D:\Downloads\prefixorder\order2copy\20.txt
D:\Downloads\prefixorder\order3\1.txt
D:\Downloads\prefixorder\order3\2.txt
D:\Downloads\prefixorder\order3\10.txt
D:\Downloads\prefixorder\order3\20.txt
So somewhat the same order as the default any win7 explorer display sorted after ascending alphabet. (Though in that version the files of a root files get to show below the folders, but that doesn't really matter).
A nice bonus would be, that this output comes no matter what object I dragged the dropped it from.
I found this, which solves this problem nicely for one folder:
Read files in directory in order of filename prefix with batch?
The diffrence to that problem that I want this recursively and while multiple files/folders can be dropped.
My current (buggy) modification looks like this:
#echo off
setlocal EnableDelayedExpansion
rem Create an array with filenames in right order
if [%1]==[] goto :eof
:loop
for /f "tokens=* delims=" %%a in ('dir "%~1" /a-d /s /b') do (
for /F "delims=-" %%i in ("%%a") do (
set "number=00000%%~ni"
set "file[!number:~-6!]=%%a"
)
)
rem Process the filenames in right order
for /F "tokens=2 delims==" %%f in ('set file[') do (
echo %%f
)
shift
if not [%1]==[] goto loop
#pause
the most buggy line in question would seem to be
set "file[!number:~-6!]=%%a"
whose array parameter in fact I don't even really begin to understand. My guess anyhow is that the array's entries are overwritten in each loop, because the parameters are pretty much the same in each loop.
I had also supected that the %%~ni is probably the cause of the overwriting, since the filenames inside the folders are all the same. Using %%~ni seems to me pbviously wrong, but using %%i simply doesn't do any sorting anymore and echoes out 10 before 1, which is even more wrong.
It works however if multiple folders are dragged, since they are in seperate echo loops. I tried putting in the echo loop within the first outer loop before and after the first inner loop. While it does make it that nothing gets omitted, it's not sorting properly at all.
Back to the major question: how do I solve that recursively and with mutiple files/folders dragged, that sorts the filenames and foldernames.
foldernames weirdly are of no prolem when recursively in one folder. It become a problem, when multiple folders/files are dragged, since it then starts off with the dragged object as the first argument.
Do I need to use an array of arrays or something like that? (Tried looking into it, didn't really get how that is possible in batch, yet, through.) Or is there any other way to do this?
Not even sure this is what you need, but just if it can help ...
#echo off
:: Without arguments, just list current folder
if "%~1"=="" (
call :recursiveSortedFileFolderList "%cd%"
goto :eof
)
:: Else, create a list of elements to sort
set "tempFile=%temp%\%~nx0.tmp"
call :generateList %* > "%tempFile%"
call :recursiveSortedFileFolderList "%tempFile%"
del "%tempFile%" >nul
goto :eof
:generateList
echo(%~1
if not "%~2"=="" shift & goto :generateList
exit /b
:recursiveSortedFileFolderList startFolder
setlocal enableextensions disabledelayedexpansion
:: Prepare padding base
set "pad=#################################"
set "pad=%pad%%pad%"
set "pad=%pad%%pad%"
set "pad=%pad%%pad%"
:: paddings for numeric or alphabetic prefixed files and folders
set "_NPad=%pad%"
set "_APad=%pad:#=$%"
:: start work
call :_doRecursiveSortedFileFolderList "%~1"
:: cleanup and exit
endlocal
exit /b
:_doRecursiveSortedFileFolderList folder
:: adjust environment for current folder
setlocal
set "timestamp=%time::=%"
set "tempFile=%temp%\%~nx0.%random%%random%%random%%random%.%timestamp:,=%.tmp"
set "folder=%~1" & if not defined folder ( set "folder=." ) else ( set "folder=%~f1" )
:: determine if we are handling a folder or a file with elements in it
if exist "%folder%\" (
set "cmd=dir /b ""%folder%\*"" 2^>nul"
) else if exist "%folder%" (
set "cmd=more ""%folder%"" "
set "folder="
for /f "tokens=* usebackq" %%a in ("%folder%") do if not defined folder (
for /f "tokens=*" %%b in ("%%~dpa\.") do set "folder=%%~dpnxb"
)
) else (
endlocal
exit /b
)
:: For each element in the indicated folder/file, prefix it with the adequated prefix
:: depending if it is a file or a folder, and send the output to a temporary file
:: that will be sorted (using the adecuate prefix), and processed
(for /f "tokens=*" %%a in ('%cmd%') do (
:: determine if file or folder
if exist "%folder%\%%~nxa\" ( set "type=f" ) else ( set "type=a" )
:: determine correct padding
if "%%~na" geq "a" ( set "name=%_APad%:%%~na" ) else ( set "name=%_NPad%:%%~na" )
:: generate final padded record
setlocal enabledelayedexpansion
echo(!type!:!name:~-260!%%~xa
endlocal
))> "%tempfile%"
:: Sort the temporary file and for each element on it, if it is a file, echo to console,
:: else it is a folder and a recursive call is made to process it
for /f "tokens=1,2,* delims=:" %%a in ('sort /L "C" "%tempfile%"') do (
if "%%a"=="a" (echo(%folder%\%%c) else (call %0 "%folder%\%%c")
)
:: clean and exit
endlocal & del "%tempfile%" > nul 2>nul
exit /b
There is a folder which contains some random files:
file1.txt
file2.exe
file3.cpp
file4.exe
How to SIMPLY display exe files connected with numbers like this:
1. file2.exe
2. file4.exe
And then I enter the number of the file, which I want to delete.. If it is even possible to do this simply..
Shortest bullet proof solution I can come up with. Like Anders, the DEL statement is disabled by the ECHO command. Remove the ECHO to make the menu functional.
#echo off
setlocal disableDelayedExpansion
for /f "delims==" %%A in ('set menu 2^>nul') do set "%%A="
for /f "tokens=1* delims=:" %%A in ('dir /b *.exe 2^>nul ^| findstr /n "^"') do (
set menu%%A=%%B
echo %%A. %%B
)
if not defined menu1 exit /b
set "delNum="
set /p "delNum=Delete which file (enter the number): "
setlocal enableDelayedExpansion
if defined menu!delNum! echo del "!menu%delNum%!"
The only thing I can think of that could go wrong is part of the menu could scroll off the screen if there are too many entries.
Additional messages can easily be incorporated. and an ELSE condition could be appended to the input validation to deal with invalid input.
A few subtle points of the code:
FINDSTR /N provides incrementing file number. Avoids need for delayed expansion or CALL within menu builder loop. Delayed expansion should not be enabled when expanding a FOR variable containing a file name because it will corrupt names containing !.
: is a safe FOR delimiter because a file name cannot contain :.
delNum is cleared prior to SET /P because SET /P will preserve existing value if <Enter> is pressed without entering anything.
Checking for the existence of the variable is the simplest way to validate the input. This is why it is critical that any existing MENU variables are undefined prior to building the menu.
Must use delayed expansion in IF DEFINED validation, otherwise space in input could crash the script (thanks Anders for pointing out the flaw in the original code)
DEL target must be quoted in case it contains spaces, even when delayed expansion is used.
Added test to make sure at least one menu entry exists before continuing. There may not be any .exe files left to delete.
#echo off
setlocal EnableDelayedExpansion
set i=0
for %%f in (*.exe) do (
set /A i+=1
set file[!i!]=%%f
echo !i!. %%f
)
set i=0
set /P i=File to delete:
del !file[%i%]!
Not exactly pretty but it gets the job done
#echo off
setlocal ENABLEEXTENSIONS DISABLEDELAYEDEXPANSION
goto main
:addit
set /A end=end + 1
set %end%=%~1
echo %end%. %~1
goto :EOF
:main
set end=0
for %%A in ("*.exe") do (
call :addit "%%~A"
)
if "%end%"=="0" goto :EOF
echo.&set idx=
set /P idx=Delete (1...%end%)
if not "%idx"=="" if %idx% GEQ 1 if %idx% LEQ %end% (
for /F "tokens=1,* delims==" %%A in ('set %idx% 2^>nul') do (
if "%idx%"=="%%~A" (
echo.Deleting %%~B...
rem del "%%~B"
)
)
)