Executing an arbitrary command vs. parsing and escaping

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

Post Reply
Message
Author
Liviu
Expert
Posts: 470
Joined: 13 Jan 2012 21:24

Executing an arbitrary command vs. parsing and escaping

#1 Post by Liviu » 14 Sep 2012 00:43

Code below is supposed to execute whatever is passed on its command line, literally. Difficulty here (and why simply doing a 'cmd /c %*' or 'call %*' or '%*' won't work in the general case) is about the usual problem characters (%^<|&>") and how to avoid parsing/escaping pitfalls.

Code: Select all

@echo off
setlocal enableExtensions disableDelayedExpansion

set "remArgs=%temp%\%random%.%time::=.%.tmp"
set prompt=@
>"%remArgs%" (
  setlocal disableExtensions
  echo on
  for %%a in (%%a) do rem { %* }
  @echo off
  endlocal
)
prompt
setlocal enableDelayedExpansion
<"%remArgs%" (
  set /p "args="
  set /p "args="
  set "args=!args:~7,-3!"
)
del "%remArgs%"

echo ^>^>^> !args!
<nul set /p "=cmd/c:  "
call :cmd/c args
<nul set /p "=call:   "
call :call args
<nul set /p "=exec:   "
call :exec args
echo ^<^<^<

endlocal & endlocal & goto :eof

:cmd/c -- ok, but requires new instance of cmd
cmd /c !%~1!
goto :eof

:call -- halves unquoted %% percents and doubles quoted ^ carets
call !%~1!
goto :eof

:exec -- doesn't parse <|>, fails if/for, rejects (&), spares all ^ carets
@rem parantheses not needed now since entire !%~2! cmd line is delayed-expanded
@rem but would be required if parts of the cmd line were to be expanded early
@rem 'unexpected jump' - http://www.dostips.com/forum/viewtopic.php?f=3&t=2851
(
  !%~1!
)
@rem might not be reached if command is another batch file
goto :eof

A test run output is copied below.

Code: Select all

C:\tmp>try-exec echo a%%"%%&b^"%%^^cd%%!cd!
>>> echo a%%"%%&b^"%%^cd%%!cd!
cmd/c:  a%%"%%&b^"%%cd%%!cd!
call:   a%"%&b^^"%^cd%!cd!
exec:   a%%"%%&b^"%%^cd%%!cd!

The only correct result is on the "cmd/c:" line.

Since it may not be obvious that it's indeed "correct", this is how it goes. When the "try-exec" line is executed at the prompt, it is first parsed by the command interpreter before the batch is actually called. The result of that parsing can be verified by replacing "try-exec" with just "echo".

Code: Select all

C:\tmp>echo echo a%%"%%&b^"%%^^cd%%!cd!
echo a%%"%%&b^"%%^cd%%!cd!
The output line above is what try-exec receives as an argument, and that's the command supposed to be executed. For verification, the end result should be

Code: Select all

C:\tmp>echo a%%"%%&b^"%%^cd%%!cd!
a%%"%%&b^"%%cd%%!cd!
which is indeed the same as the one returned by try-exec on the cmd/c line.

Question, since yes there is a question ;-) but is there some other safe problem-character-proof way to accomplish the same _without_ running a second instance of cmd?

Liviu

jeb
Expert
Posts: 1062
Joined: 30 Aug 2007 08:05
Location: Germany, Bochum

Re: Executing an arbitrary command vs. parsing and escaping

#2 Post by jeb » 14 Sep 2012 02:52

My first question is:
What do you want in a case like this

Code: Select all

try-exec echo hello ^& you


Do you want that it results in an execution of

Code: Select all

echo hello ^& you
Or ...
echo hello & you

In this case the second variant will throw an error ("you" can't be found)

jeb

Liviu
Expert
Posts: 470
Joined: 13 Jan 2012 21:24

Re: Executing an arbitrary command vs. parsing and escaping

#3 Post by Liviu » 14 Sep 2012 08:28

jeb wrote:In this case the second variant will throw an error ("you" can't be found)

Right, and that's what I'd expect. The "^&" is parsed to "&" before try-exec is called, so try-exec never sees it. Even if it wanted to, the called program could not possibly guess or reverse-engineer the command line literally typed at the prompt, since for example "echo hello ^& you" or "^h^e^l^l^o ^& ^y^o^u" would result in exactly the same "echo hello & you" being passed as an argument to try-exec. So, yes, definitely the second case.

Liviu

Liviu
Expert
Posts: 470
Joined: 13 Jan 2012 21:24

Re: Executing an arbitrary command vs. parsing and escaping

#4 Post by Liviu » 14 Sep 2012 20:26

P.S. For an example why the above isn't just an academical curiosity... Suppose you had a "wrapper" batch file to run an arbitrary command and measure the time it takes to complete. On the face of it, should be trivial - capture the %time% before and after, subtract, done. Yet, depending on how the "wrapper" batch actually runs the command, what's executed may not be what was intended, due to multi-pass re-parsing and un-escaping. Worse, the translation may not be easy to describe or explain, short of going into the more obscure corners of batch line processing.

Below is a snippet which tackles the issue by attempting to:
- retrieve the command line literally, safe against poison characters;
- execute the given line as passed, without further re-parsing.

This gives a simpe rule for verification - what's executed is exactly what would be echo'd off the same line.

Code: Select all

:: ticktock.cmd - run command and measure execution time
:: brief output sent to stderr, with just the command line and registered times
:: full output sent to stdout, including header and footer outlines
::
:: execution times expected to be shorter than a day
:: if count is off by an hour then it might just be daylight-saving late night
::
:: command line being executed is the same as if echo'd instead of ticktock'd
:: i.e. it undergoes one pass of parsing before ticktock receives the arguments
::
:: ticktock {command}               ..run and profile {command}, full output
:: ticktock {command} ^>nul         ..suppress {command} stdout output
:: ticktock {command} ^>nul 2^>^&1  ..suppress all {command} output
:: ticktock {command} >nul          ..suppress all stdout output
:: ticktock {command} 2^>nul >nul   ..suppress all but ticktock's stderr output
:: ticktock for /l %n in (1,1,10) do @{command}  ..run {command} 10 times

@echo off

if "%~1"=="" ( echo.
  @rem dump :: comment lines at the top of the file
  for /f "usebackq delims=" %%a in ("%~f0") do (
    set "z=%%~a" & setlocal enableDelayedExpansion
    if not "!z:~0,1!"==":" endlocal & goto :eof
    echo !z! & endlocal
  )
  goto :eof
)

setlocal enableExtensions disableDelayedExpansion

set "remArgs=%temp%\%random%.%time::=.%.tmp"
set prompt=@
>"%remArgs%" (
  setlocal disableExtensions
  echo on
  for %%a in (%%a) do rem { %* }
  @echo off
  endlocal
)
prompt
setlocal enableDelayedExpansion
<"%remArgs%" (
  set /p "args="
  set /p "args="
  set "args=!args:~7,-3!"
)
del "%remArgs%"

echo _______________________________________________________________________________
echo.
1>&2 echo  !args!
echo ...............................................................................

@rem calibrate empty run
set "T0=%time%"
cmd /c exit 0
set "T1=%time%"
call :hmsc2xc %T0% XC0 & call :hmsc2xc %T1% XC1
call :xc-xc %XC1% %XC0% XX

@rem profile actual command
set "T0=%time%"
cmd /c !args!
set "T1=%time%"
call :hmsc2xc %T0% XC0 & call :hmsc2xc %T1% XC1
call :xc-xc %XC1% %XC0% XC

call :xc-xc /geq %XC% %XX% XC & if errorlevel 0 (set "lss=") else (set "lss=-")
call :xc2hmsc /trim %XX% TX & call :xc2hmsc /trim %XC% T

echo -------------------------------------------------------------------------------
if not "%XC%"=="%T%" (
  1>&2 echo  %lss%%XC% sec  ^( %T% = %T1% - %T0% - %TX% ^)
) else (
  1>&2 echo  %lss%%XC% sec  ^( = %T1% - %T0% - %TX% ^)
)
echo ===============================================================================

endlocal & endlocal & goto :eof

::-----------------------------------------------------------------------------

:hmsc2xc
setlocal enableExtensions

@rem assume hh:mm:ss.cc format
for /f "tokens=1-4 delims=:., " %%A in ("%~1") do (
  set "H=%%~A" & set "M=%%~B" & set "S=%%~C" & set "C=%%~D"
)

@rem drop leading (but not sole) '0' lest mistaken for octal
if "%H:~0,1%" == "0" if not "%H:~1,1%" == "" set "H=%H:~1,1%"
if "%M:~0,1%" == "0" if not "%M:~1,1%" == "" set "M=%M:~1,1%"
if "%S:~0,1%" == "0" if not "%S:~1,1%" == "" set "S=%S:~1,1%"

set /a "X = (H * 60 + M) * 60 + S"

endlocal & (set %~2=%X%.%C%)
exit /b 0

::-----------------------------------------------------------------------------

:xc2hmsc
setlocal enableExtensions

if "%~1"=="/trim" (set "trim=1" & shift) else (set "trim=")
for /f "tokens=1* delims=.," %%s in ("%~1") do (set "X=%%s") & (set "C=%%t")
set /a "H = X / 3600", "M = (X %% 3600) / 60", "S = X %% 60"
if defined trim (call :xc2hmsc.trim) else (call :xc2hmsc.pad)

endlocal & (set %~2=%T%%C%)
exit /b 0

:xc2hmsc.trim
if %H% gtr 0 (set "T=%H%:") else (set "T=")
if %M% gtr 9 (set "T=%T%%M%:") else if not "%T%"=="" (set "T=%T%0%M%:") else if %M% gtr 0 (set "T=%M%:")
if %S% gtr 9 (set "T=%T%%S%.") else if not "%T%"=="" (set "T=%T%0%S%.") else (set "T=%S%.")
goto :eof

:xc2hmsc.pad
set "T=%H%:"
if %M% gtr 9 (set "T=%T%%M%:") else (set "T=%T%0%M%:")
if %S% gtr 9 (set "T=%T%%S%.") else (set "T=%T%0%S%.")
goto :eof

::.............................................................................

:xc-xc
setlocal enableextensions

if "%~1"=="/geq" (set "geq=1" & shift) else (set "geq=")
for /f "tokens=1* delims=.," %%s in ("%~1") do (set "X1=%%s") & (set "C1=%%t")
for /f "tokens=1* delims=.," %%s in ("%~2") do (set "X0=%%s") & (set "C0=%%t")

if %X1%%C1% lss %X0%%C0% (
  @rem if /geq return '0.00' with negative errorlevel
  if defined geq endlocal & set "%~3=0.00" & exit /b -1
  @rem wrap around midnight (86,400 = 24 * 60 * 60)
  set /a "X1 += 86400"
)
set /a "X = X1 - X0"

@rem wrap around full seconds
if "%C0:~0,1%" == "0" if not "%C0:~1,1%" == "" set "C0=%C0:~1,1%"
if "%C1:~0,1%" == "0" if not "%C1:~1,1%" == "" set "C1=%C1:~1,1%"
if %C1% lss %C0% (set /a "C1 += 100") & (set /a "X -= 1")
set /a "C = C1 - C0"
if "%C:~1,1%" == "" set "C=0%C%"

endlocal & (set "%~3=%X%.%C%")
exit /b 0

::_____________________________________________________________________________

Liviu

jeb
Expert
Posts: 1062
Joined: 30 Aug 2007 08:05
Location: Germany, Bochum

Re: Executing an arbitrary command vs. parsing and escaping

#5 Post by jeb » 15 Sep 2012 13:32

Liviu wrote:Yet, depending on how the "wrapper" batch actually runs the command, what's executed may not be what was intended, due to multi-pass re-parsing and un-escaping. Worse, the translation may not be easy to describe or explain, short of going into the more obscure corners of batch line processing.


Now it makes sense to me :)

I suppose the only possible solution is to use the CMD /C, as only there the command can be expanded in the original way.
A CALL can't handle a ampersand.
The simple !args! or also %args% can't handle any expansions of arguments like in

Code: Select all

try-echo echo %%path%%


jeb

Post Reply