powershellbatch-fileescapingquoteselevated-privileges

passing quoted arguments from batch file to `powershell start` - self-elevation on demand


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?


Solution

  • 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: