Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Batch – how to correctly assign arbitrary percent-number (%1) or percent-asterisk (%*) arguments that may contain quotes?

Tags:

batch-file

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)!

like image 224
aleksusklim Avatar asked Jun 22 '17 11:06

aleksusklim


People also ask

What does %% do in batch file?

Use double percent signs ( %% ) to carry out the for command within a batch file. Variables are case sensitive, and they must be represented with an alphabetical value such as %a, %b, or %c. Required. Specifies one or more files, directories, or text strings, or a range of values on which to run the command.

What does 1 mean in a batch file?

When used in a command line, script, or batch file, %1 is used to represent a variable or matched string. For example, in a Microsoft batch file, %1 can print what is entered after the batch file name.

What does %~ mean in a batch file?

A tilde sign before an command-line argument (such as "%~1") indicates to remove the surrounding quotes from the parameter. Such if the value for %1 is "Hi" then %~1 will expand to only Hi.

What does %* mean in batch?

%* expands to the complete list of arguments passed to the script. You typically use it when you want to call some other program or script and pass the same arguments that were passed to your script.


1 Answers

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.

like image 134
jeb Avatar answered Sep 28 '22 17:09

jeb