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?

Steve
  • 337
  • 4
  • 11
  • 2
    Wrap your function call in another function - have the outer function log before returning – Mathias R. Jessen Sep 04 '21 at 12:19
  • Alternatively, make the "write logging" code a function and call it at each return point. – lit Sep 04 '21 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? – Steve Sep 04 '21 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. – Steve Sep 04 '21 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). – mclayton Sep 04 '21 at 18:17
  • While solutions/workarounds suggested by others are valid too, I guess try catch finally is the cleanest and the most obvious solution here. Thanks. Even though I asked for an "event handler" type solution to the stated problem, unless Microsoft adds this "Scriptblock.Exiting" events into the [scriptblock] class, there is no automatic way of invoking it like how the Powershell.exiting event works. – Steve Sep 05 '21 at 07:56

1 Answers1

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

Sage Pourpre
  • 9,932
  • 3
  • 27
  • 39
  • 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`? – Tyler Montney Oct 04 '22 at 02:36
  • @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. – Sage Pourpre Oct 04 '22 at 03:49
  • Ah, so `New-Event` is like invoking or raising the event manually? – Tyler Montney Oct 04 '22 at 16:30
  • @TylerMontney Exactly. That would be the equivalent of `RaiseEvent` in C#. – Sage Pourpre Oct 04 '22 at 17:43