1

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?

6
  • 2
    Wrap your function call in another function - have the outer function log before returning Commented Sep 4, 2021 at 12:19
  • Alternatively, make the "write logging" code a function and call it at each return point. Commented Sep 4, 2021 at 16:34
  • @MathiasR.Jessen - thanks. I assume your technique works only because PS is a dynamically scoped language (since all the variables from the returned function are available in this wrapper function)? Hypothetically, how would you have tackled it if it were a statically scoped language? Commented Sep 4, 2021 at 17:29
  • @lit - Avoiding having to explicitly call the logging code at every possible return point was the exact reason why I asked the original question - my point was if there was a branch in a large chunk code that I missed/forgot calling the logging code, I wanted a clever technique that would address it reliably. Commented Sep 4, 2021 at 17:33
  • 2
    You could wrap the function body in a try … finally and put your “function exit” code in the finally block. (That will also still run your exit code even if an exception is thrown in the body). Commented Sep 4, 2021 at 18:17

1 Answer 1

1

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

Sign up to request clarification or add additional context in comments.

4 Comments

Not having any luck with this in 5.1 or 7.2. I assume I'm supposed to be able to copy the 3 blocks under "Bonus", and it should run the script block for -Action?
@TylerMontney The "bonus" section is kind of apart of the rest of the answer (which I'm now thinking is weirdly formulated). Anyway, if you want to trigger an event and handle it from within your code, you need to run the Register-EngineEvent statement first. It is done only once. Then, each time you need to raise an event (to be handled by the event handler you just registered), you just call the New-Event statement and you should see the verbose from the Register-EngineEvent -Action parameter being printed.
Ah, so New-Event is like invoking or raising the event manually?
@TylerMontney Exactly. That would be the equivalent of RaiseEvent in C#.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.