Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"Droplet" batch script - filenames containing ampersands

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?

like image 570
stephenwade Avatar asked Dec 17 '11 20:12

stephenwade


2 Answers

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
)
like image 79
dbenham Avatar answered Oct 13 '22 19:10

dbenham


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:

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

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

like image 45
Weaver Avatar answered Oct 13 '22 18:10

Weaver