Drag and drop batch file for multiple files?

I wrote a batch file to use PngCrush to optimize a .png image when I drag and drop it onto the batch file.

In the what's next section, I wrote about what I thought would be a good upgrade to the batch file.

My question is: is it possible to create a batch file like I did in the post, but capable of optimizing multiple images at once? Drag and drop multiple .png files on it? (and have the output be something like new.png, new(1).png, new(2).png, etc...


Yes, of course this is possible. When dragging multiple files on a batch file you get the list of dropped files as a space-separated list. You can verify this with the simple following batch:

@echo %*
@pause

Now you have two options:

  1. PngCrush can already handle multiple file names given to it on the command line. In this case all you'd have to do would be to pass %* to PngCrush instead of just %1 (as you probably do now):

    @pngcrush %*
    

    %* contains all arguments to the batch file, so this is a convenient way to pass all arguments to another program. Careful with files named like PngCrush options, though. UNIX geeks will know that problem :-)

    After reading your post describing your technique, however, this won't work properly as you are writing the compressed file to new.png. A bad idea if you're handling multiple files at once as there can be only one new.png :-). But I just tried out that PngCrush handles multiple files just well, so if you don't mind an in-place update of the files then putting

    @pngcrush -reduce -brute %*
    

    into your batch will do the job (following your original article).

  2. PngCrush will not handle multiple files or you want to write each image to a new file after compression. In this case you stick with your "one file at a time" routine but you loop over the input arguments. In this case, it's easiest to just build a little loop and shift the arguments each time you process one:

    @echo off
    if [%1]==[] goto :eof
    :loop
    pngcrush -reduce -brute %1 "%~dpn1_new%~x1"
    shift
    if not [%1]==[] goto loop
    

    What we're doing here is simple: First we skip the entire batch if it is run without arguments, then we define a label to jump to: loop. Inside we simply run PngCrush on the first argument, giving the compressed file a new name. You may want to read up on the path dissection syntax I used here in help call. Basically what I'm doing here is name the file exactly as before; I just stick "_new" to the end of the file name (before the extension). %~dpn1 expands to drive, path and file name (without extension), while %~x1 expands to the extension, including the dot.

    ETA: Eep, I just read your desired output with new.png, new(1).png, etc. In this case we don't need any fancy path dissections but we have other problems to care about.

    The easiest way would probably be to just start a counter at 0 before we process the first file and increment it each time we process another one:

    @echo off
    if [%1]==[] goto :eof
    set n=0
    :loop
    if %n%==0 (
        pngcrush -reduce -brute %1 new.png
    ) else (
        pngcrush -reduce -brute %1 new^(%n%^).png
    )
    shift
    set /a n+=1
    if not [%1]==[] goto loop
    

    %n% is our counter here and we handle the case where n is 0 by writing the result to new.png, instead of new(0).png.

    This approach has problems, though. If there are already files named new.png or new(x).png then you will probably clobber them. Not nice. So we have to do something different and check whether we can actually use the file names:

    rem check for new.png
    if exist new.png (set n=1) else (set n=0 & goto loop)
    rem check for numbered new(x).png
    :checkloop
    if not exist new^(%n%^).png goto loop
    set /a n+=1
    goto checkloop
    

    The rest of the program stays the same, including the normal loop. But now we start at the first unused file name and avoid overwriting files that are already there.

Feel free to adapt as needed.


To do Drag & Drop in a secure way, isn't so simple with batch.

Dealing with %1, shift or %* could fail, because the explorer is not very smart, while quoting the filenames, only filenames with spaces are quoted.
But files like Cool&stuff.png are not quoted by the explorer so you get a cmdline like

pngCr.bat Cool&stuff.png

So in %1 is only Cool even in %* is only Cool, but after the batch ends, cmd.exe tries to execute a stuff.png (and will fail).

To handle this you could access the parameters with !cmdcmdline! instead of %1 .. %n, and to bypass a potential error at the end of execution, a simple exit could help.

@echo off
setlocal ENABLEDELAYEDEXPANSION
rem Take the cmd-line, remove all until the first parameter
set "params=!cmdcmdline:~0,-1!"
set "params=!params:*" =!"
set count=0

rem Split the parameters on spaces but respect the quotes
for %%G IN (!params!) do (
  set /a count+=1
  set "item_!count!=%%~G"
  rem echo !count! %%~G
)

rem list the parameters
for /L %%n in (1,1,!count!) DO (
  echo %%n #!item_%%n!#
)
pause

REM ** The exit is important, so the cmd.ex doesn't try to execute commands after ampersands
exit

Btw. there is a line limit for drag&drop operations of ~2048 characters, in spite of the "standard" batch line limit of ~8192 characters.
As for each file the complete path is passed, this limit can be reached with few files.


FOR %%A IN (%*) DO (
    REM Now your batch file handles %%A instead of %1
    REM No need to use SHIFT anymore.
    ECHO %%A
)

And to differentiate between dropped files and folders, you can use this:

FOR %%I IN (%*) DO (
    ECHO.%%~aI | FIND "d" >NUL
    IF ERRORLEVEL 1 (
        REM Processing Dropped Files
        CALL :_jobF "%%~fI"
    ) ELSE (
        REM Processing Dropped Folders
        CALL :_jobD "%%~fI"
    )
)

This is a very late answer, Actually I was not aware of this old question and prepared an answer for this similar one where there was a discussion about handling file names with special characters because explorer only quotes file names that contain space(s). Then in the comments on that question I saw a reference to this thread, after that and not to my sureprise I realized that jeb have already covered and explained this matter very well, which is expected of him.

So without any further explanations I will contribute my solution with the main focus to cover more special cases in file names with this ,;!^ characters and also to provide a mechanism to guess if the batch file is directly launched by explorer or not, so the old fashion logic for handling batch file arguments could be used in all cases.

@echo off
setlocal DisableDelayedExpansion

if "%~1" EQU "/DontCheckDrapDrop" (
    shift
) else (
    call :IsDragDrop && (
        call "%~f0" /DontCheckDrapDrop %%@*%%
        exit
    )    
)

:: Process batch file arguments as you normally do 
setlocal EnableDelayedExpansion
echo cmdcmdline=!cmdcmdline!
endlocal

echo,
echo %%*=%*
echo,
if defined @* echo @*=%@*%
echo,
echo %%1="%~1"
echo %%2="%~2"
echo %%3="%~3"
echo %%4="%~4"
echo %%5="%~5"
echo %%6="%~6"
echo %%7="%~7"
echo %%8="%~8"
echo %%9="%~9"
pause
exit /b

:: IsDragDrop routine
:: Checks if the batch file is directly lanched through Windows Explorer
:: then Processes batch file arguments which are passed by Drag'n'Drop,
:: rebuilds a safe variant of the arguments list suitable to be passed and processed
:: in a batch script and returns the processed args in the environment variable
:: that is specified by the caller or uses @* as default variable if non is specified.
:: ErrorLevel: 0 - If launched through explorer. 1 - Otherwise (Will not parse arguments)
:IsDragDrop [retVar=@*]
setlocal
set "Esc="
set "ParentDelayIsOff=!"

setlocal DisableDelayedExpansion
if "%~1"=="" (set "ret=@*") else set "ret=%~1"
set "Args="
set "qsub=?"

:: Used for emphasis purposes
set "SPACE= "

setlocal EnableDelayedExpansion
set "cmdline=!cmdcmdline!"
set ^"ExplorerCheck=!cmdline:%SystemRoot%\system32\cmd.exe /c ^""%~f0"=!^"
if "!cmdline!"=="!ExplorerCheck!" (
    set ^"ExplorerCheck=!cmdline:"%SystemRoot%\system32\cmd.exe" /c ^""%~f0"=!^"
    if "!cmdline!"=="!ExplorerCheck!" exit /b 1
)
set "ExplorerCheck="
set ^"cmdline=!cmdline:*"%~f0"=!^"
set "cmdline=!cmdline:~0,-1!"
if defined cmdline (
    if not defined ParentDelayIsOff (
        if "!cmdline!" NEQ "!cmdline:*!=!" set "Esc=1"
    )
    set ^"cmdline=!cmdline:"=%qsub%!"
)
(
    endlocal & set "Esc=%Esc%"
    for /F "tokens=*" %%A in ("%SPACE% %cmdline%") do (
        set "cmdline=%%A"
    )
)
if not defined cmdline endlocal & endlocal & set "%ret%=" & exit /b 0

:IsDragDrop.ParseArgs
if "%cmdline:~0,1%"=="%qsub%" (set "dlm=%qsub%") else set "dlm= "
:: Using '%%?' as FOR /F variable to not mess with the file names that contain '%'
for /F "delims=%dlm%" %%? in ("%cmdline%") do (
    set ^"Args=%Args% "%%?"^"
    setlocal EnableDelayedExpansion
    set "cmdline=!cmdline:*%dlm: =%%%?%dlm: =%=!"
)
(
    endlocal
    for /F "tokens=*" %%A in ("%SPACE% %cmdline%") do (
        set "cmdline=%%A"
    )
)
if defined cmdline goto :IsDragDrop.ParseArgs

if defined Esc (
    set ^"Args=%Args:^=^^%^"
)
if defined Esc (
    set ^"Args=%Args:!=^!%^"
)
(
    endlocal & endlocal
    set ^"%ret%=%Args%^"
    exit /b 0
)

OUTPUT with sample files dragged and dropped onto the batch file:

cmdcmdline=C:\Windows\system32\cmd.exe /c ""Q:\DragDrop\DragDrop.cmd" Q:\DragDrop\ab.txt "Q:\DragDrop\c d.txt" Q:\DragDrop\!ab!c.txt "Q:\DragDrop\a b.txt" Q:\DragDrop\a!b.txt Q:\DragDrop\a&b.txt Q:\DragDrop\a(b&^)).txt Q:\DragDrop\a,b;c!d&e^f!!.txt Q:\DragDrop\a;b.txt"

%*=/DontCheckDrapDrop  "Q:\DragDrop\ab.txt" "Q:\DragDrop\c d.txt" "Q:\DragDrop\!ab!c.txt" "Q:\DragDrop\a b.txt" "Q:\DragDrop\a!b.txt" "Q:\DragDrop\a&b.txt" "Q:\DragDrop\a(b&^)).txt" "Q:\DragDrop\a,b;c!d&e^f!!.txt" "Q:\DragDrop\a;b.txt"

@*= "Q:\DragDrop\ab.txt" "Q:\DragDrop\c d.txt" "Q:\DragDrop\!ab!c.txt" "Q:\DragDrop\a b.txt" "Q:\DragDrop\a!b.txt" "Q:\DragDrop\a&b.txt" "Q:\DragDrop\a(b&^)).txt" "Q:\DragDrop\a,b;c!d&e^f!!.txt" "Q:\DragDrop\a;b.txt"

%1="Q:\DragDrop\ab.txt"
%2="Q:\DragDrop\c d.txt"
%3="Q:\DragDrop\!ab!c.txt"
%4="Q:\DragDrop\a b.txt"
%5="Q:\DragDrop\a!b.txt"
%6="Q:\DragDrop\a&b.txt"
%7="Q:\DragDrop\a(b&^)).txt"
%8="Q:\DragDrop\a,b;c!d&e^f!!.txt"
%9="Q:\DragDrop\a;b.txt"

In :IsDragDrop routine I specially tried to minimize the assumptions about command line format and spacing between the arguments. The detection (guess) for explorer launch is based on this command line signature %SystemRoot%\system32\cmd.exe /c ""FullPathToBatchFile" Arguments"

So it is very possible to fool the code into thinking it has launched by double click from explorer or by drag'n'drop and that's not an issue and the batch file will function normally.

But with this particular signature it is not possible to intentionally launch batch file this way: %SystemRoot%\system32\cmd.exe /c ""FullPathToBatchFile" Arguments & SomeOtherCommand" and expect that the SomeOtherCommand to be executed, instead it will be merged into the batch file arguments.