"Droplet" batch script - filenames containing ampersands - windows

I'm trying to create a batch file that can have other files dropped onto it. Specifically, I'm using ffmpeg to edit audio files produced by a handheld voice recorder. The problem is when using filenames with ampersands (&). Even when quoting the input, anything after the & is dropped off, but only when files are dropped onto it; if the filename input is typed on the command line, the script works fine. Before the cmd window closes, I briefly see the rest of the filename with an error saying it is not recognized as a valid command.
Here's my script:
rem Change to drive and directory of input file
%~d1
cd %~p1
rem ffmpeg: mix to one channel, double the volume
%HOMEDRIVE%%HOMEPATH%\ffmpeg.exe -i "%~nx1" -ac 1 -vol 1024 "%~n1 fixed%~x1"
pause
Here's what appears on the command line, after dropping "ch17&18.mp3":
C:\Users\computergeeksjw\Desktop>C:\Users\computergeeksjw\ffmpeg.exe -i "ch17" -ac 1 -vol 1024 "ch17 fixed"
[...]
ch17: No such file or directory
In case it matters: I'm using the Windows 8 Developer Preview. Is this causing my problem? Does the same error occur on Windows 7 or earlier?

There is a long-standing bug in Windows drag and drop functionality regarding file paths that contain & or ^ but don't contain a <space>.
If a file path contains at least one <space>, then Windows automatically encloses the path in quotes so that it gets parsed properly. Windows should do the same thing if the file path contains & or ^, but it does not.
If you create the following simple batch file and drag files onto it, you can see the problem.
#echo off
setlocal enableDelayedExpansion
echo cmd=!cmdcmdline!
echo %%1="%~1"
pause
exit
The !cmdcmdline! variable contains the actual command that launched the batch file.
The batch file prints out the command line and the first parameter.
If you drag and drop a file named "a.txt" you get
cmd=cmd /c ""C:\test\drag.bat" C:\test\a.txt"
%1=C:\test\a.txt
Press any key to continue . . .
If you disregard the quotes around the entire command you see that there are no quotes around the file argument. There are no special characters, so there is no problem.
Now drag and drop "a b.txt" and you get
cmd=cmd /c ""C:\test\drag.bat" "C:\test\a b.txt""
%1="C:\test\a b.txt"
Press any key to continue . . .
You can see how Windows detects the space in the name and encloses the file in quotes. Again there is no problem.
Now drag and drop "a&b.txt" and you get
cmd=cmd /c ""C:\test\drag.bat" C:\test\a&b.txt"
%1=C:\test\a
Press any key to continue . . .
Windows doesn't find a space in the name, so it does not enclose it in quotes. Big problem! Windows passes "C:\test\a" to the batch file and treats "b.txt" as a second file to be executed after the batch file completes. The hard EXIT command in the batch file prevents any split filenames from executing after the batch. Of course b.txt could never execute. But if the file were named "a&b.bat" and "b.bat" existed, then that could be trouble if the hard EXIT were not in the batch file.
It is possible to drag multiple files onto a batch file, and each one should be passed as a parameter.
The !cmdcmdline! is the only way to reliably access drag and drop arguments. But that will not work if files are passed as normal arguments in a normal call to the batch file.
Below is a batch file that can detect if it was called using drag and drop versus a normal call. (It is not bullet proof, but I think it should work in most situations) It will process each file argument, one at a time, regardless of the type of call. (The process simply echos the file name, but you can substitute whatever processing you want.) If the batch was called using drag and drop then it will do a hard exit to protect against split file names.
#echo off
setlocal disableDelayedExpansion
::
:: first assume normal call, get args from %*
set args=%*
set "dragDrop="
::
:: Now check if drag&drop situation by looking for %0 in !cmdcmdline!
:: if found then set drag&drop flag and get args from !cmdcmdline!
setlocal enableDelayedExpansion
set "cmd=!cmdcmdline!"
set "cmd2=!cmd:*%~f0=!"
if "!cmd2!" neq "!cmd!" (
set dragDrop=1
set "args=!cmd2:~0,-1! "
set "args=!args:* =!"
)
::
:: Process the args
for %%F in (!args!) do (
if "!!"=="" endlocal & set "dragDrop=%dragDrop%"
rem ------------------------------------------------
rem - Your file processing starts here.
rem - Each file will be processed one at a time
rem - The file path will be in %%F
rem -
echo Process file "%%~F"
rem -
rem - Your file processing ends here
rem -------------------------------------------------
)
::
:: If drag&drop then must do a hard exit to prevent unwanted execution
:: of any split drag&drop filename argument
if defined dragDrop (
pause
exit
)
It looks like your existing batch is only designed to handle one file. I can't tell if you need to make modifications to the calls to support multiple files. I modified the above batch to only process the first argument, and substituted your process into the argument processing loop. This is untested, but I think it should work for you.
#echo off
setlocal disableDelayedExpansion
::
:: first assume normal call, get args from %*
set args=%*
set "dragDrop="
::
:: Now check if drag&drop situation by looking for %0 in !cmdcmdline!
:: if found then set drag&drop flag and get args from !cmdcmdline!
setlocal enableDelayedExpansion
set "cmd=!cmdcmdline!"
set "cmd2=!cmd:*%~f0=!"
if "!cmd2!" neq "!cmd!" (
set dragDrop=1
set "args=!cmd2:~0,-1! "
set "args=!args:* =!"
)
::
:: Process the first argument only
for %%F in (!args!) do (
if "!!"=="" endlocal & set "dragDrop=%dragDrop%"
rem ------------------------------------------------
rem - Your file processing starts here.
rem - Use %%F wherever you would normally use %1
rem
rem Change to drive and directory of input file
%%~dF
cd %%~pF
rem ffmpeg: mix to one channel, double the volume
%HOMEDRIVE%%HOMEPATH%\ffmpeg.exe -i "%%~nxF" -ac 1 -vol 1024 "%%~nF fixed%%~xF"
rem
rem - Your file processing ends here
rem -------------------------------------------------
goto :continue
)
:continue
if defined dragDrop (
pause
exit
)

I admire dbenham's batch programming skills in silent awe.
I tried his solution and stumbled upon two problems that I present here as I don't have enough reputation to comment:
There seems to be an extra space in front of the last quotation mark on line 15 of his batch template. I suppose it should read:
set "args=!cmd2:~0,-1!"
Someone with not-so-stellar batch programming knowledge could have serious problems finding this, like me. I tried but was unable to edit dbenham's post because of the stupid "Edits must be at least 6 characters" limitation.
The solution is generally not suitable for files/folders containing , (comma) or ; (semicolon) in their full path.
It can be modified to work in case there is only one file/folder dropped onto a batch file by enclosing args in quotes on line 20:
for %%F in ("!args!") do (
When more than one file/folder is dropped onto a batch file, I am afraid no general workaround of the Windows bug is possible that could cope with comma/semicolon in file path. The SendTo mechanism of Windows obviously has the very same deficiency (bug), so can't be used to work around the drag-and-drop bug. It is thus up to Microsoft to finally fix this bug.

Related

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.

Trying to batch merge 2 .jpeg's horizontally and put them in a different folder after

this is for my doctoral thesis in medicine. So please excuse my noobishnis in programing.
I have a bunch (about 4000 files) of scans from patients. There is a front and a back .jpg for each patient. And there where multiple patients each day.
The folder structure looks like this:
\images
\2017-08-21
\pa_102165.jpg
\pa_10216500001.jpg
\2017-06-14
\pa_101545.jpg
\pa_10154500001.jpg
\pa_104761.jpg
\pa_10476100001.jpg
\pa_107514.jpg
\pa_10751400001.jpg
\2017-03-73
\pa_109631.jpg
\pa_10963100001.jpg
\pa_108624.jpg
\pa_10862400001.jpg
Where in the first example 2017-08-21 is the date the patient came in, pa_102165.jpg is the front and pa_10216500001.jpg is the back. So the front is always pa_10XXXX.jpg and the back is pa_10XXXX00001.jpg. I had no hand in the nameing scheme.
My goal is to make a batchscript that merges the 2 corresponding .jpgs of each patient horizontally and automatically puts them in a different folder, so that I don't have to do it manually with something like MS Paint.
For example like this:
\images
\merged
\2017-08-21
\pa_102165_merged.jpg
\2017-06-14
\pa_101545_merged.jpg
\pa_104761_merged.jpg
\pa_107514_merged.jpg
\2017-03-73
\pa_109631_merged.jpg
\pa_108624_merged.jpg
I'm working on Windows 10 and found two promising methods so far but fail to comprehend how to make this into a batch file or something like it.
IrfanView Thumbnails
1. Mark the 2 corresponding .jpgs
2. File>Create contact sheet from selected files...
3. Create
4. File>Save as... in destination folder which i have to create for every day
which is faster than merging them by hand but would consume multiple workdays to do for all the pairs
and...
ImageMagic in Windows cmd
C:\Users\me\doctor\Images\test\images\2016-03-31>convert pa_102165.jpg pa_10216500001.jpg +append pa_102165_merged.jpg
This produces the merged .jpeg in the same folder the input images are in. This looks more promising but I fail to grasp how I could automate this process given the nameing scheme and the folder structure.
Thanks for taking the time to read this! I'm happy for every input you have!
This should get you fairly close. Essentially it is using the power of the FOR command modifiers to extract the base file name and file extension. The FOR /F command is capturing the output of the DIR command that is piped to the FINDSTR command. We are doing that so we only grab files with the file mask of pa_######.jpg
Once we have that we use the command modifiers with the IF command to make sure the 00001 file exists. If it does exist then it will execute the convert command. For the sake of making sure the code is performing correctly I am just ECHOING the output to the screen. If the output on the screen looks correct then remove the ECHO so that the CONVERT command executes.
#echo off
CD /D "C:\Users\me\doctor\Images\test\images"
FOR /F "delims=" %%G IN ('DIR /A-D /B /S PA_*.jpg ^|findstr /RIC:"pa_[0-9][0-9][0-9][0-9][0-9][0-9]\.jpg$"') DO (
IF EXIST "%%~dpnG00001%%~xG" (
ECHO convert "%%G" "%%~dpnG00001%%~xG" +append "%%~dpnG_merged%%~xG"
)
)
This task could be done with IrfanView with the following batch file stored in the directory containing the folder images.
#echo off
setlocal EnableExtensions DisableDelayedExpansion
set "IrfanView=%ProgramFiles(x86)%\IrfanView\i_view32.exe"
set "SourcePath=%~dp0images"
set "TargetPath=%~dp0merged"
for /F "delims=" %%I in ('%SystemRoot%\System32\where.exe /R "%SourcePath%" pa_10????.jpg 2^>nul') do for %%J in ("%%~dpI.") do (
if not exist "%TargetPath%\%%~nxJ\%%~nI_merged%%~xI" if exist "%%~dpnI00001%%~xI" (
if not exist "%TargetPath%\%%~nxJ\" md "%TargetPath%\%%~nxJ"
if exist "%TargetPath%\%%~nxJ\" (
echo Merging "%%~nxJ\%%~nxI" and "%%~nxJ\%%~nI00001%%~xI" ...
"%IrfanView%" /convert="%TargetPath%\%%~nxJ\%%~nI_merged%%~xI" /jpgq=95 /panorama=(1,"%%I","%%~dpnI00001%%~xI"^)
)
)
)
endlocal
There must be customized the fully qualified file name of IrfanView in the third line. There can be modified also the percent value of option /jpgq which defines the quality of the output JPEG file.
The command WHERE searches recursive in subdirectory images of the directory containing the batch file for files matching the wildcard pattern pa_10????.jpg with ignoring all other files. The found file names are output with full path and this list of file names is captured by FOR and processed line by line after WHERE finished. WHERE is executed in this case by one more cmd.exe started in background with option /c and the command line within ' as additional arguments and not by cmd.exe processing the batch file.
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 where command line with using a separate command process started in background.
Each image file with full name (drive + path + name + extension) is assigned one after the other to the loop variable I. For each file name one more FOR loop is used which processes just the full path to the current image file to assign this path with a dot appended to loop variable J. The dot at end means current directory, i.e. the directory containing current image file to process.
There is next checked with the first IF condition if for that image file does not exist already a matching pa_10????_merged.jpg file in which case there is nothing to do for the current image file. That means the batch file can be executed on same folder as often as wanted because of it runs IrfanView only for the source JPEG files for which the appropriate target JPEG file does not exist already.
The second IF condition checks if the back image exists also in the directory of current front image as otherwise nothing can be merged at all.
There is next checked with the third IF condition if the target directory exists already and this directory is created if that is not the case.
The last IF condition checks once again the existence of the target directory and if that exists now as expected, IrfanView is called with the appropriate options to create the merged image file in the target directory with the appropriate file name.
The closing round bracket ) on IrfanView command line must be escaped with ^ to be interpreted literally by cmd.exe to pass this closing parenthesis to IrfanView instead of interpreting it as end of one of the command blocks opened with ( above.
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 /? ... explains %~dp0 ... drive and path of argument 0 which is the batch file path always ending with a backslash
echo /?
endlocal /?
for /?
if /?
md /?
setlocal /?
where /?
Double click on the text file i_options.txt in program files folder of IrfanView for the description of the IrfanView options as used in the batch file.

Run Batch Script Across Subfolders (Recursively)

I regularly have to rename hundreds of files across a subfolder structure. I have been creating a batch file consisting of all my rename commands, and manually pasting this into each subfolder to execute one subfolder at a time. I'd like to revise the batch script so that it executes against all subfolders in one fell swoop, run from the parent directory just once.
My renaming is very manual, and so I need to create a discrete entry for each file. For example, here are three lines:
REN STWP01_00669087* BCBSRI-01849351*
REN BCBSRI-01849357* 2011-12-19_BCBSRI-01849357*
REN STWP01_00669094* BCBSRI-01849369*
I've experimented with the FOR /R command, including trying a separate batch file that calls my renaming batch file (via the CALL command). No luck.
I have to assume that this is simple, but I'm a batch novice, so any help would be appreciated.
Thanks!
#Magoo,
Thanks so much for your response. Your approach is going to be far more efficient than my own so far.
A couple of questions. Please bear with me as I am a total novice with batch commands.
Here's what I did: I saved your code to a .BAT file ("RRename.bat"), modified my filenames as per your instructions and saved those to a text file ("Filenames.txt"), and then run this command from the command line: {RRename.bat Filenames.txt}.
The resulting command windows confirm correct renaming. And so I removed the ECHO and PAUSE commands and re-ran. No luck. Just a bunch of Command windows confirming the directory.
Ideally I'd love to save this as a .BAT file and simply drop this in the top-level directory, together with the data file that contains the old names and new names of the files. And so, a double-click of "RRename.bat" will parse the content of "Filenames.txt" and work its way through all subfolders, renaming wherever matches are encountered. Boom.
To that end:
1. How do I make it so {SET "sourcedir=} indicates the current directory (i.e. the directory in which the batch file is located)? This way I wouldn't ever need to change this variable. (I should note that I am running this script on a network location, which requires me to map the drive, resulting in a different drive letter every time.)
2. How do I hard-code the name of the data file into the script itself? My goal is an easily replicated process minimizing user input (save for the content of the data file).
3. How do I stop the individual command windows from appearing? I'll be renaming thousands of files at a time and don't want to see thousands fo corresponding command windows.
Thank you!
#ECHO OFF
SETLOCAL
SET "sourcedir=U:\sourcedir"
:: read parameters
SET "filename1=%~1"
SET "filename2=%~2"
IF DEFINED filename2 GOTO name
IF NOT DEFINED filename1 GOTO :EOF
:: 1 parameter - must be filename
FOR /f "usebackqdelims=" %%a IN ("%filename1%") DO START /min "ren %%a" "%~f0" %%a
GOTO :eof
:: we have 2 parameters so rename pattern 1 to pattern 2
:name
FOR /r "%sourcedir%" %%a IN ("%filename1%*") DO CALL :process "%%a"
PAUSE
GOTO :EOF
:: Process the filenames and actually do the rename
:process
:: Name of file to be changed - name and extension of %1
SET "changeme=%~nx1"
:: REPLACE up-to-from-pattern with nothing = remainder of name/extension
CALL SET "endpart=%%changeme:*%filename1%=%%"
:: and RENAME...
ECHO(REN "%~1" "%filename2%%endpart%"
GOTO :eof
You would need to change the setting of sourcedir to suit your circumstances.
Revised data file
STWP01_00669087 BCBSRI-01849351
BCBSRI-01849357 2011-12-19_BCBSRI-01849357
STWP01_00669094 BCBSRI-01849369
Aimed at processing the above file, renaming files starting (column1 entries) to start (column2 entries.)
Method:
Run the batch as
batchname filename
This will execute the batch, processing filename
How:
having set the directory name to start processing from, set filename1&2 to the values of the parameters supplied.
If only 1 is supplied, it is the filename, so process it line-by-line and START a new process /min minimised "with the window name in the first set of quotes" and execute this same batch with the data from each line of the file in turn, then finish by going to :eof (end-of-file - built-in to CMD)
The sub-processes all have 2 parameters (eg BCBSRI-01849357 2011-12-19_BCBSRI-01849357) so processing passes to :name. This runs a for /r loop, from the specified source directory, with the name specified from the first column+* and executes :process passing the filenames found as parameter 1.
:process sets changeme to the filename in question, calculates endpart by removing the string filename1 from changeme which will deliver the er, end part.
Then simply rename the supplied filename to the replacement name+that endpart calculated.
The required REN commands are merely ECHOed for testing purposes. After you've verified that the commands are correct, change ECHO(REN to REN to actually rename the files.
The PAUSE is just to allow the proposed changes to be seen. Once the process has been verified, change the PAUSE to EXIT.
AAMOI, running
*batchname* STWP01_00669094 BCBSRI-01849369
for instance, would execute the recursive-rename from STWP01_00669094* to BCBSRI-01849369*
Sadly, "No luck" is meaningless.
I have made a minor, but significant change to the instructions. The PAUSE should be changed to an EXIT after testing.
After testing, the ECHO(... line should become
REN "%~1" "%filename2%%endpart%"
which actually executes the rename. If you've just deleted the line, it would explain the no-visible-result.
Having restored the original code and verified against a small representative dummy subtree, change the echo(... line and test again. The filenames should change. If not, something is dreadfully wrong. Needless to say, this works perfectly happily for me...
Then try again with the PAUSE changed to EXIT. This time, the windows generated will appear on the taskbar and then disappear when the rename for that line of the input file has finished. This will happen once for BCBSRI-01849357 rentwo for instance - not once for each individual file rename occurring.
To hard-code the filename, remove the line
IF NOT DEFINED filename1 GOTO :EOF
and replace
FOR /f "usebackqdelims=" %%a IN ("%filename1%") DO START /min "ren %%a" "%~f0" %%a
with
FOR /f "usebackqdelims=" %%a IN ("YOURFILENAMEHERE") DO START /min "ren %%a" "%~f0" %%a
For the "run from here" command, change
SET "sourcedir=U:\sourcedir"
to
SET "sourcedir=."
. means "the current directory"
If you place thisbatchfilename.bat into any directory on your PATH then you can run the routine simply by executing thisbatchfilename.
You can display your path by typing
path
at the prompt. PATH is the sequence of directories searched by windows to find an executable if it isn't found in the current directory. To chane path, google "change path windows" - experienced batchers create a separate directory on the path for batch files. Sometimes, they name the directory "Belfry".

How to Increment a FileName in batch? (Part2)

I already tried the answer given by Mofi in my last question regarding this topic. But I changed the base name and it does not seem to work by now. If you want to see the previous question:How do I increment a filename in batch? What is wrong with this new code? It does not make a new file it just overwrites the previous made file.
:MainProcessNew
cd /D "%USERPROFILE%\Desktop"
for /F %%G in (*.json) do (
set "FileName=%%G"
set "BaseName=Device"
set "FileNumber=0"
)
:FileNameLoop
set /A FileNumber+=1
if exist "%BaseName%%FileNumber%.json" (
goto FileNameLoop
)
echo.>"%USERPROFILE%\Desktop\%BaseName%%FileNumber%.json"
I'm quite sure the batch code in question is not complete or reduced to a script code not really suitable to reproduce the problem because with Device.json existing in folder Desktop and no other Device*.json file existing, the empty line is first written to Device1.json. A file with name Device.json is never overwritten by the batch code in question because variable FileNumber has always at least the value 1.
Well, the FOR option /F is most likely wrong here as I suppose the FOR loop should search for *.json files as done without /F. Using a wildcard pattern like *.json together with option/F results in error message:
The system cannot find the file *.json.
Run in a command prompt window for /? or help for for help on syntax of this command.
It is completely unclear what is the purpose of the FOR loop because the FileName variable is not used at all. This variable should perhaps hold the name of last found *.json if there was any *.json file found at all. But that also does not make sense if not further used anywhere.
It is also unclear why BaseName and FileNumber are defined inside the loop and not outside.
In the complete batch code the label FileNameLoop is perhaps the beginning of a subroutine. But in the reduced batch code in question there is no call :FileNameLoop "%%G" which I would expect in this case.
So the question is hard to answer as it is unclear what is really the problem with posted batch code.
:MainProcessNew
cd /D "%USERPROFILE%\Desktop"
rem Useless FOR loop without /F commented out.
rem for %%G in (*.json) do set "FileName=%%G"
set "BaseName=Device"
set "FileNumber="
rem Skip searching for files with a file number
rem after Device if there is no Device.json file.
if not exist "%BaseName%.json" goto CreateFile
rem Otherwise keep Device.json as is and search for Device1.json,
rem Device2.json, ... until a Device*.json file with current number
rem is not found and use this number for next Device*.json file.
set "FileNumber=0"
:FileNameLoop
set /A FileNumber+=1
if exist "%BaseName%%FileNumber%.json" goto FileNameLoop
:CreateFile
rem Note: FileNumber is replaced in the line below by an empty
rem string if there is no Device.json in desktop folder.
echo.>"%USERPROFILE%\Desktop\%BaseName%%FileNumber%.json"
Hint: For debugging a batch file
comment out or remove all #echo off or echo off in the batch file or change off to on,
open a command prompt window,
enter "Path to batch file\BachFileName.bat" and press RETURN.
Now it can be seen in the command prompt window each line executed by Windows command processor after preprocessing each line and each command block which means after replacing all %VariableName% by the current value of the variable in current line or entire command block.
And error messages can be also seen as the command prompt window remains open after processing batch file stopped, except it contains the command exit without option /B which always terminates the current command process.

Trouble with renaming folders and sub folders using Batch

So I'm trying to set up a template file structure for projects that can be modified in name to suit each project. I have created the example directory containing example folders i.e. "Template project" contains "template hardware" , "template software" etc. , and have a simple batch program that copies the "template project" folder and all contained subfolders, however I would like to change the word 'template' with what ever I choose to call the project. I was wondering if this is possible to do? Ideally I could just edit the batch file with the name of the project and then run it to copy the template and rename it.
Any help is greatly appreciated!
To start learning type help at the command prompt. Then anything on that list add /? for more help.
Set /p NewName=Enter project name
md "c:\somewhere\%newname%project\%newname% software
md "c:\somewhere\%newname%project\%newname% hardware
or use xcopy (and use/l to have it do a test without copying)
xcopy "c:\tempate" "d:\%newname%" /e /h /q /i /c
See set /?, md /?, and xcopy /?. Type just set to see a list of variables.
& seperates commands on a line.
&& executes this command only if previous command's errorlevel is 0.
|| (not used above) executes this command only if previous command's errorlevel is NOT 0
> output to a file
>> append output to a file
< input from a file
| output of one command into the input of another command
^ escapes any of the above, including itself, if needed to be passed to a program
" parameters with spaces must be enclosed in quotes
+ used with copy to concatinate files. E.G. copy file1+file2 newfile
, used with copy to indicate missing parameters. This updates the files modified date. E.G. copy /b file1,,
%variablename% a inbuilt or user set environmental variable
!variablename! a user set environmental variable expanded at execution time, turned with SelLocal EnableDelayedExpansion command
%<number> (%1) the nth command line parameter passed to a batch file. %0 is the batchfile's name.
%* (%*) the entire command line.
%<a letter> or %%<a letter> (%A or %%A) the variable in a for loop. Single % sign at command prompt and double % sign in a batch file.
\\ (\\servername\sharename\folder\file.ext) access files and folders via UNC naming.
: (win.ini:streamname) accesses an alternative steam. Also separates drive from rest of path.
. (win.ini) the LAST dot in a file path seperates the name from extension
. (dir .\*.txt) the current directory
.. (cd ..) the parent directory
\\?\ (\\?\c:\windows\win.ini) When a file path is prefixed with \\?\ filename checks are turned off.
< > : " / \ | Reserved characters. May not be used in filenames.
Reserved names. These refer to devices eg,
copy filename con
which copies a file to the console window.
CON, PRN, AUX, NUL, COM1, COM2, COM3, COM4,
COM5, COM6, COM7, COM8, COM9, LPT1, LPT2,
LPT3, LPT4, LPT5, LPT6, LPT7, LPT8, and LPT9
CONIN$, CONOUT$, CONERR$
Maximum path length 260 characters
Maximum path length (\\?\) 32,767 characters (approx - some rare characters use 2 characters of storage)
Maximum filename length 255 characters
Starting a Program
===============
See start /? and call /? for help on all three ways.
Specify a program name
--------------------------------
c:\windows\notepad.exe
In a batch file the batch will wait for the program to exit. When
typed the command prompt does not wait for graphical
programs to exit.
If the program is a batch file control is transferred and the rest of the calling batch file is not executed.
Use Start command
--------------------------
start "" c:\windows\notepad.exe
Start starts a program and does not wait. Console programs start in a new window. Using the /b switch forces console programs into the same window, which negates the main purpose of Start.
Start uses the Windows graphical shell - same as typing in WinKey + R (Run dialog). Try
start shell:cache
Use Call command
-------------------------
Call is used to start batch files and wait for them to exit and continue the current batch file.
.
--

Resources