Appropriate logging in Powershell - powershell

If I have a powershell script say called caller.ps1 which looks like this
.\Lib\library.ps1
$output = SomeLibraryFunction
where library.ps1 looks like the following
function SomeLibraryFunction()
{
Write-Output "Some meaningful function logging text"
Write-Output "return value"
}
What I'd like to achieve is a way in which the library function can return it's value but also add some logging messages that allow the caller to process those internal messages as they see fit. The best I can think of is to write both to the pipeline and then the caller will have an array with the actual return value plus the internal messages which may be of use to a logger the calling script has.
Am I going about this problem the right way? Is there a better way to achieve this?

It's usually not a good idea to mix logging messages with actual output. Consumers of your function then have to do a lot of filtering to find the object they really want.
Your function should write logging messages to the verbose output stream. If the caller wants to see those messages, it can by specifying the -Verbose switch to your function.
function SomeLibraryFunction()
{
[CmdletBinding()]
param(
)
Write-Verbose "Some meaningful function logging text"
Write-Output "return value"
}
In PowerShell 3+, consumers can redirect your verbose messages to the output stream or a file:
# Show verbose logging messages on the console
SomeLibraryFunction -Verbose
# Send verbose logging messages to a file
SomeLibraryFunction -Verbose 4> verbose.txt
# redirect verbose message to the output stream
SomeLibraryFunction -Verbose 4>&1
Other options include:
Writing to a well-known file
Writing to the Event Log
Use Start-Transcript to create a log of the session.

Something like Tee-Object might be helpful to you
function SomeLibraryFunction()
{
$filepath = "C:\temp\log.txt"
write-output "Some meaningful function logging text" | Tee-Object -FilePath $filepath -Append
write-output "return value" | Tee-Object -FilePath $filepath -Append
}
For more information on Tee-Object look here
You could use an If statement based on a variable like $logging=$true but i could see that getting messy.
Another Approach
If you are looking for more of an optional solution then maybe you could use something like this Start-Transcript and Stop-Transcript which creates a record of all or part of a Windows PowerShell session in a text file.
function SomeLibraryFunction()
{
write-output "Some meaningful function logging text"
write-output "return value"
}
$logging = $True
If ($logging){
Start-Transcript -Path C:\temp\log.txt -Append
}
SomeLibraryFunction
If ($logging){
Stop-Transcript
}
This would just show that you could toggle the Start and Stop. You could even set the switch with a paramater passed to the script if you chose.
NOTE The output might be more that you are looking for but at least give it a try. Also, this will not work in the Powershell ISE as you will get an error Start-Transcript : This host does not support transcription.

Another way to do this would be to return a compound object that includes the results and the log information. This is then easy to pick apart.
function MyFunc
{
# initialize
$result = $null
$log = #()
# write to the log
$log += "This will be written to the log"
$log += "So will this"
# record the result
$result = $true
# return result and log combined into object
return New-Object -TypeName PSObject -Property #{ result = $result; log = $log -join "`r`n" }
}
# Call the function and get the results
$MyFuncResult = MyFunc
# Display the returned result value
Write-Host ( "MyFunc Result = {0}" -f $MyFuncResult.Result )
# Display the log
write-host ( "MyFunc Log = {0}" -f $MyFuncResult.Log )
Alternatively, if you want to avoid the object, pass in a log variable by reference. The function can write to the log variable and the changes will be visible in the calling scope. To do this, you need to add the [ref] prefix to the function definition AND the function call. When you write to the variable in the function you need to refer to the .value property.
function MyFunc2 ([ref]$log)
{
# initialize
$result = $null
# write to the log
$log.value += "`r`nThis will be written to the log"
$log.value += "`r`nSo will this"
# record the result
$result = $true
# return result and log combined into object
return $result
}
# Call the function and get the results
$log = "before MyFunc2"
$MyFuncResult = MyFunc2([ref]$log)
$log += "`nafter MyFunc2"
# Display the returned result value
write-host ( "MyFunc2 result = {0}" -f $MyFuncResult )
# Display the log
write-host ( "MyFunc2 Log = {0}" -f $Log )

Related

When running a command in powershell how can I prepend a date/time for all output on stdout/stderr?

Is it possible in powershell when running a script to add a date prefix to all log output?
I know that it would be possible to do something like:
Write-Host "$(Get-Date -format 'u') my log output"
But I dont want to have to call some function for each time we output a line. Instead I want to modify all output when running any script or command and have the time prefix for every line.
To insert a date in front of all output, that is stdout, stderr and the PowerShell-specific streams, you can use the redirection operator *>&1 to redirect (merge) all streams of a command or scriptblock, pipe to Out-String -Stream to format the stream objects into lines of text and then use ForEach-Object to process each line and prepend the date.
Let me start with a simple example, a more complete solution can be found below.
# Run a scriptblock
&{
# Test output to all possible streams, using various formatting methods.
# Added a few delays to test if the final output is still streaming.
"Write $($PSStyle.Foreground.BrightGreen)colored`ntext$($PSStyle.Reset) to stdout"
Start-Sleep -Millis 250
[PSCustomObject]#{ Answer = 42; Question = 'What?' } | Format-Table
Start-Sleep -Millis 250
Get-Content -Path not-exists -EA Continue # produce a non-terminating error
Start-Sleep -Millis 250
Write-Host 'Write to information stream'
Start-Sleep -Millis 250
Write-Warning 'Write to warning stream'
Start-Sleep -Millis 250
Write-Verbose 'Write to verbose stream' -Verbose
Start-Sleep -Millis 250
$DebugPreference = 'Continue' # To avoid prompt, needed for Windows Powershell
Write-Debug 'Write to debug stream'
} *>&1 | Out-String -Stream | ForEach-Object {
# Add date in front of each output line
$date = Get-Date -Format "yy\/MM\/dd H:mm:ss"
foreach( $line in $_ -split '\r?\n' ) {
"$($PSStyle.Reset)[$date] $line"
}
}
Output in PS 7.2 console:
Using Out-String we use the standard PowerShell formatting system to have the output look normally, as it would appear without redirection (e. g. things like tables stay intact). The -Stream parameter is crucial to keep the streaming output behaviour of PowerShell. Without this parameter, output would only be received once the whole scriptblock has completed.
While the output already looks quite nice, there are some minor issues:
The verbose, warning and debug messages are not colored as usual.
The word "text" in the 2nd line should be colored in green. This isn't working due to the use of $PSStyle.Reset. When removed, the colors of the error message leak into the date column, which looks far worse. It can be fixed, but it is not trivial.
The line wrapping isn't right (it wraps into the date column in the middle of the output).
As a more general, reusable solution I've created a function Invoke-WithDateLog that runs a scriptblock, captures all of its output, inserts a date in front of each line and outputs it again:
Function Invoke-WithDateLog {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[scriptblock] $ScriptBlock,
[Parameter()]
[string] $DateFormat = '[yy\/MM\/dd H:mm:ss] ',
[Parameter()]
[string] $DateStyle = $PSStyle.Foreground.BrightBlack,
[Parameter()]
[switch] $CatchExceptions,
[Parameter()]
[switch] $ExceptionStackTrace,
[Parameter()]
[Collections.ICollection] $ErrorCollection
)
# Variables are private so they are not visible from within the ScriptBlock.
$private:ansiEscapePattern = "`e\[[0-9;]*m"
$private:lastFmt = ''
& {
if( $CatchExceptions ) {
try { & $scriptBlock }
catch {
# The common parameter -ErrorVariable doesn't work in scripted cmdlets, so use our own error variable parameter.
if( $null -ne $ErrorCollection ) {
$null = $ErrorCollection.Add( $_ )
}
# Write as regular output, colored like an error message.
"`n" + $PSStyle.Formatting.Error + "EXCEPTION ($($_.Exception.GetType().FullName)):`n $_" + $PSStyle.Reset
# Optionally write stacktrace. Using the -replace operator we indent each line.
Write-Debug ($_.ScriptStackTrace -replace '^|\r?\n', "`n ") -Debug:$ExceptionStackTrace
}
}
else {
& $scriptBlock
}
} *>&1 | ForEach-Object -PipelineVariable record {
# Here the $_ variable is either:
# - a string in case of simple output
# - an instance of one of the System.Management.Automation.*Record classes (output of Write-Error, Write-Debug, ...)
# - an instance of one of the Microsoft.PowerShell.Commands.Internal.Format.* classes (output of a Format-* cmdlet)
if( $_ -is [System.Management.Automation.ErrorRecord] ) {
# The common parameter -ErrorVariable doesn't work in scripted cmdlets, so use our own error variable parameter.
if( $null -ne $ErrorCollection ) {
$null = $ErrorCollection.Add( $_ )
}
}
$_ # Forward current record
} | Out-String -Stream | ForEach-Object {
# Here the $_ variable is always a (possibly multiline) string of formatted output.
# Out-String doesn't add any ANSI escape codes to colorize Verbose, Warning and Debug messages,
# so we have to do it by ourselfs.
$overrideFmt = switch( $record ) {
{ $_ -is [System.Management.Automation.VerboseRecord] } { $PSStyle.Formatting.Verbose; break }
{ $_ -is [System.Management.Automation.WarningRecord] } { $PSStyle.Formatting.Warning; break }
{ $_ -is [System.Management.Automation.DebugRecord] } { $PSStyle.Formatting.Debug; break }
}
# Prefix for each line. It resets the ANSI escape formatting before the date.
$prefix = $DateStyle + (Get-Date -Format $DateFormat) + $PSStyle.Reset
foreach( $line in $_ -split '\r?\n' ) {
# Produce the final, formatted output.
$prefix + ($overrideFmt ?? $lastFmt) + $line + ($overrideFmt ? $PSStyle.Reset : '')
# Remember last ANSI escape sequence (if any) of current line, for cases where formatting spans multiple lines.
$lastFmt = [regex]::Match( $line, $ansiEscapePattern, 'RightToLeft' ).Value
}
}
}
Usage example:
# To differentiate debug and verbose output from warnings
$PSStyle.Formatting.Debug = $PSStyle.Foreground.Yellow
$PSStyle.Formatting.Verbose = $PSStyle.Foreground.BrightCyan
Invoke-WithDateLog -CatchExceptions -ExceptionStackTrace {
"Write $($PSStyle.Foreground.Green)colored`ntext$($PSStyle.Reset) to stdout"
[PSCustomObject]#{ Answer = 42; Question = 'What?' } | Format-Table
Get-Content -Path not-exists -EA Continue # produce a non-terminating error
Write-Host 'Write to information stream'
Write-Warning 'Write to warning stream'
Write-Verbose 'Write to verbose stream' -Verbose
Write-Debug 'Write to debug stream' -Debug
throw 'Critical error'
}
Output in PS 7.2 console:
Notes:
The code requires PowerShell 7+.
The date formatting can be changed through parameters -DateFormat (see formatting specifiers) and -DateStyle (ANSI escape sequence for coloring).
Script-terminating errors such as created by throwing an exception or using Write-Error -EA Stop, are not logged by default. Instead they bubble up from the scriptblock as usual. You can pass parameter -CatchExceptions to catch exceptions and log them like regular non-terminating errors. Pass -ExceptionStackTrace to also log the script stacktrace, which is very useful for debugging.
Scripted cmdlets such as this one don't set the automatic variable $? and also don't add errors to the automatic $Error variable when an error is written via Write-Error. Neither the common parameter -ErrorVariable works. To still be able to collect error information I've added parameter -ErrorCollection which can be used like this:
$scriptErrors = [Collections.ArrayList]::new()
Invoke-WithDateLog -CatchExceptions -ExceptionStackTrace -ErrorCollection $scriptErrors {
Write-Error 'Write to stderr' -EA Continue
throw 'Critical error'
}
if( $scriptErrors ) {
# Outputs "Number of errors: 2"
"`nNumber of errors: $($scriptErrors.Count)"
}
The objects generated by Write-Host already come with a timestamp, you can use Update-TypeData to override the .ToString() Method from the InformationRecord Class and then redirect the output from the Information Stream to the Success Stream.
Update-TypeData -TypeName System.Management.Automation.InformationRecord -Value {
return $this.TimeGenerated.ToString('u') + $this.MessageData.Message.PadLeft(10)
} -MemberType ScriptMethod -MemberName ToString -Force
'Hello', 'World', 123 | Write-Host 6>&1

Write-Host vs Write-Information in PowerShell 5

It is well known that Write-Host is evil.
In PowerShell 5, Write-Information is added and is considered to replace Write-Host.
But, really, which is better?
Write-Host is evil for it does not use pipeline, so the input message can't get reused.
But, what Write-Host do is just to show something in the console right? In what case shall we reuse the input?
Anyway, if we really want to reuse the input, why not just write something like this:
$foo = "Some message to be reused like saving to a file"
Write-Host $foo
$foo | Out-File -Path "D:\foo.log"
Another Cons of Write-Host is that, Write-Host can specified in what color the messages are shown in the console by using -ForegroundColor and -BackgroundColor.
On the other side, by using Write-Information, the input message can be used wherever we want via the No.6 pipeline. And doesn't need to write the extra codes like I write above. But the dark side of this is that, if we want to write messages to the console and also saved to the file, we have to do this:
# Always set the $InformationPreference variable to "Continue"
$InformationPreference = "Continue";
# if we don't want something like this:
# ======= Example 1 =======
# File Foo.ps1
$InformationPreference = "Continue";
Write-Information "Some Message"
Write-Information "Another Message"
# File AlwaysRunThisBeforeEverything.ps1
.\Foo.ps1 6>"D:\foo.log"
# ======= End of Example 1 =======
# then we have to add '6>"D:\foo.log"' to every lines of Write-Information like this:
# ======= Example 2 =======
$InformationPreference = "Continue";
Write-Information "Some Message" 6>"D:\foo.log"
Write-Information "Another Message" 6>"D:\foo.log"
# ======= End of Example 2 =======
A little bit redundant I think.
I only know a little aspect of this "vs" thing, and there must have something out of my mind. So is there anything else that can make me believe that Write-Information is better than Write-Host, please leave your kind answers here.
Thank you.
The Write-* cmdlets allow you to channel the output of your PowerShell code in a structured way, so you can easily distinguish messages of different severity from each other.
Write-Host: display messages to an interactive user on the console. Unlike the other Write-* cmdlets this one is neither suitable nor intended for automation/redirection purposes. Not evil, just different.
Write-Output: write the "normal" output of the code to the default (success) output stream ("STDOUT").
Write-Error: write error information to a separate stream ("STDERR").
Write-Warning: write messages that you consider warnings (i.e. things that aren't failures, but something that the user should have an eye on) to a separate stream.
Write-Verbose: write information that you consider more verbose than "normal" output to a separate stream.
Write-Debug: write information that you consider relevant for debugging your code to a separate stream.
Write-Information is just a continuation of this approach. It allows you to implement log levels in your output (Debug, Verbose, Information, Warning, Error) and still have the success output stream available for regular output.
As for why Write-Host became a wrapper around Write-Information: I don't know the actual reason for this decision, but I'd suspect it's because most people don't understand how Write-Host actually works, i.e. what it can be used for and what it should not be used for.
To my knowledge there isn't a generally accepted or recommended approach to logging in PowerShell. You could for instance implement a single logging function like #JeremyMontgomery suggested in his answer:
function Write-Log {
Param(
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[string]$Message,
[Parameter(Mandatory=$false, Position=1)]
[ValidateSet('Error', 'Warning', 'Information', 'Verbose', 'Debug')]
[string]$LogLevel = 'Information'
)
switch ($LogLevel) {
'Error' { ... }
'Warning' { ... }
'Information' { ... }
'Verbose' { ... }
'Debug' { ... }
default { throw "Invalid log level: $_" }
}
}
Write-Log 'foo' # default log level: Information
Write-Log 'foo' 'Information' # explicit log level: Information
Write-Log 'bar' 'Debug'
or a set of logging functions (one for each log level):
function Write-LogInformation {
Param(
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[string]$Message
)
...
}
function Write-LogDebug {
Param(
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[string]$Message
)
...
}
...
Write-LogInformation 'foo'
Write-LogDebug 'bar'
Another option is to create a custom logger object:
$logger = New-Object -Type PSObject -Property #{
Filename = ''
Console = $true
}
$logger | Add-Member -Type ScriptMethod -Name Log -Value {
Param(
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[string]$Message,
[Parameter(Mandatory=$false, Position=1)]
[ValidateSet('Error', 'Warning', 'Information', 'Verbose', 'Debug')]
[string]$LogLevel = 'Information'
)
switch ($LogLevel) {
'Error' { ... }
'Warning' { ... }
'Information' { ... }
'Verbose' { ... }
'Debug' { ... }
default { throw "Invalid log level: $_" }
}
}
$logger | Add-Member -Type ScriptMethod -Name LogDebug -Value {
Param([Parameter(Mandatory=$true)][string]$Message)
$this.Log($Message, 'Debug')
}
$logger | Add-Member -Type ScriptMethod -Name LogInfo -Value {
Param([Parameter(Mandatory=$true)][string]$Message)
$this.Log($Message, 'Information')
}
...
Write-Log 'foo' # default log level: Information
$logger.Log('foo') # default log level: Information
$logger.Log('foo', 'Information') # explicit log level: Information
$logger.LogInfo('foo') # (convenience) wrapper method
$logger.LogDebug('bar')
Either way you can externalize the logging code by
putting it into a separate script file and dot-sourcing that file:
. 'C:\path\to\logger.ps1'
putting it into a module and importing that module:
Import-Module Logger
To complement Ansgar's helpful and comprehensive answer:
Write-Host became (in essence) a wrapper for
Write-Information -InformationAction Continue in PSv5, presumably because:
it enables suppressing or redirecting Write-Host messages, which was not previously possible (in PowerShell 4 or below, Write-Host bypassed PowerShell's streams and output directly to the host),
while preserving backward compatibility in that the messages are output by default - unlike with Write-Information, whose default behavior is to be silent (because it respects preference variable $InformationPreference, whose default value is SilentlyContinue).
While Write-Host is therefore now (PSv5+) a bit of a misnomer — it doesn't necessarily write to the host anymore — it still has one distinct advantage over Write-Information (as you state): it can produce colored output with -ForegroundColor and -BackgroundColor.
Ansgar's answer has the conventional logging perspective covered, but PowerShell's Start-Transcript cmdlet may serve as a built-in alternative (see below).
As for your desire to output messages to the host while also capturing them in a log file:
PowerShell's session transcripts - via Start-Transcript and Stop-Transcript - may give you what you want.
As the name suggests, transcripts capture whatever prints to the screen (without coloring), which therefore by default includes success output, however.
Applied to your example:
$null = Start-Transcript "D:\foo.log"
$InformationPreference = "Continue"
Write-Information "Some Message"
Write-Information "Another Message"
$null = Stop-Transcript
The above will print messages to both the screen and the transcript file; note that, curiously, only in the file will they be prefixed with INFO:.
(By contrast, Write-Warning, Write-Verbose and Write-Debug - if configured to produce output - use prefix WARNING:, VERBOSE:, DEBUG: both on-screen and in the file; similarly, Write-Error produces "noisy" multiline input both on-screen and in the file.)
Note one bug that only affects Windows PowerShell (it has been fixed in PowerShell [Core].Thanks, JohnLBevan.): output from Write-Information shows up in the transcript file (but not on the screen) even when $InformationPreference is set to SilentlyContinue (the default); the only way to exclude Write-Information output (via the preference variable or -InformationAction parameter) appears to be a value of Ignore - which silences the output categorically - or, curiously, Continue, in which it only prints to the console, as PetSerAl points out.
In a nutshell, you can use Start-Transcript as a convenient, built-in approximation of a logging facility, whose verbosity you can control from the outside via the preference variables ($InformationPreference, $VerbosePreference, ...), with the following important differences from conventional logging:
Generally, what goes into the transcript file is also output to the console (which could generally be considered a plus).
However, success output (data output) is by default also sent to the transcript - unless you capture it or suppress it altogether - and you cannot selectively keep it out of the transcript:
If you capture or suppress it, it won't show in in the host (the console, by default) either[1].
The inverse, however, is possible: you can send output to the transcript only (without echoing it in the console), by way of Out-Default -Transcript Thanks, PetSerAl; e.g.,
'to transcript only' | Out-Default -Transcript; however, as of PowerShell 7.0 this appears to log the output twice in the transcript; also note that Out-Default is generally not meant to be called from user code - see this answer.
Generally, external redirections (applying > to a call to a script that internally performs transcription) keep streams out of the transcript, with two exceptions, as of PowerShell 7.0:
Write-Host output, even if 6> or *> redirections are used.
Error output, even if 2> or *> redirections are used.
However, using $ErrorActionPreference = 'SilentlyContinue' / 'Ignore' does keep non-terminating errors out of the transcript, but not terminating ones.
Transcript files aren't line-oriented (there's a block of header lines with invocation information, and there's no guarantee that output produced by the script is confined to a line), so you cannot expect to parse them in a line-by-line manner.
[1] PetSerAl mentions the following limited and somewhat cumbersome workaround (PSv5+) for sending success output to the console only, which notably precludes sending the output through the pipeline or capturing it:
'to console only' | Out-String -Stream | ForEach-Object { $Host.UI.WriteLine($_) }
PowerShell is about automation.
Sometimes, you run a script multiple times a day and you don't want to see the output all the time.
Write-Host has no possibility of hiding the output. It gets written on the Console, no matter what.
With Write-Information, you can specify the -InformationAction Parameter on the Script. With this parameter, you can specify if you want to see the messages (-InformationAction Continue) or not (-InformationAction SilentlyContinue)
Edit:
And please use "Some Message" | out-file D:\foo.log for logging, and neither Write-Host or Write-Information
Here's a generic version of a more specialized logging function that I used for a script of mine recently.
The scenario for this is that when I need to do something as a scheduled task, I typically create a generic script, or function in a module that does the "heavy lifting" then a calling script that handles the specifics for the particular job, like getting arguments from an XML config, logging, notifications, etc.
The inner script uses Write-Error, Write-Warning, and Write-Verbose, the calling script redirects all output streams down the pipeline to this function, which captures records the messages in a csv file with a Timestamp, Level, and Message.
In this case, it was targeted at PoSh v.4, so I basically am using Write-Verbose as a stand-in for Write-Information, but same idea. If I were to have used Write-Host in the Some-Script.ps1 (see example) instead of Write-Verbose, or Write-Information, the Add-LogEntry function wouldn't capture and log the message. If you want to use this to capture more streams appropriately, add entries to the switch statement to meet your needs.
The -PassThru switch in this case was basically a way to address exactly what you mentioned about both writing to a log file in addition to outputting to the console (or to another variable, or down the pipeline). In this implementation, I've added a "Level" property to the object, but hopefully you can see the point. My use case for this was to pass the log entries to a variable so that they could be checked for errors, and used in an SMTP notification if an error did occur.
function Add-LogEntry {
[CmdletBinding()]
param (
# Path to logfile
[Parameter(ParameterSetName = 'InformationObject', Mandatory = $true, Position = 0)]
[Parameter(ParameterSetName = 'Normal', Mandatory = $true, Position = 0)]
[String]$Path,
# Can set a message manually if not capturing an alternate output stream via the InformationObject parameter set.
[Parameter(ParameterSetName = 'Normal', Mandatory = $true)]
[String]$Message,
# Captures objects redirected to the output channel from Verbose, Warning, and Error channels
[ValidateScript({ #("VerboseRecord", "WarningRecord", "ErrorRecord") -Contains $_.GetType().name })]
[Parameter(ParameterSetName = 'InformationObject', Mandatory = $true, ValueFromPipeline = $true)]
$InformationObject,
# If using the message parameter, must specify a level, InformationObject derives level from the object.
[ValidateSet("Information", "Warning", "Error")]
[Parameter(ParameterSetName = 'Normal', Mandatory = $true, Position = 2)]
[String]$Level,
# Forward the InformationObject down the pipeline with additional level property.
[Parameter(ParameterSetName = 'InformationObject', Mandatory = $false)]
[Switch]$PassThru
)
Process {
# If using an information object, set log entry level according to object type.
if ($PSCmdlet.ParameterSetName -eq "InformationObject") {
$Message = $InformationObject.ToString()
# Depending on the object type, set the error level,
# add entry to cover "Write-Information" output here if needed
switch -exact ($InformationObject.GetType().name) {
"VerboseRecord" { $Level = "Information" }
"WarningRecord" { $Level = "Warning" }
"ErrorRecord" { $Level = "Error" }
}
}
# Generate timestamp for log entry
$Timestamp = (get-date).Tostring("yyyy\-MM\-dd\_HH\:mm\:ss.ff")
$LogEntryProps = #{
"Timestamp" = $Timestamp;
"Level" = $Level;
"Message" = $Message
}
$LogEntry = New-Object -TypeName System.Management.Automation.PSObject -Property $LogEntryProps
$LogEntry | Select-Object Timestamp, Level, Message | Export-Csv -Path $Path -NoTypeInformation -Append
if ($PassThru) { Write-Output ($InformationObject | Add-Member #{Level = $Level } -PassThru) }
}
}
Example usage would be
& $PSScriptRoot\Some-Script.ps1 -Param $Param -Verbose *>&1 | Add-LogEntry -Path $LogPath -PassThru
The -PassThru switch should essentially write the information object to the console if you don't capture the output in a variable or pass it down the pipe to something else.
Interesting: while the preference variables have been mentioned in the earlier answers, nobody has mentioned the common parameters -InformationVariable, -ErrorVariable and -WarningVariable.
They are designed to take the output from the corresponding Write-* commands in the script or command so you can do with it what you want.
They're not really suitable to use as a log of an entire session, because they need to be added to every command you run, and the command/script needs to support it. (can be as easy as adding [CmdletBinding()] at the top of you scipt if you don't have it already. See the documentation)
But it could be a good way to separate the warnings/error from the real output of a command. Just run it again with the proper parameters added.
For example -ErrorVariable ErrorList -ErrorAction SilentlyContinue.
Now all the errors are stored in the $ErrorList variable and you can write them in a file.
$ErrorList|Out-File -FilePath errors.txt
I have to admit that I hate PowerShell logging and all the Write-* commands... So I start all my scripts with the same function:
function logto{ ## Outputs data to Folder tree
Param($D,$P,$F,$C,$filename)
$LogDebug = $false
$FDomain =[System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
$SCRdir = $MyInvocation.ScriptName
$FDNSName = $FDomain.Name
$RealFile = $F
if($ScriptName -eq $null){
$ScriptName = "\LogTo\"
}
## if there is a time stamp defined make it part of the directory
if($GlobalRunTime){
$Flocaldrive = $env:SystemDrive + "\" + $FDNSName + $ScriptName + $GlobalRunTime + "\"
If ($LogDebug) {Write-host "Set path to $Flocaldrive" -foregroundcolor Magenta}
}else{
$Flocaldrive = $env:SystemDrive + "\" + $FDNSName + $ScriptName
If ($LogDebug) {Write-host "Set path to $Flocaldrive" -foregroundcolor Magenta}
}
## do not write null data
if ($D -eq $null) {
If ($LogDebug) {Write-host "$RealFile :Received Null Data Exiting Function" -foregroundcolor Magenta}
Return
}
## if no path is chosen default to
if ($P -eq $null) {
$PT = $Flocaldrive
If ($LogDebug) {Write-host "Path was Null, setting to $PT" -foregroundcolor Magenta}
}else{
$PT = $Flocaldrive + $P
If ($LogDebug) {Write-host "Path detected as $p, setting path to $PT" -foregroundcolor Magenta}
}
## anything with no file goes to Catchall
If ($RealFile-eq $null) {
If ($LogDebug) {Write-host "$D :attempting to write to Null file name, redirected out to Catchall" -foregroundcolor Magenta}
$RealFile= "\Catchall.txt"
}
##If color is blank DONT write to screen
if ($C -eq $null) {
If ($LogDebug) {Write-host "Color was blank so not writing to screen" -foregroundcolor Magenta}
}else{
If ($LogDebug) {Write-host "Attempting to write to console in $C" -foregroundcolor Magenta}
write-host $D -foregroundcolor $C
}
###### Write standard format
$DataFile = $PT + $RealFile## define path with File
## Check if path Exists if not create it
If (Test-Path $PT) {
If ($LogDebug) {Write-host "$PT :Directory Exists" -foregroundcolor Magenta}
}else{
New-Item $PT -type directory | out-null ## if directory does not exist create it
If ($LogDebug) {Write-host "Creating directory $PT" -foregroundcolor Magenta}
}
## If file exist if not create it
If (Test-Path $DataFile) { ## If file does not exist create it
If ($LogDebug) {Write-host "$DataFile :File Exists" -foregroundcolor Magenta}
}else{
New-Item $DataFile -type file | out-null ## if file does not exist create it, we cant append a null file
If ($LogDebug) {Write-host "$DataFile :File Created" -foregroundcolor Magenta}
}
## Write our data to file
$D | out-file -Filepath $DataFile -append ## Write our data to file
## Write to color coded files
if ($C -ne $null) {
$WriteSumDir = $Flocaldrive + "Log\Sorted"
$WriteSumFile = $WriteSumDir + "\Console.txt"
## Check if path Exists if not create it
If (Test-Path $WriteSumDir) {
If ($LogDebug) {Write-host "$WriteSumDir :Directory Exists" -foregroundcolor Magenta}
}else{
New-Item $WriteSumDir -type directory | out-null ## if directory does not exist create it
If ($LogDebug) {Write-host "Creating directory $WriteSumDir" -foregroundcolor Magenta}
}
## If file does not exist create it
If (Test-Path $WriteSumFile) {
If ($LogDebug) {Write-host "$WriteSumFile :File Exists" -foregroundcolor Magenta}
}else{
New-Item $WriteSumFile -type file | out-null ## if file does not exist create it, we cant append a null file
If ($LogDebug) {Write-host "$WriteSumFile :File Created" -foregroundcolor Magenta}
}
## Write our data to file
$D | out-file -Filepath $WriteSumFile -append ## write everything to same file
## Write our data to color coded file
$WriteColorFile = $WriteSumDir + "\$C.txt"
If (Test-Path $WriteColorFile) { ## If file does not exist create it
If ($LogDebug) {Write-host "$WriteColorFile :File Exists" -foregroundcolor Magenta}
}else{
New-Item $WriteColorFile -type file | out-null ## if file does not exist create it, we cant append a null file
If ($LogDebug) {Write-host "$WriteColorFile :File Created" -foregroundcolor Magenta}
}
## Write our data to Color coded file
$D | out-file -Filepath $WriteColorFile -append ## write everything to same file
}
## If A return was not specified
If($filename -ne $null){
Return $DataFile
}
}

echo in while loop get's added to my return value

I wasn't sure how to describe this problem in the title so here goes.
I call a function from a script in another script. In that function i have a while loop that basically keeps looping through a set of ip's and looks up their hostname. when the while loop times out or we have all the host names.
it returns the hostnames.
My problem is that the return value contains every single Write-Host i'm doing in that function.
i know it's because Write-Host puts stuff on the pipeline and the return just returns whatever it has.
How do i go about fixing this?
The entire script i run get's logged in a log file which is why i want to have some verbose logging.
| out-null on write-host fixes the issue but it doesn't print the write-host values in the script.
in main.psm1 i have a function like so:
$nodes = #("ip1", "ip2", "ip3", "ip4")
$nodesnames = DoStuff -nodes $nodes
then in functions.psm1 i have functions like:
Function DoStuff
{
param($nodes)
$timeout = 300
$timetaken = 0
$sleepseconds = 5
$nodenames = #("$env:COMPUTERNAME")
while(($nodenames.count -lt $nodes.count) -and ($timetaken -lt $timeout))
{
try
{
Write-Host "Stuff"
foreach($node in $nodes)
{
$nodename = SuperawesomeFunction $node
Write-Host "$nodename"
if($nodenames -notcontains $nodename)
{
$nodenames += #($nodename)
}
}
}
catch
{
Write-Host "DoStuff Failed because $_"
}
Start-Sleep $sleepseconds
$timetaken += $sleepseconds
}
return $nodenames
}
Function SuperawesomeFunction
{
param($node)
$nodename = [System.Net.Dns]::GetHostEntry("$node")
return $nodename
}
Thanks.
So the answer is, your function is working like it is by design. In PowerShell a function will return output in general to the pipeline, unless specifically directed otherwise.
You used Echo before, which is an alias of Write-Output, and output is passed down the pipe as I mentioned before. As such it would be collected along with the returned $nodenames array.
Replacing Echo with Write-Host changes everything because Write-Host specifically tells PowerShell to send the information to the host application (usually the PowerShell Console or PowerShell ISE).
How do you avoid this? You could add a parameter specifying a path for a logfile, and have your function update the logfile directly, and only output the relevant data.
Or you can make an object with a pair of properties that gets passed back down the pipe which has the DNS results in one property, and the errors in another.
You could use Write-Error in the function, and set it up as an advanced function to support -errorvariable and capture the errors in a separate variable. To be honest, I'm not sure how to do that, I've never done it, but I'm 90% sure that it can be done.

Capturing verbose output from -Verbose switch in PowerShell v2

I ran into an interesting issue today that has me puzzled. I need to capture output from the verbose stream and write it to stdout.
This can be accomplished with this:
# Create a PowerShell Command
$pscmd = [PowerShell]::Create()
# Add a Script to change the verbose preference.
# Since I want to make sure this change stays around after I run the command I set UseLocalScope to $false.
# Also note that since AddScript returns the PowerShell command, I can simply call Invoke on what came back.
# I set the return value to $null to suppress any output
$null = $psCmd.AddScript({$VerbosePreference = "Continue"},$false).Invoke()
# If I added more commands, I'd be adding them to the pipeline, so I want to clear the pipeline
$psCmd.Commands.Clear()
# Now that I've cleared the pipeline, I'll add another script that writes out 100 messages to the Verbose stream
$null = $psCmd.AddScript({Write-verbose "hello World" }).Invoke()
# Finally, I'll output the stream
$psCmd.Streams.Verbose
Now the interesting part is if I were to create a function called Hello-World and use [CmdletBinding()] to inherit the -verbose switch, I can no longer capture output:
Function Hello-World {
[CmdletBinding()]
Param()
Write-Verbose "hello world"
}
...
$null = $psCmd.AddScript({Hello-World -Verbose }).Invoke()
...
I am assuming that the function is given its own verbose stream and that visibility to the stream is lost, but I am not positive. Does this have to do with [CmdletBinding()]?
Avoiding transcripts, is there a way to accomplish this?
Thanks!
Thank you #JasonMorgan, below is the solution that appears to be working. I needed to declare the function in the pscmd I made:
$pscmd = [PowerShell]::Create()
$null = $psCmd.AddScript({$VerbosePreference = "Continue"},$false).Invoke()
$null = $psCmd.AddScript({
function Hello-World {
[CmdletBinding()]
Param()
Write-Verbose "hello world"
}
}, $false).Invoke()
$psCmd.Commands.Clear()
$null = $psCmd.AddScript({Hello-World -Verbose }).Invoke()
$psCmd.Streams.Verbose

Redirecting output to $null in PowerShell, but ensuring the variable remains set

I have some code:
$foo = someFunction
This outputs a warning message which I want to redirect to $null:
$foo = someFunction > $null
The problem is that when I do this, while successfully supressing the warning message, it also has the negative side-effect of NOT populating $foo with the result of the function.
How do I redirect the warning to $null, but still keep $foo populated?
Also, how do you redirect both standard output and standard error to null? (In Linux, it's 2>&1.)
I'd prefer this way to redirect standard output (native PowerShell)...
($foo = someFunction) | out-null
But this works too:
($foo = someFunction) > $null
To redirect just standard error after defining $foo with result of "someFunction", do
($foo = someFunction) 2> $null
This is effectively the same as mentioned above.
Or to redirect any standard error messages from "someFunction" and then defining $foo with the result:
$foo = (someFunction 2> $null)
To redirect both you have a few options:
2>&1>$null
2>&1 | out-null
ADDENDUM:
Please note that (Windows) powershell has many more streams than a linux based OS. Here's the list from MS docs:
Thus you can redirect all streams using the wildcard with *>$null, and you can also use a file instead of $null.
This should work.
$foo = someFunction 2>$null
If it's errors you want to hide you can do it like this
$ErrorActionPreference = "SilentlyContinue"; #This will hide errors
$someObject.SomeFunction();
$ErrorActionPreference = "Continue"; #Turning errors back on
Warning messages should be written using the Write-Warning cmdlet, which allows the warning messages to be suppressed with the -WarningAction parameter or the $WarningPreference automatic variable. A function needs to use CmdletBinding to implement this feature.
function WarningTest {
[CmdletBinding()]
param($n)
Write-Warning "This is a warning message for: $n."
"Parameter n = $n"
}
$a = WarningTest 'test one' -WarningAction SilentlyContinue
# To turn off warnings for multiple commads,
# use the WarningPreference variable
$WarningPreference = 'SilentlyContinue'
$b = WarningTest 'test two'
$c = WarningTest 'test three'
# Turn messages back on.
$WarningPreference = 'Continue'
$c = WarningTest 'test four'
To make it shorter at the command prompt, you can use -wa 0:
PS> WarningTest 'parameter alias test' -wa 0
Write-Error, Write-Verbose and Write-Debug offer similar functionality for their corresponding types of messages.
using a function:
function run_command ($command)
{
invoke-expression "$command *>$null"
return $_
}
if (!(run_command "dir *.txt"))
{
if (!(run_command "dir *.doc"))
{
run_command "dir *.*"
}
}
or if you like one-liners:
function run_command ($command) { invoke-expression "$command "|out-null; return $_ }
if (!(run_command "dir *.txt")) { if (!(run_command "dir *.doc")) { run_command "dir *.*" } }
Recently, I had to shut up powershell on a Linux host, this wasn't that obvious to figure out. After back and forth I found out that wrapping a command in $( ) and adding a explicit redirection after the wrapper works.
Anything else I tried, wouldn't - I still don't know why since the PowerShell Docs are of desirable quality (and full of inconsistency...)
To import all modules on startup, I added the following. This produced some stderr output by powershell that couldnt be put to rest by ErrorAction or redirection without using the wrapping...
If anyone could elaborate on why's that would be very appreciated.
# import installed modules on launch
$PsMods = $(Get-InstalledModule);
$($PsMods.forEach({ Import-Module -Name $_.Name -ErrorAction Ignore })) *> /dev/null