Search code examples
powershellstreamstdout

How to capture ProcessStartInfo stdout as a string for comparison, with Powershell?


I'm getting the output of

T h i s a c t i o n i s o n l y v a l i d f o r p r o d u c t s t h a t a r e c u r r e n t l y i n s t a l l e d .

and would like to test for that condition as well as others, however $stdout gettype is a string, but when I test for $stdout -eq $null or $stdout -eq the quote above, it always fails. Does the stream have to be converted somehow? How can I do comparisons with $null and other strings?

$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = 'msiexec'
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = "/x {22AD1D41-EDA0-4387-BF16-9045CE734FAD} /quiet /norestart /l*v `"C:\temp\msi123_MSI_UNINSTALL.log`""
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$p.WaitForExit()
$stdout = $p.StandardOutput.ReadToEnd() | Out-String
$stderr = $p.StandardError.ReadToEnd()

Solution

  • msiexec.exe is unusual in several respects:

    • It is a GUI-subsystem application (rather than a console-subsystem one), yet is has an extensive CLI (support for many parameters that can be passed on the command line).

    • It only outputs to its stdout stream if the latter is programmatically captured (rather than printing to the console of the calling process).

      • From what I can tell, even error messages are sent to stdout, so there may never be any stderr output.

      • Additionally, it seems that individual messages are terminated with only a LF (LINE FEED, U+000A) character, instead of the usual CRLF combination (a CR followed by a CR (CARRIAGE RETURN, U+000D), with the overall output seemingly terminated by just a CR.

    • When msiexec.exe does send output to stdout, it uses "Unicode" encoding, i.e. UTF-16LE encoding - it does not use the system's active legacy OEM code page, which is what console applications ordinarily do.


    If you simply want to know whether any stdout or stderr was produced, the simplest approach is to take advantage of PowerShell's implicit to-Boolean coercions, which treat both $null and the empty string as $false:

    if (-not $stdout) { 'No stdout output was produced.' }
    

    Read on for how to properly decode the messages returned so you can do string comparisons, as well as for a better way to determine success vs. failure.


    If you do want to examine the specific stdout (and, hypothetically, stderr) output, you must decode it properly:

    $pinfo = [System.Diagnostics.ProcessStartInfo] @{
      FileName = 'msiexec'
      Arguments = "/x {22AD1D41-EDA0-4387-BF16-9045CE734FAD} /quiet /norestart /l*v `"C:\temp\msi123_MSI_UNINSTALL.log`""
      UseShellExecute = $false
      RedirectStandardError = $true
      RedirectStandardOutput = $true
      StandardOutputEncoding = [System.Text.Encoding]::Unicode
      StandardErrorEncoding = [System.Text.Encoding]::Unicode
    }
    $p = [System.Diagnostics.Process]::Start($pinfo)
    $p.WaitForExit()
    $stdout = $p.StandardOutput.ReadToEnd()
    $stderr = $p.StandardError.ReadToEnd()
    

    Note:

    • .StandardOutputEncoding and .StandardErrorEncoding properties are set to [System.Text.Encoding]::Unicode, i.e. UTF-16LE.

    • In lieu of using New-Object, the System.Diagnostics.ProcessStartInfo object is constructed implicitly, by initializing its properties via a hashtable; similarly, the System.Diagnostics.Process instance is created by passing the former instance to the latter type's static System.Diagnostics.Process.Start method.

    • Decoding the output properly is the prerequisite for performing string comparisons that test for specific content; e.g.:

       # Test for a substring.
       # Note that -match performs *regex* matching.
       $stdout -match 'only valid'
      
       # Compare the whole output; trimming any trailing whitespace
       # - notably the LF and CR characters at the end. 
       $stdout.TrimEnd() -eq 'This action is only valid for products that are currently installed.'
      
    • However, you're better off testing the exit code reported by msiexec.exe, which uses distinct codes to signal specific error conditions.

      • To test for success:

        if ($p.ExitCode -eq 0) { 'Uninstallation succeeded.' }
        
      • To test for the specific error condition implied by the error message at hand, which is 1605, per the list of msiexec error codes:

        if ($p.ExitCode -eq 1605) { 
          'Nothing to do, because the specified product is not installed.' 
        }