Working with external utilities

Sometimes, PowerShell cmdlets are not enough. Although this rarely is the case nowadays, there are times you may need to resort to external utilities. This is especially valid for PowerShell Core on different operating systems with different sets of binaries.

Consider the following example: you are tasked with creating local users on a set of Windows and Linux machines with the least amount of code. You know that all machines have enabled PowerShell remoting:

# Sample 1 - Creating users on Windows and Linux hosts
$windowsMachines = 'WindowsHost1', 'WindowsHost2'
$linuxMachines = 'LinuxHost1', 'LinuxHost2'

$username = 'localuser1'
$password = 'P@ssw0rd' | ConvertTo-SecureString -AsPlainText -Force
$newCredential = New-Object -TypeName pscredential $userName, $password

$linuxSessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
$sessions = New-PSSession $windowsMachines -Credential (Get-Credential)
$sessions += New-PSSession $linuxMachines -UseSSL -SessionOption $linuxSessionOptions -Authentication Basic -Credential (Get-Credential)

Invoke-Command -Session $sessions -ScriptBlock {
param
(
$Credential
)
if ($PSVersionTable.PSEdition -eq 'Desktop' -and $PSVersionTable.PSVersion -ge 5.1)
{
New-LocalUser -Name $Credential.UserName -Password $Credential.Password -ErrorAction Stop
}
elseif ($PSVersionTable.PSEdition -eq 'Core' -and $IsLinux)
{
$userCreation = Start-Process -FilePath '/sbin/useradd' -ArgumentList $Credential.UserName -Wait -NoNewWindow -PassThru

if ($userCreation.ExitCode -ne 0)
{
Write-Error -Message "Failed to create $($Credential.UserName)"
return
}

$Credential.GetNetworkCredential().Password | passwd --stdin $Credential.UserName
}
} -ArgumentList $newCredential

In the example, we are using the external useradd tool on Linux, and New-LocalUser on Windows, using Windows PowerShell. Both the cmdlet and executable take their parameters from the credential object that has been created. While we can make use of internal error handling with the PowerShell cmdlet on Windows, we need to examine the exit code of the process on Linux to decide whether to continue or not.

Instead of using Start-Process, we could have used the following code as well:

/usr/sbin/useradd $Credential.UserName
if (-not $?)
{
Write-Error -Message "Failed to create $($Credential.UserName)"
return
}
$Credential.GetNetworkCredential().Password | passwd --stdin $Credential.UserName

Other examples where you want to use PowerShell to control external commands are the Windows classics such as robocopy, diskpart, and msiexec. All expect certain command-line parameters and switches that can just as well be passed using PowerShell. The results can be captured and processed further.

The first example we would like to look at just calls the process inline with all necessary arguments, without bothering to check for an exit code:

# Sample 2 - External commands with Start-Process
$executable = 'msiexec' # Executable in PATH
$arguments = @( # An array of commandline arguments
'/i "C:TempmyMsiFile.msi"'
'/l*v "D:Some log foldermyMsiFile.log'
'/quiet'
'/norestart'
)

# Sample 3 - External commands inline

$logPath = [System.Io.Path]::GetTempFileName()
robocopy C:Temp C:Tmp /S /E /LOG:$logPath # Variables may appear inline

# Sample 4 - Redirecting (cmdlet) output to external commands
$diskpartCmd = 'LIST DISK'
$disks = $diskpartCmd | diskpart.exe
$disks # Contains the string[] that diskpart.exe returned

In the next example, we will make use of redirection to redirect STDOUT and STDERR to a file or to combine the error stream into the output stream:

# Redirect STDERR to err.txt and STDOUT to out.txt
Get-Item $PSHome,C:DoesNotExist 2>err.txt 1>out.txt
Get-Content out.txt # Displays the one folder $PSHome that was accessible
Get-Content err.txt # Displays the error record that was captured

# Some commands fill the wrong streams

# Success output lands in the error stream
$outputWillBeEmpty = git clone https://github.com/AutomatedLab/AutomatedLab.Common 2>NotAnError.txt
$outputWillBeEmpty -eq $null

# Instead of redirecting the error to a file, combine it into the output stream
$outputIsFine = git clone https://github.com/AutomatedLab/AutomatedLab.Common 2>&1
$outputIsFine

Lastly, we will create a proper process, which should become your preferred way of creating new processes unless you encounter problems. The main benefit is that you can better react to the life cycle of your process by waiting for it to exit, redirecting its output, and reacting to its exit code. Passing arguments this way improves the readability of your code, which will ultimately make it easier to maintain:

$executable = 'msiexec' # Executable in PATH
$arguments = @( # An array of commandline arguments
'/i "C:TempmyMsiFile.msi"'
'/l*v "D:Some log foldermyMsiFile.log'
'/quiet'
'/norestart'
)

# Fork a new process, wait for it to finish while not creating a new window
# We use PassThru to capture the result of our action
$installation = Start-Process -FilePath $executable -ArgumentList $arguments -Wait -NoNewWindow -PassThru

# Evaluate standard MSI exit code 0 (OK) and 3010 (Reboot Required)
if ($installation.ExitCode -notin 0, 3010)
{
Write-Error -Message "The installation failed with exit code $($installation.ExitCode)"
}

You can see in the example that the cmdlet Start-Process cmdlet is used to fork a new process with an array of arguments passed to it. We refrain from creating a new window and wait for it to exit. PassThru passes System.Diagnostics.Process back to the caller so that we can easily retrieve the exit code afterwards.

Many cmdlets have the switch -PassThru—be sure to use it immediately to work with objects you created, changed, or removed!

The Start-Process cmdlet has one weakness: redirecting the standard output, error, and input works only with files on disk. If you would like to collect the output and error stream while foregoing creating files entirely, why not create System.Diagnostics.Process with the proper options directly? Take a look at the following code for one way of accomplishing this:

$process = New-Object -TypeName System.Diagnostics.Process

# The ProcessStartInfo is the relevant part here
$startInfo = New-Object -TypeName System.Diagnostics.ProcessStartInfo
$startInfo.RedirectStandardError = $true
$startInfo.RedirectStandardOutput = $true
$startInfo.UseShellExecute = $false # This is a requirement to redirect the streams.
$startInfo.FileName = 'git.exe'
$path = [System.IO.Path]::GetTempFileName() -replace '.tmp'
$startInfo.Arguments = 'clone "https://github.com/AutomatedLab/AutomatedLab.Common" {0}' -f $path

$process.StartInfo = $startInfo
$process.Start()

# Read all output BEFORE waiting for the process to exit
# otherwise you might provoke a hang
$errorOutput = $process.StandardError.ReadToEnd()
$standardOutput = $process.StandardOutput.ReadToEnd()

$process.WaitForExit()

if ($process.ExitCode -ne 0)
{
Write-Error -Message $errorOutput
return
}

# In case of git, the success output is on the error stream
Write-Verbose -Message $errorOutput
# In most cases, the standard output should be fine however
Write-Verbose -Message $standardOutput
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset