Write-Output not working inside a callback - powershell

I have the following code inside a PowerShell function:
$client.List($target, {
param ($sender, $e)
Write-Host "Here it is: $e"
Write-Output $e
})
(client is a SharpSvn.SvnClient, and $target is some repository URL)
When I run it (just calling my function in console), I get a list of "Here it is: SharpSvn.SvnListEventArgs" for each of the item in repository, so I know the callback is executed.
However actual items are not put into the output (either pipeline or console).
If I put Write-Output outside the callback, the item is processed correctly, so I assume it has something with being in a callback block.
How do I make callback block process Write-Output correctly?

What's happening is that the ScriptBlock is getting converted into a delegate (EventHandler<SvnListEventArgs>) which returns void so Write-Output doesn't have anywhere to send the output. It's a bit like this:
[Action]$action = { Write-Host "Test"; Write-Output "Test" }
$action.Invoke()
Since Action doesn't return anything there is no where for the result of Write-Output to go. Now if the delegate returned an object it would work:
[Func[object]]$func = { Write-Host "Test"; Write-Output "Test"; Write-Output "Test2" }
$func.Invoke()
I guessing the way Write-Output works under the hood is that it sends the output somewhere which will cause all the outputs to be returned, if possible, when execution exits the script block. It doesn't have a reference the "current pipeline" or "currently executing function", so it can't write there directly.
I think the way to go about capturing the output is to do something like this:
$list = new-object System.Collections.Generic.List[Object]
$client.List($target, {
param ($sender, $e)
Write-Host "Here it is: $e"
$list.Add($e)
})
Write-Output $list

You could try to use:
$PSCmdlet.WriteObject("Here it is: $e")

Related

PowerShell execute an event handler code on function return

Analogous to how the PowerShell.Exiting engine event works, is it possible to fire an event when a function returns (i.e. immediately before the function returns)? The reason I ask this is if I have a big function with many forks to return from that function and I want to avoid writing that repeat code that does the pre-returning tasks at every fork (e.g. write logging), instead would like an event-type code to execute no matter which fork in the function code that causes the return.
Or is there a better way of doing it in PowerShell?
The closest is the Try / Catch / Finally statement, because it ensure that whatever is in the Finally statement get executed before returning.
Normally, I would not use this to wrap all my function because of an unverified assumption that it might impact my code performance.
Giant Try / Catch / Finally
(#Mclayton suggestion)
function Get-Stuff() {
try {
Write-Host 'Getting stuff... ' -ForegroundColor Cyan
$Exception = Get-Random -InputObject #($true, $false)
if ($Exception) { throw 'sss'}
return 'Success !'
}
catch {
Write-Host 'oh no .... :)' -ForegroundColor Red
}
Finally {
Write-Verbose 'Logging stuff... ' -Verbose
}
}
Instead I would probably have used a wrapper function.
Wrapper function (#Mathias suggestion)
For a module, you simply do not export the _ prefixed functions and the users will never see the underlying function. (Note: Using advanced function, you can pass down the parameters to the underlying function through splatting $PSBoundParameters
Function Get-Stuff {
[CmdletBinding()]
Param($Value)
$Output = _Get-Stuff #PSBoundParameters
Write-Verbose 'Logging stuff... ' -Verbose
return $Output
}
#Internal function
function _Get-Stuff {
[CmdletBinding()]
Param($Value)
Write-Host 'Getting stuff... ' -ForegroundColor Cyan
if ($Value) { Write-Host $Value }
}
Otherwise, I would either use a function call and / or a scriptblock invoke (useful when you don't want to repeat the code within a function but that the code itself is not used outside of that scope.
Basic way
Otherwise, I would either use a function call and / or a scriptblock invoke (useful when you don't want to repeat the code within a function but that the code itself is not used outside of that scope. You would need to think about calling the logic before each return point... If you wanted to make sure to never miss a spot, then that's where you'd implement some Pester testing and create unit tests for your functions.
Function Get-Stuff() {
$Logging = { Write-Verbose 'Logging stuff... ' -Verbose }
Write-Host 'Getting stuff... ' -ForegroundColor Cyan
&$Logging # you just have to call this bit before returning at each return point.
return 'Something'
}
Bonus
I initially read "events" in your questions and my brain kind of discarded the rest.
Should you want to have a custom logic attached to your function that is definable by the user using your module and / or set of function, you could raise an event in your function.
New-Event -SourceIdentifier 'Get-Stuff_Return' -Sender $null -EventArguments #(([PSCustomObject]#{
'Hello' = 'World'
})) -MessageData $MyArg | Out-Null
That event, which you would trigger before returning (using any of the methods highlighted above), would do nothing by itself but anybody that needed to do something could hook itself into your function by creating an event handler and adding its own custom logic (for logging or otherwise)
Register-EngineEvent -SourceIdentifier "Get-Stuff_Return" -Action {
# Write-Verbose ($Event | gm | Out-String) -Verbose
Write-Verbose #"
Generated at: $($Event.TimeGenerated)
Handling event: $($Event.SourceIdentifier)
Event Id: $($Event.EventIdentifier)
MessageData (Hello): $($Event.MessageData.Hello)
"# -Verbose
Write-Verbose '---' -Verbose
$Global:test = $Event
Write-Verbose ($Event.SourceArgs[0] | Out-String) -Verbose
} -SupportEvent
# Unregister statement as a reference
# Unregister-Event -SourceIdentifier 'Get-Stuff_Return' -Force
The advantage being that then anybody could optionally bind itself to a function call and do anything he likes.
References
about_Splatting
How to implement event handling in PowerShell with classes
about Functions Advanced
about_Automatic_Variables
about_Try_Catch_Finally

Powershell - Error handling (Dont invoke finally block if Catch block has been invoked)

In Powershell, it seem like it will always execute Finally block. Is there a way to tell Powershell not execute statement in Finally block if Catch block
has been executed. Example my below code, if email has been trigger then I don't want powershell generate log file.
Catch
{
$ErrorMessage = $_.Exception.Message
$FailedItem = $_.Exception.ItemName
Send-MailMessage -From xxxxxxxxxx
Break
}
Finally
{
$Time=(Get-Date).ToString('MM/dd/yyyy hh:mm:ss tt')
"This script made a read attempt at" + "$d1" | out-file $logfile -append
}
I do not endorse using this code, but to answer your question, yes, it is possible doing something like this.
$badIdea = $true
try
{
Write-Output "About to do something dumb."
$ugh = 1 / 0;
Write-Output "Did I do something dumb?"
}
catch
{
Write-Output "Caught something dumb."
$badIdea = $false
}
finally
{
if ($badIdea -eq $true)
{
Write-Output "Performing finally..."
}
else
{
Write-Output "Skipping finally..."
}
}
Code does what you tell it...and you can use conditional statements anywhere you want.
This produces:
About to do something dumb.
Caught something dumb.
Skipping finally...
The purpose of the Finally clause is to always execute, whether an exception
occurred or not.
Thus, as Jason Boyd points out in a comment on the question, you would typically place code you only want executed if no exception occurred after the try statement.
break in your Catch handler ensures that any code below the try statement is only reached in case no exception occurred - but note that break simply breaks out of any enclosing loop - even across function and script boundaries(!); only if there is no enclosing loop is break equivalent to exiting the enclosing script.

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.

Appropriate logging in 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 )

Debugging PowerShell

I'm not certain what is wrong with this scriptlet.
I'm trying to break out functionality into several other functions (I have a programming background not a scripting one per se) and to me LOGICALLY the following should execute starting at the "main" function Test-SgnedMpsPackage, accepting the various optional parameters (the script is not yet complete) then when the function Check-Path is called, that is run, then work would resume in the original calling function.
Am I missing something here?
On a side note, how does one return a value to the calling function? a simple return?
function CheckPath($path)
{
if ( test-path -Path $path )
{ Write-Host "{0} confirmed to exist." -f $path }
else
{ Write-Host "{0} DOES NOT exis.\nPlease check and run the script again" -f $path }
exit { exit }
}
function Test-SignedMpsPackage
{
Param(
[string] $PkgSource,
[string] $SigSource,
[string] $Destination
)
Process
{
#Check that both files exist
Write-Host "Check for file existence..."
CheckPath($PkgSource)
CheckPath($SigSource)
#retrieve signatures from file
}
}
Unlike C, C++ or C# there is no "main" entry point function. Any script at the top level - outside of a function - executes. You have defined two functions above but you haven't called either one. You need to do something like this:
function Test-SignedMpsPackage
{
...
}
Test-SignedMpsPackage params
Also as mentioned by #Bill_Stewart, you call your defined functions just like you call PowerShell commands - arguments are space separated and you don't use parens except to evaluate an expression inside the parens.
As for returning a value from a function, any output (Output stream) not captured by assigning to a variable or being redirected to a file is automatically part of the function's output. So I would modify your CheckPath function to this:
function CheckPath($path)
{
if (Test-Path -Path $path) {
Write-Verbose "{0} confirmed to exist." -f $path
$true
}
else {
Write-Verbose "{0} DOES NOT exist.\nPlease check and run the script again" -f $path
$false
}
}
You can use Write-Host as you had before but sometimes, perhaps in a script, you don't want to see the extra output. That is where Write-Verbose comes in handy. Set $VerbosePreference = 'Continue' to see the verbose output.