diff --git a/1-Draft/RFCNNNN-ScriptBlock-Action-Preference.md b/1-Draft/RFCNNNN-ScriptBlock-Action-Preference.md new file mode 100644 index 00000000..11296532 --- /dev/null +++ b/1-Draft/RFCNNNN-ScriptBlock-Action-Preference.md @@ -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 ]` 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,
+I can use a `ScriptBlock` with `*Preference` variables and `-*Action` +parameters,
+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.