I'm writing a Powershell cmdlet that needs to execute a command and store its stderr output to a temporary file for later processing. This output lists COM ports that the cmdlet may use later.
# mostly side-effect-free information gathering
$info = [IO.Path]::GetTempFileName()
info-gather.exe 2>$info
Get-Content $info | ForEach-Object {
# processing
}
Remove-Item $info
# serious things with serious side effects start here
I would like this cmdlet to implement -WhatIf since it will have non-trivial side effects. However, the -WhatIf behavior takes over the info-gather.exe command and it is simply never executed. Instead, it prints:
What if: Performing the operation "Output to File" on target "temp\path\to\tmp78A4.tmp"
Because this command is never executed, the inner processing doesn't happen either, and the rest of my cmdlet that has actual side effects isn't executed because it doesn't know which ports to use, making -WhatIf largely useless.
How can I bypass -WhatIf for this block without overriding it in the rest of the cmdlet?
You could use try...finally to set and then reset the $WhatIfPreference variable.
$script:oldWhatIfPrefernence = $WhatIfPreference
try {
$WhatIfPreference = $false
$info = [IO.Path]::GetTempFileName()
info-gather.exe 2>$info
Get-Content $info | ForEach-Object {
# processing
}
Remove-Item $info
} finally {
$WhatIfPreference = $script:oldWhatIfPreference
}
If you weren't using the 2> redirection, there is another way. You can pass the -WhatIf switch explicitly to many cmdlets and override -WhatIf for just that part.
Remove-Item $info -WhatIf:$false
Though, zneak's answer that doesn't involve a temporary file is pretty elegant too.
One solution is to "redirect" stderr to stdout. Doing this, Powershell appends error records to the standard output, which can be converted to regular strings using Out-String. In my case (YMMV depending on the program), the first error record says "there was an error with this program" and the second error record is the actual, full stderr output. This is also simplified by the fact that the command doesn't write anything to the standard output, so there's no filtering to do there.
I was able to change the foreach loop to use this:
((info-gather.exe 2>&1)[1] | Out-String) -split "[`r`n]" | foreach-object
Since this does not write to a file, -WhatIf no longer overrides it.
There is probably a simpler and cleaner way to do this (for instance, if there was a way to pipe stderr to Out-String directly).
Related
I have been working on this for a while and I cannot find this utilization anywhere. I am using a powershell array and the foreach statement:
#('program 1','program 2','program 3').foreach{winget show $_ -args}
I then want to have a pause between each one so I added ;sleep 1
This does not work. It pauses for 3s (based on this eg.) and then lists the items. What am I missing here?
Indeed it doesn't seem to respect the order, I don't know the technical reason why. You could either use a normal ForEach-Object
'program 1','program 2','program 3' | ForEach-Object {
winget show $_
sleep 1
}
or force the output to go to the console instead of being "buffered"
('program 1','program 2','program 3').ForEach{
winget show $_ | Out-Host
sleep 1
}
Doug Maurer's helpful answer provides effective solutions.
As for an explanation of the behavior you saw:
The intrinsic .ForEach() method first collects all success output produced by the successive, per-input-object invocations of the script block ({ ... }) passed to it, and only after having processed all input objects outputs the collected results to the pipeline, i.e. the success output stream, in the form of a [System.Collections.ObjectModel.Collection[psobject]] instance.
In other words:
Unlike its cmdlet counterpart, the ForEach-Object cmdlet, the .ForEach() method does not emit output produced in its script block instantly to the pipeline.
As with any method, success output is only sent to the pipeline when the method returns.
Note that non-PowerShell .NET methods only ever produce success output (which is their return value) and only ever communicate failure via exceptions, which become statement-terminating PowerShell errors.
By contrast, the following do take instant effect inside a .ForEach() call's script block:
Suspending execution temporarily, such as via a Start-Sleep
Forcing instant display (host) output, such as via Out-Host or Write-Host.
Note that to-host output with Out-Host prevents capturing the output altogether, whereas Write-Host output, in PowerShell v5+, can only be captured via the information output stream (number 6).
Writing to an output stream other than the success output stream, such as via Write-Error, Write-Warning or Write-Verbose -Verbose.
Alternatively, you may use the foreach statement, which, like the ForEach-Object cmdlet, also instantly emits output to the success output stream:
# Stand-alone foreach statement: emits each number right away.
foreach ($i in 1..3) { $i; pause }
# In a pipeline, you must call it via & { ... } or . { ... }
# to get streaming behavior.
# $(...), the subexpression operator would NOT stream,
# i.e. it would act like the .ForEach() method.
& { foreach ($i in 1..3) { $i; pause } } | Write-Output
I'm trying to write a small powershell script that does a few things
1) Parses Inbox items in my outlook
2) Searches for a RegEx string
3) Dumps the line that matches the RegEx string into a CSV
I can't get #3 to work. It definitely runs for about 10 minutes, but the resulting csv is empty.
Here's a snippet of what I want it to look for:
Account Name: Jbond
I tried just slapping a "Write-Host $variable" in various parts to see what was happening, but all I get is "System.____comobject". I can't find a solution online to just convert this into plain text.
Add-Type -Assembly "Microsoft.Office.Interop.Outlook"
$Outlook = New-Object -ComObject Outlook.Application
$namespace = $Outlook.GetNameSpace("MAPI")
$inbox = $namespace.GetDefaultFolder([Microsoft.Office.Interop.Outlook.OlDefaultFolders]::olFolderInbox)
$RE = [RegEx]'(?sm)Account Name\s*:\s*(?<AccName>.*?)$.*'
$Data = ForEach ($item in $inbox.items){
$resultText = $item.item.Value
Write-Host $resultText
if ($item.from -like "email#email.org"){
if ($item.body -match $RE){
[PSCustomObject]#{
AccName = $Matches.AccName
}
}
}
}
$Data
$Data | Export-CSv '.\data.csv' -NoTypeInformation
tl;dr:
Use:
$variable | Out-Host # or: Out-Host InputObject $variable
rather than
Write-Host $variable
to get meaningful output formatting.
Background information and debugging tips below.
Try a combination of the following approaches:
Use interactive debugging:
Use GUI editor Visual Studio Code with the PowerShell extension to place breakpoints in your code and inspect variable values interactively. (In Windows PowerShell you can also use the ISE, but it is obsolescent.)
Less conveniently, use the *-PSBreakpoint cmdlets to manage breakpoints that are hit when you run your script in a console (terminal) window. A simple alternative is to add Wait-Debugger statements to your script, which, when hit, break unconditionally.
Produce helpful debugging output:
Generally, use Write-Debug rather than Write-Host, which has two advantages:
You can leave Write-Debug calls in your code for on-demand debugging at any time:
They are silent by default, and only produce output on an opt-in basis, via (temporarily) setting $DebugPreference = 'Continue' beforehand or passing the -Debug switch (if your script / function is an advanced one, though note that in Windows PowerShell this will present a prompt whenever a Write-Debug call is hit).
You do, however, pay a performance penalty for leaving Write-Debug calls in your code.
Debug output is clearly marked as such, colored and prefixed with DEBUG:.
The problem you experienced with Write-Host is that all Write-* cmdlets perform simple .ToString() stringification of their arguments, which often results in unhelpful representations, such as System.____comobject in your case.
To get the same rich output formatting you would get in the console, use the following technique, which uses Out-String as a helper command:
$variable | Out-String | Write-Debug
If you want to control the view (list vs. table vs. wide vs. custom) explicitly, insert a Format-* call; e.g.:
$variable | Format-List | Out-String | Write-Debug
It is generally only the standard Out-* cmdlets that use PowerShell's output formatting system.
A quick-and-dirty alternative to Write-Debug is to use Out-Host rather than Write-Host - e.g., for quick insertion of debugging commands that you'll remove later; Out-Host itself performs the usual output formatting, which simplifies matters:
# Default for-display formatting
$variable | Out-Host # or: Out-Host -InputObject $variable
# Explicit formatting
$variable | Format-List | Out-Host
Caveat: Aside from formatting, another important difference between Write-Host and Out-Host is that in PSv5+ only Write-Host writes to the host via the information stream (stream number 6), whereas Out-Host truly writes directly to the host, which means that its output cannot be captured with redirections such as 6> or *> - see about_Redirection.
I'm finishing a script in PowerShell and this is what I must do:
Find and retrieve all .txt files inside a folder
Read their contents (there is a number inside that must be less than 50)
If any of these files has a number greater than 50, change a flag which will allow me to send a crit message to a monitoring server.
The piece of code below is what I already have, but it's probably wrong because I haven't given any argument to Get-Content, it's probably something very simple, but I'm still getting used to PowerShell. Any suggestions? Thanks a lot.
Get-ChildItem -Path C:\temp_erase\PID -Directory -Filter *.txt |
ForEach-Object{
$warning_counter = Get-Content
if ($warning_counter -gt '50')
{
$crit_counter = 1
Write-Host "CRITICAL: Failed to kill service more than 50 times!"
}
}
but it's probably wrong because I haven't given any argument to Get-Content
Yes. That is the first issue. Have a look at Get-Help <command> and or docs like TechNet when you are lost. For the core cmdlets you will always see examples.
Second, Get-Content, returns string arrays (by default), so if you are doing a numerical comparison you need to treat the value as such.
Thirdly you have a line break between foreach-object cmdlet and its opening brace. That will land you a parsing problem and PS will prompt for the missing process block. So changing just those mentioned ....
Get-ChildItem -Path C:\temp_erase\PID -Directory -Filter *.txt | ForEach-Object{
[int]$warning_counter = Get-Content $_.FullName
if ($warning_counter -gt '50')
{
$crit_counter = 1
Write-Host "CRITICAL: Failed to kill service more than 50 times!"
}
}
One obvious thing missing from this is you do not show which file triggered the message. You should update your notification/output process. You also have no logic validating file contents. The could easily fail, either procedural or programically, on files with non numerical contents.
Currently my log function spits out the information in a single column and is hard to read. Is there a way to make it split up into different columns which each (DisplayName, PoolName, PoolSnapshot, and DesktopSVIVmSnapshot) and its respective information is put correctly?
function log ([string]$entry) {
Write-Output $entry | Out-File -Append "C:\logs\SNAPSHOT.csv"
}
Add-PSSnapin Quest.ActiveRoles.ADManagement
$date = Get-Date -Format "MM-dd-yyyy"
$time = Get-Date -Format "hh:mm:sstt"
# begin log
log $(Get-Date)
log "The below Desktops are not using the correct Snapshot."
if (#($DesktopExceptions).Count -lt 1) {
Write-Output "All desktops in $pool are currently using the correct snapshots." |
Out-File -Append "C:\logs\SNAPSHOT.csv"
} else {
Write-Output $DesktopExceptions |
Select-Object DisplayName,PoolName,PoolSnapshot,DesktopSVIVmSnapshot |
sort DisplayName |
Out-File -Append "C:\logs\SNAPSHOT.csv"
}
log $(Get-Date)
09/11/2017 12:16:17
DisplayName PoolName PoolSnapshot DesktopSVIVmSnapshot
----------- -------- ------------ --------------------
xxxc-13v xxxc-xxx /8-11-2017/09-07-2017 /8-11-2017
xxxc-15v xxxc-xxx /8-11-2017/09-07-2017 /8-11-2017
xxxc-1v xxxc-xxx /8-11-2017/09-07-2017 /8-11-2017
xxxc-20v xxxc-xxx /8-11-2017/09-07-2017 /8-11-2017
Note: I removed parts of the log for in the hopes to not make the post long.
CSV files require uniform lines: a header line with column names, followed by data lines containing column values.
By writing the output from Get-Date first - a single date/time string - followed by another single-string output, followed by multi-column output from your $DesktopExceptions | Select-Object ... call, you're by definition not creating a valid CSV file.
If you still want to create such a file:
log (Get-Date) # With a single command, you don't need $(...) - (...) will do.
log "The below Desktops are not using the correct Snapshot."
If ($DesktopExceptions) # a non-empty array / non-$null object
{
log ($DesktopExceptions |
Select-Object DisplayName,PoolName,PoolSnapshot,DesktopSVIVmSnapshot |
Sort-Object DisplayName |
ConvertTo-Csv -NoTypeInformation)
}
Else
{
log "All desktops in $pool are currently using the correct snapshots."
}
log (Get-Date)
By defining your log() function's parameter as type [string], you're effectively forcing stringification of whatever object you pass to it. This stringification is the same you get when you embed a variable reference or command inside "..." (string expansion / interpolation) - but it is not the same as what you get by default, when you print to the console.
Out-File, by contrast, does result in the same output you get when printing to the console, which, however, is a format for human consumption, not for machine parsing (as CSV is, for instance).
To get CSV-formatted output, you must either use Export-Csv - to write directly to a file - or ConvertTo-Csv- to get a string representation.
Also note that there's typically no reason to use Write-Output explicitly - any command / expression's output that is not explicitly assigned to a variable / redirected (to a file or $null) is implicitly sent to PowerShell's [success] output stream; e.g., Write-Output Get-Date is the same as just Get-Date.
It looks like you're just writing an object, and taking the default PowerShell formatter behavior.
A better thing to do is make your log only responsible for one thing - writing messages to a file (no formatting). Here's an example of what you might try:
function Write-LogMessage {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, HelpMessage = "The text-content to write to the log file.",
ValueFromPipeline = $true)]
[string]$Text
)
Process {
Write-Host -ForegroundColor Green $Text
}
}
Set-Alias log Write-LogMessage
Note: This example writes directly to the PowerShell console, but you would in practice need to direct output to a file (using Out-File or one of the redirection operators - see Get-Help about_Operators).
To use it, you would write something like this:
"This is a message that would be written" | Write-LogMessage
For your specific example, you could just format the message inline, and pipe it:
Write-Output $DesktopExceptions | Select-Object DisplayName,PoolName,PoolSnapshot,DesktopSVIVmSnapshot | sort DisplayName | ForEach-Object { "{0}: Host = {1}, Pool = {2}, Pool SN = {3}, SVIV Snapshot = {4}" -f (Get-Date), $_.DisplayName, $_.PoolName, $_.PoolSnapshot, $_.DesktopSVIVmSnapshot } | log
Note that you don't need the log statement: just add formatting before piping to the Out-File cmdlet, and you'll get what you're after.
Edit: The OP asked in the original post how to format columns (tabular output). To achieve this, you can use either the ConvertTo-Csv or Export-Csv cmdlets (generally, you would use the -NoTypeInformation switch parameter with these commands, to avoid the first line of the output being a type definition). An example of this is:
$DesktopExceptions | Select-Object DisplayName,PoolName,PoolSnapshot,DesktopSVIVmSnapshot | sort DisplayName | Export-Csv C:\Temp\Datum.csv -NoTypeInformation
As pointed out in another answer, using Write-Output is not required, because PowerShell automatically writes all output to the output stream unless otherwise directed (using file redirection, a redirection operator, or the Out-Null cmdlet).
Please read my answer as part solution and part advice.
The "problem" with PowerShell is that it doesn't capture only the output of your code. It will capture output from other scripts, modules and executables. In other words, any attempt to make logging behave like it's generated by e.g. C# with NLOG, has an inherent problem.
I looked into this subject myself for a complex continuous delivery pipeline I'm building. I understood that a structured log will not be 100% possible and therefore I accepted the purpose of PowerShell transcription (Start-Transcript). But still I wanted to avoid creating functions like Write-Log and if possible provide an enhanced output for all code that uses Write-Debug, Write-Verbose functionality.
I ended up creating XWrite PowerShell module which works very well, even to my own suprize. I use it because it enhances the produced trace message by the caller's name (cmdlet or script) and a timestamp. The caller's name helps a lot with troubleshooting and the timestamp I use to implicitly benchmark. here are a couple of example
DEBUG: Test-MyXWrite.ps1: Hello
DEBUG: Script: Test-MyXWrite.ps1: 20170804: 10:57:27.845: Hello
There are some limitations though. Any binary's code trace output will not be enhanced. Also if a cmllet refers explicitly to the Write-* using their full namespace it will not work. To capture line by line all trace and output requires some very deep into the .net types of PowerShell implementation hooking. There is a guy who has done this, but I don't want to get influence the PowerShell process's behavior that aggresively. And at this moment I believe that to be the role of the transcription.
If you like the idea, install the module from XWrite
At some point, I would like to extend the module with a redirection to telemetry services, but I've still not decided I want to do that, because I will not capture the above mentioned exceptions and other executable. It will just offer me visible progress as the script is executing.
I'm writing a function in PowerShell that I want to be called via other PowerShell functions as well as be used as a standalone function.
With that objective in mind, I want to send a message down the pipeline using Write-Output to these other functions.
However, I don't want Write-Output to write to the PowerShell console. The TechNet page for Write-Output states:
Write-Output:
Sends the specified objects to the next command in the pipeline. If the command is the last command in the pipeline, the objects are displayed in the console.
-NoEnumerate:
By default, the Write-Output cmdlet always enumerates its output. The NoEnumerate parameter suppresses the default behavior, and prevents Write-Output from enumerating output. The NoEnumerate parameter has no effect on collections that were created by wrapping commands in parentheses, because the parentheses force enumeration.
For some reason, this -NoEnumerate switch will not work for me in either the PowerShell ISE or the PowerShell CLI. I always get output to my screen.
$data = "Text to suppress"
Write-Output -InputObject $data -NoEnumerate
This will always return 'Text to suppress' (no quotes).
I've seen people suggest to pipe to Out-Null like this:
$data = "Text to suppress"
Write-Output -InputObject $data -NoEnumerate | Out-Null
$_
This suppresses screen output, but when I use $_ I have nothing in my pipeline afterwards which defeats the purpose of me using Write-Output in the first place.
System is Windows 2012 with PowerShell 4.0
Any help is appreciated.
Write-Output doesn't write to the console unless it's the last command in the pipeline. In your first example, Write-Output is the only command in the pipeline, so its output is being dumped to the console. To keep that from happening, you need to send the output somewhere. For example:
Write-Output 5
will send "5" to the console, because Write-Output is the last and only command in the pipeline. However:
Write-Output 5 | Start-Sleep
no longer does that because Start-Sleep is now the next command in the pipeline, and has therefore become the recipient of Write-Output's data.
Try this:
Write your function as you have written it with Write-Output as the last command in the pipeline. This should send the output up the line to the invoker of the function. It's here that the invoker can use the output, and at the same time suppress writing to the console.
MyFunction blah, blah, blah | % {do something with each object in the output}
I haven't tried this, so I don't know if it works. But it seems plausible.
My question is not the greatest.
First of all Write-Output -NoEnumerate doesn't suppress output on Write-Output.
Secondly, Write-Output is supposed to write its output. Trying to make it stop is a silly goal.
Thirdly, piping Write-Output to Out-Null or Out-File means that the value you gave Write-Output will not continue down the pipeline which was the only reason I was using it.
Fourth, $suppress = Write-Output "String to Suppress" also doesn't pass the value down the pipeline.
So I'm answering my question by realizing if it prints out to the screen that's really not a terrible thing and moving on. Thank you for your help and suggestions.
Explicitly storing the output in a variable would be more prudent than trying to use an implicit automatic variable. As soon as another command is run, that implicit variable will lose the prior output stored in it. No automatic variable exists to do what you're asking.
If you want to type out a set of commands without storing everything in temporary variables along the way, you can write a scriptblock at the command line as well, and make use of the $_ automatic variable you've indicated you're trying to use.
You just need to start a new line using shift + enter and write the code block as you would in a normal scriptblock - in which you could use the $_ automatic variable as part of a pipeline.