I have a self elevate snippet which is quite wordy, so I decided instead of duplicating it at the top of every script that needs to be run as admin to move it into a separate .ps1:
function Switch-ToAdmin {
# Self-elevate the script if required
if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')) {
if ([int](Get-CimInstance -Class Win32_OperatingSystem | Select-Object -ExpandProperty BuildNumber) -ge 6000) {
$Cmd = @(
"-Command Set-Location `"$(Get-Location)`"; & `"$PSCommandPath`""
"-$($PSBoundParameters.Keys)"
)
$ProcArgs = @{
FilePath = 'PowerShell.exe'
Verb = 'RunAs'
ArgumentList = $Cmd
}
Start-Process @ProcArgs
Exit
}
}
}
So for every script that needs elevation I'd prepend
. "$PSScriptRoot\self-elevate.ps1"
Switch-ToAdmin
# rest of script
Doing above successfully procs the UAC prompt, but the rest of the script won't get executed. Is this sorta stuff disallowed?
Darin and iRon have provided the crucial pointers:
Darin points out that the automatic $PSCommandPath
variable variable in your Switch-ToAdmin
function does not contain the full path of the script from which the function is called, but that of the script in which the function is defined, even if that script's definitions are loaded directly into the scope of your main script via .
, the dot-sourcing operator.
$PSScriptRoot
variable, which reflects the defining script's full directory path.Also, more generally, the automatic $PSBoundParameters
variable inside a function reflects that function's bound parameters, not its enclosing script's.
iRon points out that the Get-PSCallStack
cmdlet can be used to get information about a script's callers, starting at index 1
; the first object returned - index 0
, when Get-PSCallStack
output is captured in an array, represents the current command. Index 1
therefore refers to the immediate caller, which from the perspective of your dot-sourced script is your main script.
Therefore:
Replace $PSCommandPath
with $MyInvocation.PSCommandPath
, via the automatic $MyInvocation
variable. $MyInvocation.PSCommandPath
truly reflects the caller's full script path, irrespective of where the called function was defined.
(Get-PSCallStack)[1].ScriptName
, which despite what the property name suggests, returns the full path of the calling script too.Replace $PSBoundParameters
with (Get-PSCallStack)[1].InvocationInfo.BoundParameters
(Get-PSCallStack)[1].Arguments
, but it seems to contain a single string only, containing a representation of all arguments that is only semi-structured and therefore doesn't allow robust reconstruction of the individual parameters.As an aside:
Even if $PSBoundParameters
contained the intended information, "-$($PSBoundParameters.Keys)"
would only succeed in passing the bound parameters through if your script defines only one parameter, if that parameter is a [switch]
parameter, and if it is actually passed in every invocation.
Passing arguments through robustly in this context is hard to do, and has inherent limitations - see this answer for a - complex - attempt to make it work as well as possible.