3

Consider these filenames.

; f - Copy.txt
;f.txt
f - Copy ;- Copy.txt
f - Copy.txt

I've got this code which resolves the & symbol in any files with this answer: Drag and drop batch file for multiple files?

@echo off
title %~nx0
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
)
for /l %%c in (1,1,!count!) DO (
for /f "delims=;" %%A in ("!item_%%c!") do (
  echo path: %%~dpA
  echo file: %%~A
  )
)
pause
rem ** The exit is important, so the cmd.exe doesn't try to execute commands after ampersands
exit

If I drag the files to the batch file it uses ; as the delimiter resulting in undesired results.

To get around this with the for /f you can do the following below but I'm at a loss as to how to incorporate that fix into the drag and drop code above.


There's an issue with the for /f whereby it uses ; as the delimiter also but this can be resolved with (This trick can be found here) for /f tokens^=*^ delims^=^ eol^=^¬ %%A in ('dir "%cd%" /a-d /b') do ( echo %%~A )

Which results in:

; f - Copy.txt
;f.txt
f - Copy ;- Copy.txt
f - Copy.txt

as opposed to: for /f "tokens=* delims=" %%A in ('dir "%cd%" /a-d /b') do ( echo %%~A )

Which results in:

f - Copy ;- Copy.txt
f - Copy.txt

How can I fix this issue with the drag and drop code?

Edit: I'm getting kinda close with this.

@echo off
setlocal enabledelayedexpansion
for /f tokens^=*^ delims^=^ ^ eol^=^¬ %%A in (""!%*!"") do ( echo "%%~fA" )

Edit: (more examples) Another example that can extend the dropped items to any those at this link: Extended Filename syntax to %* with the following workaround. This also works in a half-hearted sort of way.

setlocal enabledelayedexpansion
for %%A in (%*) do ( call :FIX "%%~A" )
pause
:FIX
SET _params=!%*!
CALL :sub "%_params%"
GOTO :eof
:: Now display just the filename and extension (not path)
:sub
ECHO "%~nx1"
:: All of these returns are when each file (one at a time) is dragged to the batch file.
rem folder name , goes = here...        works      returns "folder name , goes = here..."
rem ; f - copy.txt                      works      returns "; f - copy.txt"
rem ;f.txt                              doesn't    returns "." ""
rem a.txt                               works      return  "a.txt"
rem b.txt                               works      return  "b.txt"
rem c.txt                               works      return  "c.txt"
rem cool&stuff.png                      doesn't    returns "Cool""
rem copy = copy.txt                     works      returns "copy = copy.txt"
rem f - copy - copy.txt                 works      returns "f - copy - copy.txt"
rem f - copy ,- copy - copy.txt         works      returns "f - copy ,- copy - copy.txt"
rem f - copy ;- copy.txt                works      returns "f - copy ;- copy.txt"
rem f - copy.txt                        works      returns "f - copy.txt "
rem f - copy1.txt                       works      returns "f - copy1.txt"
rem FILENAME WITH & IN IT!! - COPY.TXT  doesn't    returns "FILENAME WITH & IN IT - COPY.TXT""
rem FILENAME WITH & IN IT!!.TXT         doesn't    returns "FILENAME WITH & IN IT.TXT""
Ste
  • 1,729
  • 1
  • 17
  • 27
  • 1
    The problem is not only the `;`, it's any delimiter `,;=`, because the windows `drag&drop` doesn't enclose them into quotes. You can't solve it with a single `FOR /F` loop. But it can be solved with a bit replace magic – jeb Jan 27 '20 at 13:46

3 Answers3

3

I've found a thread here that addresses this issue and as far as I can see, works with any special characters.

Big thanks to user aGerman on that forum... All credit goes to him.

Link to that thread:

Solved How to escape special characters on InputFile with Drag and Drop ?

Other useful links:

Simplest loop to perform actions on x dragged files?

'Pretty print' windows %PATH% variable - how to split on ';' in CMD shell

Drag and drop batch file for multiple files?

@echo off &setlocal DisableDelayedExpansion

setlocal EnableDelayedExpansion
set "params=!cmdcmdline:~,-1!"
set "params=!params:*" =!"

if "!params!"=="" exit

endlocal&set "params=%params:"=""%"
set "params=%params:^=^^%"
set "params=%params:&=^&%"
set "params=%params: =^ ^ %"
set params=%params:""="%
set "params=%params:"=""Q%"
set "params=%params:  ="S"S%"
set "params=%params:^ ^ = %"
set "params=%params:""="%"

setlocal EnableDelayedExpansion
set "params=!params:"Q=!"

for %%i in ("!params:"S"S=" "!") do (
  if "!!"=="" endlocal
  REM only files ...
  if not exist "%%~i\" (
  set "file_folder=%%~i"
  call :proc
  )
  )

echo ready.
pause
exit

:proc
echo process "%file_folder%" here ...
goto :eof
Ste
  • 1,729
  • 1
  • 17
  • 27
  • Impressive. The pretty print technique was exactly the *replace magic* I meant :-) – jeb Jan 27 '20 at 19:11
  • All credit to user **aGerman** on the dostips forum. Yes, there's also a way to use [freecommander](https://freecommander.com/en/summary/) the explorer to work for all cases. Even `C:\nospacepath\folder\peter&(mary).txt` which failed here but works in that. I was too stubborn to admit defeat with this and have spent far too long on it. The FC method just needs `%ActivSel%` to be passed to the batch file as param `%1` and then wrap each with quotes enabled in the Define favourite toolbars settings. – Ste Jan 27 '20 at 21:02
  • 1
    `a&(b).txt` is problematic, because it`s parsed to `a & (b).txt`, the `a &` part is okay, but the `(b).txt` creates a syntax error, as the `.txt` is directly appended to a command block. And the syntax error is detected before the batch file even starts. It works if you drag a second file *before* with name `d5_& REM #` – jeb Jan 28 '20 at 08:19
  • 1
    But even the `A&()` problematic can be solved with a small hack. – jeb Jan 28 '20 at 08:29
  • It would be interesting if that just takes another line or two to fix. I don't think there's any point of creating a new thread. If you have a solution, I'd love to hear it. As this would solve the problem completely. – Ste Jan 28 '20 at 11:22
  • 1
    Sorry, it's a nasty hack, not one or two lines (but still only 300 lines). I add [my answer](https://stackoverflow.com/a/59974834/463115) – jeb Jan 29 '20 at 20:11
1

As jeb noted, the main problem comes from a file such as ;f.txt not being enclosed in double-quotes. Ste's answer is probably better, since it doesn't involve "eating" chunks of params, but as I was most of the way through revising my original attempt when that was posted, I decided to finish it and offer it as another approach.

@echo off
title %~nx0
setlocal enabledelayedexpansion
rem Take the cmd-line, remove all until the first parameter
set "params=!cmdcmdline:~0,-1!"
set "params=::!params:*" =!"
set count=0
:loop
  for /f usebackq^ tokens^=1^ delims^=^" %%A in ('!params:~2!') do (
    REM The second test below tests if "params" started with a double-quote.
    REM Taken from https://ss64.org/viewtopic.php?pid=850#p850
    if "%%A" == " " (
      set "params=::!params:~3!"
    ) else if [!params:~2^,1!]==[^"] (
      call :proc "%%A"
      set "params=::!params:::"%%A"=!"
    ) else (
      set "spaces=%%A"
      set "spaces=^"!spaces: =^" ^"!^""
      for %%B in (!spaces!) do if not "%%~B" == "" (
        call :proc "%%~B"
      )
      set "params=::!params:::%%A=!"
    )
  )
  if not "!params!" == "::" goto :loop
  pause
  exit

:proc
    echo process "%~1" here ...
    goto :eof

Dropping the above four files (plus f ; & ; f.txt and a few others for good measure) into this script produces:

process "C:\X\Dev\Semicolons\q" here ...
process "C:\X\Dev\Semicolons\q q q" here ...
process "C:\X\Dev\Semicolons\; f - Copy.txt" here ...
process "C:\X\Dev\Semicolons\;f.txt" here ...
process "C:\X\Dev\Semicolons\f - Copy ;- Copy.txt" here ...
process "C:\X\Dev\Semicolons\f - Copy.txt" here ...
process "C:\X\Dev\Semicolons\f ; & ; f.txt" here ...
process "C:\X\Dev\Semicolons\f.safe.txt" here ...
process "C:\X\Dev\Semicolons\f.safe2.txt" here ...
Press any key to continue . . .

Notes

  • The drag-and-drop interface wraps some unsafe filenames in double-quotes (e.g. those containing spaces) but not all (e.g. those containing ;), Thus we cannot just use for %%a in (!params!) to process all files.

  • Instead, the basic idea is to split the params variable at the first double-quote (if present), process that "chunk", then remove it from params and repeat.

  • If the chunk is a space (" ") then we simply remove it (this can happen as part of processing the other chunks).

  • If params started with a double-quote when the current chunk was extracted, then it represents a single file and it is processed (call :proc). That filename (with its surrounding double-quotes) is then removed from params and we loop back.

  • If params didn't start with a double-quote, then we have one or more filenames in the current chunk. We know that none of them contain spaces (because otherwise they would be wrapped in double-quotes), but they might contain other "problem" characters such as ;. Therefore, we wrap each space-delimited component of the chunk in double-quotes (turning aaa bbb ccc into "aaa" "bbb" "ccc") and use a simple for loop to process each one. (The "" test is because there can be stray spaces at the beginning or end of the chunk). After all file(s) have been processed, the original chunk is removed from params and we loop back.

  • The :: prefix prepended to params (and skipped in the main for command) is to catch a particular issue if one file name is a prefix of another (e.g. C:\Files\Q and C:\Files\Q Q). Without the ::s, when removing C:\Files\Q from params, the replace-all-occurrences nature of CMD means the second file's name is mangled. Keeping params prefixed with :: (which can never occur in a valid file) stops this from happening.

  • The script should work for any number of files (up to the roughly 8,192-character limit for the original command-line).

TripeHound
  • 2,721
  • 23
  • 37
  • 1
    This only works for optimal test cases :-) Try it with simple filenames `a.txt`, `b.txt`, `c.txt`. I don't think that it can be solved with a FOR/F loop and delimiters – jeb Jan 27 '20 at 14:35
  • 1
    @jeb You're right... I just realised it needs at least every other file to be enclosed in double-quotes... back to drawing board! – TripeHound Jan 27 '20 at 14:37
  • Thanks for helping. There will be no limit to the number of files I would like to drop. Is there a hard limit of 26 with this? I'll update the question with more examples. – Ste Jan 27 '20 at 15:07
  • @Ste Regard the above as a "work in progress"... as jeb noted, it won't work with "simple" files that don't need any double-quotes. _**In passing**_ even with "normal" files, there's a hard limit on the length of the command-line of a bit less than 8,192 bytes. Depending on the depth of the paths being dragged, this puts an upper limit on the number of files that can be processed. – TripeHound Jan 27 '20 at 15:15
  • Oh, I've found what looks like a great solution. And if I `cd /d` into any found directories, I can then run the process as per normal. It's just the initial drag and dropped files and folders that get chewed up. Here's the **How to escape special characters on InputFile with Drag and Drop ?** [dostips thread here](https://www.dostips.com/forum/viewtopic.php?f=3&t=9301&p=60445&hilit=drag+and+drop#p60441) – Ste Jan 27 '20 at 15:25
  • @Ste Well done... that solution looks best, although being "bloody-minded" I've finished adapting my original approach (splitting on double-quotes) so that it shouldn't be limited to 26 items or less (and, as jeb pointed out, it should also handle multiple files that don't _need_ double-quotes). – TripeHound Jan 27 '20 at 17:36
  • Thanks for finishing that off. I tested against my _simply weird filenames_ and all work fine apart from files with `=` in them, such as `folder name , goes = here` and `copy = copy.txt`. With the answer I found the only one that doesn't work is `C:/nospaces/peter &(mary).txt` but there's literally nothing sent to the command line with `setlocal EnableDelayedExpansion & set "params=!cmdcmdline:~,-1!" & echo !params! & pause ` so nothing can be done in this instance AFAIK. – Ste Jan 27 '20 at 18:01
  • 1
    Some more challenges `d1_equal=yes` (loops forever), `d2_caret^^double`, `d3_missing!bang!` Btw. When you test d2 and d3 together, then d2 works! – jeb Jan 28 '20 at 08:13
  • @jeb I'll probably just stick to not naming my files like that :-) – TripeHound Jan 28 '20 at 08:16
  • 1
    @TripeHound That's a pity, I name 90% of my files like that – jeb Jan 28 '20 at 08:20
0

My own version to solve all drag&drop problems:

Can handle any filenames, like

d1_;,=
d2_four spaces
d3_One&Amp
d4_Amp&with space

But there is still a strong problem, filenames like a&() can't be handled by the drag.bat, because the filename creates a syntax error even before the batch file is called.

But with a nasty hack it can be solved, then even the hard ones works!

d5_syntax&()error

cmd.exe can start an AutoRun (batch) command, if registered.
This AutoRun batch file gets no arguments, but the complete command in cmdcmdline.
I simply parse that data and if necessary I rebuild new quoted arguments (only 300 lines).
For modifying the registry entry use

AutoRun --show
AutoRun --install
AutoRun --remove

drag.bat


@echo off

setlocal ENABLEDELAYEDEXPANSION
rem *** Take the cmd-line, remove all until the first parameter
rem *** Copy cmdcmdline without any modifications, as cmdcmdline has some strange behaviour
set "params=!cmdcmdline!"
set "params=!params:~0,-1!"
set "params=!params:*" =!"
call :fn_$arg_parser params

REM ** FOR /L %%# in (0 1 !arg#-1!) DO (
REM **  echo arg%%# = '!arg%%#!'
REM ** )

set count=0

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

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

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


:fn_$arg_parser arg_line
REM *** @TODO: ENDLOCAL all arg-... variables
::: Output arg0, ..., arg and arg#
::: arg0 ... arg contains unquoted arguments
::: an argument that contains only one quote (last character in line) is treated as quoted  arg=" -> arg=, arg.quoted=1
::: arg0.quoted contains 1 if the argument was quoted
::: \ escapes the next character (also inside quotes), can escape delimiters outside quotes, can escape quotes
::: Only \" is replaced to a single quote ", all other \ are unchanged

(set^ arg_line=!%1!)
call :strlen arg_len arg_line
set /a arg_len-=1

set "quoted="
rem set "escapeChar="   --- "Say: \"Hello\" to frank\n"
set /a arg#=0
set "arg0="
for /L %%I in (0 1 !arg_len!) DO (
    for %%# in (!arg#!) DO  (
        set "char=!arg_line:~%%I,1!"
        set "isDelim="

        if "!escapeChar!!char!" == ^""" set "quoted=!quoted:~,0!"

        REM echo %%I: "!char!" quote: !quote!
        %= only outside quotes =%
        %= check if char is a delim =%
        FOR /F "tokens=1,2 delims= " %%D in ("isDelim!char!NO") DO (
            if "%%D" == "isDelim" (
                set isDelim=1
            )
        )

        REM *** Split arguments by not escaped delimiter
        if "!isDelim!,!quoted!,!escapeChar!" == "1,," (
            REM *** Finalize arg, test if arg is quoted
            if defined arg%%# (
                set arg%%#.quoted=
                REM echo    #in %%# "!arg%%#:~,1!!arg%%#:~-1!"
                if "!arg%%#:~,1!!arg%%#:~-1!" == """" (
                    if "!arg%%#:~-2,-1!" NEQ "\" (
                        set arg%%#.quoted=1
                        set "arg%%#=!arg%%#:~1,-1!"
                    )
                )
                if defined arg%%# (
                    set ^"arg%%#=!arg%%#:"\"="!"
                )
                set /a arg#+=1
            )
        )

        if defined escapeChar (
            set "escapeChar="
            if "!char!" EQU ^""" (
                REM *** Replace escaped quotes with "\" that can be safely detected and replaced later
                set "char="\""
            ) ELSE IF defined isDelim (
                set "isDelim="
            ) ELSE (
                set "char=\!char!"
            )
        ) ELSE if "!char!" == "\" set "escapeChar=\"

        if defined quoted set "isDelim="

        REM *** Append char to arg
        if "!isDelim!!escapeChar!" == "" (
            set "arg%%#=!arg%%#!!char!"
        )
    )
)

for %%# in (!arg#!) DO (
    REM *** Last character is \
    if defined escapeChar (
        set "arg%%#=!arg%%#!\"
    )

    REM *** Duplicated code
    REM *** Finalize arg, test if arg is quoted
    if defined arg%%# (
        set arg%%#.quoted=
        REM echo out %%# "!arg%%#:~,1!!arg%%#:~-1!"
        if "!arg%%#:~,1!!arg%%#:~-1!" == """" (
            if "!arg%%#:~-2,-1!" NEQ "\" (
                set arg%%#.quoted=1
                set "arg%%#=!arg%%#:~1,-1!"
            )
        )
        if defined arg%%# (
            set ^"arg%%#=!arg%%#:"\"="!"
        )
        set /a arg#+=1
    )
)
REM *** Create a helper variable with name "arg#-1"
set /a tmp=!arg#!-1
set "arg#-1=!tmp!"

exit /b
::: detector line %~* *** FATAL ERROR: missing parenthesis or exit /b
:strlen  
(   
    setlocal EnableDelayedExpansion
    (set^ tmp=!%~2!)
    if defined tmp (
        set "len=1"
        for %%P in (4096 2048 1024 512 256 128 64 32 16 8 4 2 1) do (
            if "!tmp:~%%P,1!" NEQ "" ( 
                set /a "len+=%%P"
                set "tmp=!tmp:~%%P!"
            )
        )
    ) ELSE (
        set len=0
    )
)
( 
    endlocal
    set "%~1=%len%"
    exit /b
)

AutoRun.bat


@echo off
REM *** To enable this script, call it by  --install

REM *** To see the current values
REM *** reg query "HKEY_CURRENT_USER\Software\Microsoft\Command Processor" /v AutoRun
REM *** reg query "HKEY_LOCAL_MACHINE\\Software\Microsoft\Command Processor" /v AutoRun

REM *** To delete the keys
REM *** reg DELETE "HKEY_CURRENT_USER\Software\Microsoft\Command Processor" /v AutoRun /f
REM *** reg DELETE "HKEY_LOCAL_MACHINE\\Software\Microsoft\Command Processor" /v AutoRun /f

setlocal EnableDelayedExpansion
REM *** ALWAYS make a copy of the complete CMDCMDLINE, else you destroy the originial!!!
set "_ccl_=!cmdcmdline!"
echo(>CON

REM *** %1 contains only data, when the script itself was called from the command line
if "%~1" NEQ "" (
    goto :direct_call
)
REM echo #0 '!_ccl_!' > CON
REM *** The check is necessary to distinguish between a new cmd.exe instance for a user or for a "FOR /F" sub-command
if "!_ccl_:~1,-2!" == "!comspec!" (
    REM ***** INTERACTIVE ****

    echo %DATE% %TIME% comspec >> "%~dp0\started.log"
    FOR %%M in ("%~dp0\cmdMacros.mac") DO (
        endlocal    
        echo ********************************************************************
        echo * AutoRun executed from "%~f0"
        if exist "%%~M" (
            doskey /macrofile="%%~M"
            echo * Macros loaded from "%%~M"
        ) ELSE (
            echo * Macrofile missing at "%%~M"
        )
        echo ********************************************************************
    )
    REM set "path=%path%;%~dp0"

    SET "BATLIB=C:\Project\BatchLibrary"

    call cd /D "%%BATLIB%%"
) ELSE (
    REM *** This is a FOR command, a call by an explorer click or a drag & drop operation

    REM *** Try to detect a PROBLEMATIC Drag&Drop operation, if it has the format `%comspec% /c ""`
    REM *** It's only problematic when there is one & in the data
    REM *** But it's not bullet proof, FOR /F in ('""C:\temp\myfile.bat" C:\temp\file&second.bat"') can't be distinguished

    REM *** First check, that the cmdline starts with `%comspec% /c ""....`
    if "!_ccl_!" NEQ "!_ccl_:*/c ""=#!" if "!comspec! /c ""!_ccl_:*/c ""=!" == "!_ccl_!" (
        REM *** Only handle it, if there is at least one & and no pipe |
        if "!_ccl_!" NEQ "!_ccl_:&=!" if "!_ccl_!" EQU "!_ccl_:|=!" (
            call :DRAG_and_drop
            %DEBUG_DRAG% echo # BACK from DRAG_and_drop
        )
    )
    REM *** Leaving now
    REM *** This is a "FOR /F" sub-command 
    REM *** or a "harmless" drag&drop 
    REM *** or the second run of a problematic drag&drop
    REM echo %DATE% %TIME% internal "!_ccl_!" >> "%~dp0\started.log"
    endlocal
)

exit /b

:DRAG_and_drop
REM *** This results into recursive execution of this script
REM *** Be sure, that it's not an endless recursion

REM *** Remove the front 
REM set "_ccl_=!_ccl_:&=+!"
set "_ccl_=!_ccl_:~0,-1!"
set "_ccl_=!_ccl_:*/c "=!"
set "DEBUG_DRAG=REM " 

%DEBUG_DRAG% echo #A '!_ccl_!' > CON

call :arg_parser argv _ccl_ 

REM *** Check if there is at least one unquoted, problematic &
REM *** This arg must not have spaces
set "isDragAndDrop="

if defined argv.onlySingleSpace (
    set "isDragAndDrop=1"
    set "problematic="
    set "args="
    FOR /L %%# in (0 1 !argv#-1!) DO (
        set "arg=!argv[%%#]!"
        REM *** Check if it's an absolute path
        FOR /F "tokens=1,*" %%1 in (": !arg!") DO (
            if "%%~f2" NEQ "%%~2" (
                set "isDragAndDrop="
                %DEBUG_DRAG% echo *** 'NO PATH' !arg! NEQ %%~f2 > CON
                exit /b
            )
        )

        if defined argv[%%#].quoted (
            set "arg="!arg!""
        ) ELSE (
            REM *** If not quoted but contains a &

            if "!arg!" NEQ "!arg:&=!" (
                if "!arg:~1,2!" EQU ":\" (
                    set "arg="!arg!""
                    set "problematic=1"             
                ) ELSE (
                    REM *** This can't be drap&drop, because arg doesn't begin with ":\"
                    set "isDragAndDrop="
                    %DEBUG_DRAG% echo *** NO DRIVE '!arg!' > CON
                    exit /b
                )
            )
        )
        set "args=!args!!arg! "
    )
    set "args=!args:~,-1!"
)

if not defined problematic (
    set "isDragAndDrop="
    REM echo *** 'problematic = 0' > CON
)

if defined isDragAndDrop (
REM ECHO *** HANDLE isDragAndDrop *** > CON
REM echo *** '!args!' > CON
    REM *** The TWO SPACES between comspec and /c are essential!
    REM *** The AutoRun script detects the recursion call by this difference
    (
        endlocal
        %comspec%  /c ^"%args%"
    )

    REM *** Back from the dragDrop script
    REM *** EXIT to avoid a second call
    exit
)

REM *** This is the path for unproblematic content
REM *** Or it was detected, that this isn't a drap&drop operation
exit /b

:direct_call
if "%~1" == "--install" (
    reg add "HKEY_CURRENT_USER\Software\Microsoft\Command Processor" /v "AutoRun" /t REG_SZ /d "%~f0"
    exit /b
) 

if "%~1" == "--show" (  
    reg query "HKEY_CURRENT_USER\Software\Microsoft\Command Processor" /v AutoRun
    exit /b
)

if "%~1" == "--remove" (
    reg DELETE "HKEY_CURRENT_USER\Software\Microsoft\Command Processor" /v AutoRun /f
)
exit /b

:arg_parser  arg_line
REM *** @TODO: ENDLOCAL all arg-... variables
::: Output arg0, ..., arg and arg#
::: arg0 ... arg contains unquoted arguments
::: an argument that contains only one quote (last character in line) is treated as quoted  arg=" -> arg=, arg.quoted=1
::: arg0.quoted contains 1 if the argument was quoted
::: \ escapes the next character (also inside quotes), can escape delimiters outside quotes, can escape quotes
::: Only \" is replaced to a single quote ", all other \ are unchanged

set "var=%1"
(set^ arg_line=!%2!)
call :strlen arg_len arg_line
set /a %var%_len-=1

set "quoted="
rem set "escapeChar="   --- "Say: \"Hello\" to frank\n"
set /a %var%#=0
set "%var%[0]="
set "%var%.onlySingleSpace=1"
for /L %%I in (0 1 !arg_len!) DO (
    for %%# in (!%var%#!) DO  (
        set "char=!arg_line:~%%I,1!"
        set "isDelim="

        if "!escapeChar!!char!" == ^""" set "quoted=!quoted:~,0!"

        REM echo %%I: "!char!" quote: !quote!
        %= only outside quotes =%
        %= check if char is a delim =%
        if not defined quoted (
            FOR /F "tokens=1,2 delims= " %%D in ("isDelim!char!NO") DO (
                if "%%D" == "isDelim" (
                    set isDelim=1
                    if "!arg_line:~%%I,2!" == "  " (
                        %DEBUG_DRAG% echo ##### NOT onlySingleSpace > CON
                        set "%var%.onlySingleSpace="
                    )
                )
            )
        )

        REM *** Split arguments by not escaped delimiter
        if "!isDelim!,!quoted!,!escapeChar!" == "1,," (
            REM *** Finalize arg, test if arg is quoted
            if defined %var%[%%#] (
                set %var%[%%#].quoted=
                REM echo    #in %%# "!%var%[%%#]:~,1!!%var%[%%#]:~-1!"
                if "!%var%[%%#]:~,1!!%var%[%%#]:~-1!" == """" (
                    if "!%var%[%%#]:~-2,-1!" NEQ "\" (
                        set %var%[%%#].quoted=1
                        set "%var%[%%#]=!%var%[%%#]:~1,-1!"
                    )
                )
                REM * NOT USED HERE * if defined %var%[%%#] (
                REM * NOT USED HERE *   set ^"%var%[%%#]=!%var%[%%#]:"\"="!"
                REM * NOT USED HERE * )
                set /a %var%#+=1
            )
        )

        REM * NOT USED HERE * if defined escapeChar (
        REM * NOT USED HERE *   set "escapeChar="
        REM * NOT USED HERE *   if "!char!" EQU ^""" (
        REM * NOT USED HERE *       REM *** Replace escaped quotes with "\" that can be safely detected and replaced later
        REM * NOT USED HERE *       set "char="\""
        REM * NOT USED HERE *   ) ELSE IF defined isDelim (
        REM * NOT USED HERE *       set "isDelim="
        REM * NOT USED HERE *   ) ELSE (
        REM * NOT USED HERE *       set "char=\!char!"
        REM * NOT USED HERE *   )
        REM * NOT USED HERE * ) ELSE if "!char!" == "\" set "escapeChar=\"

        if defined quoted set "isDelim="

        REM *** Append char to arg
        if "!isDelim!!escapeChar!" == "" (
            set "%var%[%%#]=!%var%[%%#]!!char!"
        )
    )
)

for %%# in (!%var%#!) DO (
    REM *** Last character is \
    if defined escapeChar (
        set "%var%[%%#]=!%var%[%%#]!\"
    )

    REM *** Duplicated code
    REM *** Finalize arg, test if arg is quoted
    if defined %var%[%%#] (
        set %var%[%%#].quoted=
        if "!%var%[%%#]:~,1!!%var%[%%#]:~-1!" == """" (
            if "!%var%[%%#]:~-2,-1!" NEQ "\" (
                set %var%[%%#].quoted=1
                set "%var%[%%#]=!%var%[%%#]:~1,-1!"
            )
        )
        REM * NOT USED HERE * if defined %var%[%%#] (
        REM * NOT USED HERE *   set ^"%var%[%%#]=!%var%[%%#]:"\"="!"
        REM * NOT USED HERE * )
        set /a %var%#+=1
    )
)
REM *** Create a helper variable with name "arg#-1"
set /a tmp=!%var%#!-1
set "%var%#-1=!tmp!"

exit /b
::: detector line %~* *** FATAL ERROR: missing parenthesis or exit /b
:strlen  
(   
    setlocal EnableDelayedExpansion
    (set^ tmp=!%~2!)
    if defined tmp (
        set "len=1"
        for %%P in (4096 2048 1024 512 256 128 64 32 16 8 4 2 1) do (
            if "!tmp:~%%P,1!" NEQ "" ( 
                set /a "len+=%%P"
                set "tmp=!tmp:~%%P!"
            )
        )
    ) ELSE (
        set len=0
    )
)
( 
    endlocal
    set "%~1=%len%"
    exit /b
)
jeb
  • 78,592
  • 17
  • 171
  • 225