1
votes

Consider following code:

Function ShowSave-Log {
  Param ([Parameter(Mandatory=$true)][String] $text)
  $PSDefaultParameterValues=@{'Out-File:Encoding' = 'utf8'}
  $date=[string](Get-Date).ToString("yyyy/MM/dd HH:mm:ss")
  Tee-Object -InputObject "$date $text" -FilePath $LOG_FILE -Append
  #Write-Host "$date $text"
}

Function Is-Installed {
  Param ([parameter(Mandatory=$true)][String] $app_name, [String] $app_version)
  $apps = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" | 
  Select-Object DisplayName, DisplayVersion
  $apps += Get-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Select-Object DisplayName, DisplayVersion
  $apps = $apps.Where{$_.DisplayName -like "$app_name"}
  if ($apps.Count -eq 0) {
    ShowSave-Log "`'$app_name`' not found in the list of installed applications."
    return $false
  } else {
    ShowSave-Log "`'$app_name`' is installed."
    return $true
  }
}

$LOG_FILE="$Env:TEMP\LOG.log"
if (Is-Installed "Notepad++ (64-bit x64)") {Write-Host "TRUE"}

I'd expect to see message from Tee-Object command in ShowSave-Log function, however it is never shown in terminal. I am guessing it's because it is called from 'if' statement. How can I get Tee-Object output to terminal screen ? It is saved to log file. BTW Write-Host command correctly outputs message to terminal. I am using PowerShell ISE, Visual Studio code and PowerShell terminal. PowerShell version 5.1

2
Shows just fine in my test.Doug Maurer
what version of PowerShell did you use for testing ?mauek unak
PSVersion 5.1.19041.610 PSEdition Desktop PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...} BuildVersion 10.0.19041.610 CLRVersion 4.0.30319.42000Doug Maurer

2 Answers

2
votes

There is a common misconception about how Powershell functions return data. Actually there isn't a single return value or object as you are used to from other programming languages. Instead, there is an output stream of objects.

There are several ways to add data to the output stream, e. g.:

  • Write-Output $data
  • $data
  • return $data

Confusing to PS newcomers coming from other languages is the fact that return $data does not define the sole "return value" of a function. It is just a convenient way to combine Write-Output $data with an early exit from the function. Whatever data that was written to the output stream befor the return statement also contributes to the output of the function!

Analysis of the code

Tee-Object -InputObject "$date $text" -FilePath $LOG_FILE -Append

... appends the InputObject to the output stream of ShowSave-Log

ShowSave-Log "`'$app_name`' is installed."

... appends the message to the output stream of Is-Installed

return $true

... appends the value $true to the output stream of Is-Installed

Now we actually have two objects in the output stream of Is-Installed, the string message and the $true value!

if (Is-Installed "Notepad++ (64-bit x64)") {Write-Host "TRUE"}

Let me split up the if-statement to explain in detail what it does:

$temp = Is-Installed "Notepad++ (64-bit x64)"

... redirects the output stream of Is-Installed to temporary variable. As the output stream has been stored into a variable, it won't go further up in the function call chain, so it won't show up in the console anymore! That's why you don't see the message from Tee-Object.

In our case there is more than one object in the output stream, so the variable will be an array like @('... is installed', $true)

if ($temp) {Write-Host "TRUE"}

... does an implicit boolean conversion of the array $temp. A non-empty array converts to $true. So there is a bug here, because the function Is-Installed always "returns" a non-empty array. When the software is not installed, $temp would look like @('... not found ...', $false), which also converts to $true!

Proof:

$temp = Is-Installed "nothing"
$temp.GetType().Name    # Prints 'Object[]'
$temp[0]                # Prints '2020.12.13 12:39:37 'nothing' not found ...'
$temp[1]                # Prints 'False'
if( $temp ) {'Yes'}     # Prints 'Yes' !!!    

How can I get Tee-Object output to terminal screen?

Don't let it write to the output stream, which should be used only for actual data to be "returned" from a function, not for log messages.

A simple way to do that is to redirect the output of Tee-Object to Write-Host, which writes to the information stream:

Tee-Object -InputObject "$date $text" -FilePath $LOG_FILE -Append | Write-Host

A more sensible way would be to redirect to the verbose stream:

Tee-Object -InputObject "$date $text" -FilePath $LOG_FILE -Append | Write-Verbose

Now the log message doesn't clutter the terminal by default. Instead, to see detailed logging the caller has to enable verbose output, e. g. by setting $VerbosePreference = 'Continue' or calling the function with -Verbose parameter:

if( Is-Installed 'foo' -Verbose ){<# do something #>}
0
votes

It might be easier to understand if you think of it as

$result = Is-Installed "Notepad++ (64-bit x64)"
if ($result) {Write-Host "TRUE"}

It's pretty clear that way that the result isn't output to the console at any time.


You may also be misunderstanding how return works

    ShowSave-Log "`'$app_name`' not found in the list of installed applications."
    return $false

is functionally the same as

    ShowSave-Log "`'$app_name`' not found in the list of installed applications."
    $false
    return

You'd be better of having your functions return simple PowerShell objects rather than human readable text and truth values.

function Get-InstalledApps {
    param (
        [parameter(Mandatory=$true)][string] $app_name,
        [string] $app_version
    )
    $installPaths = @(
        'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
        'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
    )
    Get-ItemProperty -Path $installPaths | Where-Object DisplayName -like $app_name
}

And leave the formatting for the user to the top level of your script.


It could be worth looking at custom types with the DefaultDisplayPropertySet property. For example:

Update-TypeData -TypeName 'InstalledApp' -DefaultDisplayPropertySet 'DisplayName', 'DisplayVersion'

function Get-InstalledApps {
    param (
        [parameter(Mandatory=$true)][string] $app_name,
        [string] $app_version
    )
    $installPaths = @(
        'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
        'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
    )
    Get-ItemProperty -Path $installPaths | Where-Object DisplayName -like $app_name | Add-Member -TypeName 'InstalledApp' -PassThru
}

Or without a custom type, this abomination of a one liner:

Get-ItemProperty -Path $installPaths | Where-Object DisplayName -like $app_name | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value ([System.Management.Automation.PSMemberInfo[]](New-Object System.Management.Automation.PSPropertySet DefaultDisplayPropertySet, ([string[]]('DisplayName', 'DisplayVersion')))) -PassThru

Also worth taking a look at is the Approved Verbs for PowerShell page.