5

While working on my previous question, I faced a similar issue that I was solving initially.

Actually, I spent this month in attempts to create one universal .bat helper library that will handle drag-and-dropped files with any names as best as it is possible. I want to do it transparently for the caller, and backward-compatible with any existing scripts, so filenames are delivered as %1, %2, and I have no problems with that.

Solution of @MCND is to read %CMDCMDLINE% and then tricky parse it. So I can simply write like set "a=%CMDCMDLINE:"=?%" – and have the full string into variable A, but all quotes inside are instantly replaced with question marks ?, resulting in an unquoted line that I can easily process further.

But it works only for .bat drag-and-drop, since that way I can’t read parameters passed from command prompt. I have to use %*.

My current question is: how to read it if the parameters may also contain quotation characters, even unpaired?

Consider the following BAT file: (let’s call "C:\test.bat")

@echo off
echo START
setlocal enabledelayedexpansion

set a=%1
echo a=[!a!]

set b="%1"
echo b=[!b!]

set "c=%1"
echo c=[!c!]

set d=%~1
echo d=[!d!]

set e="%~1"
echo e=[!e!]

set "f=%~1"
echo f=[!f!]

set g=%*
echo g=[!g!]

set h="%*"
echo h=[!h!]

set "i=%*"
echo i=[!i!]

endlocal
echo DONE
exit /b

Basically it is everything that I can imagine to read incoming batch ("call") percent-parameters: forms %1, %~1, %* in unquoted set x=y, quoted set "x=y" and explicitly quoted set x="y" variations. Delayed expansion is only to safely echo the value (here I don’t care about examination marks ! in parameter for simplicity).

My final aim is to create a script that will never throw an error: so every possible errors should appear either before my code gets execution (at CMD prompt itself), or after it finished (when the caller will try to access a file with a broken name).

So in this example, between START and DONE ideally there should be no errors, but they will appear at "set" instructions, before the corresponding "echo".

Attempt #1

Command: test

Output:

START
a=[]
b=[""]
c=[]
d=[]
e=[""]
f=[]
g=[]
h=[""]
i=[]
DONE

Good, not errors at zero arguments. Let’s get some:

Attempt #2

Command: test 123 "56"

Output:

START
a=[123]
b=["123"]
c=[123]
d=[123]
e=["123"]
f=[123]
g=[123 "56"]
h=["123 "56""]
i=[123 "56"]
DONE

Looks like for "normal" strings any method works without errors.

Attempt #3

Command: test ^&-

Output:

START
'-' is not recognized as an internal or external command,
operable program or batch file.
a=[]
b=["&-"]
c=[&-]
'-' is not recognized as an internal or external command,
operable program or batch file.
d=[]
e=["&-"]
f=[&-]
'-' is not recognized as an internal or external command,
operable program or batch file.
g=[]
h=["&-"]
i=[&-]
DONE

As you can see, cases A, D and G failed (=%1, =%~1 and =%*).

Attempt #4

Command: test "&-"

Output:

START
a=["&-"]
'-""' is not recognized as an internal or external command,
operable program or batch file.
b=[""]
'-""' is not recognized as an internal or external command,
operable program or batch file.
c=[]
'-""' is not recognized as an internal or external command,
operable program or batch file.
d=[]
e=["&-"]
f=[&-]
g=["&-"]
'-""' is not recognized as an internal or external command,
operable program or batch file.
h=[""]
'-""' is not recognized as an internal or external command,
operable program or batch file.
i=[]
DONE

Now cases B, C, D, H and I are failed (="%1", "=%1", =%~1, ="%*" and "=%*").

Attempt #5

Command: test ""^&-"

Output:

START
'-"' is not recognized as an internal or external command,
operable program or batch file.
a=[""]
b=["""&-""]
c=[""&-"]
d=["&-]
'-"' is not recognized as an internal or external command,
operable program or batch file.
e=[""]
'-"' is not recognized as an internal or external command,
operable program or batch file.
f=[]
'-"' is not recognized as an internal or external command,
operable program or batch file.
g=[""]
h=["""&-""]
i=[""&-"]
DONE

Failed the rest two cases E and F (="%~1" and "=%~1") along with A and G (=%1 and %*).

So, none of "set" variants will work reliably in all attempts to break my code!

Am I missing some other approach to read current parameters that come from untrusted source?

Can I get %… in a normal variable %…%? Can I replace quotes " directly in %…? Can I put %* in some special context where things like ampersand & may not cause change of execution? Is there another way to get arguments (calling CMD again, use temporary files – anything?)

I’m fine even if it will destroy the original string in these cases (unpaired quotes, unescaped specials, etc.), but I need to get rid of parse error (causing uncontrolled code execution)!

Aravind A R
  • 2,674
  • 1
  • 15
  • 25
aleksusklim
  • 361
  • 3
  • 11

1 Answers1

5

You can't solve it with normal percent expansion!
Proof:
The sample argument is "&"&.
To call a batch file with this argument, you have to escape the second ampersand.

test.bat "&"^&

It's not possible to handle this with normal commands, as you always get one unquoted ampersand.

set arg=%1
set "arg=%1"
set arg=%~1
set "arg=%~1"

Each line will fail with an error message.

Is there any command that will not fail with %1 or %*?
Yes, REM can handle them proper.

This works without erros.

REM %1

...

Funny, but how do you have any benefits from using a REM statement to get the content?
First you have to enable ECHO ON to see that it really works.

echo on
REM %1

Nice now you see the debug output

c:\temp>REM "&"&

Now you only need to redirect the debug output to a file.
There are many ways that doesn't work.

@echo on
> arg.txt REM %1
( REM %1 ) > arg.txt
call :func > arg.txt

..
:func 
REM %1

The call redirections itself works, but in the func itself you can't access the%1anymore.
But there is one solution to grab the output, with a
FOR` loop

(
    @echo on
    for %%a in (42) do (
        rem %1
    ) 
) > arg.txt

Is it better to store the argument in a file instead of %1?
Yes, as a file can be read in a safe way independent of the content.

There are still some gotchas left, like /?, ^ or %%a in the argument.
But that can all be solved.

@echo off
setlocal DisableDelayedExpansion
set "prompt=X"
setlocal DisableExtensions
(
    @echo on
    for %%a in (4) do (
        rem #%1#
    ) 
) > XY.txt
@echo off
echo x
endlocal
for /F "delims=" %%a in (xy.txt) DO (
  set "param=%%a"
)
setlocal EnableDelayedExpansion
set param=!param:~7,-4!
echo param='!param!'

More about the topic at
SO:How to receive even the strangest command line parameters?
SO:Receive multiline arguments

And for Drag&Drop you need an even more complex solution SO:Drag and drop batch file for multiple files?

Edit: Solution for %*
The extensions has to be disabled, else arguments like %a--%~a will be modified.
But without extensions the %* expansion doesn't work anymore.
So the extensions are enabled just to expand the %* but before the FOR shows the content the extensions will be disabled.

@echo off
setlocal EnableExtensions DisableDelayedexpansion
set "prompt=-"
(
    setlocal DisableExtensions    
    @echo on
    for %%a in (%%a) do (
        rem # %*#
    )
) > args.tmp
@echo off
endlocal

set "args="
for /F "tokens=1* delims=#" %%G in (args.tmp) DO if not defined args set "args=%%H"
setlocal EnableDelayedExpansion
set "args=!args:~1,-4!"
echo('!args!'

This can fetch all arguments in a safe way, but for arguments from a drag&drop operations even this isn't enough, as windows have a bug (design flaw).
A file like Documents&More can't be fetched this way.

jeb
  • 78,592
  • 17
  • 171
  • 225
  • Wow, the famous Jeb! I tried your approach and it worked. Up until I pass something like `;=,`, since those are delimiters and aren’t delivered to %1 at all if unquoted. Also it’s hard (I think) to get the argument count (and extract all of them, not only first). Is there a solution with %* (which not working here because of disabled extensions), with as less side-effects as possible? I have my own try, I tested it and couldn’t find any breaks; all works (`/?`, `%~…`, `^` – what else is in trouble?). Can you look at it, please? I’ll just post it in the following comment here: – aleksusklim Jun 23 '17 at 09:28
  • 'Cause it is multiline, replace all `#` with newlines after copying from here. There is "backspace" character involved, it is substituted with `$` – you also have to replace it with "7Fh" char (Ctrl+Backspace in Notepad or Alt+0127): `#@setlocal enableextensions disabledelayedexpansion#@echo on&@(for %%$ in (_) do (@call#rem @ %*@#))>tmp.txt&@echo off&endlocal##set "!="&set "&="&for /F "tokens=1* delims=@" %%G in (tmp.txt) DO set "!=%%H"&if defined ! set "&=%%H"#set "&=%&:"=$%"#set "&=%&:~1,-2%"#echo "%&%"#`. Using backspace as a variable prevents expanding; also used as quote-substitution. – aleksusklim Jun 23 '17 at 09:28
  • @aleksusklim It fails with arguments like `%$--%~$`, see my edited answer. I don't understand the part _"Using backspace as a variable prevents expanding"_. – jeb Jun 24 '17 at 12:43
  • You ought to replace EACH character `$` in my code with 0x7F byte, which is a "Backspace" character, that also could be entered in notepad.exe by pressing Ctrl+Backspace or Alt+0127. This character couldn't be entered to the CMD console window, so you cannot put your break-string `%$--%~$`, because here the backspace substitution is non-existed. – aleksusklim Jun 24 '17 at 14:11
  • 1
    @aleksusklim Okay I understand now your point, but it's not safe, as a program (also another batch file) can use such a character in the arguments and I suppose that it's even possible from the command line – jeb Jun 24 '17 at 15:43
  • You lack one `endlocal` (two, actually); the first stub `rem` is better to write as `@rem` (but `skip=` is already wise!); also `set "prompt= "` (with space) + `set "prompt="` (empty) later will be good too. But really, thanks a lot! Now I will use your approach to solve my primary task, and show the final code in my own answer to my previous question (I hope it will not take for me another month…) – aleksusklim Jun 25 '17 at 08:48
  • @aleksusklim I didn't add the endlocals as they are implicitly at the exit of the batch file. I use now `set prompt` to avoid problems with multiline prompts, therefore the first `REM` is now superfluous. Btw. For a DRAG&DROP operations this code isn't safe, as Windows have a bug there – jeb Jun 26 '17 at 08:00