“Droplet” batch script – filenames containing ampersands

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
)

Leave a Comment