Combine outputs in Powershell - 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

Related

In Powershell, out-file doesn't redirect write-host from start-job and invoke-command

I distilled this from a larger script. The problem I'm having is that the out-file doesn't work to save "sdfsdfad" to a file and instead prints to the console "sdfsdfad".
$ErrorActionPrefence = "Stop"
$s1=#'
for($i=0; $i -lt 10; $i++) {
write-host "sdfsdfad"
}
'#
$s1 > $env:userprofile\Documents\s1.ps1
$sb = {
$ErrorActionPreference = "Stop"
Set-PSDebug -Trace 0
$alog = "$env:userprofile\Documents\a2.txt"
$null > $alog
invoke-expression $env:userprofile\Documents\s1.ps1 | out-file -Append $alog
}
start-job -ScriptBlock $sb | Receive-job -wait
Get-Content $env:userprofile\Documents\a2.txt
Console Output that is suppose to be in "~\Documents\a2.txt" instead:
sdfsdfad
sdfsdfad
sdfsdfad
sdfsdfad
sdfsdfad
sdfsdfad
sdfsdfad
sdfsdfad
sdfsdfad
sdfsdfad
Don't use Write-Host to output data - use Write-Output or, preferably, implicit output.
Don't use Invoke-Expression to call scripts and avoid it in general - use &, the call operator, to call a command whose name or path is based on a variable or quoted.
In cases where you don't have control over a script that uses Write-Host, you can use 6>&1 to redirect that output to the success output stream in order to send it through the pipeline, so that Out-File saves it.
Note that this only works in PowerShell v5 and above.
See this answer for more information about capturing Write-Host output, and this answer for information about redirection syntax such as 6>&1.
Applied to your code (if you really must stick with Write-Host):
& $env:userprofile\Documents\s1.ps1 6>&1 | out-file -Append $alog
Or, more simply, given that >> is effectively an alias of Out-File -Append:
& $env:userprofile\Documents\s1.ps1 6>&1 >> $alog
Note:
Unlike in POSIX-compatible shells such as bash, the order of the redirections does not matter.
You can merge multiple streams selectively into the success output stream, or simply target all streams (see below).
Or, even simpler, to catch all (*) output streams:
& $env:userprofile\Documents\s1.ps1 *>> $alog

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

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.

Scanning a .log for specific strings in latest lines using Powershell

I have a .log file that constantly adds lines to itself and I am trying to make a Powershell script that will launch 1 of two batch scripts when the respective string of characters is detected in the latest line of the .log file. Here's what I have so far:
while ($True) {
Write-Output 'Enter <ctrl><c> to break out of this loop.'
Start-Sleep -Seconds 1
Copy-Item -LiteralPath "C:\LocationOfFile\latest.log" -Destination "C:\Users\Diogo\Desktop\Detector"
Rename-Item -Path "C:\Users\Diogo\Desktop\Detector\latest.log" -NewName "latest.txt"
Get-Content -Path "latest.txt" -tail 1 -wait | Select-String -Quiet '§6§lP§e§lrof'
if (System.Boolean -eq True) {
Invoke-Item result1.bat
Read-Host -Prompt "Press Enter to continue"
}
else {
Get-Content -Path "latest.txt" -tail 1 -wait | Select-String -Quiet 'spawned'
if (System.Boolean -eq True) {
Invoke-Item result2.bat
Read-Host -Prompt "Press Enter to continue"
}
else {
}
}
}
I first copy the .log file from it's location and Change it into a .txt.
Then I search for the strings ("§6§lP§e§lrof" and "spawned")
And finally I try to get it to do it over again, but this doesn't seem to be working as well as the seearching.
Any help?
Thanks in advance <3
EDIT:
Thank you so much for the comprehensive reply, that really helped me grasp some Powershell concepts and it worked flawlessly. The second script was a tiny overkill tho, I actually have the exact opposite problem: the lines are added quite slowly. In a perfect world I want the script to keep going after finding one result and not have me keep resetting it after each result found. There is another rule about the log file that is really interesting: Lines with the strings I'm after never occur one after another, there is always one in between, at least. This means if the script finds the same string twice in a row, it's just the same line and I don't want my batch script to go off. The PowerShell script I am using right now (which is the code you showed me with minor changes to make it loop) is at the end and it is working with only a single small hiccup: If I'm using my computer for something else Powershell becomes the window on top when it finds a result and I would like that not to happen, could you help me with that last thing? Thank you very much in advance!
while ($True) {
Write-Output 'Enter <ctrl><c> to break out of this loop.'
Start-Sleep -Seconds 1
$LastLogLine = Get-Content -Path "C:\LocationOfFile\latest.log" -tail 1
if ($LastLogLine -ne $LastLine) {
if ($LastLogLine -like '*§6§lP§e§lrof*') {
Start-Process -FilePath "result1.bat" -WindowStyle Minimized
$LastLine = $LastLogLine
} elseif ($LastLogLine -like '*spawned*') {
Start-Process -FilePath "result2.bat" -WindowStyle Minimized
$LastLine = $LastLogLine
}
}
}
First off, your script doesn't work for two reasons:
Get-Content -Path "latest.txt" -tail 1 -wait | Select-String -Quiet '§6§lP§e§lrof'
Get-Content -Wait will keep running as long as the file it reads exists or until it gets Ctrl-C'd, so your script will never go beyond that. You can just remove -Wait here.
if (System.Boolean -eq True)
I don't see what you're trying to do here. Collect the results from the previous Select-String ? Select-String does not set any variable or flag on it's own. Also, you're comparing a type to a string: you're asking "is the concept of a boolean equal to the string 'True' ?". What you can do is store the result of Select-String and just do if ($Result -eq $True) (emphasis on $True, not "True").
Additionally, a couple things I would rewrite or correct in your script:
Copy-Item every second: Is it necessary ? Why not just read the original file and store it in a variable ? If it is just so you can change the extension from .log to .txt, know that powershell does not care about the extension and will happily read anything.
Select-String: have you considered just using the comparison operator -like, as in if ($MyString -like "*$MyKeyword*") {...} ?
If blocks do not need an Else block. If your Else does nothing, you can just not write it. And there is an elseif block that you can use instead of chaining an else and an if.
Code style: Please pick an indentation style and stick to it. The one I see most of the time is 1TBS, but K&R or Allman are well known too. I may or may not have requested an edit to get some indentation on your question :p
So, we end up with this:
while ($True) {
Write-Output 'Enter <ctrl><c> to break out of this loop.'
Start-Sleep -Seconds 1
$LastLogLine = Get-Content -Path "C:\LocationOfFile\latest.log" -tail 1
if ($LastLogLine -like '*§6§lP§e§lrof*') {
Invoke-Item result1.bat
Read-Host -Prompt "Press Enter to continue"
} elseif ($LastLogLine -like '*spawned*') {
Invoke-Item result2.bat
Read-Host -Prompt "Press Enter to continue"
}
}
However, this will not work if the program that writes your logs can write faster than you can process the lines, batch script included. If it does that, your script will skip lines as you only handle the last line. If two lines get written you won't see the second to last.
To solve that, we can do a bit of asynchronous magic using Powershell jobs, and we'll be able to see all lines written since the last loop, be it 1 line written, 0 lines, or 100 lines. about_jobs is a very good primer on Powershell jobs and asynchronous operations, read it.
$stream = Start-Job -ArgumentList $LogPath -Name "StreamFileContent" -ScriptBlock {Get-Content $args -Wait}
Receive-Job -Job $Stream # Discard everything that was already written in the file, we only want the stuff that is added to the file after we've started.
while($true) { # As long as the script is left running
foreach($NewLine in (Receive-Job -Job $stream)) { # Fetch the lines that Get-Content gave us since last loop
if ($NewLine -like '*§6§lP§e§lrof*') { # Check for your keyword
C:\MyScriptPath\MyScript1.bat # Start batch script
} elseif ($NewLine -like '*spawned*') {
C:\MyScriptPath\MyScript2.bat
}
}
}

Please explain, function to add string to first line of a file

Would you like to explain what is happing in the PowerShell code at the bottom of this post?
I got my first, lets say "hello world" in PowerShell, and it needed these lines of code. It works like a charm, but I am not sure what it does, exactly.
The questions starts at
$( ,$_; Get-Content $Path -ea SilentlyContinue) | Out-File $Path
So this is what I understand so far.
We create a function called Insert-Content. With the params (input that will be interpeted as a string and will be added to $path).
function Insert-Content {
param ( [String]$Path )
This is what the function does/processes:
process {
$( ,$_;
I am not sure what this does, but I guess it gets "the input" (the "Hello World" before the | in "Hello World" | Insert-Content test.txt).
And then we got -ea SilentylyContinue, but what does it do?
process {
$( ,$_; Get-Content $Path -ea SilentlyContinue) | Out-File $Path
It would be greatly appreciated if you could explain these two parts
$( ,$_;
-ea SilentylyContinue
Code needed/used: Add a string to the first line of a doc.
function Insert-Content {
param ( [String]$Path )
process {
$( ,$_;Get-Content $Path -ea SilentlyContinue) | Out-File $Path
}
}
"Hello World" | Insert-Content test.txt
process {...} is used for applying the code inside the scriptblock (the {...}) to each parameter argument that the function reads from a pipeline.
$_ is an automatic variable containing the current object. The comma operator , preceding the $_ converts the string value to a string array with a single element. It's not required, though. The code would work just as well with just $_ instead of ,$_.
Get-Content $Path reads the content of the file $Path and echoes it as to the success output stream as an array of strings (each line as a separate string).
The ; separates the two statements ,$_ and Get-Content $Path from each other.
| Out-File $Path writes the output back to the file $Path.
The subexpression operator $() is required to decouple reading the file from writing to it. You can't write to a file when a process is already reading from it, so the subexpression ensures that reading is completed before writing starts.
Basically this whole construct
$( ,$_;Get-Content $Path -ea SilentlyContinue) | Out-File $Path
echoes the input from the pipeline (i.e. the "Hello World") followed by the current content of the file $Path (effectively prepending the input string to the file content) and writes everything back to the file.
The -ea SilentlyContinue (or -ErrorAction SilentlyContinue) suppresses the error that would be thrown when $Path doesn't already exist.
The relevant section of code must be handled as a whole:
$( ,$_;Get-Content $Path -ea SilentlyContinue) | Out-File $Path
First, as others have said, -ea is the shortened version of -ErrorAction. -ErrorAction SilentlyContinue tells the cmdlet "Suppress any error messages and continue executing." See Get-Help about_Common_Parameters -ShowWindow.
Next, the $() is the sub-expression operator. It means "Evaluate what is between the parentheses as its own command and return the result(s)." See Get-Help about_Operators -ShowWindow.
This subexpression here is:
,$_;Get-Content $Path -ea SilentlyContinue
It contains two statements: ,$_ and Get-Content $Path -ea SilentlyContinue. The semicolon is just the end of statement identifier to separate the two.
,$_; is two kind of complex parts.
$_ is the special pipeline variable. It always contains whatever object is in the current pipeline. See Get-Help about_Automatic_Variables -ShowWindow for more about $_ (it's mostly used with ForEach-Object and Where-Object cmdlets, so check those out, too), and Get-Help about_pipelines -ShowWindow for more help with pipelines.
The comma here is the comma operator (see Get-Help about_Operators -ShowWindow again). It creates an array from the objects on either side. For example, 1,2,3 creates an array with three elements 1, 2, and 3. If you want a two item array, you can say 1,2.
What if you want a one item array? Well, you can't say 1, because Powershell will think you forgot something. Instead, you can say ,1.
You can test it with the -is operator:
PS C:\> 1,2,3 -is [Array]
True
PS C:\> 1 -is [Array]
False
PS C:\> ,1 -is [Array]
True
Why might you want to create a one item array? Well, if later on your code is assuming the item is an array, it can be useful. In early editions of Powershell, properties like .Count would be missing for single items.
For completeness, yes, I believe you could write:
$( #($_);Get-Content $Path -ea SilentlyContinue)
And I think you could rewrite this function:
function Insert-Content {
param ( [String]$Path )
process {
#Read from pipeline
$strings = #($_);
#Add content of specified file to the same array
$strings += Get-Content $Path -ea SilentlyContinue;
#Write the whole array to the file specified at $Path
$strings | Out-File $Path;
}
}
So this adds content from the pipeline to the start of a file specified by -Path.
It's also somewhat poor practice not to create a parameter for the pipeline object itself and define it. See... well, see all the topics under Get-Help "about_Functions*", but mostly the Advanced ones. This is an advanced topic.

Build command parameters based on input

I am attempting to build a script that will import data from a CSV and process commands based on the data that is filled in. Small Example:
CSV formatted with following headers
Name TargetOU Description ManagingGroup Permission
Some of these are required and some are optional.
This would be easy to accomplish using Parameters, but this is attempting to run without user interaction. The below code would solve the problem if I just had Name and a optional Description:
If ($_.Description -ne $null) {
New-GPO -Name $_.Name -Description $_.Description }
ElseIF ($_.Description -eq $null) {
New-GPO -Name $_.Name }
If I want to make it more complex I end up having to write an If statement for every possible combination of required and optional parameters:
If ($_.Description -ne $null -and $_.TargetOU -ne $null) {
New-GPO -Name $_.Name -Description $_.Description | New-GPLink -Target $_.TargetOU }
ElseIf ($_.Description -eq $null -and $_.TargetOU -ne $null) {
New-GPO -Name $_.Name | New-GPLink -Target $_.Target }
etc... for every possible combination.
Is there any simpler way to build this command without 100's of If statements? The CSV will potentially contain 10 options to fill in based on what users want completed.
you just need to separate mandatories parameters and optional parameters. You initialize optional parameters with default values and If a mandatory parameter is missing you just cancel the operation for the line.
You could use splatting. In splatting, you pass a hashtable to a function, which is interpreted as arguments.
for example:
$params=#{Name='Fred'}
my-func #params
By using splatting, you don't have to have a bunch of nested if-then constructs, just build the list of parameters and send it.