Using FOR /R for recursive search only in a subset of folder hierarchy - for-loop

I want to create a batch file able to apply some processing on each JPG file in a folder hierarchy. The following script file works very well for that case (here I only echo the name of each file, but this should be replaced by some more complex statements in the real application):
:VERSION 1
#echo off
set "basefolder=C:\Base"
for /r %basefolder% %%f in (*.jpg) do echo %%f
Actually, I don't want to explore all the folder hierarchy under %basefolder%, but only a given list of subfolders. This modified script is able to deal with that case :
:VERSION 2
#echo off
set "basefolder=C:\Base"
set "subfolders=A B C"
for %%s in (%subfolders%) do (
pushd %basefolder%\%%~s"
for /r %%f in (*.jpg) do echo %%f
popd
)
Is there a solution to remove the pushd/popd pair of statements, to get something closer to the initial script. I thought that one of the following scripts would do the job:
:VERSION 3
#echo off
set "basefolder=C:\Base"
set "subfolders=A B C"
for %%s in (%subfolders%) do (
for /r %basefolder%\%%~s" %%f in (*.jpg) do echo %%f
)
or, using delayed expansion:
:VERSION 4
#echo off
setlocal enabledelayedexpansion
set "basefolder=C:\Base"
set "subfolders=A B C"
for %%s in (%subfolders%) do (
set "folder=%basefolder%\%%~s"
echo !folder!
for /r !folder! %%f in (*.jpg) do echo %%f
)
but none of them is working. When running the second one, the echo !folder! command in the external loop shows C:\Base\A, C:\Base\B and C:\Base\C as expected, but the inner loop doesn't echo any JPG file, so I guess that the recursive for /r command does not run correctly.
What am I doing wrong ?
Final edit after answers :
Thanks to #aschipfl who provided a link to the answer posted by #jeb on another question, quoted below:
The options of FOR, IF and REM are only parsed up to the special character phase. Or better the commands are detected in the special character phase and a different parser is activated then. Therefore it's neither possible to use delayed expansion nor FOR meta-variables in these options.
In other words, my versions 3 and 4 do not work because when defining the root folder of the FOR /R command, neither the %%~s nor the !folder! are correctly expanded by the expression parser. There is no way to change that, as this is a parser limitation. As I said in a comment below: the root folder option in the FOR /R command is basically only syntactic sugar to avoid the use of pushd/popd before and after the command. As this syntactic sugar is incomplete, we have to stick to the original syntax for some specific use cases, as the one presented here. The alternatives proposed by #Gerhard (using a subroutine CALL) or by #Mofi (parsing the result of a DIR command) are working, but they are neither more readable nor more efficient than the simple pushd/popd version I proposed initially.

My Approach for this would be really straight forward:
#echo off
set "basedir=C:\Base"
set "subfolders="A","B","C""
for %%i in (%subfolders%) do for /R "%basedir%" %%a in ("%%~i\*.jpg") do echo %%~fa
The double quotes inside of the subfolders variable is important here, it will ensure that folder names with whitespace are not seen as separators for the folder names. For instance:
set "subfolders="Folder A","Folder B","Folder C""
Edit
#echo off
set "basedir=C:\Base"
set "subfolders="A","B","C""
for %%i in (%subfolders%) do call :work "%%~i"
goto :eof
:work
for /R "%basedir%\%~1" %%a in (*.jpg) do echo %%~fa

It is in general not advisable to assign the value of a loop variable to an environment variable and next use the environment variable unmodified without or with concatenation with other strings being coded in batch file or defined already above the FOR loop within body of a FOR loop. That causes just problems as it requires the usage of delayed expansion which results in files and folders with one or more ! are not correct processed anymore inside body of the FOR loop caused by double parsing of the command line before execution, or command call is used on some command lines, or a subroutine is used called with call which makes the processing of the batch file much slower.
I recommend to use this batch file for the task:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
set "basefolder=C:\Base"
set "subfolders=A B C "Subfolder D" SubfolderE"
for %%I in (%subfolders%) do for /F "delims=" %%J in ('dir "%basefolder%\%%~I\*.jpg" /A-D /B /S 2^>nul') do echo %%J
endlocal
The inner FOR loop starts for each subfolder defined in subfolders in background one more command process with %ComSpec% /c and the DIR command line appended as additional arguments. So executed is with Windows installed to C:\Windows for example for the first subfolder:
C:\Windows\System32\cmd.exe /c dir "C:\Base\A\*.jpg" /A-D /B /S 2>nul
The command DIR searches
in specified directory C:\Base\A and all it subdirectories because of option /S
for files because of option /A-D (attribute not directory) including those with hidden attribute set
matching the pattern *.jpg in long or short file name
and outputs to handle STDOUT of background command process just the matching file names because of option /B (bare format)
with full path because of option /S.
The error message output by DIR on nothing found matching these criteria is redirecting from handle STDERR to device NUL to suppress it.
Read the Microsoft documentation about Using command redirection operators for an explanation of 2>nul. The redirection operator > must be escaped with caret character ^ on FOR command line to be interpreted as literal character when Windows command interpreter processes this command line before executing command FOR which executes the embedded dir command line with using a separate command process started in background.
The output to handle STDOUT of background command process is captured by FOR respectively the command process which is processing the batch file. FOR processes the captured output line by line after started cmd.exe terminated itself. This is very often very important. The list of files to process is already in memory of command process before processing the first file name. This is not the case on using for /R as this results in accessing file system, getting first file name of a non-hidden file matching the wildcard pattern, run all commands in body of FOR and accessing the file system once again to get next file name. The for /R approach is problematic if the commands in body of FOR change a file to process like deleting, moving, modifying, copying it in same folder, or renaming a found file because of the entries in file system changes while for /R is iterating over these entries. That can easily result in some files are skipped or some files are processed more than once and it could result also an endless running loop, especially on FAT file system like FAT32 or exFAT. It is never good to iterate over a list of files on which the list changes on each iteration.
Command FOR on usage of /F ignores empty lines which do not occur here. A non-empty line is split up into substrings using a normal space and a horizontal tab as string delimiters by default. This line splitting behavior is not wanted here as there could be full qualified file names containing anywhere inside full name one or more spaces. For that reason delims= is used to define an empty list of delimiters which disables the line splitting behavior.
FOR with option /F would also ignore lines on which first substring starts with ; which is the default end of line character. This is no problem here because of command DIR was used with option /S and so each file name is output with full path which makes it impossible that any file name starts with ;. So the default eol=; can be kept.
FOR with option /F assigns by default just first substring to specified loop variable as tokens=1 is the default. This default can be kept here as splitting the lines (full file names) into substrings is disabled already with delims= and so there is always the full file name assigned to the loop variable.
This example uses just echo %%I to output the file names with full path. But it is now safe to replace this single command by a command block which does more with the JPEG files because of the list of JPEG files for each specified subfolder tree in base folder is always already completely in memory of command process processing the batch file.

Related

Trying to rename all files ending with "VA.pdf" to "PA.pdf" using batch code

Hello I am trying to rename all files ending with "VA.pdf" to "PA.pdf" using batch code
I tired this code but it is not working
REN *VA.pdf *PA.pdf
Appreciate any help
There can be used for this file renaming task:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
if exist "*!*VA.pdf" goto ExtendedVersion
setlocal EnableDelayedExpansion
for /F "eol=| delims=" %%I in ('dir *VA.pdf /A-D-L /B 2^>nul') do (
set "FileNamePDF=%%~nI"
set "FileNameNew=!FileNamePDF:~0,-2!PA%%~xI"
if not exist "!FileNameNew!" ren "!FileNamePDF!%%~xI" "!FileNameNew!"
)
endlocal
goto EndBatch
:ExtendedVersion
echo INFO: Extended version required because of a PDF file with exclamation marks.
for /F "eol=| delims=" %%I in ('dir *VA.pdf /A-D-L /B 2^>nul') do (
set "FileNamePDF=%%~nI"
setlocal EnableDelayedExpansion
set "FileNameNew=!FileNamePDF:~0,-2!PA%%~xI"
if not exist "!FileNameNew!" ren "!FileNamePDF!%%~xI" "!FileNameNew!"
endlocal
)
:EndBatch
endlocal
There is defined first the required execution environment with the first two command lines.
The IF condition in the third command line quickly checks if there is any PDF file with case-insensitive VA in the file name before the file extension .pdf containing one or more exclamation marks in the file name. The extended version of the processing loop is required if this condition is true.
The standard version enables first required delayed expansion. Then a FOR loop is used which runs in background with Windows installed into C:\Windows:
C:\Windows\System32\cmd.exe /c dir *VA.pdf /A-D-L /B 2>nul
The internal command DIR of cmd.exe searches
in the current directory as defined by the process starting cmd.exe for processing the batch file
for just file names because of option /A-D-L (attribute not directory and not link)
matching case-insensitive the wildcard pattern *VA.pdf in long or short 8.3 name
and outputs in bare format because of option /B just the file names with file extension, but without file path.
An error message output to handle STDERR (standard error) on DIR does not find any file system entry matching the criteria is suppressed by redirecting this error message to the device NUL.
Read the Microsoft documentation about Using command redirection operators for an explanation of 2>nul. The redirection operator > must be escaped with caret character ^ on FOR command line to be interpreted as literal character when Windows command interpreter processes this command line before executing command FOR which executes the embedded dir command line with using a separate command process started in background.
FOR respectively cmd.exe processing the batch file captures all output written to standard output stream of in background started cmd.exe and processes it line by line after started cmd.exe closed itself after finishing executing the command DIR.
FOR with option /F is used here to get a list of file names of *VA.pdf files loaded into memory of cmd.exe before really doing the file renames as otherwise it could happen especially on FAT drives (FAT32, exFAT) that some PDF files are skipped or processed more than once (on rename not possible).
FOR on using option /F ignores always empty lines which is no problem here as DIR with the used options does not output empty lines.
FOR would next split up the lines into substrings using horizontal tab and normal space as string delimiters, would look next if first tab/space separated string begins with a semicolon in which case it would also ignore the entire line for further processing, and would otherwise assign just the first tab/space separated string to the specified loop variable I before running the commands in body of FOR.
The default line splitting behavior is not wanted as PDF file names can contain one or more spaces. The usage of the option delims= defines an empty list of delimiters which turns off the line splitting behavior.
It is very unusual but nevertheless possible that a PDF file name begins with ; (semicolon). Such a file name should not be ignored by FOR. The option eol=| defines a vertical bar as end of line character which no file name can contain ever. Microsoft lists the characters not allowed in a file name on Windows file systems in the documentation about Naming Files, Paths, and Namespaces.
The current file name without file extension .pdf is assigned first to the environment variable FileNamePDF.
Next a string substitution is used to get from the string value of the environment variable FileNamePDF the file name without the last two characters VA concatenated with the string PA and the file extension .pdf assigned to the environment variable FileNameNew.
If there is not already a PDF file ending with PA in the file name before the file extension, there is next executed the command REN to rename the *VA.pdf file to *PA.pdf.
The command ENDLOCAL after the loop restores the previous environment before enabling delayed expansion and the command GOTO instructs the Windows Command Processor to continue processing the batch file with the command line below the label EndBatch which contains one more ENDLOCAL to restore the environment on starting the batch file processing.
The extended version is nearly the same as the standard version. The difference is that delayed variable expansion is not enabled on assigning the file name of the current VA.pdf file without the file extension to the environment variable FileNamePDF. That avoids interpreting the exclamation mark(s) in the file name as beginning/end of a delayed expanded variable reference resulting in a manipulation of the file name string before assigning it to the environment variable as it would happen with delayed expansion already enabled.
The extended version enables next delayed variable expansion inside the loop, does the same as the standard version and restores finally the previous environment before processing the next *VA.pdf file.
The extended version is slower because of the environment variables list copy and the other operations made in background by every execution of SETLOCAL as explained in full details in this answer. The command ENDLOCAL in the loop is required to avoid a stack overflow on processing lots of PDF files.
To understand the commands used and how they work, open a command prompt window, execute there the following commands, and read the displayed help pages for each command, entirely and carefully.
echo /?
endlocal /?
for /?
goto /?
if /?
ren /?
set /?
setlocal /?

How to pass a command that may contain special characters (such as % or !) inside a variable to a for /f loop?

I have a few nested loops in my code and in some point, they're divided by a call to a label like this:
#echo off
chcp 65001
for /r %%a in (*.mkv *.mp4 *.avi *.mov) do (
echo Processing "%%~a"
call :innerloop "%%a" "%%~fa"
)
:: Instead of goto :eof, I chose cmd /k because I want the command prompt to still be open after the script is done, not sure if this is correct though
cmd /k
:innerloop
setlocal EnableExtensions EnableDelayedExpansion
for /f "delims=" %%l in ('mkvmerge.exe -i "%~1"') do (
:: Probably this would be a safer place for setlocal, but I believe that would mean that I wouldn't get to keep a single, different !propeditcmd! per processed file
echo Processing line "%%~l"
for /f "tokens=1,4 delims=: " %%t in ("%%l") do (
:: This section checks for mkv attachments. There are similar checks for chapters and global tags, all of those are handled by mkvpropedit.exe
if /i "%%t" == "Attachment" (
if not defined attachments (
set /a "attachments=1"
) else (
set /a "attachments+=1"
)
if not defined propeditcmd (
set "propeditcmd= --delete-attachment !attachments!"
) else (
set "propeditcmd=!propeditcmd! --delete-attachment !attachments!"
)
)
)
)
:: Since !propeditcmd! (which contains the parameters to be used with the executable) is called after all lines are processed, I figured setlocal must be before the first loop in this label
if defined propeditcmd (
mkvpropedit.exe "%~f1" !propeditcmd!
)
endlocal
goto :eof
The script works for most files and is divided like that to allow breaking the inner loop without breaking the outer when a pass is reached. While it works for most files, I noticed it can't handle filenames containing parenthesis % in their names, likely due to EnableDelayedExtensions.
Normally, I know I would have to escape these characters with a caret (^), but I don't know how I can do it if the special characters are inside a variable (%~1).
Is there a way to do it?
Update: I've been working a way to separate the section that needs delayed expansion from the one that needs it off just find in the end of my code the line mkvpropedit.exe "%~f1" !propeditcmd!, which both needs it off and on due to "%~f1" and !propeditcmd! respectively. I think this means there's no way around the question and escaping will be necessary.
Continuing my research, this answer seem to suggest this could be achieved with something like set filename="%~1:!=^^!". Nevertheless, this doesn't seem to be the proper syntax according to SS64. I'm also unsure if this will replace all occurrences of ! with ^! and I'm also concerned this kind of substitution could create an infinite loop and if wouldn't it be more adequate to perform this by first replacing ! with, say, ¬ before replacing it ^!.
While I intend to do testing soon to determine all of this, I'm worried I may not cover it all, so more input would definitely be appreciated.
PS: full code (88 lines) is available here if more context is needed, although I'll edit the snippet in this question as it may be requested!
Edit: I didn't think it was relevant at first, but now I think it helps to know what is an standard output from mkvmerge.exe -i:
File 'test.mkv': container: Matroska
Track ID 0: video (AVC/H.264/MPEG-4p10)
Track ID 1: audio (Opus)
Track ID 2: subtitles (SubRip/SRT)
Attachment ID 1: type 'image/jpeg', size 30184 bytes, file name 'test.jpg'
Attachment ID 2: type 'image/jpeg', size 30184 bytes, file name 'test2.jpg'
Attachment ID 3: type 'image/jpeg', size 30184 bytes, file name 'test3.jpg'
Chapters: 5 entries
Global tags: 3 entries
There is not really a need for a subroutine. Delayed variable expansion is needed finally, but it is possible to first assign the fully qualified file name to an environment variable like FileName to avoid troubles with file names containing an exclamation mark.
The rewritten code according to the code posted in the question with some comments:
#echo off
setlocal EnableExtensions DisableDelayedExpansion
set "WindowTitle=%~n0"
rem Find out if the batch file was started with a double click which means
rem with starting cmd.exe with option /C and the batch file name appended
rem as argument. In this case start one more Windows command processor
rem with the option /K and the batch file name to keep the Windows command
rem processor running after finishing the processing of this batch file
rem and exit the current command processor processing this batch file.
rem This code does nothing if the batch file is executed from within a
rem command prompt window or it was restarted with the two options /D /K.
setlocal EnableDelayedExpansion
for /F "tokens=1,2" %%G in ("!CMDCMDLINE!") do (
if /I "%%~nG" == "cmd" if /I "%%~H" == "/c" (
endlocal
start %SystemRoot%\System32\cmd.exe /D /K %0
if not errorlevel 1 exit /B
setlocal EnableDelayedExpansion
)
)
rem Set the console window title to the batch file name.
title !WindowTitle!
endlocal
set "WindowTitle="
rem Get the number of the current code page and change the code page
rem to 65001 (UTF-8). The initial code page is restored at end.
for /F "tokens=*" %%G in ('%SystemRoot%\System32\chcp.com') do for %%H in (%%G) do set "CodePage=%%~nH"
%SystemRoot%\System32\chcp.com 65001 >nul 2>&1
for /R %%G in (*.mkv *.mp4 *.avi *.mov) do (
echo(
echo Processing "%%~G"
set "Attachments="
for /F "delims=" %%L in ('mkvmerge.exe -i "%%G"') do (
rem echo Processing line "%%L"
for /F "delims=: " %%I in ("%%L") do if /I "%%I" == "Attachment" set /A Attachments+=1
)
if defined Attachments (
set "FileName=%%G"
setlocal EnableDelayedExpansion
set "propeditcmd=--delete-attachment 1"
for /L %%I in (2,1,!Attachments!) do set "propeditcmd=!propeditcmd! --delete-attachment %%I"
mkvpropedit.exe "!FileName!" !propeditcmd!
endlocal
)
)
rem Restore the initial code page.
%SystemRoot%\System32\chcp.com %CodePage% >nul
endlocal
Why is the window title passed to TITLE with using delayed expansion?
An argument string must be enclosed in " if it contains after the expansion of dynamic variable, environment variable, loop variable or batch file argument references a space or one of these characters &()[]{}^=;!'+,`~<>| if all these characters should be interpreted literally by the Windows command processor cmd.exe. For that reason the third line encloses the argument string WindowTitle=%~n0 in double quotes because of %~n0 references the batch file name without file extension and without path which could contain, for example, an ampersand although that would be a very usual file name for a batch file.
See also: How does the Windows Command Interpreter (CMD.EXE) parse scripts?
The command TITLE is like the command ECHO regarding to ". It always interprets double quotes as literal characters and do not remove them from the argument string. So the usage of title "%WindowTitle%" would result in having a title for the console window which starts and ends with a double quote. That would not look nice. Therefore the batch file name as window title should be passed to the cmd.exe internal command TITLE without double quotes. But that is problematic in case of the batch file name contains a character with a special meaning for cmd.exe processing the command line before executing the command TITLE like &. For that reason delayed variable expansion is enabled and used here to reference the batch file name assigned to the environment variable WindowTitle which makes it possible to get the window title really set according to the batch file name.
Why is current code page determined and restored at end?
A good written batch file for usage by many people changing something on execution environment should always restore the initial execution environment, except the batch file is explicitly designed to define the execution environment for applications and scripts executed after batch file execution finished.
What does that mean for batch file development?
The following properties of the execution environment should be unmodified after finishing the execution of a batch file in comparison to the property values on starting the batch file:
the list of environment variables and their values;
the status of command extensions;
the status of delayed expansion;
the current directory;
the command prompt;
text and background color;
the code page to use for character encoding;
the number of rows and columns of the console window.
The first four properties of the execution environment are unmodified on using at top of the batch file SETLOCAL and optionally at bottom also ENDLOCAL. An explicit ENDLOCAL at bottom of a batch file is optional because of cmd.exe calls it implicit for each SETLOCAL without an executed matching ENDLOCAL before exiting the processing of a batch file independent on the cause of exiting the batch file processing.
See also: How to pass environment variables as parameters by reference to another batch file?
It explains in full details what happens on each execution of SETLOCAL and ENDLOCAL.
For each successfully executed PUSHD should be executed also a POPD to restore the initial current directory.
The command prompt needs to be restored only on changing it with command PROMPT which most batch files don't do at all.
The usage of CHCP to change the code page should result in using CHCP once again at end of a batch file to restore the original code page. The same should be done on using the command COLOR to change the text color and the background color and command MODE to change the rows and the columns of the console window.
See DosTips forum topic [Info] Saving current codepage, especially the post written by Compo, for an explanation about getting current code page number assigned to an environment variable which is used at end of the batch file to restore the initial code page.
It is a bit difficult to understand why getting the current code page number is done with two FOR loops whereby the second one uses the modifier %~n although the output of chcp.com is definitely not a file name. So let us look on what happens on a German Windows on which the command CHCP outputs the string:
Aktive Codepage: 850.
The dot at end of the output is not wanted, just the code page number like on English Windows on which the output is:
Active code page: 850
See the referenced DosTips topic for other variants depending on the language of Windows.
The output of chcp.com is first assigned completely to the loop variable G with removing leading normal spaces and horizontal tabs if chcp.com would output the code page information with leading spaces/tabs. The second FOR loop processes this list of words with using normal space, comma, semicolon, equal sign and OEM encoded no-break space as word delimiters.
The second FOR loop runs the command SET for German code page information three times with the strings:
Aktive
Codepage:
850.
The usage of the modifier %~n results now three times in accessing the file system by cmd.exe and searching in current directory for a file with the string assigned to the loop variable H as file name. There is most likely no file Aktive. Codepage: with the colon at end is an invalid file name, and a file 850 with trailing dot removed by the Windows file IO API functions is most likely also not found in current directory. However, it does not really matter if there is by chance a file system entry matching one of the three strings or not because of %~n results in using just the string from beginning to the character before the last dot. So the command SET is first executed with Aktive, a second time with Codepage: and finally a third time with 850. So the environment variable CodePage is defined finally with just the number 850.
Description of the main FOR loops processing the video files
The most outer FOR assigns the name of the found file always with full path without surrounding " to the specified loop variable G because of using option /R. For that reason just "%%G" is used instead of "%%~G" wherever the fully qualified file name must be referenced to speed up the processing of the file names.
echo( outputs an empty line, see the DosTips forum topic ECHO. FAILS to give text or blank line - Instead use ECHO/
If an undefined environment variable like Attachments is referenced in an arithmetic expression evaluated by SET, the value 0 is used as explained by the usage help output on running set /? in a command prompt window. For that reason set /A Attachments+=1 can be used to either define the variable with 1 on first execution or increment the value of environment variable Attachments by one on all further executions for the current file.
The final value of environment variable Attachments is evaluated after processing all lines output by mkvmerge. If there are attachments, the file name is assigned to the environment variable FileName with still disabled delayed variable expansion and for that reason ! is interpreted as literal character. The environment variable propeditcmd is created next dynamically according to the number of attachments.
Optimized code for the entire video files processing task
I have installed neither mkvmerge.exe nor mkvpropedit, but I looked also on the referenced full code. Here is a rewritten optimized version of your full code without any comment which I could not really test completely.
#echo off
setlocal EnableExtensions DisableDelayedExpansion
set "WindowTitle=%~n0"
setlocal EnableDelayedExpansion
for /F "tokens=1,2" %%G in ("!CMDCMDLINE!") do (
if /I "%%~nG" == "cmd" if /I "%%~H" == "/c" (
endlocal
start %SystemRoot%\System32\cmd.exe /D /K %0
if not errorlevel 1 exit /B
setlocal EnableDelayedExpansion
)
)
title !WindowTitle!
endlocal
for /F delims^=^=^ eol^= %%G in ('set ^| %SystemRoot%\System32\findstr.exe /B /I /L /V "ComSpec= PATH= PATHEXT= SystemRoot= TEMP= TMP="') do set "%%G="
if exist "%~dp0mkvmerge.exe" (set "ToolsPath=%~dp0") else if exist mkvmerge.exe (set "ToolsPath=%CD%") else for %%I in (mkvmerge.exe) do set "ToolsPath=%%~dp$PATH:I"
if not defined ToolsPath echo ERROR: Could not find mkvmerge.exe!& exit /B 2
if "%ToolsPath:~-1%" == "\" set "ToolsPath=%ToolsPath:~0,-1%"
if not exist "%ToolsPath%\mkvpropedit.exe" echo ERROR: Could not find mkvpropedit.exe!& exit /B 2
for /F "tokens=*" %%G in ('%SystemRoot%\System32\chcp.com') do for %%H in (%%G) do set /A "CodePage=%%H" 2>nul
%SystemRoot%\System32\chcp.com 65001 >nul 2>&1
del /A /F /Q Errors.txt ExtraTracksList.txt 2>nul
(
set "ToolsPath="
set "CodePage="
for /F "delims=" %%G in ('dir *.mkv /A-D-H /B /S 2^>nul') do (
echo --^> Processing file "%%G" ...
setlocal
set "FullFileName=%%G"
for /F "tokens=1,4 delims=: " %%H in ('^""%ToolsPath%\mkvmerge.exe" -i "%%G" --ui-language en^"') do (
if /I "%%I" == "audio" (
set /A AudioTracks+=1
setlocal EnableDelayedExpansion
if !AudioTracks! == 2 echo !FullFileName!>>ExtraTracksList.txt
endlocal
) else if not defined SkipFile if /I "%%I" == "subtitles" (
echo --^> "%%~nxG" has subtitles
"%ToolsPath%\mkvmerge.exe" -o "%%~dpnG.nosubs%%~xG" -S -M -T -B --no-global-tags --no-chapters --ui-language en "%%G"
if not errorlevel 1 (
echo --^> Deleting old file ...
del /F "%%G"
echo --^> Renaming new file ...
ren "%%~dpnG.nosubs%%~xG" "%%~nxG"
) else (
echo Warnings/errors generated during remuxing, original file not deleted, check Errors.txt
"%ToolsPath%\mkvmerge.exe" -i --ui-language en "%%G">>Errors.txt
del "%%~dpnG.nosubs%%~xG" 2>nul
)
set "SkipFile=1"
) else if /I "%%H" == "Attachment" (
set /A Attachments+=1
) else if /I "%%H" == "Global" (
set "TagsAll=--tags all:"
) else if /I "%%H" == "Chapters" (
set "Chapters=--chapters """
)
)
if not defined SkipFile (
set "OnlyFileName=%%~nxG"
setlocal EnableDelayedExpansion
if defined Attachments (
set "PropEditOptions= --delete-attachment 1"
for /L %%H in (2,1,!Attachments!) do set "PropEditOptions=!PropEditOptions! --delete-attachment %%H"
)
if defined TagsAll set "PropEditOptions=!PropEditOptions! !TagsAll!"
if defined Chapters set "PropEditOptions=!PropEditOptions! !Chapters!"
if defined PropEditOptions (
echo --^> "!OnlyFileName!" has extras ...
"%ToolsPath%\mkvpropedit.exe" "!FullFileName!"!PropEditOptions!
)
endlocal
)
echo(
echo ##########
echo(
endlocal
)
for /F "delims=" %%G in ('dir *.avi *.mp4 *.mov /A-D-H /B /S 2^>nul') do (
echo Processing file "%%G" ...
"%ToolsPath%\mkvmerge.exe" -o "%%~dpnG.mkv" -S -M -T -B --no-global-tags --no-chapters --ui-language en "%%G"
if not errorlevel 1 (
echo --^> Deleting old file ...
del /F "%%G"
) else (
echo --^> Warnings/errors generated during remuxing, original file not deleted.
"%ToolsPath%\mkvmerge.exe" -i --ui-language en "%%G">>Errors.txt
del "%%~dpnG.mkv" 2>nul
)
echo(
echo ##########
echo(
)
if exist Errors.txt for %%G in (Errors.txt) do if %%~zG == 0 del Errors.txt 2>nul
%SystemRoot%\System32\chcp.com %CodePage% >nul
)
endlocal
Removal of not needed environment variables in local environment
The batch file has to process perhaps hundreds or even thousands of files using multiple environment variables.
There is at least once per MKV file used SETLOCAL and ENDLOCAL creating a copy of current environment variables list which is discarded after finishing processing of the current MKV file.
There are also other programs executed for each video files on which the Windows kernel library function CreateProcess creates also a copy of the current list of environment variables of current process.
For that reason it is helpful to use a local environment variables list which contains only the environment variables really needed during processing of the video files.
The first FOR after setting the window title runs in background one more cmd.exe as follows:
C:\Windows\System32\cmd.exe /c set | C:\Windows\System32\findstr.exe /B /I /L /V "ComSpec= PATH= PATHEXT= SystemRoot= TEMP= TMP="
There is output by set of started cmd.exe in background the same list of environment variables with their values as the command process currently uses which processes the batch file. The lines are passed to findstr which searches case-insensitive (/I) and literally (/L) for the space separated strings at beginning of each line (/B) and outputs the inverted result (/V) which means all lines NOT beginning with one of the space separated strings. So there are output all the environment variables separated with a = from their values, except those searched for and found by findstr.
The captured lines are processed by FOR with using the equal sign as string delimiter and no character as end of line character to process even an environment variable of which name starts with a semicolon and assigns to the loop variable G just the variable name which is used to remove the variable from the current environment variables list.
So there are only remaining the environment variables ComSpec, PATH, PATHEXT, SystemRoot, TEMP and TMP.
Use fully qualified file names to avoid unnecessary file system accesses
Most people use in batch files just the file names of executables without file extension and without file path which forces cmd.exe to search in current directory and next in all directories as specified in environment variable PATH for the file with a file extension as specified in environment variable PATHEXT. That results in thousands of file system accesses on processing hundreds of files in a loop calling executables on each file.
All these file system accesses can be avoided by specifying each executable with its fully qualified file name in the batch file. That does not mean that a batch file must contain already the fully qualified file name for each executable as the code above demonstrates because of the full file names of the executables can be determined also once at beginning of the batch file.
The batch file first checks if mkvmerge.exe is in the directory of the batch file and defines the environment variable ToolsPath with the full batch file path if that file check is positive. Otherwise there is searched in the current directory for the executable mkvmerge.exe and the current directory path is assigned to ToolsPath if there is a file system entry (hopefully a file and not a directory) with the name mkvmerge.exe. Last there is searched for mkvmerge.exe in the directories of environment variable PATH and if found this directory path is assigned to ToolsPath.
The batch file outputs an error message, restores the initial environment and exits on executable mkvmerge.exe or the other one mkvpropedit.exe could not be found at all.
%~dp0 and %%~dp$PATH:I expand to a path string always ending with a backslash. %CD% expands to a path string not ending with a backslash, except the current directory is the root directory of a drive. For that reason an IF condition with a string comparison is used to check if the path string assigned to ToolsPath ends with a backslash in which case the environment variable is redefined with this backslash removed. The backslash is added in the code below on referencing the path string of ToolsPath.
Determination of current code page using a different method
This time the first solution developed by Compo is used to determine the number of the current code page. It is similar to the other solution as using the same two FOR loops, but the command SET executed by the second FOR loop evaluates now an arithmetic expression to get on last iteration the code page number without the dot assigned to the environment variable CodePage.
Let us look again what happens on processing the string: Aktive Codepage: 850.
There is first executed set /A "CodePage=Aktive" which results in environment variable CodePage is defined with value 0 because of Aktive is interpreted as environment variable name and there is no such environment variable. Next is executed set /A "CodePage=Codepage:" with the same interpretation and the same result 0. And last is executed set /A "CodePage=850." which results in the error message Missing operator. to handle STDERR redirected to the device NUL to suppress it. However, the value assigned to the environment variable CodePage is 850 as wanted.
The advantage of this solution is the usage of %%H inside the arithmetic expression which does not result in any file system access. So this solution is in general better in my opinion.
How to avoid batch file accesses during processing the video files?
I recommend reading Why is a GOTO loop much slower than a FOR loop and depends additionally on power supply?
Conclusion: It is a good idea to put the entire code required to process hundreds or thousands of files into one command block which the Windows command processors reads and parses just once.
The problem is in most cases how to handle variables of which values changes within the command block without using all the time delayed expansion as that affects processing of strings like file names. That is in most cases not easy, but it is often possible as it can be seen on the code above.
The environment variables ToolsPath and CodePage can be undefined immediately at beginning of the main code block because of the command processor replaced already all %ToolsPath% and %CodePage% by the appropriate path and code page number strings before executing the first command set "ToolsPath=". So the current environment variables list on execution of the first main FOR loop contains just the five environment variables found by findstr.
The Windows command processor does not access anymore the batch file until having finished processing all video files and restored the original code page.
Other special information about the code in second batch file
The two text files with information collected during processing of the video files are always deleted first using the command DEL if the file system does not prevent the deletion of the files.
There is used twice for /F instead of for /R as main FOR loops to get first all file names of video files to process with full path loaded into memory of the Windows command processor and then process the video files instead of iterating over the current file system entries as done by for /R. This makes a big difference for the loop processing *.mkv files, especially on video files being stored on a FAT32 or exFAT formatted drive on which the file allocation table does not only change on processing an MKV file as also on NTFS formatted drives, but are not updated in file allocation table in a local alphabetic sort as on an NTFS formatted drive. The usage of for /R could result on a FAT32 or exFAT formatted drive in either processing an MKV file more than once or skipping unexpected one or more MKV files due to the file allocation table changes caused by the execution of mkvmerge or mkvpropedit on an MKV file.
The commands SETLOCAL and ENDLOCAL are used to quickly restore always the minimal environment variables list defined outside of the main FOR loops for each MKV file which results in discarding always all the changes made on the environment variables list on processing an MKV file.
The execution of mkvmerge.exe with its full path with option -i and the full name of current MKV file by one more cmd.exe started with /c and the specified command line is a bit tricky on taking into account that %ToolsPath% and %%G could contain also characters like & to be interpreted as literal characters by cmd.exe processing the batch file and also by cmd.exe started in background.
It is necessary to enclose the entire command line to execute by cmd.exe in background in double quotes to be correct processed by this cmd.exe instance. But the cmd.exe instance processing the batch file must interpret these two " as literal characters and not as beginning or end of an argument string. Otherwise "" at beginning would be interpreted by cmd.exe processing the batch file as the beginning and the end of an empty argument string. Therefore the tools path string would be not anymore enclosed in double quotes for cmd.exe processing the batch file which of course is problematic on containing & or ' or ).
For that reason the two double quotes to enclose the entire command line in " are specified in the batch file with the caret character ^ to be escaped which results in cmd.exe processing the batch file is interpreting these two double quotes as literal characters and not as beginning/end of an argument string.
The result is that "%ToolsPath%\mkvmerge.exe" and "%%G" are interpreted by both cmd.exe as double quoted argument strings and therefore can contain all characters interpreted as literal characters which would otherwise be interpreted with a special meaning.
The information about audio tracks are processed always independent in which order mkvmerge.exe outputs the information data about the current MKV file. But all other information are not further processed once the environment variable SkipFile is defined because of the current MKV file has subtitles.
The file Errors.txt is deleted on being created, but has finally a size of 0 bytes.
Usage help for the used Windows commands
For understanding the used commands and how they work, open a command prompt window, execute there the following commands, and read entirely all help pages displayed for each command very carefully.
call /?
chcp /?
cmd /?
dir /?
del /?
echo /?
endlocal /?
exit /?
findstr /?
for /?
if /?
rem /?
set /?
setlocal /?
start /?
title /?
See also Issue 7: Usage of letters ADFNPSTXZadfnpstxz as loop variable and the other chapters about general issues made by beginners in batch file coding.

Removing first n characters from folder names

I'm fairly new to batch, my problem is the following:
I have a long list of folders and need to delete the first 3 characters from each of their names. Think 01_Folder1, 02_Folder2, 03_Folder3 and so on. I've tried patching together pieces of CMD commands I've found on the web but could not come up with a script that does what I want it to do. I've even tried using VBScript as I'm more familiar with VB in general but failed to find a solution as well.
Is there an easy way to solve this?
Edit:
Here's my attempt; it's giving me a syntax error but as I am not versed enough in CMD, I cannot really see why:
setlocal enableextensions enabledelayedexpansion
for /d %%i in ("%~dp0*") do (set name=%%i && ren "!name!" "!name:~3!")
endlocal
The FOR command line does not work because of assigned to loop variable i is the name of a directory with full path and so removing the first three characters results in removing drive letter, colon and backslash from path of the directory and not the first three characters from directory name. Further the full qualified directory name is assigned with an additional space to environment variable name because of the space between %%i and operator &&.
One solution would be:
#echo off
setlocal EnableExtensions EnableDelayedExpansion
for /d %%i in ("%~dp0*") do set "name=%%~nxi" && ren "%%i" "!name:~3!"
endlocal
The disadvantage of this solution is that directory names with one or more exclamation marks in name or path are not processed correct because of enabled delayed expansion resulting in interpreting ! in full directory name as begin/end of a delayed expanded environment variable reference.
Another solution is quite simple with using just the command line:
#for /D %%i in ("%~dp0*_*") do for /F "tokens=1* delims=_" %%j in ("%%~nxi") do #ren "%%i" "%%k"
The outer FOR searches in directory of the batch file for non-hidden subdirectories matching the pattern *_*.
For each directory name assigned with full path to loop variable i one more FOR command is used which processes just the string after last backlash (directory name without path) and splits the string up into substrings (tokens).
The string delimiter is an underscore as defined with option delims=_. The option tokens=1* tells FOR to assign first underscore delimited string to specified loop variable j and everything after one or more underscores after first underscore delimited string to next but one loop variable k according to ASCII table.
The inner FOR would ignore a directory name on which first substring starts with a semicolon as being the default end of line character. But in this case no directory has ; at beginning of its name.
There is one problem remaining with this command line. It does not work on drives with FAT32 or exFAT as file system, just by chance on drives with NTFS file system. The reason is that the list of non-hidden directories changes in file system while the outer FOR iterates over the directory entries matching the pattern.
A better solution loads first the list of directories to rename into memory of Windows command process which is processing the batch file before starting with renaming the directories.
#for /F "delims=" %%i in ('dir "%~dp0*_*" /AD-H /B 2^>nul') do for /F "tokens=1* delims=_" %%j in ("%%i") do #ren "%~dp0%%i" "%%k"
FOR executes in this case in background one more command process with %ComSpec% /c and the command line within ' appended as additional arguments. So executed in background is with Windows installed to C:\Windows:
C:\Windows\System32\cmd.exe /c dir "C:\Batch\File\Path\*_*" /AD-H /B 2>nul
DIR searches in directory of the batch file for
non-hidden directories because of option /AD-H (attribute directory and not hidden)
matching the wildcard pattern *_*
and outputs just the directory names in bare format because of option /B without path to handle STDOUT (standard output) of background command process.
Read the Microsoft documentation about Using command redirection operators for an explanation of 2>nul. The redirection operator > must be escaped with caret character ^ on FOR command line to be interpreted as literal character when Windows command interpreter processes this command line before executing command FOR which executes the embedded dir command line with using a separate command process started in background.
FOR respectively the Windows command process processing the batch file captures everything written to standard output handle of background command process and starts processing it as described above after started cmd.exe terminated itself. So there is in memory already a list of directory names and so the executed REN command does not result anymore in a changed list of directory names on processing one after the other.
Please note that a directory with name 01__Underscore_at_beginning is renamed to Underscore_at_beginning and not to _Underscore_at_beginning by both single line solutions.
For understanding the used commands and how they work, open a command prompt window, execute there the following commands, and read entirely all help pages displayed for each command very carefully.
echo /?
endlocal /?
for /?
ren /?
set /?
setlocal /?

Add prefix to filenames using batch files [duplicate]

This question already has an answer here:
At which point does `for` or `for /R` enumerate the directory (tree)?
(1 answer)
Closed 3 years ago.
I can add a prefix to a series of text files using:
:: rename files
for %%a in (*.txt) do (
ren "%%a" "Seekret file %%a"
:: ECHO %%a Seekret file %%a
)
which will turn
a.txt
b.txt
c.txt
into
Seekret file a.txt
Seekret file b.txt
Seekret file c.txt
However, the above code seems to rename the first file twice with the prefix. I end up with
Seekret file Seekret file a.txt
and I have no idea why. Any ideas?
Use
for /f "delims=" %%a in ('dir /b /a-d *.txt') do (
What is happening is that the version you are using sees the renamed-file as a new file. The dir version builds a list of the filenames and then executes the for on each line, so the list is already built and static and cmd isn't trying to operate on a moving target.
Also - use rem, not :: within a code-block (parenthesised sequence of instructions) as this form of comment is in fact a broken label and labels are not allowed in a code block.
Yes, this can happen, especially on FAT32 and exFAT drives because of these file systems do not return the list of directory entries matched by a wildcard pattern to calling executable in an alphabetic order. for processes the directory entries matching *.txt one after the other and the command ren results in changing the directory entries, i.e. the file names list is modified while iterating over it.
The solution is using:
for /F "eol=| delims=" %%I in ('dir *.txt /A-D /B 2^>nul') do ren "%%I" "Seekret file %%I"
FOR runs in this case in background %ComSpec% /c with the command line specified between ' which means with Windows installed into directory C:\Windows:
C:\Windows\System32\cmd.exe /C dir *.txt /A-D /B 2>nul
So one more command process is started in background which executes DIR which
searches in current directory
just for files because of option /A-D (attribute not directory)
including files with hidden attribute set (use /A-D-H to exclude hidden files)
matching the wildcard pattern *.txt
and outputs in bare format just the file names because of option /B.
An error message output by DIR to handle STDERR in case of not finding any directory entry matching these criteria is suppressed by redirecting it to device NUL.
Read the Microsoft article about Using Command Redirection Operators for an explanation of 2>nul. The redirection operator > must be escaped with caret character ^ on FOR command line to be interpreted as literal character when Windows command interpreter processes this command line before executing command FOR which executes the embedded dir command line with using a separate command process started in background.
The file names without path are output by DIR to handle STDOUT of background command process. This output is captured by FOR respectively the command process executing the batch file.
After started command process terminated itself, FOR processes the captured list of file names. All changes done on directory during the loop iterations do not matter anymore for that reason. The file names list does not change anymore.
The options eol=| delims= are needed to get the complete file names assigned one after the other to loop variable I even on starting with ; or containing a space character. eol=| redefines default end of line character ; to a vertical bar which no file name can contain. delims= defines an empty list of delimiters to disable default line splitting behavior on normal spaces and horizontal tabs.
Note: :: is an invalid label and not a comment. Labels inside a command block are not allowed and usually result in undefined behavior on execution of the command block. Use command REM (remark) for a comment.
Even better would be:
for /F "eol=| delims=" %%I in ('dir *.txt /A-D /B 2^>nul ^| %SystemRoot%\System32\findstr.exe /B /I /L /V /C:"Seekret file "') do ren "%%I" "Seekret file %%I"
FINDSTR is used here to output from list of file names output by DIR and redirected to STDIN of FINDSTR all file names which
do not because of /V (inverted result)
begin because of option /B
case-insensitive because of option /I
with the literally interpreted because of option /L (redundant to /C:)
string Seekret file .
Option /C: is needed to specify the search string containing two spaces as using just "Seekret file" would result in searching literally and case-insensitive for either Seekret OR file at begin of a line. In a search string specified with just "..." each space is interpreted by FINDSTR as an OR expression like | in a Perl regular expression string.
A search string specified with /C: is interpreted implicitly as literal string, but with using /R (instead of /L) it would be possible to get this string interpreted as regular expression string on which a space is interpreted as space and not as OR expression. It is possible to specify multiple search strings using multiple times /C:.
My recommendation on using FINDSTR: Use always either /L or /R to make it clear for FINDSTR and for every reader of the command line how FINDSTR should interpret the search string(s) specified with "..." or with /C:"...".
I guess I'll throw my hat in too, since I'm not really a fan of looping through dir output and no one else is currently accounting for this script already having been run:
#echo off
set "dir=C:\Your\Root\Directory"
set "pfx=Seekret file "
setlocal enabledelayedexpansion
for /r "%dir%" %%A in (*.txt) do (
set "txt=%%~nA"
if not "!txt:~0,13!"=="%pfx%" ren "%%A" "%pfx%%%~nxA"
)
pause
for /r will loop recursively through all .txt files, set each one as parameter %%A (per iteration), set a variable txt as parameter %%A reduced to just its name (%%~nA), and then it compares the first 13 characters of the text file to your example prefix (which is 13 characters long when you include the space: Seekret file) - if they match the loop does nothing; if they do not match, the loop will rename %%A to include the prefix at the beginning. If you don't want it to be recursive, you can use for %%A in ("%dir%"\*.txt) do ( instead. Other than that, you'll just change !txt:~0,13! depending on what your prefix is or how many letters into a filename you want to check. You also don't have to set your directory and prefix variables, I just prefer to do so because it makes the block look cleaner - and it's easier to go back and change one value as opposed to every place that value occurs in a script.
Reference: for /r, ren, variable substrings

For loop in batch file reading a file of File Paths

I want to write a Windows batch file script that will loop through a text file of FILE PATHS, do some work using data from each file path, then ultimately delete the file.
I started by running the FORFILES command and sending its output (the #PATH parameter is the full path of any file it matches) to a text file (results.txt).
I end up with a results.txt file like this:
"C:/Windows/Dir1/fileA.log"
"C:/Windows/Dir1/fileA.log"
"C:/Windows/Dir2/fileC.log"
"C:/Windows/Dir3/fileB.log"
What I want to do is:
Use a FOR loop and read each line in the results.txt file
For each line (file path), strip out the directory name that the log file is sitting in (ie: Dir1, Dir2, etc..) and create a directory with that SAME name in a different location (ie. D:/Archive/Backups/Dir1, D:/Archive/Backups/Dir2, etc..) -- assuming the directory doesn't exist.
Move the actual .log file to a zip file in that directory [I have code to do this].
Delete the .log file from its original location. [Pretty straightforward]
I'm having trouble figuring out the best way to accomplish the first 2 steps. My FOR loop seems to stop after reading the very first line:
FOR /F "tokens=1,2,3,4,5,6,7,8,9,10 delims=\" %%G in ("results.txt") DO (
...
)
You don't want to parse the path with the tokens/delims options because you don't know how many directory levels you are dealing with. You want to preserve each line in its entirety. TO parse the path you want to use the FOR variable modifiers. (type HELP FOR from the command line and look at the last section of the output)
%%~pG gives the path (without the drive or file name). If we then strip off the last \, we can go through another FOR iteration and get the name (and possible extension) of the file's directory by using %%~nxA.
The toggling of delayed expansion is just to protect against a possible ! in the path. If you know that no path contains ! then you can simply enable delayed expansion at the top of the script and be done with it.
EDIT - this code has been modified significantly since Aacini pointed out that I misread the requirements. It should satisfy the requirements now.
for /f "usebackq delims=" %%G in ("results.txt") do (
set "myPath=%~pG"
setlocal enableDelayedExpansion
for /f "eol=: delims=" %%A in ("!myPath:~0,-1!") do (
endlocal
if not exist d:\Archive\Backups\%%~nxA md d:\Archive\Backups\%%~nxA
rem ::zip %%G into zip file in the D: location
rem ::you should be able to create the zip with the move option
rem ::so you don't have to del the file
)
)
I wrote this to timestamp files before offloading to SFTP.
Hope you find it useful.
The timestamp coding may seem irrelevant to your issue, but I left it because it's a good example of dissecting the filename itself.
I suggest you put an ECHO in front of the REN command for testing. Different shells may have different results.
In the end, the delayedexpansion command wasn't necessary. It was the sub-routine that fixed my issues with variables inside the loop. That could possibly be because of my OS ver. (Win 8.1) - It wouldn't hurt to leave it.
#echo off
cls
setlocal enabledelayedexpansion
if %time:~0,2% geq 10 set TIMESTAMP=%date:~10,4%%date:~4,2%%date:~7,2%_%time:~0,2%%time:~3,2%%time:~6,2%
if %time:~0,2% leq 9 set TIMESTAMP=%date:~10,4%%date:~4,2%%date:~7,2%_0%time:~1,1%%time:~3,2%%time:~6,2%
echo TimeStamp=%TIMESTAMP%
echo.
for %%G in (*.txt) do (
set OLDNAME=%%G
call :MXYZPTLK
)
dir *.txt
goto :EOF
:MXYZPTLK
echo OldName=%OLDNAME%
ren %OLDNAME% %OLDNAME:~0,-4%_%TIMESTAMP%%OLDNAME:~-4,4%
echo.
:END
You have two minor problems:
The path separator in the file is '/' but you use '\' in the for loop.
The quotes around "results.txt" stop it working.
This works. Don't write quotes to results.txt and you won't get a quote at the end of the filename.
#echo off
FOR /F "tokens=3,4 delims=/" %%I in (results.txt) DO (
REM Directory
echo %%I
REM File
echo %%J
)

Resources