Pointing out common pitfalls when running executables in PowerShell

Pointing out common pitfalls when running executables in PowerShell

Running Executables in PowerShell

How to avoid common pitfalls

Windows PowerShell is designed as an interactive Command-Line Interface (CLI). One of the primary purposes of a CLI is to let you run programs. However, I've lost count of the number of times I've seen questions like this: "I need to run the such-and-such command-line tool in PowerShell. I've tried quoting the parameters multiple ways without success. How do I get this program to run properly in PowerShell?"

Running an executable with correct quoting in Cmd.exe isn't a problem because Cmd.exe doesn't do any extra parsing of the executable's command line. If you're writing a Cmd.exe shell script (i.e., a batch file) that runs an executable, you can see exactly what the executable's command line will be by simply prefixing the executable's command line with Echo, which tells Cmd.exe to output the command line to the screen rather than run it. This is a simple and effective debugging technique.

However, PowerShell complicates this a bit because its command-line parser is more complex than Cmd.exe's parser out of necessity. The Echo command in PowerShell is really an alias for Write-Host, so you can't use the Echo command in PowerShell to view the exact command line like you can in Cmd.exe. The bottom line is that PowerShell lacks a built-in way to see an executable's exact command line.

To get around this limitation, I wrote a short command-line program named ShowArgs.exe. This program's purpose is to output its command-line parameters without any parsing or interpretation. By substituting ShowArgs.exe for the program you're trying to run (keeping your program's parameters), you can see the actual command-line parameters that PowerShell will use.

The ShowArgs.exe program and its source code are available for download by clicking the Download the Code button near the top of the page. The ShowArgs.zip file also contains the ShowArgs-GUI.exe program, which works the same way as ShowArgs.exe, except that it displays its command-line parameters in a popup dialog box instead of outputting them in the console window.

Using ShowArgs.exe, I'm going to show you how to work around the most common "how do I quote things correctly" problems when running executables in PowerShell. For the examples in this article, I created a directory named C:\Sample Tools and copied ShowArgs.exe into it.

Starting Executables in PowerShell

To run an executable in PowerShell, you just need to specify its name. This is the same as running an executable in Cmd.exe. For example, Figure 1 shows two examples of running ShowArgs.exe directly in PowerShell. In Figure 1, the .\ prefix is needed to run ShowArgs.exe because PowerShell doesn't run executables from the current directory by default.

Figure 1: Running an Executable Directly in PowerShell
Figure 1: Running an Executable Directly in PowerShell

If the executable's filename, path, or pathname doesn't contain spaces, using the call (&) operator is optional, as shown in Figure 2. Otherwise, the call operator is required.

Figure 2: Using the Call Operator Is Optional If There Are No Spaces in the Executable's Filename, Path, or Pathname
Figure 2: Using the Call Operator Is Optional If There Are No Spaces in the Executable's Filename, Path, or Pathname

However, you can't use the call operator to invoke an entire command line. Figure 3 illustrates this common mistake. The first command in Figure 3 fails because the quoted string after the call operator isn't a filename (just as the error message states). The second command in Figure 3 corrects this error. In this command, only the executable's name is placed in quotes, followed by the parameters.

Figure 3: Demonstrating a Common Error When Using the Call Operator
Figure 3: Demonstrating a Common Error When Using the Call Operator

You can capture an executable's output in a variable, as shown in Figure 4. The first command in Figure 4 runs Find.exe /? and captures the output in the $findHelp variable. The second command in Figure 4 shows that the variable contains an array, and the last command outputs the array's contents. (If the program outputs only one line, the variable will contain a single string rather than an array.)

Figure 4: Capturing an Executable's Output in a Variable
Figure 4: Capturing an Executable's Output in a Variable

Quoting Parameters in an Executable's Command Line

When a parameter contains spaces, you need to place that parameter in quotes. The quotes themselves aren't part of the parameter, which means you can quote parameters that don't contain spaces if desired, but the quotes are optional.

The following guidelines can help you avoid trouble when specifying executable parameters in PowerShell. All the examples in this section use ShowArgs.exe with hypothetical parameters. I encourage you to run these examples so that you'll see the exact command-line parameters that PowerShell will use.

Guideline 1. You only need to add quotes when you're specifying a parameter directly on the command line and that parameter contains spaces. Here's an example:

.\ShowArgs "Gil Bates"

This command behaves exactly as expected. PowerShell sees that the quoted string contains a space, and quotes it when passing it to the executable. There's no need to embed extra quotes in the string. In other words, you don't need to do any of the following:

.\ShowArgs "`"Gil Bates`""
.\ShowArgs '"Gil Bates"'
.\ShowArgs """Gil Bates"""

PowerShell will remove the extra quotes so that the parameter only has one set of quotes. Thus, the extra quotes don't accomplish anything, except making the command harder to read. If the parameter doesn't contain spaces, the quotes are optional.

Guideline 2. If you want to pass a variable as an executable's parameter, you can simply put the variable on the executable's command line. Here's an example:

$name = "Gil Bates"
.\ShowArgs $name

If the variable's content contains spaces, PowerShell will automatically add quotes. As with the previous example, you don't need to embed extra quotes.

Guideline 3. If a parameter uses an argument that is "attached" to the parameter (i.e., you have to write the parameter and its argument together, without an intervening space), you can quote the entire parameter, including its argument. (For those of you unfamiliar with the difference between parameters and arguments, a parameter is something specified after the command that directs its behavior, whereas an argument provides additional information for a parameter.) Alternatively, you can quote the parameter's argument separately. For example, the following commands are equivalent:

.\ShowArgs /name"Gil Bates"
.\ShowArgs "/nameGil Bates"

This also applies if the parameter and argument have a character between them, such as a colon (:) or an equals sign (=). In other words, the following two commands are equivalent:

.\ShowArgs /name:"Gil Bates"
.\ShowArgs "/name:Gil Bates"

These two commands are also equivalent:

.\ShowArgs /name="Gil Bates"
.\ShowArgs "/name=Gil Bates"

As before, embedding extra quotes is unnecessary.

Guideline 4. If you use a variable as a parameter's argument, you don't need to do any extra quoting, even if the variable's content has spaces. For example, all of the following commands will work correctly:

.\ShowArgs /name $name
.\ShowArgs /name$name
.\ShowArgs /name=$name
.\ShowArgs /name:$name

Guideline 5. If the parameter starts with a hyphen (-), the parameter's argument is connected to the parameter (no space between), and the parameter's argument is in a variable, you need to escape the initial hyphen with a back tick (`) or quote the entire parameter and connected argument. For example, the following command won't work correctly:

.\ShowArgs -name:$name

Instead, you need to use one of these commands:

.\ShowArgs `-name:$name
.\ShowArgs "-name:$name"

This rule applies whether the parameter and its argument are connected directly (e.g., -name$name) or with a character (such as : or =) between them. However, it doesn't apply if the parameter's argument isn't in a variable. For example, the following two commands are equivalent:

.\ShowArgs -name:"Gil Bates"
.\ShowArgs "-name:Gil Bates"

If you're not sure about the actual command line PowerShell is going to use, replace your executable name with ShowArgs.exe and you'll see the exact command line parameters that PowerShell will use to run the executable.

Getting an Executable's Exit Code

Cmd.exe uses the ERRORLEVEL dynamic environment variable to store the exit code of the last executable that ran. PowerShell uses the $LASTEXITCODE variable instead. Typically, you can tell whether an executable completed without errors by checking to see if $LASTEXITCODE is equal to zero.

Building an Executable's Command Line Based on Conditions

If you need to build a command-line string that depends on conditions, you might need more flexibility. For example, consider the Test.ps1 script in Listing 1.

param(
  [Switch] $Test
)

$arg = "A B C"
$params = "/a:$arg"
if ( $Test ) {
  $params += " /Test"
}

# Won't work as expected if using -Test
ShowArgs $params

If you run Test1.ps1 with the -Test parameter, PowerShell will execute the command:

.\ShowArgs "/a:A B C /Test"

However, we actually want PowerShell to execute this command instead:

.\ShowArgs "/a:A B C" /Test

That is, we want PowerShell to interpret the $params variable as the command line itself, not a single string parameter to the executable.

One solution is to use the Start-Process cmdlet, as shown in Listing 2. The Start-Process cmdlet has the -ArgumentList parameter, which is an array of command-line parameters. PowerShell doesn't do any automatic quoting of these parameters, so you'll have to insert quotes where needed.

param(
  [Switch] $Test
)

$arg = "A B C"
# You have to insert your own quotes
$params = @("/a:`"$arg`"")
if ( $Test ) {
  $params += "/Test"
}
Start-Process ShowArgs.exe -ArgumentList $params `
  -NoNewWindow -Wait

Using the Start-Process cmdlet has a couple of drawbacks:

  • If you want to capture the executable's output, you need to use the -RedirectStandardOutput parameter. Test3.ps1 in Listing 3 illustrates this technique. This script creates a temporary file, runs the executable (redirecting the output to the temporary file), and retrieves the output using the Get-Content cmdlet.
  • The Start-Process cmdlet doesn't update the $LASTEXITCODE variable.
param(
  [Switch] $Test
)

$arg = "A B C"
$params = @("/a:`"$arg`"")
if ( $Test ) {
  $params += "/Test"
}
$tempName = [IO.Path]::GetTempFileName()
$output = ""
$spArgs = @{
  "FilePath" = "ShowArgs.exe"
  "ArgumentList" = $params
  "NoNewWindow" = $true
  "Wait" = $true
  "RedirectStandardOutput" = $tempName
}
Start-Process @spArgs
if ( test-path $tempName ) {
  $output = get-content $tempName
  remove-item $tempName
}

For a bit more flexibility, you can use the Start-Executable function shown in Listing 4. The Start-Executable function doesn't use a temporary file and it updates the $LASTEXITCODE variable. The function's -ArgumentList parameter works the same as the Start-Process cmdlet's -ArgumentList parameter.

function Start-Executable {
  param(
    [String] $FilePath,
    [String[]] $ArgumentList
  )
  $OFS = " "
  $process = New-Object System.Diagnostics.Process
  $process.StartInfo.FileName = $FilePath
  $process.StartInfo.Arguments = $ArgumentList
  $process.StartInfo.UseShellExecute = $false
  $process.StartInfo.RedirectStandardOutput = $true
  if ( $process.Start() ) {
    $output = $process.StandardOutput.ReadToEnd() `
      -replace "\r\n$",""
    if ( $output ) {
      if ( $output.Contains("`r`n") ) {
        $output -split "`r`n"
      }
      elseif ( $output.Contains("`n") ) {
        $output -split "`n"
      }
      else {
        $output
      }
    }
    $process.WaitForExit()
    & "$Env:SystemRoot\system32\cmd.exe" `
      /c exit $process.ExitCode
  }
}

Construct Your Commands Correctly

Correctly constructing command lines in PowerShell seems to generate a lot of confusion, but this need not be the case. The guidelines in this article will help you avoid common pitfalls when running executables in PowerShell. I also recommend adding ShowArgs.exe to your troubleshooting toolkit so you can see the exact parameters that PowerShell passes to an executable.

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish