I have a complicated batch file where some parts need to run with elevated/admin rights (e.g. interacting with Windows services) and I found a Powershell way to do that:
powershell.exe -command "try {$proc = start-process -wait -Verb runas -filepath '%~nx0' -ArgumentList '<arguments>'; exit $proc.ExitCode} catch {write-host $Error; exit -10}"
But there's a huge caveat! The elevated instance of my script (%~nx0) starts with a fresh copy of environment variables and everything I set "var=content" before is unavailable.
This Powershell script doesn't help either because Verb = "RunAs" requires UseShellExecute = $true which in turn is mutually exclusive to/with StartInfo.EnvironmentVariables.Add()
$p = New-Object System.Diagnostics.Process
$p.StartInfo.FileName = "cmd.exe";
$p.StartInfo.Arguments = '/k set blasfg'
$p.StartInfo.UseShellExecute = $true;
$p.StartInfo.Verb = "RunAs";
$p.StartInfo.EnvironmentVariables.Add("blasfg", "C:\\Temp")
$p.Start() | Out-Null
$p.WaitForExit()
exit $p.ExitCode
And even if that would work I'd still need to transfer dozens of variables...
because circumventing the problem is no proper solution.
runas plus wait and errorlevel/ExitCode processing isn't possible with/in vbs).Here's a proof of concept that uses the following approach:
Make the powershell call invoke another, aux. powershell instance as the elevated target process.
This allows the outer powershell instance to "bake" Set-Item statements that re-create the caller's environment variables (which the outer instance inherited, and which can therefore be enumerated with Get-ChilItem Env:) into the  -command string passed to the aux. instance, followed by a re-invocation of the original batch file.
Caveat: This solution blindly recreates all environment variables defined in the caller's process in the elevated process - consider pre-filtering, possibly by name patterns, such as by a shared prefix; e.g., to limit variable re-creation to those whose names start with foo, replace Get-ChildItem Env: with Get-ChildItem Env:foo* in the command below.
@echo off & setlocal
:: Test if elevated.
net session 1>NUL 2>NUL && goto :ELEVATED 
:: Set sample env. vars. to pass to the elevated re-invocation.
set foo1=bar
set "foo2=none      done"
set foo3=3" of snow
:: " dummy comment to fix syntax highlighting
:: Helper variable to facilitate re-invocation.
set "thisBatchFilePath=%~f0"
:: Re-invoke with elevation, synchronously, reporting the exit
:: code of the elevated run.
:: Two sample arguments, ... and "quoted argument" are passed on re-invocation.
powershell -noprofile -command ^
  trap { [Console]::Error.WriteLine($_); exit -667 } ^
  exit ( ^
    Start-Process -Wait -PassThru -Verb RunAs powershell ^
      "\" -noprofile -command `\" $(Get-ChildItem Env: | ForEach-Object { 'Set-Item \\\"env:' + $_.Name + '\\\" \\\"' + $($_.Value -replace '\""', '`\\\""') + '\\\"; ' }) cmd /c '\`\"%thisBatchFilePath:'=''%\`\" ... \`\"quoted argument\`\" & exit'; exit `$LASTEXITCODE`\" \"" ^
  ).ExitCode 
echo -- Elevated re-invocation exited with %ERRORLEVEL%.
:: End of non-elevated part.
exit /b
:ELEVATED
echo Now running elevated...
echo -- Arguments received:
echo [%*]
echo -- Env. vars. whose names start with "foo":
set foo 
:: Determine the exit code to report.
set ec=5
echo -- Exiting with exit code %ec%...
:: Pause, so you can inspect the output before exiting.
pause
exit /b %ec%
Note:
trap { [Console]::Error.WriteLine($_); exit -667 } handles the case where the user declines the elevation prompt, which causes a statement-terminating error that the trap statement catches (using a try / catch statement around the Start-Process call is also an option, and usually the better choice, but in this case trap is syntactically easier).
Specifying pass-through arguments (arguments to pass directly to the re-invocation of the (elevated) batch file, after the cmd /c '\`\"%thisBatchFilePath:'=''%\`\" part above):
', you must double them ('')'\`\"...\`\" (sic), as shown with \`\"quoted argument\`\" above.The cmd /c '<batch-file> & exit' re-invocation technique is required to ensure robust exit-code reporting, unfortunately - see this answer for details.
The explicit exit $LASTEXITCODE statement after the batch-file re-invocation is required to make the PowerShell CLI report the specific exit code reported by the batch file - without that, any nonzero exit code would be mapped to 1. See this answer for a comprehensive discussion of exit codes in PowerShell.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With