Tuesday, February 22, 2011

Executing Native Commands in PowerShell

I love PowerShell! I've been scripting in PowerShell almost exclusively for almost three years now. PowerShell's scripting environment is extremely powerful, exposing .NET, COM, WMI, as well as native commands to the scripter.

I try to answer questions on the PowerShellCommunity forums as often as I can. One fairly common question is how do we execute native commands. Of course, posters don't ask the question in that fashion. Typically, they are trying to execute a command and they're trying to use Invoke-Item, Invoke-Expression, Invoke-Command, or some other facility to execute the command. What they are doing doesn't work and they need help. I almost always give the same advice, call the command directly.

Usually the root cause of the problem is that the command they are trying to execute contains spaces. To execute a command in a BATCH script with spaces you simply wrap the command in double quotes.

"C:\Program Files\7-Zip\7z.exe" l C:\temp\7za920.zip

This doesn't work in PowerShell causing many people to try using Invoke-Command, Invoke-Item, Invoke-Expression, start, cmd, and even [System.Diagnostics.Process] to try to execute their command. All these methods can be used to accomplish the goal but they add additional complexity that we don't need.

PowerShell knows how to execute native commands directly. We can see this when we attempt to execute the tasklist native command.

PS C:\> tasklist /fi "imagename eq explorer.exe" /fo list

Image Name:   explorer.exe
PID:          4012
Session Name: Console
Session#:     0
Mem Usage:    28,248 K
PS C:\>

So what happens when the command to execute is wrapped in quotes? PowerShell interprets the command as a string literal. A string literal is an expression in PowerShell. String literals can be combined in to larger expressions with the use of operators.

PS C:\> "red brick" -match "red"
True
PS C:\>

In that expression -match is the operator linking two separate string literals into a larger expression. The native command example above has a string literal followed by another, implicit, string literal resulting in a parse error.

PS C:\> "C:\Program Files\7-Zip\7z.exe" l C:\temp\7za920.zip
Unexpected token 'l' in expression or statement.
At line:1 char:34
+ "C:\Program Files\7-Zip\7z.exe" l <<<<  C:\temp\7za920.zip
    + CategoryInfo          : ParserError: (l:String) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : UnexpectedToken
 
PS C:\>

The PowerShell designers knew that users would need to execute commands like this so they added a special operator to interpret string literals as commands, the call operator (&). See Get-Help about_operators for details. Executing our command using the call operator is simple.

PS C:\> & "C:\Program Files\7-Zip\7z.exe" l C:\temp\7za920.zip

7-Zip 4.65  Copyright (c) 1999-2009 Igor Pavlov  2009-02-03

Listing archive: C:\temp\7za920.zip


   Date      Time    Attr         Size   Compressed  Name
------------------- ----- ------------ ------------  ------------------------
2010-11-18 08:08:04 ....A        91020        83925  7-zip.chm
2010-11-18 08:27:33 ....A       587776       299189  7za.exe
2010-03-13 00:06:33 ....A         1162          544  license.txt
2010-11-18 08:09:09 ....A         1254          644  readme.txt
------------------- ----- ------------ ------------  ------------------------
                                681212       384302  4 files, 0 folders
PS C:\>

The call operator is needed whenever the command to execute, and only the command, is represented by an expression. We could calculate the path to the 7z.exe native command using the ProgramFiles environment variable and execute the command in a single command.

PS C:\> & $(Join-Path -Path $env:ProgramFiles -ChildPath 7-Zip\7z.exe) l C:\temp\7za920.zip

We could have assigned the result of Join-Path to a variable and used the call operator to execute the command represented by the variable too.

PS C:\> $7z = Join-Path -Path $env:ProgramFiles -ChildPath 7-Zip\7z.exe
PS C:\> & $7z l C:\temp\7za920.zip

Always remember KISS, even with PowerShell.