Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support ScriptBlocks in -*Action common parameters and *Preference variables to process errors and messages #219

Closed
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 213 additions & 0 deletions 1-Draft/RFCNNNN-ScriptBlock-Action-Preference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
---
RFC: RFCnnnn
Author: Kirk Munro
Status: Draft
SupercededBy:
Version: 0.1
Area: Engine
Comments Due: September 15, 2019
Plan to implement: Yes
---

# ScriptBlocks to handle error and message processing

@jpsnover suggested in
[PowerShell Issue #6010](https://github.com/PowerShell/PowerShell/issues/6010)
that an `[-OnError <ScriptBlock>]` be added to the common parameters in
PowerShell that takes precedence over `-ErrorAction` and
`$ErrorActionPreference`. In response to that issue, PR #182 has been opened by
@TylerLeonhardt with an RFC that proposes we change the trap statement to
accommodate non-terminating errors. There are several challenges between the
original issue and the proposed RFC:

1. Both designs are only for error messages. It would be more useful to be able
to provide a solution that works for type of message (warning, verbose, debug,
information, progress) so that everything can be handled (eg. logged) the same
way.
1. Blurring the line between terminating and non-terminating errors is a risky
proposition. There is a reason that terminating errors are terminating.
Executing code beyond a terminating error should require intentional logic to
allow that to happen. The fact that the line between terminating and
non-terminating errors is already blurred is a long standing problem with
PowerShell (terminating errors don't actually terminate in PowerShell unless
they are wrapped in try/catch, resulting in widespread use of an anti-pattern
in scripts today), and any further blurring of that line risks even more
mishandling of terminating errors in PowerShell than we already see today.

With those challenges in mind, this RFC proposes instead that we extend what is
allowed in `-*Action` common parameters, such that a `ScriptBlock` can be
passed into those parameters. Further, it also proposes that we allow a
`ScriptBlock` to be assigned to any `$*Preference` variable as well. This will
allow scripters and script, function and module authors to apply custom message
processing to their scripts for any type of non-terminating message that is not
silenced or ignored.

Terminating messages will remain handled by try/catch statements or trap
statements the way they are defined in PowerShell 6.2 and earlier releases.

## Motivation

As a scripter or a script, function, or module author,<br/>
I can use a `ScriptBlock` with `*Preference` variables and `-*Action`
parameters,<br/>
so that I can perform custom processing for messages generated by any number of
different commands in my scripts without having to use redirection operators in
many different locations.

## User experience

Here is an example that demonstrates how a scripter may handle non-terminating
(as well as terminating) messages in PowerShell once this RFC is implemented:

```powershell

# region Some functions used in the examples

$messageLog = [System.Collections.ArrayList]::new()

function Write-MessageLog {
[CmdletBinding(DefaultParameterSetName='Error')]
param(
[Parameter(Position=0, Mandatory=$true, ParameterSetName='Error')]
[ValidateNotNull()]
[System.Management.Automation.ErrorRecord]
$ErrorRecord,

[Parameter(Position=0, Mandatory=$true, ParameterSetName='Message')]
[ValidateNotNull()]
[System.Management.Automation.InformationRecord]
$InformationRecord
)
$messageLog.Add([pscustomobject]@{
Timestamp = [System.DateTime]::UtcNow
Message = if ($PSCmdlet.ParameterSetName -eq 'Error') {
$ErrorRecord | Out-String
} else {
$InformationRecord.Message
}
})
}

Set-StrictMode -Version Latest
function Get-MyProcess {
[CmdletBinding()]
param([int]$Id = $PID)
Write-Verbose -Verbose -Message "Looking for process with ID ${Id}..."
$process = Get-Process -Id $Id
if ($process -ne $null) {
Write-Verbose -Verbose -Message "Found process with ID ${Id}."
Write-Output "Name: $($process.DisplayName)"
Write-Output "Id: $($process.Id)"
} else {
Write-Warning -Message "Process ${Id} was not found."
}
}

#endregion

# EXAMPLE 1:
# Run the script, recording all non-terminating errors in the error log.
# Since the error action script block does not return an [ActionPreference]
# enumeration value, default error handling will occur after the message is
# written to the message log, showing the error in the terminal and writing
# it into $error.
Get-MyProcess -Id 12345678 -ErrorAction {Write-MessageLog $_}

# Run the script again, recording all messages, including verbose and debug,
# as well as any terminating or non-terminating error that occurs, in the
# message log without showing them on screen. Errors will still be stored in
# $error, as per the usual behavior of SilentlyContinue.
$ErrorActionPreference = $WarningPreference = $VerbosePreference =
$DebugPreference = {
Write-MessageLog $_
[ActionPreference]::SilentlyContinue
}
try {
Get-MyProcess
} catch {
Write-MessageLog $_
throw
}
```

In the case of the first example, the message log used by the caller will
contain the non-terminating error raised in `Get-MyProcess`.

In the case of the second example, the message log will contain all warning,
verbose, and debug messages, as well as any error messages, in the order in
which they occurred.

This approach offers more functionality than the RFC in PR #182 without mixing
up the important distinction and decisions that need to made when handing
terminating and non-terminating errors.

## Specification

If a `ScriptBlock` is present in a `$*Preference` variable when a message of
the appropriate type is raised, the `ScriptBlock` would be run with `$_`
assigned to the appropriate `ErrorRecord` or `InformationalRecord` instance.
These `ScriptBlock` instances would be used to process whatever messages they
received, and they would identify the action the scripter would like taken once
the processing is complete by returning an `ActionPreference` enumeration
value.

To make logging messages easier, if the `ScriptBlock` does not return an
`ActionPreference`, PowerShell would automatically apply the default
`ActionPreference` for that type of message (`Continue` for progress, warning
and error messages, `SilentlyContinue` for information, verbose and debug
messages).

While those two paragraphs explain the functionality simply enough, this would
probably be a decent amount of work to implement.

It is important to note that this design would not be a breaking change because
today you cannot assign a `ScriptBlock` to a `-*Action` common parameter, nor
can you assign them to a `$*Preference` variables.

## Alternate proposals and considerations

### Use a variable to capture the result of an ActionPreference ScriptBlock

Instead of relying on a return value from any script blocks used to process
messages or errors, define an automatic variable that would contain the default
result for the appropriate message type, and that could be changed to whatever
result the user wanted. For example, a variable called `$action` that is of
type `ActionPreference` could be defined in an action handler, and script
authors who want to apply a different action preference would simply have to
assign the preference of their choice to that variable. When the script block
exits, whatever value was assigned to that `$action` variable would be used as
the action to take in PowerShell.

With this approach, `$_` would still be assigned to the message record to
allow the `ScriptBlock` logic to remain as simple as possible.

The benefits of this alternative are as follows:

* The way you return an `ActionPreference` from the `ScriptBlock` is a little
more explicit (PowerShell will return whatever is output from the
`ScriptBlock` by default, so this makes the important part of what is returned
clear).
* Users who just want to log messages or perform some other handling without
mucking around with the return type can simply ignore the `$action` variable,
keeping their code simple.

The downsides to this approach are as follows:

* Some users may prefer a return `ActionPreference` value over a variable to
work with.

### Add `-VerboseAction`, `-DebugAction` and `-ProgressAction` common parameters

[PowerShell/PowerShell#10238](https://github.com/PowerShell/PowerShell/pull/10238)
adds `-VerboseAction`, `-DebugAction`, and `-ProgressAction` common parameters
to PowerShell. These common parameters should be updated to support script
blocks as well, so that all message types can have custom processing added via
common parameters.

### Common parameters do not propagate beyond module scope

It is important to note that common parameters do not propagate beyond script
module scope at this time, which means that handlers passed as parameters to a
function defined in a script module would not process messages from commands
invoked by that script module that are outside of that script module. That
issue is being discussed in another RFC.