3

My Windows Batch shall be started by the user without administrator privileges. At some step, it shall call itself with elevated privileges. I have learned that this is possible using the PowerShell's runas feature (batch.bat ⭢ PowerShell ⭢ batch.bat). This works like a charm.

Unfortunately, I am not able to receive the exit code from the elevated batch execution. I always get 1, although there is not any error message. I have no idea at which return the exit code gets lost, 1st (batch back to PowerShell) or 2nd (PowerShell back to batch).

I believe, I have tried all of the plenty suggested answers from similar questions, but apparently I am unable to get it going. I need advice.

MVE which should indicate that the elevated batch returns 0:

@echo off
echo param=%~1
openfiles /local >nul 2>&1

IF %ERRORLEVEL% EQU 0 (
    echo elevated, exit 0
    pause
    exit 0
) ELSE (
    echo not elevated. trying to elevate.
    powershell start-process -wait -verb runas '%0' -argumentlist /foo
    echo powershell returned %errorlevel%.
)

Nota bene (edited to eliminate misunderstanding): while the non-elevated call (by the user) does not require any parameter, the elevated call introduces an additional parameter '/foo'. This makes things worse for me because I did not find a solution to not lose this parameter. However, this appears to be a rather unusual use case.

Twonky
  • 796
  • 13
  • 31

2 Answers2

2

To solve the argument problem, you could use

powershell start-process -wait -verb runas '%0' -argumentlist '/additional-arg %*'

The exit code problem:
The first problem is the line

echo powershell returned %errorlevel%.

This can't work, because it's inside a code block and %errorlevel% will be expanded even before powershell will be called and therefore it is always 1 - the result of openfiles /local ...

But even with delayed expansion, I got always 0, probably because it's the exitcode of the successful runas, that it was able to start your batch.

You could use a work around and store the exitcode in a temporary file

@echo off
setlocal EnableDelayedExpansion
echo param=%*
openfiles /local >nul 2>&1

IF %ERRORLEVEL% EQU 0 (
    echo elevated, exit 13
    pause
    echo 13 > "%temp%\_exitcode.tmp"
    rem *** using 14 here to show that it doesn't be stored in errorlevel
    exit 14
) ELSE (
    echo not elevated. trying to elevate.
    powershell start-process -wait -verb runas '%0' -argumentlist '/additional-arg %*'
    set /p _exitcode= < "%temp%\_exitcode.tmp"
    del "%temp%\_exitcode.tmp"
    echo powershell returned !_exitcode!, lvl !errorlevel!.
)
jeb
  • 78,592
  • 17
  • 171
  • 225
  • Sorry. I removed the `EnableDelayedExpansion` to make the example _minimal_ after "finding out" that it had no influence. I noticed I was wrong. Next, I learned from your answer and your post at [another question](https://stackoverflow.com/a/14347131/2457149) that I found the bbb (batch beginner bug) and need the exclamation mark expansion. However, I do not like the proposed solution to create the temp file for this appears rather hacky to me, but well. It works at least and that's great! – Twonky Jan 16 '20 at 13:20
  • @Twonky You are right, it's only a workaround. I tested it with `powershell ... -PassThru` but with no better results – jeb Jan 16 '20 at 13:27
  • Moved `_exitcode.tmp` to `%temp%` and guarded the `set` command with `IF EXISTS`. Considering this solution good enough! Thanks, you're great! – Twonky Jan 16 '20 at 14:48
0

You aren't putting the PowerShell commands to execute in quotes, and you would do well to use the full path as well as include any arguments to the script. A generic way to invoke this, so it could be copied across scripts, your PowerShell invocation should look like so:

powershell -c "if([bool]'%*'){ Start-Process -Wait -Verb runas '%~dpnx0' -ArgumentList ('%*' -split '\s+') } else { Start-Process -Wait -Verb runas '%~dpnx0' }"

For your needs above, this could be simplified since you know you have arguments passed into the batch file to process:

powershell -c "Start-Process -Wait -Verb runas '%~dpnx0' -ArgumentList '/foo'
  • %~dpnx0 - Automatic batch variable, this is the full path to the current script, including the script name
  • %* - Automatic batch variable, this is all arguments passed into the script.
  • ('%*' -split '\s'): This is a PowerShell expression takes the space-delimited %* variable and splits it on continuous whitespace, returning an array. For simplicity this does have a shortcoming in that it will split on spaces in between double quotes, but the regex can be tuned to account for that if needed.

This answer is worth a read for other automatic batch variables you may find use for in the future.

codewario
  • 19,553
  • 20
  • 90
  • 159
  • This could be a comment to optimize the use of `%~dpnx0` or better use `%~f0`. But it doesn't solve the problem at all. Btw `''%~dpnx0 %*'"` fails with if any parameter exists – jeb Jan 15 '20 at 16:57
  • Ah yeah I see my mistake, i'll fix it – codewario Jan 15 '20 at 17:01
  • Enlightening explanation on variables appreciated! Minor misunderstanding: my use case requires the additional parameter `/foo` being introduced by the elevated call, so the simplified solution would be `powershell -c "Start-Process -Wait -Verb runas '%~dpnx0' -ArgumentList /foo"` (mind the closing `"` which was missing in your post). But this is probably only my specialized use case and passing all arguments transparently is the more common case. Unfortunately, your generalized suggestion does not work. Even with `EnableDelayedExpansion` I get `1` as exit code instead of `0`. – Twonky Jan 16 '20 at 09:07