PowerShell console output from Tee-Object command inside function inside IF statement - powershell

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

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 #>}

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.

Related

Powershell error writing writing to all users' registry

I'm attempting to add a wallpaper, along with certain parameters, to each user on a computer. It's been hit and miss with this working/not working on computers. The ones that fail I get the error "Method invocation failed because [System.Management.Automation.PSObject] does not contain a method named 'op_Addition'."
The variables $WallpaperPath and $Style are coming from another source within Automation Manager (using N-Central).
# Get each user profile SID and Path to the profile
$UserProfiles = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | Where {$_.PSChildName -match "S-1-5-21-(\d+-?){4}$" } | Select-Object #{Name="SID"; Expression={$_.PSChildName}}, #{Name="UserHive";Expression={"$($_.ProfileImagePath)\NTuser.dat"}}
# Add in the .DEFAULT User Profile
$DefaultProfile = "" | Select-Object SID, UserHive
$DefaultProfile.SID = ".DEFAULT"
$DefaultProfile.Userhive = "C:\Users\Public\NTuser.dat"
$UserProfiles += $DefaultProfile
# Loop through each profile on the machine</p>
Foreach ($UserProfile in $UserProfiles) {
    # Load User ntuser.dat if it's not already loaded
    If (($ProfileWasLoaded = Test-Path Registry::HKEY_USERS\$($UserProfile.SID)) -eq $false) {
        Start-Process -FilePath "CMD.EXE" -ArgumentList "/C REG.EXE LOAD HKU\$($UserProfile.SID) $($UserProfile.UserHive)" -Wait -WindowStyle Hidden
    }
# Write to the registry
$key = "Registry::HKEY_USERS\$($UserProfile.SID)\Control Panel\Desktop"
Set-ItemProperty -Path $key -name Wallpaper -value "$WallpaperPath"
Set-ItemProperty -Path $key -name TileWallpaper -value "0"
Set-ItemProperty -Path $key -name WallpaperStyle -value "$Style" -Force
# Unload NTuser.dat
If ($ProfileWasLoaded -eq $false) {
    [gc]::Collect()
    Start-Sleep 1
    Start-Process -FilePath "CMD.EXE" -ArgumentList "/C REG.EXE UNLOAD HKU\$($UserProfile.SID)" -Wait -WindowStyle Hidden| Out-Null
}
}
I'm looking for this to load a temporary HKU hive for each user that's not currently logged in, and has an NTuser.dat file, and write the registry entries specified. It should then unload any hive for users it added.
Instead of $UserProfiles = ..., use either [array] $UserProfiles = ... or $UserProfiles = #(...) in order to ensure that $UserProfiles always contains an array, even if the command happens to return just one object.
That way, your += operation is guaranteed to work as intended, namely to (loosely speaking) append an element to the array.[1]
Note that PowerShell's pipeline has no concept of an array, just a stream of objects. When such a stream is collected, a single object is captured as itself; only two or more objects are captured in an array ([object[]]) - see this answer for more information.
A simple demonstration:
2, 1 | ForEach-Object {
$result = Get-ChildItem / | Select-Object Name -First $_
try {
$result += [pscustomobject] #{ Name = 'another name' }
"`$result has $($result.Count) elements."
} catch {
Write-Warning "+= operation failed: $_"
}
}
In the first iteration, 2 objects are returned, which are
stored in an array. += is then used to "append" another element.
In the second iteration, only 1 object is returned and stored as such.
Since [pscustomobject], which is the type of object returned by Select-Object, doesn't define a + operation (which would have
to be implemented via an op_Addition() method at the .NET level), the error you saw occurs.
Using an [array] type constraint or #(...), the array-subexpression operator operator, avoids this problem:
2, 1 | ForEach-Object {
# Note the use of #(...)
# Alternatively:
# [array] $result = Get-ChildItem \ | Select-Object Name -First $_
$result = #(Get-ChildItem / | Select-Object Name -First $_)
$result += [pscustomobject] #{ Name = 'another name' }
"`$result has $($result.Count) elements."
}
As noted, [array] $results = Get-ChildItem \ | Select-Object Name -First $_ works too, though there are subtle differences between the two approaches - see this answer.
As an aside:
To synchronously execute console applications or batch files and capture their output, call them directly (c:\path\to\some.exe ... or & $exePath ...), do not use Start-Process (or the System.Diagnostics.Process API it is based on) - see this answer. GitHub docs issue #6239 provides guidance on when use of Start-Process is and isn't appropriate.
That is, you can just make calls such as the following:
REG.EXE LOAD "HKU\$($UserProfile.SID)" "$($UserProfile.UserHive)"
Also, it's easier and more efficient to construct [pscustomobject] instances with their literal syntax (v3+; see the conceptual about_PSCustomObject help topic):
$UserProfiles += [pscustomobject] #{
SID = ".DEFAULT"
Userhive = "C:\Users\Public\NTuser.dat"
}
[1] Technically, a new array must be created behind the scenes, given that arrays are fixed-size data structures. While += is convenient, it is therefore inefficient, which matters in loops - see this answer.

How to find a specific text is available in .txt file using powershell

I want to write a PowerShell script where I will give two string values as parameters, It should check the .txt file and should tell whether the strings are available or not in the given file. For example, if I have a list of employees details. I will give the emp_id and emp_name as input. If the name and id exist in that .txt file it should print that. If not it should print the else statement.
Function Empdetails {
[CmdletBinding(SupportsShouldProcess)]
param (
[Parameter(Mandatory=$true)]$empid,
[Parameter(Mandatory=$true)]$empname)
$path = Get-Content C:\empdetails.txt | Where-Object {$_ -like '*name*'}
if ($path -eq $true) {
Write-Host "Found"
}
else {
Write-Host "Not Found"
}
}
I tried the above code, But it is working. Could you please help me to figure it out?
You have two parameters but you are not using them in your function, since it's not clear which parameter should be used for the file path and which for the word you're searching for in the file, I have changed the parameter names for something more explanatory.
Also note, the result of below expression will be either an array of strings, a single string or $null:
$path = ... | Where-Object {$_ -like '*name*'}
Hence, your if condition if ($path -eq $true) can never be met unless $path has assigned the literal string True. If, however, you change the order of the condition to $true -eq $path, then the condition can be met and will be $true as long as $path is not $null / empty string.
$content = 'something'
$content -eq $true # => False
$true -eq $content # => True
$content = 'True'
$content -eq $true # => True
$true -eq $content # => True
From equality operators:
The equality operator can compare objects of different types. It is important to understand that the value is on the right-hand side of the comparison can be converted to the type of the left-hand side value for comparison.
Function Empdetails {
[CmdletBinding(SupportsShouldProcess)]
param (
[Parameter(Mandatory=$true)]$FilePath,
[Parameter(Mandatory=$true)]$WordToSearch
)
$content = Get-Content $FilePath | Where-Object {$_ -like "*$wordToSearch*"}
if ($content) {
# if `$content` is populated, use return to end the function here
return Write-Host "Found"
}
Write-Host "Not Found"
}
Empdetails -FilePath ./path/to/file.ext -WordToSearch somekeyword

How to modify local variable within Invoke-Command

I am trying to modify a variable within Invoke-Command in order to get out of a loop, however I'm having trouble doing that.
In the sample script below, I'm connecting to a host, grabbing information from NICs that are Up and saving the output to a file (Baseline). Then on my next iteration I will keep grabbing the same info and then compare Test file to Baseline file.
From a different shell, I've connected to the same server and disabled one of the NICs to force Compare-Object to find a difference.
Once a difference is found, I need to get out of the loop, however I cannot find a way to update the local variable $test_condition. I've tried multiple things, from Break, Return, $variable:global, $variable:script, but nothing worked so far.
$hostname = "server1"
$test_condition = $false
do {
Invoke-Command -ComputerName $hostname -Credential $credential -ScriptBlock{
$path = Test-Path -LiteralPath C:\Temp\"network_list_$using:hostname-Baseline.txt"
if ($path -eq $false) {
Get-NetAdapter | Where-Object Status -EQ "Up" | Out-File -FilePath (New-Item C:\Temp\"network_list_$using:hostname-Baseline.txt" -Force)
} else {
Get-NetAdapter | Where-Object Status -EQ "Up" | Out-File -FilePath C:\Temp\"network_list_$using:hostname-Test.txt"
$objects = #{
ReferenceObject = (Get-Content C:\Temp\"network_list_$using:hostname-Baseline.txt")
DifferenceObject = (Get-Content C:\Temp\"network_list_$using:hostname-Test.txt")
}
$test_condition = (Compare-Object #objects).SideIndicator -ccontains "<="
$test_condition #this is returning True <-----
}
}
} until ($test_condition -eq $true)
Any tips? What am I doing wrong?
TIA,
ftex
You can pass variables into a remote script block with the $Using:VarName scope modifier, but you can't use typical $Global: or $Script to modify anything in the calling scope. In this scenario the calling scope isn't the parent scope. The code is technically running in a new session on the remote system and $Global: would refer to that session's global scope.
For example:
$var = "something"
Invoke-Command -ComputerName MyComuter -ScriptBlock { $Global:var = "else"; $var}
The remote session will output "else". However, after return in the calling session $var will output "something" remaining unchanged despite the assignment in the remote session.
Based on #SantiagoSquarzon's comment place the assignment inside the Do loop with a few other modifications:
$hostname = "server1"
do {
$test_condition = $false
$test_condition =
Invoke-Command -ComputerName $hostname -Credential $credential -ScriptBlock{
$path = Test-Path -LiteralPath C:\Temp\"network_list_$using:hostname-Baseline.txt"
if ($path -eq $false) {
Get-NetAdapter | Where-Object Status -eq "Up" | Out-File -FilePath (New-Item C:\Temp\"network_list_$using:hostname-Baseline.txt" -Force)
} else {
Get-NetAdapter | Where-Object Status -eq "Up" | Out-File -FilePath C:\Temp\"network_list_$using:hostname-Test.txt"
$objects = #{
ReferenceObject = (Get-Content C:\Temp\"network_list_$using:hostname-Baseline.txt")
DifferenceObject = (Get-Content C:\Temp\"network_list_$using:hostname-Test.txt")
}
(Compare-Object #objects).SideIndicator -contains "<=" # this is returning True <-----
}
}
} until ($test_condition -eq $true)
I don't know why you were using -ccontains considering "<=" has no casing implications. Also it's very unusual to capitalize operators.
Notice there's no explicit return or assignment. PowerShell will emit the Boolean result of the comparison and that will be returned from the remote session and end up assigned to the $test_condition variable.
An aside:
I'm not sure why we want to use -contains at all. Admittedly it'll work fine in this case, however, it may lead you astray elsewhere. -contains is a collection containment operator and not really meant for testing the presence of one string within another. The literal meaning of "contains" makes for an implicitly attractive hazard, as demonstrated in this recent question.
In short it's easy to confuse the meaning, purpose and behavior on -contains.
This "<=" -contains "<=" will return "true" as expected, however "<==" -contains "<=" will return "false" even though the left string literally does contain the right string.
The answer, to the aforementioned question says much the same. My addendum answer offers a some additional insight for the particular problem and how different operators can be circumstantially applied.
So, as a matter of practice for this case wrap the Compare-Object command in the array sub-expression operator like:
#( (Compare-Object #objects).SideIndicator ) -contains "<="
Given the particulars, this strikes me as the least intrusive way to implement such a loosely stated best practice.

Combine outputs in Powershell

I currently have this script that checks the registry and if the key exists then it will output a value to the console.
How can I modify this script so that it saves each output to a variable and then that variable will be exported to a text/csv file?
if ((Get-ItemPropertyValue -Path "HKLM:\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_HTTP_USERNAME_PASSWORD_DISABLE" -Name HelpPane.exe) -eq '1')
{
Write-Output 'Yes'
}
else
{
Write-Output 'No'
}
if ((Get-ItemPropertyValue -Path "HKLM:\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_DISABLE_SQM_UPLOAD_FOR_APP" -Name iexplore.exe) -eq '1')
{
Write-Output 'Yes'
}
else
{
Write-Output 'No'
}
if ($Host.Name -eq "ConsoleHost")
{
Write-Host "Press any key to continue..."
$Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyUp") > $null
Use Tee-Object for this, which moves data through the pipeline as well as saves it to a file:
$content | Tee-Object -FilePath C:\some\path\on\disk.txt
This will take your variable $content, pipe it to Tee-Object which writes the output to a file, then takes the same output and pushes it through the pipeline. You should see that $content is also written to the output stream in this case but you could also pass it to another cmdlet in the pipeline if you choose to do so.
You have options.
3 ways to store and display PowerShell Variable simultaneously
https://ridicurious.com/2017/06/30/3-ways-to-store-display-results-infrom-a-powershell-variable-at-the-same-time
# Using -OutVariable parameter
Get-Process a* -OutVariable process
# PowerShell Variable squeezing
($process = Get-Process a*)
# Using Tee-Object Cmdlet
Tee-Object Cmdlet T’s results to o/p stream and Variable $process at the same time
Point of note:
Avoid using Write-Host/echo, unless you are using screen text coloring. There is little reason to use it as output to the screen is the PowerShell default.
Also, if you are planning to use data down the line/ pipe, etc, then Write-Host empties the buffer and the data is gone. Well depending on what version of PowerShell you are using.
Resources:
From the creator of Powershell.
Write-Host Considered Harmful
http://www.jsnover.com/blog/2013/12/07/write-host-considered-harmful
... Jeffrey Snover changes his stance on this as of May 2016.
With PowerShell v5 Write-Host no longer "kills puppies". data is
captured into info stream ...
https://twitter.com/jsnover/status/727902887183966208
https://learn.microsoft.com/en-us/powershell/module/Microsoft.PowerShell.Utility/Write-Information?view=powershell-5.1
Your code without the Write-Host thing.
if ((Get-ItemPropertyValue -Path 'HKLM:\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_HTTP_USERNAME_PASSWORD_DISABLE' -Name HelpPane.exe) -eq '1')
{'Yes'}
else {'No'}
if ((Get-ItemPropertyValue -Path 'HKLM:\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_DISABLE_SQM_UPLOAD_FOR_APP' -Name iexplore.exe) -eq '1')
{'Yes'}
else { 'No'}
if ($Host.Name -eq "ConsoleHost")
{
'Press any key to continue...'
$Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyUp') > $null
}
Lastly, be cognizant about quoting. Single quotes for simple strings, and double quotes for variable expansion or other specific string handling.
As defined in the help files and other resources:
about_Quoting_Rules - PowerShell | Microsoft Docs
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules
A Story of PowerShell Quoting Rules
https://trevorsullivan.net/2016/07/20/powershell-quoting
Windows PowerShell Quotes
https://www.computerperformance.co.uk/powershell/quotes

Show output before Read-Host

I'm having some issues getting some info to write to the console before Read-Host. Let me throw out a simplified example.
Function Add-Build {
[CmdletBinding()]
Param ([Parameter(Mandatory=$True,Position=1)][String]$Build
,[Parameter(Mandatory=$False,Position=2)][System.Nullable``1[[System.Int32]]]$VersionID
,[Parameter(Mandatory=$False,Position=3)][String]$BuildDescription
)
Write-Host -BackgroundColor DarkYellow "Adding SQL Build $($Build)"
IF ($VersionID -eq $null)
{
Get-SqlVersions | Out-String
$VersionID = Read-Host -Prompt "SELECT Version (Enter To Skip)" | % { IF ($_ -eq '') {$null} ELSE {$_}}
}
}
FUNCTION Test-Function {
$BuildID = (Get-BuildID -Build "11.0.3156.0").ToString()
}
If I call Add-Build directly then the Get-SqlVersions | Out-String output before the Read-Host. If I call Test-Function though the Get-SqlVersions no longer outputs to the console at all. Get-SqlVersions makes a SQL proc call and the output is a couple Datarows.
Is there a way to ensure the Get-SqlVersions data shows up when calling Test-Function?
Make it explicitly output to the host.
$GetSQL = Get-SqlVersions | Out-String
Write-Host $GetSQL
Could you please store Get-SqlVersions | Out-String; in a Variable and display that. I think that should work.
$versions = Get-SqlVersions | Out-String;
$versions
I know this is old, but I stumbled on it and can't help but contribute.
The problem is here:
Get-SqlVersions | Out-String
Change it to this:
Get-SqlVersions | Out-Host
I did some quick looking around, and Out-String seems to collect and prepare things for displaying. Out-Host just does it.