Sudo for PowerShell – or – I meant to open the shell with elevated privileges!

In this article, our guest blogger—Brent Challis—provides an example of how to emulate the Linux sudo (super user do) command in PowerShell.


I am confident that I am not the only person to experience the problem of opening a PowerShell console to execute a command and getting:

PS C:\>Start-Service MSSQLServer
Start-Service : Service 'SQL Server (MSSQLSERVER) (MSSQLServer)' cannot be started due to the following error: Cannot open MSSQLServer service on computer '.'.
At line:1 char:1
+ Start-Service MSSQLServer
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OpenError: (System.ServiceProcess.ServiceController:ServiceController) [Start-Service], ServiceCommandException
    + FullyQualifiedErrorId : CouldNotStartService,Microsoft.PowerShell.Commands.StartServiceCommand
 

PS C:\> 

These are the joys of needing to run certain commands in a shell run as The Administrator. Somewhat irritating but unavoidable. 

A colleague, who is a keen Linux person, introduced me to the sudo (super user do) command, which can execute a command as the super user. My immediate reaction was that there must be a way to emulate this in PowerShell, and here is my approach.

The Start-Process command has a -Verb parameter. If you use runas as the verb on its own, the executable will launch with elevated permissions. You will be prompted by the UAC to confirm the execution.

The most straightforward solution to my problem would be:

function sudo
{
    $commandLine = (Get-History | Select-Object -Last 1).CommandLine
    Start-Process -FilePath powershell.exe -ArgumentList $commandLine -Verb runas
}

This would then run the last command as The Administrator.

There are, obviously, problems with this: 

  1. The name does not conform to the Verb-Noun convention.
  2. There is no flexibility in what it does.
  3. The shell to be launched is hard coded and launches PowerShell 5.

My solution is a command called Invoke-CCCommandAsTheAdministator. The CC prefix on the noun indicates that it is in the ChallisConsultancy module. While the name is verbose, intellisense helps.

The objectives for my version of the command were:

  1. To define a Linux-compatible alias, sudo.
  2. To enable the command to execute another command with elevated permissions.
    sudo Start-Service MSSQLServer
    sudo wsl –shutdown
  3. To support the use of ! to run the last command.
    sudo !
  4. To support the use of !! to display the history of commands so that I can select the one that I want to run.
    sudo !!

The function is defined as:

function Invoke-CCCommandAsTheAdministrator
{
    [CmdletBinding()]
    [Alias("sudo")]

This means that the alias is part of the command, so individual users do not need to define their own aliases. This provides compatibility with Linux in the same way the alias of ls for Get-ChildItem does. (The alias sudo is used throughout the article as it matches my intended use, makes my typing easier, and keeps the sample code shorter.)

Now when I forget to open an elevated shell, the solution is straightforward:

PS C:\>Start-Service MSSQLServer
Start-Service : Service 'SQL ServerSQLSERVER) (MSSQLServer)' cannot be started due to the following error: Cannot open MSSQLServer service on 
computer '.'.
At line:1 char:1
+ Start-Service MSSQLServer
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OpenError: (System.ServiceProcess.ServiceController:ServiceController) [Start-Service], ServiceCommandException
    + FullyQualifiedErrorId : CouldNotStartService,Microsoft.PowerShell.Commands.StartServiceCommand
 

PS C:\>sudo ! 

As I am writing this for myself, which makes the coding as much a journey as it is a destination, I can explore other functionality without being concerned with feature creep—which is a legitimate concern for developers working within the constraints of formal projects. So, to the starting version of the command, I added some more requirements:

  1. Select which edition of PowerShell to use: Desktop, Core, or use the same one as the calling shell.
  2. Specify whether or not to automatically close the elevated shell after the command has executed.
  3. Specify whether or not the calling shell will block until the elevated shell is closed. This would mean that the command could be used in a script which needs to wait until the elevated shell closes before moving to the next line of code.
  4. Support using sudo to simply launch an elevated shell.

This command must be able to process any command entered by the user. As a developer, the code customarily asks the user for the information it needs. In this case, the user tells my code what needs to be done. Hence, as I do not know what is going to be in the command line—as it will be controlled by the user—I can simply illustrate some possible responses. It could be:

  • sudo launch an elevated shell
  • sudo ! launch an elevated shell and run the last command
  • sudo !! launch an elevated shell and run the selected command
  • sudo Start-Service MSSQLServer launch an elevated shell running the Start-Service command to start the SQL Server instance
  • sudo Start-Service -Name MSSQLServer same as above but with more elements on the line
  • sudo Start-Service -Name MSSQLServer -Verbose same as above but with even more elements on the line to demonstrate the uncertainty regarding the input
  • sudo wsl –shutdown reboot my Linux installations based on Windows Support for Linux

My approach to dealing with this is a defined parameter Command at position 0 to collect the first element and then CommandLine to collect all the entries that are not associated with parameters:

Param
    (
        [Parameter(Position=0)]
        [String]$Command,
        [parameter(ValueFromRemainingArguments=$true)]
        [String[]]$CommandLine

Having collected the first entry from the command line, it could be:

  1. Blank or Null
  2. !
  3. !!
  4. Start-Service (for example)

To deal with these options, I create a parsedCommandLine from the value of the Command Parameter and the CommandLine collection of entries that are not associated with parameters:

$parsedCommandLine = ""
switch($Command)
{
"!" {$parsedCommandLine = (Get-History | Select-Object -Last 1).CommandLine}
"!!" {$parsedCommandLine = (Get-History | Sort-Object -Property iD 
       -Descending |Select-Object -Property CommandLine | 
Out-GridView -OutputMode Single -Title "Select Command").CommandLine}
default {$parsedCommandLine = ($Command + " " + ($CommandLine -join " ")).Trim()}
}

I could have used Select-ItemFromList (a command that I wrote before Out-GridView supported selections) and have it display a text menu to enable the selection of a command from the command history. Instead, I used Out-GridView as it was convenient.

This provided the functionality that I initially wanted. 

To get a better understanding of the options available when launching the shells I used:

PowerShell -?
and
pwsh -?

These shells allow me to see what options exist and then create parameters to support my desired functionality.

The full parameter block definition is:

Param
(
    [Parameter(Position=0)]
    [String]$Command,
    [Alias("noe")]
    [Switch]$NoExit,
    [Alias("wfc")]
    [Switch]$WaitForCompletion,
    [ValidateSet("Default","Desktop","Core")]
    [String]$PowerShellEdition = "Default",
    [parameter(ValueFromRemainingArguments=$true)]
    [String[]]$CommandLine
)

If the PowerShellEdition is defined as Default I determine what shell is being used by:

if ($PowerShellEdition -eq "Default")
{
	$PowerShellEdition = $PSVersionTable["PSEdition"]
}

Then I process the result by starting the appropriately identified shell with the constructed argument:

Process
{
	switch ($PowerShellEdition)
	{
		"Core" {
			Start-Process -FilePath pwsh.exe -ArgumentList ($noExitValue +
				" -Command " + $parsedCommandLine) -Wait:$WaitForCompletion
			-Verb runas
		}
		"Desktop" {
			Start-Process -FilePath powershell.exe -ArgumentList
			($noExitValue + " " + $parsedCommandLine)
			-Wait:$WaitForCompletion -Verb runas
		}
	}
}

And, of course, no command is complete without comment-based help. I used the command:
    New-CCCommentBasedHelp -Name Invoke-CCCommandAsTheAdministrator

This allowed me to create a skeleton that includes entries for each parameter and populates it with information from attributes and the code for the command and parameters. This is the skeleton as the starting point:

<#
.SYNOPSIS
Invoke - A brief description of what the function Invoke-CCCommandAsTheAdministrator does. 
    This keyword can be used only once in each topic.
.DESCRIPTION
Invoke - A detailed description of what the function Invoke-CCCommandAsTheAdministrator does. 
    This keyword can be used only once in each topic.
.PARAMETER Command
	Parameter Description
.PARAMETER NoExit
	Aliases: noe
	Parameter Description
.PARAMETER WaitForCompletion
	Aliases: wfc
	Parameter Description
.PARAMETER PowerShellEdition
	Valid values: Default, Desktop, Core
	Default Value: "Default"
	Parameter Description
.PARAMETER CommandLine
	Parameter Description
.EXAMPLE
	Invoke-CCCommandAsTheAdministrator -Command -NoExit -WaitForCompletion -PowerShellEdition -CommandLine 
.INPUTS
    The Microsoft .NET Framework types of objects that can be piped to the
    function or script. You can also include a description of the input 
    objects.
.OUTPUTS
.NOTES
    Command Alias: "sudo"
    Confirm Impact: Medium
    Supports Positional Binding
#>

The final help block is:

<#
.SYNOPSIS
    Invoke-CCCommandAsTheAdministrator is designed to run a command with the
    elevated permissions of The Administrator
.DESCRIPTION
    When running Invoke-CCCommandAsTheAdministrator, or using its alias of sudo,
    the command identified will be executed in another PowerShell process
    which will run as The Administrator.
 
    You will be asked for permission to use the elevated context by the UAC.
.PARAMETER Command
	This identifies the command to be executed with elevated permissions.
    There are some special options:
        !   Execute the previous command without waiting for the new process
            to complete before returning control.  The new powershell process
            does not automatically exit.
        !!  Displays the history of commands and will execute the selected 
            command without waiting for the new process to complete before 
            returning control.  The new powershell process does not 
            automatically exit.
.PARAMETER NoExit
	Aliases: noe
	When this switch is used the elevated shell does not close after completing
    the command.
.PARAMETER WaitForCompletion
	Aliases: wfc
	This switch causes the calling script to block until the elevated shell
    completes the command and exits - either automatically or manually if 
    called with the NoExit switch.
.PARAMETER PowerShellEdition
	Valid values: Default, Desktop, Core
    Default Value: "Default"
	This parameter defines which shell to use.  If set to Default the same 
    shell as the calling shell will be used.
.PARAMETER CommandLine
	This parameter is designed to collect the rest of the command line to 
    enable the Invoke-CCCommandAsTheAdministrator command to be used to launch
    an elevated shell and run a specific command.
.EXAMPLE
	Invoke-CCCommandAsTheAdministrator -Command Start-Service MSSQLServer -NoExit -WaitForCompletion -UseWindowsPowerShell
.EXAMPLE
    Invoke-CCCommandAsTheAdministrator !
    Execute the previous command as The Administrator using the same shell as the
    calling environments and do not exit the new Powershell process.
.EXAMPLE
    sudo Start-Service MSSQLServer -NoExit -WaitForCompletion -PowerShellEdition Core -Verbose
    will run PowerShell Core to start the SQL Server service.  The elevated shell will
    not close and the calling shell will block until the elevated shell is closed
    manually.
    The Verbose message will be:
    VERBOSE: Running Core edition to execute Start-Service MSSQLServer, The calling code will wait until the shell
    closes.. The shell needs to be manually closed.
.NOTES
    Confirm Impact: Medium
    Supports Positional Binding
    This command has an alias of sudo (Linux 'Super User Do').
#>

I have found this to be very useful and hope you do as well. 

For me, this is an example of how readily PowerShell can be customized to suit individual users’ needs and extend to provide the desired functionality. 

Happy scripting.

P. Brent Challis MTech, MCT, MACS (Snr)


We love to hear from you! Please comment below, or in our feedback forum and reference this post.