A piping hot solution
Derived from mklement0's working proof of concept and Jeroen Mostert's Base64 suggestion I've built a solution with this approach:
- Pipe in data from inside the batch to the outer Powershell.
- Let it convert the piped data into a Base64 string.
- which is passed on into the command line of the elevated Powershell.
- which in turn converts it back and pipes it into the new batch's instance.
It is more flexible because you're not limited to environment variables (you can essentially pass on anything (text based)) and the Powershell command doesn't need to be edited to choose what gets piped through. But it has a few limitations mklement0's implementation doesn't suffer from:
- Environment variables containing newlines will not be passed on correctly and can cause chaos (depending on what comes after the LF, see
barz
).
- Currently every line piped through (except for the first one) gets one whitespace prepended to it (so far I couldn't figure out how to fix that). It's usually not a problem and can be worked around (
fooDoubleQouting
is a negative example).
- the elevated instance doesn't react to console input as usual any more (see notes).
Example / test batch:
@echo off & setlocal EnableDelayedExpansion
::# Test if elevated.
net session 1>NUL 2>NUL && goto :ELEVATED
(set LF=^
%=this line is empty=%
)
::# Set sample env. vars. to pass to the elevated instance.
set foo1=bar
set "foo2=none done"
set foo3=3" of snow
set "barz= Line1!LF! foo1=Surprise^! foo1 isn't '%foo1%' anymore. It was unintentionally overwritten."
set barts=1 " 3_consecutive_" "_before_EOL
set "barl=' sdfs' ´´`` =43::523; \/{[]} 457wb457;; %%%^!2^!11^!^!"
::# ' dummy comment#1 to fix syntax highlighting.
::# Helper variable to facilitate re-invocation (in case %~f0 contains any single quotes).
set "_selfBat=%~f0"
::# DDE - so "!" don't get expanded anymore. Was only needed for "set barz=..."
setlocal DisableDelayedExpansion
::# print variables etc. to console before self invocation & elevation.
call :testPrint
::# Generate pipe input. Be aware of CMD's handicaps of whats allowed in a command block.
::# eg. "REM" is not allowed and neither is echoing an unescaped closing parenthesis: ")" -> "^^^)"
(
echo[foo_Setting_one=extra-varval.
set ^^"
echo[bar_stuff=in between.^^^)^^^"
set bar
echo["fooDoubleQouting=testertest"
) | powershell.exe -nologo -noprofile -command ^
trap { [Console]::Error.WriteLine($_); exit -667 } ^
exit ( ^
Start-Process -PassThru -Wait -WindowStyle Maximized -Verb RunAs 'powershell.exe' ^
"\"-nol -nop -comm `\" $('Write-Output $([Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String(\\\"' + $([Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($(foreach ($i in $input) {\"$i`n\"})))) + '\\\")))') | cmd.exe '/D','/U','/T:4F','/S','/C',' \`\"%_selfBat:'=''%\`\" \`\"quoted argument\`\" nonQtdArg & exit'; exit `$LastExitCode `\" \"" ^
).exitCode
echo[
echo[ ---- Returned errorlevel is: %ERRORLEVEL%
pause
endlocal & endlocal & exit /b %ERRORLEVEL%
:testPrint
echo[
echo[ ---- WhiteSpaceTest: "%barts%"
set foo
set bar
echo[
set ^"
exit /B
::# " dummy comment#2 to fix syntax highlighting again.
:ELEVATED
setlocal DisableDelayedExpansion
::# Read and parse piped in data.
::# (with "delims" & "eol" truly defined as empty so every line is read as-is, even empty lines)
for /F delims^=^ eol^= %%A in ('findstr.exe "^"') do (
echo[ Parsing %%~A
for /F "tokens=1,* delims=="eol^= %%B in ("%%~A") do (
echo[ into "%%~B"
echo[ equals "%%~C"
::# Convert the piped in data back into environment variables+values.
set "%%~B=%%~C" 2>NUL
)
echo[
)
echo[-------- END PIPEREADING --------
echo[-- Arguments received:
echo[ [%*]
call :testPrint
set "ERR=42"
echo[
::# to actually pause and/or wait for / react to user input(!) one needs to pipe in CON (console).
<con set /P ERR=Enter arbitrary exitcode / errorlevel:
endlocal & exit /B %ERR%
Notes:
- see mklement0's notes.
- The
CMD /C '<batch-file_withEscaped'> & exit'
re-invocation technique isn't required if you consistently exit /b X
in your batch file. Then &\`\"%_selfBat%\`\"
instead of CMD /C ... & exit
is enough (with separately separated arguments: 'arg1','arg2'
).
'/D','/T:4F',
- Ignore CMD's registry AutoRun commands and set fore-/background colors to white on dark red.
echo[
instead of echo
is safer and quicker (cmd doesn't need to search for actual executables named echo.*
).
<con
is required in the elevated instance for anything needing user interaction (eg. pause
or set /P ...
). Without <con
the now empty(?) piped in standard input (pipe#0) still delivers nul
(?) to anything asking for it (my assumption). I'm sure there is a way too rescue stdin from the pipe and reattach it to con
(maybe some kinde of break
throu from in here).
barl
's backticks get mangled.
Escaping hell
Here are the dynamic middle and inner command lines to show whats going on and shave away some escaping magic:
powershell.exe -nol -nop -comm "Write-Output $([Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String(\"<<BASE64_BLOB>>\"))) | cmd.exe '/D','/U','/T:4F','/S','/C',' \"<<path\this.cmd_withEscaped'>>\" \"quoted argument\" nonQtdArg & exit'; exit $LastExitCode"
Even with just my default environment that command line is somewhere around 5kB big(!), thanks to the <<BASE64_BLOB>>
.
cmd.exe /D /U /T:4F /S /C " "<<path\this.cmd_withNormal'>>" "quoted argument" nonQtdArg & exit"