2013-01-25

Call command-line in PowerShell

Often I have to build a command-line statement dynamic and then execute the statement.
The PowerShell call operator (&) can be cumbersome to use, but following some simple rules or using a general template can do the trick. This operator is also called the invoke operator.

Parameters

When using the call operator you have to remember it executes the string to the first space, and because of this it can not take a command-line string with parameters like „ping www.sqladmin.info -n 5“.
A solution to this is given by Devlin Bentley in the blog entry „PowerShell Call Operator (&): Using an array of parameters to solve all your quoting problems“.

In general the trick is to call the command with the operator and put parameters in an additional array.
[String]$cmd = 'ping'
$cmd += '.exe'
[String[]]$param = @('www.sqladmin.info','-n','5')
& $cmd $param

A output of this could be
Pinging www.sqladmin.info [212.97.133.23] with 32 bytes of data:
Reply from 212.97.133.23: bytes=32 time=16ms TTL=120
Reply from 212.97.133.23: bytes=32 time=24ms TTL=120
Reply from 212.97.133.23: bytes=32 time=23ms TTL=120
Reply from 212.97.133.23: bytes=32 time=20ms TTL=120
Reply from 212.97.133.23: bytes=32 time=19ms TTL=120

Ping statistics for 212.97.133.23:
Packets: Sent = 5, Received = 5, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
Minimum = 16ms, Maximum = 24ms, Average = 20ms


By putting the command in a string by itself, it can be build in script with path and so on. I have tried to illustrate this by adding the file extension in a seperate line of the script above.

The parameter values can be defined element by element into a parameter array.
[String]$cmd = 'ping'
$cmd += '.exe'
[String[]]$param = @()
$param += 'www.sqladmin.info'
$param += '-n'
$param += '5'
& $cmd $param


Catch output

To catch the output line by line it can be taken from the output stream.
[String]$cmd = 'ping'
$cmd += '.exe'
[String[]]$param = @('www.sqladmin.info','-n','5')
& $cmd $param |
ForEach-Object { "{0:s}Z $($_)" -f $([System.DateTime]::UtcNow) }

A output of this could be
2013-03-26T13:10:39Z
2013-03-26T13:10:39Z Pinging www.sqladmin.info [212.97.133.23] med 32 byte data:
2013-03-26T13:10:39Z Reply from 212.97.133.23: byte=32 time=3ms TTL=118
2013-03-26T13:10:40Z Reply from 212.97.133.23: byte=32 time=2ms TTL=118
2013-03-26T13:10:41Z Reply from 212.97.133.23: byte=32 time=3ms TTL=118
2013-03-26T13:10:42Z Reply from 212.97.133.23: byte=32 time=2ms TTL=118
2013-03-26T13:10:43Z Reply from 212.97.133.23: byte=32 time=2ms TTL=118
2013-03-26T13:10:43Z
2013-03-26T13:10:43Z Ping statistics for 212.97.133.23:
2013-03-26T13:10:43Z Packets: Sent = 5, Recieved = 5, Lost = 0 (0% loss),
2013-03-26T13:10:43Z Approximate round trip times in milli-seconds:
2013-03-26T13:10:43Z Minimum = 2ms, Maximum = 3ms, Average = 2ms


Be aware of large amounts of output. I have experienced that several thousand lines of output will make the script execution unstable in a unpredictable way.

Exception

A simple exception handling with Try-Catch-Finally blocks could be like
[String]$cmd = 'ping'
$cmd += '.exe'
[String[]]$param = @('www.sqladmin.info','-n','5')
try {
  & $cmd $param |
  ForEach-Object { "{0:s}Z $($_)" -f $([System.DateTime]::UtcNow) }
}
catch {
  "{0:s}Z ERROR: $($_.Exception.Message)" -f $([System.DateTime]::UtcNow)
}

If the command in the variable $cmd is change to „noping.exe“, the output will like
2013-03-29T14:42:31Z ERROR: The term 'noping.exe' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.

Trap

In the beginning PowerShell only had the Trap command, but it had some limitations. Passing on a error from a Trap can be done with the Throw.
It is generally recommended to use Try-Catch-Finally that we got with PowerShell v2.
Jeffrey Snover shared a very short but precise comparison in „Traps vs Try/Catch“.

Error

The automatic variable $LastExitCode contains the exit code of the last program that was executed.
This is nice in a simple setup, but sometimes there are several errors from one program. In such case I would like to know all errors and their details.
The automatic variable $Error is an array of all errors (System.Management.Automation.ErrorRecord) in the current PowerShell session. The latest error is the first element in the array ($Error[0]).
This is a simple demonstration how $Error can be used:
$Error.Clear()

try {
  throw [System.DivideByZeroException]
}
catch {
  $PSItem.CategoryInfo
}
finally {
  ":: Final"
}

if ($Error) {
  "{0:s} Error in script execution. See log for details." -f $([System.DateTime]::Now)
  if ($Error[0].Exception -match 'System.DivideByZeroException') {
    "-- Divide-By-Zero Exception"
  }
}

"$($Error.Count) Error(-s)."


The result will be something like this:
Category : OperationStopped
Activity :
Reason : RuntimeException
TargetName : System.DivideByZeroException
TargetType : RuntimeType

:: Final
2014-01-27T20:45:29 Error in script execution. See log for details.
-- Divide-By-Zero Exception
1 Error(-s).


When building/developing a script $Error is a great tool to look into an error.
Details on the exception itself can be very usefull. This you can get by piping the Exception to the CmdLet Get-Member. The professional lazy scripting guy can use the alias:
$error[0].Exception | gm

Injection attack

Several blog posts and forum answers uses the cmdlet Invoke-Expression, but please notice that this could make the script open to injection attacs. This is described by the Windows PowerShell Team in the blog post „Invoke-Expression considered harmful“.

Running Executables

This subject is already described in a TechNet Wiki article: „PowerShell: Running Executables“.
The article covers several other methods to call a command-line in PowerShell, but I will for now stay with the call operator.

History

2014-01-27 : Exception handling added note on Trap. Error section added.
2013-03-29 : Exception handling example added.
2013-03-26 : The parts on piping the output and defining the parameter values element by element are added.