I am writing a Windows batch file that automatically escalates itself to administrative permissions, provided the user clicks "Yes" on the User Access Control dialog that appears.
I am using a technique I learned here to detect whether we already have admin rights and another from here to escalate. When appropriate, the following script, let's call it foo.bat
, re-launches itself via a powershell-mediated call to runas
:
@echo off
net session >NUL 2>NUL
if %ERRORLEVEL% NEQ 0 (
powershell start -wait -verb runas "%~dpfx0" -ArgumentList '%*'
goto :eof
)
echo Now we are running with admin rights
echo First argument is "%~1"
echo Second argument is "%~2"
pause
My problem is with escaping quotes in the -ArgumentList
. The code above works fine if I call foo.bat one two
from the command prompt, but not if one of the arguments contains a space, for example as in foo.bat one "two three"
(where the second argument should be two words, "two three").
If I could even just get the appropriate behavior when I replace %*
with static arguments:
powershell start -wait -verb runas "%~dpfx0" -ArgumentList 'one "two three"'
then I could add some lines in foo.bat
that compose an appropriately-escaped substitute for %*
. However, even on that static example, every escape pattern I have tried so far has either failed (I see Second argument is "two"
rather than Second argument is "two three"
) or caused an error (typically Start-Process: A positional parameter cannot be found that accepts argument 'two'
). Drawing on the docs for powershell's Start-Process I have tried all manner of ridiculous combinations of quotes, carets, doubled and tripled quotes, backticks, and commas, but there's some unholy interaction going on between batch-file quoting and powershell quoting, and nothing has worked.
Is this even possible?
You've run into a perfect storm of two quoting hells (cmd
and PowerShell), garnished with a PowerShell bug (as of PowerShell Core 6.2.0).
To work around the bug, the batch file cannot be reinvoked directly and must instead be reinvoked via cmd /c
.
LotPings' helpful answer, which takes that into account, typically works, but not in the following edge cases:
c:\path\to\my batch file.cmd
)cmd
metacharacters (even inside "..."
): & | < > ^
; e.g., one "two & three"
The following solution addresses all these edge cases. While it is far from trivial, it should be reusable as-is:
@echo off & setlocal
:: Test whether this invocation is elevated (`net session` only works with elevation).
:: If already running elevated (as admin), continue below.
net session >NUL 2>NUL && goto :elevated
:: If not, reinvoke with elevation.
set args=%*
if defined args set args=%args:^=^^%
if defined args set args=%args:<=^<%
if defined args set args=%args:>=^>%
if defined args set args=%args:&=^&%
if defined args set args=%args:|=^|%
if defined args set "args=%args:"=\"\"%"
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
" Start-Process -Wait -Verb RunAs -FilePath cmd -ArgumentList \"/c \"\" cd /d \"\"%CD% \"\" ^&^& \"\"%~f0\"\" %args% \"\" \" "
exit /b
:elevated
:: =====================================================
:: Now we are running elevated, in the same working dir., with args passed through.
:: YOUR CODE GOES HERE.
echo First argument is "%~1"
echo Second argument is "%~2"
pause
Note:
%CD%
above, before \"
, is not an accident, and actually required to also make the batch file work when invoked from a drive's root directory - see this answer, which also offers a more modular alternative to the solution above, with the help of subroutines invoked with call
.