From 3263667914ff3c0ff62975e05d087a01c59ec32e Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Fri, 7 Jun 2019 17:42:27 -0300 Subject: [PATCH 01/15] first draft --- ...minating-Errors-Terminate-The-Right-Way.md | 169 ++++++++++++++++++ 1-Draft/RFCNNNN-Optional-Features.md | 165 +++++++++++++++++ ...ecution-Preferences-Beyond-Module-Scope.md | 133 ++++++++++++++ .../RFCNNNN-ScriptBlock-Action-Preference.md | 142 +++++++++++++++ 4 files changed, 609 insertions(+) create mode 100644 1-Draft/RFCNNNN-Make-Terminating-Errors-Terminate-The-Right-Way.md create mode 100644 1-Draft/RFCNNNN-Optional-Features.md create mode 100644 1-Draft/RFCNNNN-Propagate-Execution-Preferences-Beyond-Module-Scope.md create mode 100644 1-Draft/RFCNNNN-ScriptBlock-Action-Preference.md diff --git a/1-Draft/RFCNNNN-Make-Terminating-Errors-Terminate-The-Right-Way.md b/1-Draft/RFCNNNN-Make-Terminating-Errors-Terminate-The-Right-Way.md new file mode 100644 index 00000000..ef95ad47 --- /dev/null +++ b/1-Draft/RFCNNNN-Make-Terminating-Errors-Terminate-The-Right-Way.md @@ -0,0 +1,169 @@ +--- +RFC: RFCnnnn +Author: Kirk Munro +Status: Draft +SupercededBy: +Version: 1.0 +Area: Engine +Comments Due: July 15, 2019 +Plan to implement: Yes +--- + +# Make terminating errors terminate the right way in PowerShell + +By default in PowerShell, terminating errors do not actually terminate. For example, if you invoke this command in global scope, you will see the output "Why?" after the terminating error caused by the previous command: + +```PowerShell +& { + 1/0 + 'Why?' +} +``` + +PowerShell has upheld this behaviour since version 1.0 of the language. You can make the terminating error actually terminate execution of the command, by wrapping the command in try/catch, like this: + +```PowerShell +try { + 1/0 + 'Why?' +} catch { + throw +} +``` + +In that example, the exception raised by dividing by zero properly terminates execution of the running command. + +The difference between these two examples poses a risk to scripters who share scripts or modules with the community. The risk is that end users using a shared resource such as a script or module may see different behaviour from the logic within that module depending on whether or not they were inside of a `try` block when they invoked the script or a command exported by the module. That risk is very undesirable, and as a result many community members who share scripts/modules with the community wrap their logic in a `try/catch{throw}` (or similar) scaffolding to ensure that the behavior of their code is consistent no matter where or how it was invoked. + +Now consider this code snippet: + +```PowerShell +New-Module -Name ThisShouldNotImport { + $myList = [System.Collections.Generics.List[string]]::new() + + function Test-RFC { + [CmdletBinding()] + param() + 'Some logic' + 1 / 0 # Oops + 'Some more logic' + } +} | Import-Module +``` + +If you invoke that snippet, the `ThisShouldNotImport` module imports successfully because the terminating error (`[System.Collections.Generics.List[string]]` is not a valid type name) does not actually terminate the loading of the module. This could cause your module to load in an unexpected state, which is a bad idea. If you loaded your module by invoking a command defined with that module, you won't see the terminating error that was raised during the loading of the module (the terminating error that was raised during the loading of the module is not shown at all in that scenario!), so you could end up with some undesirable behaviour when that command executes even though the loading of the module generated a "terminating" error, and not have a clue why. Further, the Test-RFC command exported by this module produces a terminating error, yet continues to execute after that error. Last, if the caller either loads your module or invokes your command inside of a `try` block, they will see different behaviour. Any execution of code beyond a terminating error should be intentional, not accidental like it is in both of these cases, and it most certainly should not be influenced by whether or not the caller loaded the module or invoked the command inside of a `try` block. Binary modules do not behave this way. Why should script modules be any different? + +Now have a look at the same module definition, this time with some extra scaffolding in place to make sure that terminating errors actually terminate: + +```PowerShell +New-Module -Name ThisShouldNotImport { + trap { + break + } + $myList = [System.Collections.Generic.List[string]]::new() + + function Test-RFC { + [CmdletBinding()] + param() + $callerEAP = $ErrorActionPreference + try { + 'Some logic' + 1 / 0 # Oops + 'Some more logic' + } catch { + Write-Error -ErrorRecord $_ -ErrorAction $callerEAP + } + } +} | Import-Module +``` + +With this definition, if the script module generates a terminating error, the module will properly fail to load (note, however, that the type name has been corrected in case you want to try this out). Further, if the command encounters a terminating error, it will properly terminate execution and the error returned to the caller will properly indicate that the `Test-RFC` command encountered an error. This scaffolding is so helpful that members of the community apply it to every module and every function they define within that module, just to get things to work the right way in PowerShell. + +All of this is simply absurd. Any script module that generates a terminating error in the module body should fail to import without extra effort, with an appropriate error message indicating why it did not import. Any advanced function defined within a script module that encounters a terminating error should terminate gracefully, such that the error message indicates which function the error came from, without requiring extra scaffolding code to make it work that way. + +Between the issues identified above, and the workarounds that include anti-patterns (naked `try/catch` blocks and `trap{break}` statements are anti-patterns), the PowerShell community is clearly in need of a solution that automatically resolves these issues in a non-breaking way. + +## Motivation + +As a script, function, or module author, +I can write scripts with confidence knowing that terminating errors will terminate those commands the right way, without needing to add any scaffolding to correct inappropriate behaviours in PowerShell +so that I can keep my logic focused on the work that needs to be done. + +## User experience + +The way forward for this issue is to add an optional feature (see: RFCNNNN-OptionalFeatures) that makes terminating errors terminate correctly. The script below demonstrates that a manifest can be generated with the `ImplicitTerminatingErrorHandling` optional feature enabled, and with that enabled the module author can write the script module and the advanced functions in that module knowing that terminating errors will be handled properly. No scaffolding is required once the optional feature is enabled, because it will correct the issues that need correcting to make this just work the right way, transparently. + +```powershell +$moduleName = 'ModuleWithBetterErrorHandling' +$modulePath = Join-Path -Path $([Environment]::GetFolderPath('MyDocuments')) -ChildPath PowerShell/Modules/${moduleName} +New-Item -Path $modulePath -ItemType Directory -Force > $null +$nmmParameters = @{ + Path = "${modulePath}/${moduleName}.psd1" + RootModule = "./${moduleName}.psm1" + FunctionsToExport = @('Test-ErrorHandling') +} + +# +# Create the module manifest, enabling the optional ImplicitTerminatingErrorHandling feature in the module it loads +# +New-ModuleManifest @nmmParameters -OptionalFeatures ImplicitTerminatingErrorHandling + +$scriptModulePath = Join-Path -Path $modulePath -ChildPath "${moduleName}.psm1" +New-Item -Path $scriptModulePath -ItemType File | Set-Content -Encoding UTF8 -Value @' + # If the next command is uncommented, Import-Module would fail when trying to load + # this module due to the terminating error actually terminating like it should + # 1/0 # Oops! + function Test-ErrorHandling { + [cmdletBinding()] + param() + 'Some logic' + # The next command generates a terminating error, which will be treated as + # terminating and Test-ErrorHandling will fail properly, with the error text + # showing details about the Test-ErrorHandling invocation rather than details + # about the internals of Test-ErrorHandling. + Get-Process -Id 12345678 -ErrorAction Stop + 'Some more logic' + } +'@ +``` + +Module authors who want this behaviour in every module they create can invoke the following command to make it default for module manifests created with `New-ModuleManifest`. + +```PowerShell +Enable-OptionalFeature -Name ImplicitTerminatingErrorHandling -NewModuleManifests +``` + +Scripters wanting the behaviour in their scripts can use the #requires statement: + +```PowerShell +#requires -OptionalFeatures ImplicitTerminatingErrorHandling +``` + +## Specification + +Implementation of this RFC would require the following: + +### Implementation of optional feature support + +See RFCNNNN-Optional-Features for more information. + +### Addition of the `ImplicitTerminatingErrorHandling` optional feature definition + +This would require adding the feature name and description in the appropriate locations so that the feature can be discovered and enabled. + +### PowerShell engine updates + +The PowerShell engine would have to be updated such that: + +* scripts invoked with the optional feature enabled treat terminating errors as terminating +* scripts and functions with `CmdletBinding` attributes when this optional feature is enabled treat terminating errors as terminating and gracefully report errors back to the caller (i.e. these commands should not throw exceptions) + +## Alternate proposals and considerations + +### Make this optional feature on by default for new module manifests + +This feature is so useful that I would recommend it as a best practice. If making it just work this way globally wouldn't incur a breaking change in PowerShell, I would want it to always work that way by default. Since making it work this way globally would incur a breaking change, my recommendation is to make this optional feature on in new module manifests by default so that anyone not wanting it to work this way has to turn the optional feature off. That corrects the behaviour going forward while allowing authors of older modules/scripts can opt-in to the feature when they are ready. + +### Related issue + +[PowerShell Issue #9855](https://github.com/PowerShell/PowerShell/issues/9855) is very closely related to this RFC, and it would be worth considering fixing that issue as part of this RFC if it is not already resolved at that time. diff --git a/1-Draft/RFCNNNN-Optional-Features.md b/1-Draft/RFCNNNN-Optional-Features.md new file mode 100644 index 00000000..551a79e6 --- /dev/null +++ b/1-Draft/RFCNNNN-Optional-Features.md @@ -0,0 +1,165 @@ +--- +RFC: RFCnnnn +Author: Kirk Munro +Status: Draft +SupercededBy: +Version: 1.0 +Area: Engine +Comments Due: July 15, 2019 +Plan to implement: Yes +--- + +# Optional features in PowerShell + +There are several important issues in the PowerShell language that cannot be corrected without introducing breaking changes. At the same time, the number of breaking changes introduced in a new version of PowerShell needs to be as minimal as possible, so that there is a low barrier to adoption of new versions, allowing community members can transition scripts and modules across versions more easily. Given that those two statements are in conflict with one another, we need to consider how we can optionally introduce breaking changes into PowerShell over time. + +PowerShell has support for experimental features, which some may think covers this need; however, the intent of experimental features is to allow the community to try pre-release versions of PowerShell with breaking changes that are deemed necessary so that they can more accurately assess the impact of those breaking changes. For release versions of PowerShell, an experimental feature has one of three possible outcomes: + +1. The breaking change in the experimental feature is deemed necessary and accepted by the community as not harmful to adoption of new versions, in which case the experimental feature is no longer marked as experimental. +1. The breaking change in the experimental feature is deemed necessary, but considered harmful to adoption of new versions, in which case the experimental feature is changed to an optional feature. +1. The breaking change in the experimental feature is deemed not useful enough, in which case the experimental feature is deprecated. + +In some cases a breaking change may be implemented immediately as an optional feature, when it is known up front that such a breaking change would be considered harmful to adoption of new versions of PowerShell. + +Given all of that, we need to add support for optional features in PowerShell so that what is described above becomes a reality. + +As an example of a feature that will be optional if implemented, see RFCNNNN-Propagate-Execution-Preferences-Beyond-Module-Scope or RFCNNNN-Make-Terminating-Errors-Terminate. + +## Motivation + +As a script, function, or module author, +I can enable optional features in my scripts or modules, +so that I can leverage new functionality that could break existing scripts. + +## User experience + +```powershell +# Create a module manifest, specifically enabling one or more optional features in the manifest +New-ModuleManifest -Path ./test.psd1 -OptionalFeatures @('OptionalFeature1','OptionalFeature2') -PassThru | Get-Content + +# Output: +# +# @{ +# +# +# +# # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +# PrivateData = @{ +# +# +# +# PSData = @{ +# +# # Optional features enabled in this module. +# OptionalFeatures = @( +# 'OptionalFeature1' +# 'OptionalFeature2' +# ) +# +# +# +# } # End of PSData hashtable +# +# +# +# } # End of PrivateData hashtable +# +# } + +# Create a script file, enabling one or more optional features in the file +@' +#requires -OptionalFeature OptionalFeature1,OptionalFeature2 + + +'@ | Out-File -FilePath ./test.ps1 + +# Get a list of optional features that are available +Get-OptionalFeature + +# Output: +# +# Name EnabledIn Source Description +# ---- ------- ------ ----------- +# OptionalFeature1 Manifest PSEngine Description of optional feature 1 +# OptionalFeature2 Session PSEngine Description of optional feature 2 + +# Enable an optional feature by default in PowerShell +Enable-OptionalFeature -Name OptionalFeature1 + +# Output: +# This works just like Enable-ExperimentalFeature, turning the optional +# feature on by default for all future sessions in PowerShell. + +# Disable an optional feature by default in PowerShell +Disable-OptionalFeature -Name OptionalFeature1 + +# Output: +# This works ust like Disable-ExperimentalFeature, turning the optional +# feature off by default for all future sessions in PowerShell. + +# Enable an optional feature by default in all new module manifests +# created with New-ModuleManifest in all future sessions in PowerShell. +Enable-OptionalFeature -Name OptionalFeature1 -NewModuleManifests + +# Disable an optional feature by default in all new module manifests +# created with New-ModuleManifest in all future sessions in PowerShell. +Disable-OptionalFeature -Name OptionalFeature1 -NewModuleManifests +``` + +## Specification + +Aside from closely (but not exactly, see below) mirroring what is already in place internally for experimental features in PowerShell, this RFC includes a few additional enhancements that will be useful for optional features, as follows: + +### Add parameter to New-ModuleManifest + +`[-OptionalFeatures ]` + +This new parameter would assign specific optional features to new modules. Note that these would be in addition to optional features that are enabled by default in manifests created with `New-ModuleManifest`. + +### Add parameter to #requires statement + +`#requires -OptionalFeatures ` + +This new parameter would enable optional features in the current script file. + +### New command: Get-OptionalFeature + +```none +Get-OptionalFeature [[-Name] ] [] +``` + +This command would return the optional features that are available in PowerShell. The default output format would be of type table with the properties `Name`, `Enabled`, `Source`, and `Description`. All of those properties would be of type string except for `Enabled`, which would be an enumeration with the possible values of `NotEnabled`, `Session`, `Manifest`, and `Script`. This differs from experimental features where `Enabled` is a boolean value. Given the locations in which an optional feature can be enabled, it would be more informative to identify where it is enabled than simply showing `$true` or `$false`. + +### New command: Enable-OptionalFeature + +```none +Enable-OptionalFeature [-Name] [-NewModuleManifests] [-WhatIf] [-Confirm] [] +``` + +This command would enable an optional feature either globally (if the `-NewModuleManifests` switch is not used) or only in new module manifests created by `New-ModuleManifest`. + +### New command: Disable-OptionalFeature + +```none +Disable-OptionalFeature [-Name] [-NewModuleManifests] [-WhatIf] [-Confirm] [] +``` + +This command would disable an optional feature either globally (if the `-NewModuleManifests` switch is not used) or only in new module manifests created by `New-ModuleManifest`. If the optional feature is not enabled that way in the first place, nothing would happen. + +### New command: Use-OptionalFeature + +```none +Use-OptionalFeature [-Name] [-ScriptBlock] [-Confirm] [] +``` + +This command would enable an optional feature for the duration of the `ScriptBlock` identified in the `-ScriptBlock` parameter, and return the feature to its previous state afterwards. This allows for easy use of an optional feature over a small section of code. + +## Alternate proposals and considerations + +### Extend experimental features to support the enhancements defined in this RFC + +Experimental features and optional features are very similar to one another, so much so that they really only differ in name. Given the model for how both of these types of features are used, it may make sense to have them both use the same functionality when it comes to enabling/disabling them in scripts and modules. The downside I see to this approach is that optional features are permanent features in PowerShell while experimental features are not, so it may not be a good idea to support more permanent ways to enable experimental features such as `#requires` or enabling an experimental feature in a new module manifest. + +### Supporting a `-Scope` parameter like the experimental feature cmdlets do + +The `Enable-OptionalFeature` and `Disable-OptionalFeature` cmdlets could support a `-Scope` parameter like their experimental feature cmdlet counterparts do. I felt it was better to remove this for optional features, because it may be risky to allow a command to enable an optional feature in a scope above the one in which it is invoked, influencing behaviour elsewhere. diff --git a/1-Draft/RFCNNNN-Propagate-Execution-Preferences-Beyond-Module-Scope.md b/1-Draft/RFCNNNN-Propagate-Execution-Preferences-Beyond-Module-Scope.md new file mode 100644 index 00000000..8431d2af --- /dev/null +++ b/1-Draft/RFCNNNN-Propagate-Execution-Preferences-Beyond-Module-Scope.md @@ -0,0 +1,133 @@ +--- +RFC: RFCnnnn +Author: Kirk Munro +Status: Draft +SupercededBy: +Version: 1.0 +Area: Engine +Comments Due: July 15, 2019 +Plan to implement: Yes +--- + +# Execution preferences must persist beyond module or script scope + +PowerShell has a long-standing issue where execution preferences such as those defined by the `-ErrorAction`, `-WarningAction`, `-InformationAction`, `-Debug`, `-Verbose`, `-WhatIf` and `-Confirm` common parameters, or those defined by any of the `$*Preference` variables, do not persist from a module to another module or script, nor do they persist from a script to another module or script. This is a result of how modules and scripts are scoped within PowerShell. It impacts modules written by Microsoft as well as scripts and modules written by the community, and it is often identified as a bug when it shows up in various places. You can see some of the discussion around this in [PowerShell Issue #4568](https://github.com/PowerShell/PowerShell/issues/4568). + +Regardless of whether you are authoring a script, an advanced function, or a cmdlet, you should be able to do so knowing that all execution preferences will be carried through the entire invocation of a command, end to end, regardless of the implementation details of that command. Cmdlets already work this way today. You can invoke cmdlets within a script, or within an advanced function defined in a module, and the execution preferences used during the invocation of that script or advanced function will be respected by the cmdlets that are invoked. This RFC is about making it possible for scripts or advanced functions to work that way as well. + +It is important to note that the only way to implement this feature such that it is not a breaking change is by making it optional; however, even with it optional, it can be enabled by default for new modules to correct this problem going forward, and since it is optional, existing scripts and modules could be updated to support it as well, when they are ready to take on the responsibility of testing that out. That would allow this feature to be adopted more easily for new modules, while existing modules could be updated over time. While we have experimental feature support, those are for a different purpose so an additional RFC is being published at the same time as this RFC to add support for optional feature definition in PowerShell (see RFCNNNN-Optional-Features.md). + +## Motivation + +As a scripter or a command author, +I should never have to care what type of command (cmdlet vs advanced function) I am invoking, +so that I can can focus on my script without having to worry about the implementation details of commands I use. + +As a command author, +I should be able to change a published command from an advanced function to a cmdlet or vice versa, +so that as long as I keep the same functionality in the command, scripts and modules where that command is in use should behave the same way. + +## User experience + +```powershell +# First, create a folder for a new module +$moduleName = 'RFCPropagateExecPrefDemo' +$modulePath = Join-Path -Path $([Environment]::GetFolderPath('MyDocuments')) -ChildPath PowerShell/Modules/${moduleName} +New-Item -Path $modulePath -ItemType Directory -Force > $null + +# Then, create the manifest (which would have the PersistCommandExecutionPreferences +# optional feature enabled by default, to correct the behaviour moving forward in a +# non-breaking way; downlevel versions of PowerShell would ignore the optional feature +# flags) +$nmmParameters = @{ + Path = "${modulePath}/${moduleName}.psd1" + RootModule = "./${moduleName}.psm1" + FunctionsToExport = @('Test-1') + PassThru = $true +} +New-ModuleManifest @nmmParameters | Get-Content + +# Output: +# +# @{ +# +# RootModule = './RFCPropagateExecPrefDemo.psm1' +# +# # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +# PrivateData = @{ +# +# +# +# PSData = @{ +# +# # Optional features enabled in this module. +# OptionalFeatures = @( +# 'PersistCommandExecutionPreferences' +# ) +# +# +# +# } # End of PSData hashtable +# +# +# +# } # End of PrivateData hashtable +# +# } + +# Then, create the script module file, along with a second module in memory that it invokes, +# and import both modules +$scriptModulePath = Join-Path -Path $modulePath -ChildPath ${moduleName}.psm1 +New-Item -Path $scriptModulePath -ItemType File | Set-Content -Encoding UTF8 -Value @' + function Test-1 { + [cmdletBinding()] + param() + Test-2 + } +'@ +Import-Module $moduleName +New-Module Name test2 { + function Test-2 { + [cmdletBinding()] + param() + Write-Verbose 'Verbose output' + } +} | Import-Module + +# When invoking the Test-2 command with -Verbose, it shows verbose output, as expected +Test-2 -Verbose + +# Output: +# +# VERBOSE: Verbose output + +# Thanks to this feature, when invoking the Test-1 command with -Verbose, it also shows +# verbose output. In PowerShell 6.2 and earlier, no verbose output would appear as a +# result of this command due to the issue preventing execution preferences from propagating +# beyond module/script scope +Test-1 -Verbose + +# Output: +# +# VERBOSE: Verbose output +``` + +## Specification + +To resolve this problem, a new optional feature called `PersistCommandExecutionPreferences` would be defined in PowerShell. When this feature is enabled in a script or module, it would change how common parameters work in that script or module. + +Today if you invoke a script or advanced function with `-ErrorAction`, `-WarningAction`, `-InformationAction`, `-Debug`, `-Verbose`, `-WhatIf`, or `-Confirm`, the corresponding `$*Preference` variable will be set within the scope of that command. That behaviour will remain the same; however when the optional feature is enabled, in addition to that behaviour, the names and values you supplied to those common parameters stored in a new `ExecutionPreferences` dictionary property of the `$PSCmdlet` instance. Once `$PSCmdlet.ExecutionPreferences` is set, any common parameters that are stored in `$PSCmdlet.ExecutionPreferences` that are not explicitly used in the invocation of another command within that command scope will be automatically passed through if the command being invoked supports common parameters. + +It is important to note that parameter/value pairs in `$PSCmdlet.ExecutionPreferences`, which represent command execution preferences, would take priority and be applied to a command invocation before values in `$PSDefaultParameterValues`, which represents user/module author parameter preferences (i.e. if both dictionaries have a value to be applied to a common parameter, only the value in `$PSCmdlet.ExecutionPreferences` would be applied). + +As per the optional feature specification, the optional feature can be enabled in a module manifest (see example above), or a script file via `#requires`. For more details on how that works, see RFCNNNN-Optional-Features. + +## Alternate proposals and considerations + +### Rip off the bandaid + +[Some members of the community](https://github.com/PowerShell/PowerShell/issues/6745#issuecomment-499740080) feel it would better to break compatibility here. On the plus side, not having to deal with this an an optional parameter would be ideal; however, to increase adoption of PowerShell 7, it would be better to make the transition from PowerShell 5.1 into 7 easier by having as few breaking changes as possible. + +### Support `-DebugAction`, `-VerboseAction`, and `-ProgressAction` if those common parameters are added + +RFCNNNN-ScriptBlock-Action-Preference suggests that we consider adding `-DebugAction`, `-VerboseAction`, and `-ProgressAction` common parameters. These are important to consider adding, because beyond the `-Debug` and `-Verbose` switch common parameters (which only support `ActionPreference.Continue`), the new common parameters would be the only way to propagate execution preferences for debug, verbose, and progress messages to all commands that are invoked. See RFCNNNN-ScriptBlock-Action-Preference for more details. diff --git a/1-Draft/RFCNNNN-ScriptBlock-Action-Preference.md b/1-Draft/RFCNNNN-ScriptBlock-Action-Preference.md new file mode 100644 index 00000000..f9714e02 --- /dev/null +++ b/1-Draft/RFCNNNN-ScriptBlock-Action-Preference.md @@ -0,0 +1,142 @@ +--- +RFC: RFCnnnn +Author: Kirk Munro +Status: Draft +SupercededBy: +Version: 1.0 +Area: Engine +Comments Due: July 15, 2019 +Plan to implement: Yes +--- + +# ScriptBlocks to handle non-terminating message processing + +@jpsnover suggested in [PowerShell Issue #6010](https://github.com/PowerShell/PowerShell/issues/6010) that an `[-OnError ]` is 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 (e.g. 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. Blurring the line between terminating and non-terminating errors 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, I propose instead that we extend what is allowed in `-*Action` common parameters, such that a `ScriptBlock` can be passed into those parameters. Further, I also propose 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 +$messageLog = [System.Collections.ArrayList]::new() + +function Write-MessageLog { + [CmdletBinding(DefaultParameterSetName='ErrorRecord')] + param( + [Parameter(Position=0, Mandatory=$true, ParameterSetName='ErrorRecord')] + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord, + + [Parameter(Position=0, Mandatory=$true, ParameterSetName='InformationRecord')] + [ValidateNotNull()] + [System.Management.Automation.InformationRecord] + $InformationRecord + ) + $now = [System.DateTime]::UtcNow + if ($PSCmdlet.ParameterSetName -eq 'ErrorRecord') { + # Record the error however you would record it in a log file or database here + $message = $ErrorRecord | Out-String + } else { + # Record the information record however you record it in a log file or database here + $message = $InformationRecord.Message + } + $messageLog.Add([pscustomobject]@{ + Timestamp = $now + Message = $message + }) +} + +Set-StrictMode -Version Latest +$sb = { + [CmdletBinding()] + param([int]$Id = $PID) + Write-Verbose -Verbose -Message "Looking for process with ID ${Id}..." + $process = Get-Process -Id $Id -ErrorAction {WriteInternalErrorLog $_; [ActionPreference]::Ignore} + 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." + } +} + +# Run the script, recording all non-terminating errors that are not internally silenced +# or ignored in the error log, output them on the screen, and store them in $error +& $sb -Id 12345678 -ErrorAction {Write-MessageLog $_; [ActionPreference]]::Continue} + +# Run the script again, recording all messages, including verbose and debug, as well as +# any terminating error that occurs in the message log without showing them on screen. +# Errors will be stored in $error. +$ErrorActionPreference = $WarningPreference = $VerbosePreference = $DebugPreference = { + Write-MessageLog $_ + [ActionPreference]::SilentlyContinue +} +try { + & $sb +} catch { + Write-MessageLog $_ + throw +} +``` + +In the case of the first example, the message log will contain the first verbose message and the warning message, and the internal error message log (that may be from a module) will contain the internal errors that were silenced. + +In the case of the second example, the message log will contain both verbose messages. + +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 + +### Make the `ScriptBlock` an `EventHandler` + +The `ScriptBlock` implementation looks like event handlers, so an alternative approach would be to define a specific event handler type and having the `ScriptBlock` design conform to that event handler. For example, in PowerShell we could define a `StreamedMessageEventArgs` class that has a `Action` property of type `ActionPreference`, and require that the `ScriptBlock` take parameters `($MessageRecord, $EventArgs)`, where `$MessageRecord` is the message that was raised, and `$EventArgs` is an instance of `StreamedMessageEventArgs` used to define the `ActionPreference` to take once the message is processed. For this approach, `$_` would still be assigned to the message record to allow the `ScriptBlock` logic to remain as simple as possible. Scripters would need to assign a value to `$EventArgs.Action` in the `ScriptBlock` in order to change the default behavior (it would be assigned to the default behavior for the corresponding message type by default). + +The benefits of this alternative are as follows: + + * The `ScriptBlock` return `ActionPreference` 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 still leave the param block out and not bother with updating `$EventArgs.Action` in the `ScriptBlock`, keeping their code simple. + * There can only be one handler for each type of message at a time, so even using an event handler definition, scripters wouldn't have to worry about adding or removing event handlers. They just need to assign the `ScriptBlock` to the parameter or variable they want, and PowerShell will typecast it as an event handler appropriately. + +The downsides to this approach are as follows: + +* Scripters need to explicitly define params and work with those parameters in the `ScriptBlock` if they want to change the default `ActionPreference`, which may be a little more complicated than simply working with letting an `ActionPreference` enumeration value (which could even be a string) be returned from the `ScriptBlock`. + +### Add `-VerboseAction`, `-DebugAction` and `-ProgressAction` common parameters + +It is important to consider RFCNNNN-Propagate-Execution-Preferences-Beyond-Module-Scope here because it uses common parameters to pass execution preferences to other modules and/or scripts. In order for that to work properly for all message types, such that `ScriptBlock` action preferences for verbose, debug, and progress messages also propagate beyond module/script scope, we would need to add `-VerboseAction`, `-DebugAction`, and `-ProgressAction` to the common parameter lists. The implementation of these would simply be the same as `-WarningAction` or `-InformationAction`, but for their respective streams. + +The benefits of having these additional common parameters are as follows: + +* Users are provided a consistent mechanism for dealing with non-terminating messages of any type. +* Scripters can run scripts that leverage progress messages heavily unattended with logging so that users can see how far the script has made it after the fact, or they can silence progress messages since they are running scripts unattended and the display processing is not necessary. +* Tool makers and advanced scripters can display or log messages of any type however they need. +* With RFCNNNN-Propagate-Execution-Preferences-Beyond-Module-Scope implemented, even verbose, debug and progress `ActionPreference` values or `ScriptBlock` message handlers can propagate beyond the scope of modules or scripts, allowing them to function more like cmdlets do. + +The downsides to these common parameters are as follows: + +* We already have `-Verbose` and `-Debug` common parameters, so there is some overlap; however, the PowerShell engine would raise an error if both `-Verbose` and `-VerboseAction` were used in an invocation, or `-Debug` and `-DebugAction` were used in an invocation, so there would be no conflict in invocation. Scripters would simply choose one or the other. From 98411a3fe6933e74d2441a940ee52596006f94e1 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Sun, 9 Jun 2019 20:55:11 -0300 Subject: [PATCH 02/15] minor updates to alternate proposals based on feedback --- 1-Draft/RFCNNNN-Optional-Features.md | 31 ++++++++++++++++--- ...ecution-Preferences-Beyond-Module-Scope.md | 2 ++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/1-Draft/RFCNNNN-Optional-Features.md b/1-Draft/RFCNNNN-Optional-Features.md index 551a79e6..ebe592b5 100644 --- a/1-Draft/RFCNNNN-Optional-Features.md +++ b/1-Draft/RFCNNNN-Optional-Features.md @@ -78,10 +78,19 @@ Get-OptionalFeature # Output: # -# Name EnabledIn Source Description -# ---- ------- ------ ----------- -# OptionalFeature1 Manifest PSEngine Description of optional feature 1 -# OptionalFeature2 Session PSEngine Description of optional feature 2 +# EnabledIn: Script +# +# Name Source Description +# ---- ------ ----------- +# OptionalFeature1 PSEngine Description of optional feature 1 +# OptionalFeature2 PSEngine Description of optional feature 2 +# +# +# EnabledIn: NotEnabled +# +# Name Source Description +# ---- ------ ----------- +# OptionalFeature3 PSEngine Description of optional feature 3 # Enable an optional feature by default in PowerShell Enable-OptionalFeature -Name OptionalFeature1 @@ -128,7 +137,15 @@ This new parameter would enable optional features in the current script file. Get-OptionalFeature [[-Name] ] [] ``` -This command would return the optional features that are available in PowerShell. The default output format would be of type table with the properties `Name`, `Enabled`, `Source`, and `Description`. All of those properties would be of type string except for `Enabled`, which would be an enumeration with the possible values of `NotEnabled`, `Session`, `Manifest`, and `Script`. This differs from experimental features where `Enabled` is a boolean value. Given the locations in which an optional feature can be enabled, it would be more informative to identify where it is enabled than simply showing `$true` or `$false`. +This command would return the optional features that are available in PowerShell. The default output format would be of type table with the properties `Name`, `Source`, and `Description`, and with the results grouped by the value of the `EnableIn` property. All of those properties would be of type string except for `EnabledIn`, which would be an enumeration with the possible values of `NotEnabled`, `Session`, `Manifest`, `Script`, and `Scope`. This differs from experimental features where `Enabled` is a boolean value. Given the locations in which an optional feature can be enabled, it would be more informative to identify where it is enabled than simply showing `$true` or `$false`. The enumeration values have the following meaning: + +|Value|Description|How to set the feature up this way| +|--|--|--| +|NotEnabled|The optional feature is not enabled at all|Disable-OptionalFeature command| +|Session|The optional feature is enabled by default in all PowerShell sessions|Enable-OptionalFeature command| +|Manifest|The optional feature is enabled in the manifest for the current module|OptionalFeatures entry in module manifest| +|Script|The optional feature is enabled in the current script|#requires entry in script file| +|Scope|The optional feature is enabled the current scope|Use-OptionalFeature command| ### New command: Enable-OptionalFeature @@ -163,3 +180,7 @@ Experimental features and optional features are very similar to one another, so ### Supporting a `-Scope` parameter like the experimental feature cmdlets do The `Enable-OptionalFeature` and `Disable-OptionalFeature` cmdlets could support a `-Scope` parameter like their experimental feature cmdlet counterparts do. I felt it was better to remove this for optional features, because it may be risky to allow a command to enable an optional feature in a scope above the one in which it is invoked, influencing behaviour elsewhere. + +### Allow optional features to be disabled at a certain level + +Certain optional features may be so important that PowerShell should be installed with them to be on by default. In cases where this happens, scripters should be able to indicate that they want the opposite behaviour in a script file or module, so that they can ensure any compatibility issues are addressed before the feature is enabled in that module/script. With this in mind, we could either allow optional features to be disabled at a certain level, or stick with enable only, inversing how the optional feature is designed such that turning it on effectively disables a breaking fix that was deemed important enough to have fixed by default. diff --git a/1-Draft/RFCNNNN-Propagate-Execution-Preferences-Beyond-Module-Scope.md b/1-Draft/RFCNNNN-Propagate-Execution-Preferences-Beyond-Module-Scope.md index 8431d2af..ad1b7256 100644 --- a/1-Draft/RFCNNNN-Propagate-Execution-Preferences-Beyond-Module-Scope.md +++ b/1-Draft/RFCNNNN-Propagate-Execution-Preferences-Beyond-Module-Scope.md @@ -128,6 +128,8 @@ As per the optional feature specification, the optional feature can be enabled i [Some members of the community](https://github.com/PowerShell/PowerShell/issues/6745#issuecomment-499740080) feel it would better to break compatibility here. On the plus side, not having to deal with this an an optional parameter would be ideal; however, to increase adoption of PowerShell 7, it would be better to make the transition from PowerShell 5.1 into 7 easier by having as few breaking changes as possible. +One way to achieve this while supporting users who don't want the breaking change would be to inverse the optional feature, where the breaking change is in place and users opt out of the breaking change instead of opting into it. Another way would be to change the optional feature design such that users can turn them off in scripts/modules if those scripts/modules are not ready to use the breaking change. See the alternate proposals and considerations section of RFCNNNN-Optional-Features for more information. + ### Support `-DebugAction`, `-VerboseAction`, and `-ProgressAction` if those common parameters are added RFCNNNN-ScriptBlock-Action-Preference suggests that we consider adding `-DebugAction`, `-VerboseAction`, and `-ProgressAction` common parameters. These are important to consider adding, because beyond the `-Debug` and `-Verbose` switch common parameters (which only support `ActionPreference.Continue`), the new common parameters would be the only way to propagate execution preferences for debug, verbose, and progress messages to all commands that are invoked. See RFCNNNN-ScriptBlock-Action-Preference for more details. From 701e9df826d6ac84f0a9d1c37238c3f9cb370668 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Sun, 9 Jun 2019 23:24:04 -0300 Subject: [PATCH 03/15] moved new manifest key location to top level --- 1-Draft/RFCNNNN-Optional-Features.md | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/1-Draft/RFCNNNN-Optional-Features.md b/1-Draft/RFCNNNN-Optional-Features.md index ebe592b5..f14ef23d 100644 --- a/1-Draft/RFCNNNN-Optional-Features.md +++ b/1-Draft/RFCNNNN-Optional-Features.md @@ -43,26 +43,11 @@ New-ModuleManifest -Path ./test.psd1 -OptionalFeatures @('OptionalFeature1','Opt # # # -# # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. -# PrivateData = @{ -# -# -# -# PSData = @{ -# -# # Optional features enabled in this module. -# OptionalFeatures = @( -# 'OptionalFeature1' -# 'OptionalFeature2' -# ) -# -# -# -# } # End of PSData hashtable -# -# -# -# } # End of PrivateData hashtable +# # Optional features enabled in this module. +# OptionalFeatures = @( +# 'OptionalFeature1' +# 'OptionalFeature2' +# ) # # } From 7c9743a16317987225969458b403b06f774653d0 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Mon, 10 Jun 2019 17:39:02 -0300 Subject: [PATCH 04/15] broken longwinded paragraph into numbered list --- ...NN-Make-Terminating-Errors-Terminate-The-Right-Way.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/1-Draft/RFCNNNN-Make-Terminating-Errors-Terminate-The-Right-Way.md b/1-Draft/RFCNNNN-Make-Terminating-Errors-Terminate-The-Right-Way.md index ef95ad47..6697514b 100644 --- a/1-Draft/RFCNNNN-Make-Terminating-Errors-Terminate-The-Right-Way.md +++ b/1-Draft/RFCNNNN-Make-Terminating-Errors-Terminate-The-Right-Way.md @@ -51,7 +51,14 @@ New-Module -Name ThisShouldNotImport { } | Import-Module ``` -If you invoke that snippet, the `ThisShouldNotImport` module imports successfully because the terminating error (`[System.Collections.Generics.List[string]]` is not a valid type name) does not actually terminate the loading of the module. This could cause your module to load in an unexpected state, which is a bad idea. If you loaded your module by invoking a command defined with that module, you won't see the terminating error that was raised during the loading of the module (the terminating error that was raised during the loading of the module is not shown at all in that scenario!), so you could end up with some undesirable behaviour when that command executes even though the loading of the module generated a "terminating" error, and not have a clue why. Further, the Test-RFC command exported by this module produces a terminating error, yet continues to execute after that error. Last, if the caller either loads your module or invokes your command inside of a `try` block, they will see different behaviour. Any execution of code beyond a terminating error should be intentional, not accidental like it is in both of these cases, and it most certainly should not be influenced by whether or not the caller loaded the module or invoked the command inside of a `try` block. Binary modules do not behave this way. Why should script modules be any different? +There are several things wrong with this snippet: + +1. If you invoke that snippet, the `ThisShouldNotImport` module imports successfully because the terminating error (`[System.Collections.Generics.List[string]]` is not a valid type name) does not actually terminate the loading of the module. This could cause your module to load in an unexpected state, which is a bad idea. +1. If you loaded your module by invoking a command defined with that module, you won't see the terminating error that was raised during the loading of the module (the terminating error that was raised during the loading of the module is not shown at all in that scenario!), so you could end up with some undesirable behaviour when that command executes even though the loading of the module generated a "terminating" error, and not have a clue why. +1. The Test-RFC command exported by this module produces a terminating error, yet continues to execute after that error. +1. If the caller either loads your module or invokes your command inside of a `try` block, they will see different behaviour. + +Any execution of code beyond a terminating error should be intentional, not accidental like it is in both of these cases, and it most certainly should not be influenced by whether or not the caller loaded the module or invoked the command inside of a `try` block. Binary modules do not behave this way. Why should script modules be any different? Now have a look at the same module definition, this time with some extra scaffolding in place to make sure that terminating errors actually terminate: From 739e427efcaff5818d66fb4060d94dcc856735fb Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Thu, 13 Jun 2019 16:21:16 -0300 Subject: [PATCH 05/15] update opt feats RFC for enable/disable scenarios --- 1-Draft/RFCNNNN-Optional-Features.md | 185 ++++++++++++++++++--------- 1 file changed, 126 insertions(+), 59 deletions(-) diff --git a/1-Draft/RFCNNNN-Optional-Features.md b/1-Draft/RFCNNNN-Optional-Features.md index f14ef23d..28472c36 100644 --- a/1-Draft/RFCNNNN-Optional-Features.md +++ b/1-Draft/RFCNNNN-Optional-Features.md @@ -34,8 +34,8 @@ so that I can leverage new functionality that could break existing scripts. ## User experience ```powershell -# Create a module manifest, specifically enabling one or more optional features in the manifest -New-ModuleManifest -Path ./test.psd1 -OptionalFeatures @('OptionalFeature1','OptionalFeature2') -PassThru | Get-Content +# Create a module manifest, specifically enabling or disabling one or more optional features in the manifest +New-ModuleManifest -Path ./test.psd1 -OptionalFeatures @('OptionalFeature1',@{Name='OptionalFeature2';Enabled=$false}) -PassThru | Get-Content # Output: # @@ -43,110 +43,181 @@ New-ModuleManifest -Path ./test.psd1 -OptionalFeatures @('OptionalFeature1','Opt # # # -# # Optional features enabled in this module. +# # Optional features enabled or disabled in this module. # OptionalFeatures = @( # 'OptionalFeature1' -# 'OptionalFeature2' +# @{Name='OptionalFeature2';Enabled=$false} # ) # # } -# Create a script file, enabling one or more optional features in the file +# Create a script file, enabling or disabling one or more optional features in the file @' -#requires -OptionalFeature OptionalFeature1,OptionalFeature2 +#requires -OptionalFeature OptionalFeature1,@{Name='OptionalFeature2';Enabled=$false} '@ | Out-File -FilePath ./test.ps1 -# Get a list of optional features that are available -Get-OptionalFeature +# Get the current optional feature configuration for the current user and all users +Get-OptionalFeatureConfiguration # Output: # -# EnabledIn: Script +# Scope: AllUsers +# +# Name Session NewManifest +# ---- ------ ----------- +# OptionalFeature1 False True +# OptionalFeature2 True False +# OptionalFeature4 True True +# OptionalFeature5 False False # -# Name Source Description -# ---- ------ ----------- -# OptionalFeature1 PSEngine Description of optional feature 1 -# OptionalFeature2 PSEngine Description of optional feature 2 # +# Scope: CurrentUser # -# EnabledIn: NotEnabled +# Name Session NewManifest +# ---- ------ ----------- +# OptionalFeature2 False True +# OptionalFeature3 False True +# + +# Get a list of optional features, their source, and their descriptions +Get-OptionalFeature + +# Output: # # Name Source Description # ---- ------ ----------- +# OptionalFeature1 PSEngine Description of optional feature 1 +# OptionalFeature2 PSEngine Description of optional feature 2 # OptionalFeature3 PSEngine Description of optional feature 3 +# OptionalFeature4 PSEngine Description of optional feature 4 + +# Enable an optional feature in current and future PowerShell sessions for all +# users in PowerShell. +Enable-OptionalFeature -Name OptionalFeature1 -UserScope AllUsers + +# Output: +# None -# Enable an optional feature by default in PowerShell -Enable-OptionalFeature -Name OptionalFeature1 +# Disable an optional feature in current and future PowerShell sessions for the +# current user in PowerShell. +Disable-OptionalFeature -Name OptionalFeature1 -UserScope CurrentUser # Output: -# This works just like Enable-ExperimentalFeature, turning the optional -# feature on by default for all future sessions in PowerShell. +# None, unless the feature was explicitly enabled for all users and is being +# disabled only for the current user as an override, as is the case here, +# in which case they get prompted to confirm. -# Disable an optional feature by default in PowerShell -Disable-OptionalFeature -Name OptionalFeature1 +# Enable an optional feature in all new module manifests created with +# New-ModuleManifest in the current and future PowerShell sessions for any user +# in PowerShell. +Enable-OptionalFeature -Name OptionalFeature2 -NewModuleManifests -UserScope AllUsers # Output: -# This works ust like Disable-ExperimentalFeature, turning the optional -# feature off by default for all future sessions in PowerShell. +# None + +# Enable an optional feature in all new module manifests created with +# New-ModuleManifest in the current and future PowerShell sessions for the +# current user in PowerShell. +Disable-OptionalFeature -Name OptionalFeature3 -NewModuleManifests -# Enable an optional feature by default in all new module manifests -# created with New-ModuleManifest in all future sessions in PowerShell. -Enable-OptionalFeature -Name OptionalFeature1 -NewModuleManifests +# Output: +# None -# Disable an optional feature by default in all new module manifests -# created with New-ModuleManifest in all future sessions in PowerShell. -Disable-OptionalFeature -Name OptionalFeature1 -NewModuleManifests +# Enable an optional feature the duration of the script block being invoked. +Use-OptionalFeature -Name OptionalFeature1 -ScriptBlock { + # Do things using OptionalFeature1 here +} ``` ## Specification -Aside from closely (but not exactly, see below) mirroring what is already in place internally for experimental features in PowerShell, this RFC includes a few additional enhancements that will be useful for optional features, as follows: +Unlike experimental features, which can only be enabled or disabled in PowerShell sessions created after enabling or disabling them, optional features can be enabled or disabled in the current PowerShell session as well as in future PowerShell sessions. This is necessary to allow certain functionality to be "lit up" in packaged modules or scripts. + +Below you will find details describing how this functionality will be implemented. + +### System and User scope powershell.config.json + +Enabling optional features automatically in future PowerShell sessions requires creating or updating one of two `powershell.config.json` configuration files that are read on startup of a new PowerShell session: + +* one in `$PSHOME`, which applies to all user sessions +* one in `$HOME\Documents\PowerShell\powershell.config.json` on Windows or `$HOME/.config/powershell/powershell.config.json` on Linux and macOS, which applies only to current user sessions. + +This RFC will enable optional feature defaults to be read from these configuration files, with current user configuration taking precedence over system (all users) configuration. System config is not policy so this should be acceptable and expected. ### Add parameter to New-ModuleManifest -`[-OptionalFeatures ]` +`[-OptionalFeatures ]` -This new parameter would assign specific optional features to new modules. Note that these would be in addition to optional features that are enabled by default in manifests created with `New-ModuleManifest`. +This parameter would configure specific optional features in the new module manifest that is generated. -### Add parameter to #requires statement +The values provided to this parameter would be combined with optional features that are enabled or disabled by default according to the session configuration files, with the values specified in the `New-ModuleManifest` command overriding the settings for optional features with the same name that are configured in the configuration files. Entries in this collection would either be string (the name of the optional feature to enable) or a hashtable with two keys: `name` (a string) and `enabled` (a boolean value). The hashtable allows an optional feature to be specifically disabled instead of enabled within a module, which is necessary if an older module does not support a newer optional feature yet. -`#requires -OptionalFeatures ` +A terminating error is generated if the same optional feature name is used twice in the collection passed into the `-OptionalFeatures` parameter. -This new parameter would enable optional features in the current script file. +### Add parameter set to #requires statement -### New command: Get-OptionalFeature +`#requires -OptionalFeatures ` + +This parameter set would enable optional features in the current script file. + +Entries in this collection would either be string (the name of the optional feature to enable) or a hashtable with two keys: `name` and `enabled`. The hashtable allows an optional feature to be specifically disabled instead of enabled within a script. + +A terminating error is generated if the same optional feature name is used twice in the collection passed into the `-OptionalFeatures` parameter. + +### New command: Get-OptionalFeatureConfiguration ```none -Get-OptionalFeature [[-Name] ] [] +Get-OptionalFeatureConfiguration [[-Name] ] [-UserScope { CurrentUser | AllUsers | Any }] [] ``` -This command would return the optional features that are available in PowerShell. The default output format would be of type table with the properties `Name`, `Source`, and `Description`, and with the results grouped by the value of the `EnableIn` property. All of those properties would be of type string except for `EnabledIn`, which would be an enumeration with the possible values of `NotEnabled`, `Session`, `Manifest`, `Script`, and `Scope`. This differs from experimental features where `Enabled` is a boolean value. Given the locations in which an optional feature can be enabled, it would be more informative to identify where it is enabled than simply showing `$true` or `$false`. The enumeration values have the following meaning: +This command would return the current configuration of optional features that are available in PowerShell, read from the configuration files. -|Value|Description|How to set the feature up this way| -|--|--|--| -|NotEnabled|The optional feature is not enabled at all|Disable-OptionalFeature command| -|Session|The optional feature is enabled by default in all PowerShell sessions|Enable-OptionalFeature command| -|Manifest|The optional feature is enabled in the manifest for the current module|OptionalFeatures entry in module manifest| -|Script|The optional feature is enabled in the current script|#requires entry in script file| -|Scope|The optional feature is enabled the current scope|Use-OptionalFeature command| +The properties on the `S.M.A.OptionalFeatureConfiguration` object would be `Name`, `Session`, `NewManifest`, and `Scope`, defined as follows: -### New command: Enable-OptionalFeature +|Property Name|Description| +|--|--| +|`Name`|A string value that identifies the optional feature name| +|`Session`|A boolean value that identifies whether the optional feature is enabled or disabled in the current and new PowerShell sessions| +|`NewManifest`|A boolean value that identifies whether the optional feature is enabled or disabled in manifests created by new module manifests in the current and new PowerShell sessions| +|`Scope`|An enumeration identifying whether the optional feature configuration was set up for the `CurrentUser` or `AllUsers`| + +The default output format is of type table with the properties `Name`, `Session`, and `NewManifest` with the results grouped by `Scope`. + +When this command is invoked with the `-UserScope` parameter, the results are automatically filtered for that scope. The default value for `-UserScope` is `Any`, showing configuration values from both configuration files. + +### New command: Get-OptionalFeature ```none -Enable-OptionalFeature [-Name] [-NewModuleManifests] [-WhatIf] [-Confirm] [] +Get-OptionalFeature [[-Name] ] [] ``` -This command would enable an optional feature either globally (if the `-NewModuleManifests` switch is not used) or only in new module manifests created by `New-ModuleManifest`. +This command will return a list of the optional features that are available in PowerShell, along with their source and description. + +The properties on the `S.M.A.OptionalFeature` object would be `Name`, `Source`, `Description`, defined as follows: -### New command: Disable-OptionalFeature +|Property Name|Description| +|--|--| +|`Name`|A string value that identifies the optional feature name| +|`Source`|A string value that identifies the area of PowerShell that is affected by this optional feature| +|`Description`|A string value that describes the optional feature| + +The default output format would be of type table with the properties `Name`, `Source`, and `Description`. + +### Enabling and disabling optional features in current and future PowerShell sessions ```none -Disable-OptionalFeature [-Name] [-NewModuleManifests] [-WhatIf] [-Confirm] [] +Enable-OptionalFeature [-Name] [-NewModuleManifests] [-UserScope { CurrentUser | AllUsers }] [-WhatIf] [-Confirm] [] + +Disable-OptionalFeature [-Name] [-NewModuleManifests] [-UserScope { CurrentUser | AllUsers }] [-WhatIf] [-Confirm] [] ``` -This command would disable an optional feature either globally (if the `-NewModuleManifests` switch is not used) or only in new module manifests created by `New-ModuleManifest`. If the optional feature is not enabled that way in the first place, nothing would happen. +It is important to note up front that there are three default states for an optional feature: enabled by default, implicitly disabled by default, and explicitly disabled by default. The only time an optional feature needs to be explicitly disabled by default is if it is enabled by default in the AllUsers configuration file and a specific user wants to disable it for their sessions. This impacts how `Disable-OptionalFeature` works. + +The `Enable-OptionalFeature` command will enable an optional feature in current and future PowerShell sessions either globally (if the `-NewModuleManifests` switch is not used) or only in manifests created by `New-ModuleManifest`. + +The `Disable-OptionalFeature` command will disable an optional feature in current and future PowerShell sessions either globally (if the `-NewModuleManifests` switch is not used) or only in manifests created by `New-ModuleManifest`. If the `AllUsers` scope is used and the optional feature is completely disabled in that scope as a result of this command, the entry is removed from the configuration file. If the `AllUsers` scope is used and there is no entry in the system (all users) configuration file, nothing happens. If the `CurrentUser` scope is used there is no entry in the system (all users) or current user configuration files, nothing happens. If the `CurrentUser` scope is used and the optional feature is enabled in the `AllUsers` configuration file, users will be informed that this feature is enabled for all users and asked to confirm that they want to explicitly disable this feature for the current user in the current and future PowerShell sessions. They can always re-enable it later. ### New command: Use-OptionalFeature @@ -156,16 +227,12 @@ Use-OptionalFeature [-Name] [-ScriptBlock] [-Confirm] [ This command would enable an optional feature for the duration of the `ScriptBlock` identified in the `-ScriptBlock` parameter, and return the feature to its previous state afterwards. This allows for easy use of an optional feature over a small section of code. -## Alternate proposals and considerations +### Checking optional feature states within the PowerShell runtime -### Extend experimental features to support the enhancements defined in this RFC - -Experimental features and optional features are very similar to one another, so much so that they really only differ in name. Given the model for how both of these types of features are used, it may make sense to have them both use the same functionality when it comes to enabling/disabling them in scripts and modules. The downside I see to this approach is that optional features are permanent features in PowerShell while experimental features are not, so it may not be a good idea to support more permanent ways to enable experimental features such as `#requires` or enabling an experimental feature in a new module manifest. - -### Supporting a `-Scope` parameter like the experimental feature cmdlets do +Optional features can be enabled or disabled in a session, module, script, or script block. Since enabling or disabling an optional feature can happen at different levels, the current state of an optional feature should be maintained in a stack, where the validation logic simply peeks at the top of the stack to see if the feature is enabled or not, popping the top of the stack off when appropriate (when leaving the scope of the module, script, or script block where the feature is enabled). -The `Enable-OptionalFeature` and `Disable-OptionalFeature` cmdlets could support a `-Scope` parameter like their experimental feature cmdlet counterparts do. I felt it was better to remove this for optional features, because it may be risky to allow a command to enable an optional feature in a scope above the one in which it is invoked, influencing behaviour elsewhere. +## Alternate proposals and considerations -### Allow optional features to be disabled at a certain level +### Extend experimental features to support the enhancements defined in this RFC -Certain optional features may be so important that PowerShell should be installed with them to be on by default. In cases where this happens, scripters should be able to indicate that they want the opposite behaviour in a script file or module, so that they can ensure any compatibility issues are addressed before the feature is enabled in that module/script. With this in mind, we could either allow optional features to be disabled at a certain level, or stick with enable only, inversing how the optional feature is designed such that turning it on effectively disables a breaking fix that was deemed important enough to have fixed by default. +At a glance, experimental features and optional features are very similar to one another, so it was proposed that it may make sense to have them both use the same functionality when it comes to enabling/disabling them in scripts and modules; however, experimental features have a completely different intent (to try out new functionality in a PowerShell session), are only for future PowerShell sessions, and they only have a single on/off state. On the other hand, optional features are for the current and future PowerShell sessions, and may be enabled or disabled in various scopes within those sessions. For that reason, this approach doesn't seem like a viable solution. If we want to change that, perhaps someone should file an RFC against experimental features to make that change. From e162407f470bf48af0129aab05795e3e715ca0fd Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Thu, 13 Jun 2019 21:05:55 -0300 Subject: [PATCH 06/15] clean up wording and simplify --- 1-Draft/RFCNNNN-Optional-Features.md | 43 +++++++++++++++------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/1-Draft/RFCNNNN-Optional-Features.md b/1-Draft/RFCNNNN-Optional-Features.md index 28472c36..1c35f69d 100644 --- a/1-Draft/RFCNNNN-Optional-Features.md +++ b/1-Draft/RFCNNNN-Optional-Features.md @@ -11,15 +11,15 @@ Plan to implement: Yes # Optional features in PowerShell -There are several important issues in the PowerShell language that cannot be corrected without introducing breaking changes. At the same time, the number of breaking changes introduced in a new version of PowerShell needs to be as minimal as possible, so that there is a low barrier to adoption of new versions, allowing community members can transition scripts and modules across versions more easily. Given that those two statements are in conflict with one another, we need to consider how we can optionally introduce breaking changes into PowerShell over time. +There are several important issues in the PowerShell language that cannot be corrected without introducing breaking changes. At the same time, the number of breaking changes introduced in a new version of PowerShell needs to be as minimal as possible, so that there is a low barrier to adoption of new versions, allowing community members to transition scripts and modules across versions more easily. Given that those two statements are in conflict with one another, we need to consider how we can optionally introduce breaking changes into PowerShell over time. -PowerShell has support for experimental features, which some may think covers this need; however, the intent of experimental features is to allow the community to try pre-release versions of PowerShell with breaking changes that are deemed necessary so that they can more accurately assess the impact of those breaking changes. For release versions of PowerShell, an experimental feature has one of three possible outcomes: +PowerShell has support for experimental features, which some may think covers this need; however, the intent of experimental features is to allow the community to try pre-release versions of PowerShell with breaking changes that are deemed necessary or new features that are not necessarily fully tested/polished so that they can more accurately assess the impact of those features. For release versions of PowerShell, an experimental feature has one of three possible outcomes: -1. The breaking change in the experimental feature is deemed necessary and accepted by the community as not harmful to adoption of new versions, in which case the experimental feature is no longer marked as experimental. -1. The breaking change in the experimental feature is deemed necessary, but considered harmful to adoption of new versions, in which case the experimental feature is changed to an optional feature. -1. The breaking change in the experimental feature is deemed not useful enough, in which case the experimental feature is deprecated. +1. The the experimental feature is deemed necessary, adequately tested/polished and accepted by the community as not harmful to adoption of new versions, in which case the experimental feature is no longer marked as experimental. +1. The experimental feature is deemed necessary, and adequately tested/polished, but considered harmful to adoption of new versions, in which case the experimental feature is changed to an optional feature. +1. The experimental feature is deemed not useful enough, in which case the experimental feature is deprecated. -In some cases a breaking change may be implemented immediately as an optional feature, when it is known up front that such a breaking change would be considered harmful to adoption of new versions of PowerShell. +In some cases a breaking change may be implemented immediately as an optional feature, when it is known up front that such a breaking change would be considered harmful to adoption of new versions of PowerShell if it was in place by default yet is still found important enough to implement as an optional feature. Given all of that, we need to add support for optional features in PowerShell so that what is described above becomes a reality. @@ -28,8 +28,8 @@ As an example of a feature that will be optional if implemented, see RFCNNNN-Pro ## Motivation As a script, function, or module author, -I can enable optional features in my scripts or modules, -so that I can leverage new functionality that could break existing scripts. +I can enable optional features in specific scopes, +so that I can leverage new functionality that may break existing scripts without risk. ## User experience @@ -53,7 +53,8 @@ New-ModuleManifest -Path ./test.psd1 -OptionalFeatures @('OptionalFeature1',@{Na # Create a script file, enabling or disabling one or more optional features in the file @' -#requires -OptionalFeature OptionalFeature1,@{Name='OptionalFeature2';Enabled=$false} +#requires -OptionalFeature OptionalFeature1 +#requires -OptionalFeature OptionalFeature2 -Disabled '@ | Out-File -FilePath ./test.ps1 @@ -107,7 +108,7 @@ Disable-OptionalFeature -Name OptionalFeature1 -UserScope CurrentUser # Output: # None, unless the feature was explicitly enabled for all users and is being # disabled only for the current user as an override, as is the case here, -# in which case they get prompted to confirm. +# in which case they get prompted to confirm. This is described below. # Enable an optional feature in all new module manifests created with # New-ModuleManifest in the current and future PowerShell sessions for any user @@ -125,10 +126,16 @@ Disable-OptionalFeature -Name OptionalFeature3 -NewModuleManifests # Output: # None -# Enable an optional feature the duration of the script block being invoked. -Use-OptionalFeature -Name OptionalFeature1 -ScriptBlock { +# Enable and disable an optional feature the duration of the script block being +# invoked. +Use-OptionalFeature -Enable OptionalFeature1 -Disable OptionalFeature2 -ScriptBlock { # Do things using OptionalFeature1 here + # OptionalFeature2 cannot be used here } +# If OptionalFeature1 was not enabled before this invocation, it is still no +# longerenabled here. If OptionalFeature2 was enabled before this invocation, +# it is still enabled here. All to say, their state before the call is +# preserved. ``` ## Specification @@ -158,13 +165,9 @@ A terminating error is generated if the same optional feature name is used twice ### Add parameter set to #requires statement -`#requires -OptionalFeatures ` +`#requires -OptionalFeatures [-Disabled]` -This parameter set would enable optional features in the current script file. - -Entries in this collection would either be string (the name of the optional feature to enable) or a hashtable with two keys: `name` and `enabled`. The hashtable allows an optional feature to be specifically disabled instead of enabled within a script. - -A terminating error is generated if the same optional feature name is used twice in the collection passed into the `-OptionalFeatures` parameter. +This parameter set would enable, or disable if `-Disabled` is used, optional features identified by `-Name` in the current script file. ### New command: Get-OptionalFeatureConfiguration @@ -222,10 +225,10 @@ The `Disable-OptionalFeature` command will disable an optional feature in curren ### New command: Use-OptionalFeature ```none -Use-OptionalFeature [-Name] [-ScriptBlock] [-Confirm] [] +Use-OptionalFeature [[-Enable] ] [[-Disable] ] [-ScriptBlock] [-Confirm] [] ``` -This command would enable an optional feature for the duration of the `ScriptBlock` identified in the `-ScriptBlock` parameter, and return the feature to its previous state afterwards. This allows for easy use of an optional feature over a small section of code. +This command would enable or disable the optional features whose names are identified in the `-Enable` and `-Disable` parameters for the duration of the `ScriptBlock` identified in the `-ScriptBlock` parameter, and return the features to their previous state afterwards. This allows for easy use of optional features over a small section of code. If neither `-Enable` or `-Disable` are used, a terminating error is thrown. ### Checking optional feature states within the PowerShell runtime From e5b476d6aa5949d47138f53265e2cabdb25f49f3 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Fri, 14 Jun 2019 11:34:06 -0300 Subject: [PATCH 07/15] clean-up parameter names and use of scope --- 1-Draft/RFCNNNN-Optional-Features.md | 36 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/1-Draft/RFCNNNN-Optional-Features.md b/1-Draft/RFCNNNN-Optional-Features.md index 1c35f69d..ca4372ae 100644 --- a/1-Draft/RFCNNNN-Optional-Features.md +++ b/1-Draft/RFCNNNN-Optional-Features.md @@ -28,7 +28,7 @@ As an example of a feature that will be optional if implemented, see RFCNNNN-Pro ## Motivation As a script, function, or module author, -I can enable optional features in specific scopes, +I can enable optional features for specific users or in specific scopes, so that I can leverage new functionality that may break existing scripts without risk. ## User experience @@ -64,7 +64,7 @@ Get-OptionalFeatureConfiguration # Output: # -# Scope: AllUsers +# For: AllUsers # # Name Session NewManifest # ---- ------ ----------- @@ -74,7 +74,7 @@ Get-OptionalFeatureConfiguration # OptionalFeature5 False False # # -# Scope: CurrentUser +# For: CurrentUser # # Name Session NewManifest # ---- ------ ----------- @@ -96,14 +96,14 @@ Get-OptionalFeature # Enable an optional feature in current and future PowerShell sessions for all # users in PowerShell. -Enable-OptionalFeature -Name OptionalFeature1 -UserScope AllUsers +Enable-OptionalFeature -Name OptionalFeature1 -For AllUsers # Output: # None # Disable an optional feature in current and future PowerShell sessions for the # current user in PowerShell. -Disable-OptionalFeature -Name OptionalFeature1 -UserScope CurrentUser +Disable-OptionalFeature -Name OptionalFeature1 -For CurrentUser # Output: # None, unless the feature was explicitly enabled for all users and is being @@ -113,7 +113,7 @@ Disable-OptionalFeature -Name OptionalFeature1 -UserScope CurrentUser # Enable an optional feature in all new module manifests created with # New-ModuleManifest in the current and future PowerShell sessions for any user # in PowerShell. -Enable-OptionalFeature -Name OptionalFeature2 -NewModuleManifests -UserScope AllUsers +Enable-OptionalFeature -Name OptionalFeature2 -For AllUsers -InNewModuleManifests # Output: # None @@ -121,7 +121,7 @@ Enable-OptionalFeature -Name OptionalFeature2 -NewModuleManifests -UserScope All # Enable an optional feature in all new module manifests created with # New-ModuleManifest in the current and future PowerShell sessions for the # current user in PowerShell. -Disable-OptionalFeature -Name OptionalFeature3 -NewModuleManifests +Disable-OptionalFeature -Name OptionalFeature3 -InNewModuleManifests # Output: # None @@ -144,7 +144,7 @@ Unlike experimental features, which can only be enabled or disabled in PowerShel Below you will find details describing how this functionality will be implemented. -### System and User scope powershell.config.json +### System and User powershell.config.json configuration files Enabling optional features automatically in future PowerShell sessions requires creating or updating one of two `powershell.config.json` configuration files that are read on startup of a new PowerShell session: @@ -172,23 +172,23 @@ This parameter set would enable, or disable if `-Disabled` is used, optional fea ### New command: Get-OptionalFeatureConfiguration ```none -Get-OptionalFeatureConfiguration [[-Name] ] [-UserScope { CurrentUser | AllUsers | Any }] [] +Get-OptionalFeatureConfiguration [[-Name] ] [-For { CurrentUser | AllUsers }] [] ``` This command would return the current configuration of optional features that are available in PowerShell, read from the configuration files. -The properties on the `S.M.A.OptionalFeatureConfiguration` object would be `Name`, `Session`, `NewManifest`, and `Scope`, defined as follows: +The properties on the `S.M.A.OptionalFeatureConfiguration` object would be `Name`, `Session`, `NewManifest`, and `For`, defined as follows: |Property Name|Description| |--|--| |`Name`|A string value that identifies the optional feature name| |`Session`|A boolean value that identifies whether the optional feature is enabled or disabled in the current and new PowerShell sessions| |`NewManifest`|A boolean value that identifies whether the optional feature is enabled or disabled in manifests created by new module manifests in the current and new PowerShell sessions| -|`Scope`|An enumeration identifying whether the optional feature configuration was set up for the `CurrentUser` or `AllUsers`| +|`For`|An enumeration flag identifying whether the optional feature configuration was set up for the `CurrentUser` or `AllUsers`| -The default output format is of type table with the properties `Name`, `Session`, and `NewManifest` with the results grouped by `Scope`. +The default output format is of type table with the properties `Name`, `Session`, and `NewManifest` with the results grouped by `For`. -When this command is invoked with the `-UserScope` parameter, the results are automatically filtered for that scope. The default value for `-UserScope` is `Any`, showing configuration values from both configuration files. +When this command is invoked with the `-For` parameter, the results are automatically filtered for that configuration file. The default value for `-For` is both flags, showing configuration values from both configuration files. ### New command: Get-OptionalFeature @@ -211,16 +211,16 @@ The default output format would be of type table with the properties `Name`, `So ### Enabling and disabling optional features in current and future PowerShell sessions ```none -Enable-OptionalFeature [-Name] [-NewModuleManifests] [-UserScope { CurrentUser | AllUsers }] [-WhatIf] [-Confirm] [] +Enable-OptionalFeature [-Name] [-For { CurrentUser | AllUsers }] [-InNewModuleManifests] [-WhatIf] [-Confirm] [] -Disable-OptionalFeature [-Name] [-NewModuleManifests] [-UserScope { CurrentUser | AllUsers }] [-WhatIf] [-Confirm] [] +Disable-OptionalFeature [-Name] [-For { CurrentUser | AllUsers }] [-InNewModuleManifests] [-WhatIf] [-Confirm] [] ``` It is important to note up front that there are three default states for an optional feature: enabled by default, implicitly disabled by default, and explicitly disabled by default. The only time an optional feature needs to be explicitly disabled by default is if it is enabled by default in the AllUsers configuration file and a specific user wants to disable it for their sessions. This impacts how `Disable-OptionalFeature` works. -The `Enable-OptionalFeature` command will enable an optional feature in current and future PowerShell sessions either globally (if the `-NewModuleManifests` switch is not used) or only in manifests created by `New-ModuleManifest`. +The `Enable-OptionalFeature` command will enable an optional feature in current and future PowerShell sessions either globally (if the `-InNewModuleManifests` switch is not used) or only in manifests created by `New-ModuleManifest`. -The `Disable-OptionalFeature` command will disable an optional feature in current and future PowerShell sessions either globally (if the `-NewModuleManifests` switch is not used) or only in manifests created by `New-ModuleManifest`. If the `AllUsers` scope is used and the optional feature is completely disabled in that scope as a result of this command, the entry is removed from the configuration file. If the `AllUsers` scope is used and there is no entry in the system (all users) configuration file, nothing happens. If the `CurrentUser` scope is used there is no entry in the system (all users) or current user configuration files, nothing happens. If the `CurrentUser` scope is used and the optional feature is enabled in the `AllUsers` configuration file, users will be informed that this feature is enabled for all users and asked to confirm that they want to explicitly disable this feature for the current user in the current and future PowerShell sessions. They can always re-enable it later. +The `Disable-OptionalFeature` command will disable an optional feature in current and future PowerShell sessions either globally (if the `-InNewModuleManifests` switch is not used) or only in manifests created by `New-ModuleManifest`. If the feature is being disabled for `AllUsers` and the optional feature is completely disabled in that configuration file as a result of this command, the entry is removed from the configuration file. If the feature is being disabled for `AllUsers` and there is no entry in the system (all users) configuration file, nothing happens. If the feature is being disabled for the `CurrentUser` there is no entry in the system (all users) or current user configuration files, nothing happens. If the feature is being disabled for the `CurrentUser` and the optional feature is enabled in the `AllUsers` configuration file, users will be informed that this feature is enabled for all users and asked to confirm that they want to explicitly disable this feature for the current user in the current and future PowerShell sessions. They can always re-enable it later. ### New command: Use-OptionalFeature @@ -238,4 +238,4 @@ Optional features can be enabled or disabled in a session, module, script, or sc ### Extend experimental features to support the enhancements defined in this RFC -At a glance, experimental features and optional features are very similar to one another, so it was proposed that it may make sense to have them both use the same functionality when it comes to enabling/disabling them in scripts and modules; however, experimental features have a completely different intent (to try out new functionality in a PowerShell session), are only for future PowerShell sessions, and they only have a single on/off state. On the other hand, optional features are for the current and future PowerShell sessions, and may be enabled or disabled in various scopes within those sessions. For that reason, this approach doesn't seem like a viable solution. If we want to change that, perhaps someone should file an RFC against experimental features to make that change. +At a glance, experimental features and optional features are very similar to one another, so it was proposed that it may make sense to have them both use the same functionality when it comes to enabling/disabling them in scripts and modules; however, experimental features have a completely different intent (to try out new functionality in a PowerShell session), are only for future PowerShell sessions, and they only have a single on/off state. On the other hand, optional features are for the current and future PowerShell sessions, for the current user or all users, and may be enabled or disabled in various scopes within those sessions. For that reason, this approach doesn't seem like a viable solution. If we want to change that, perhaps someone should file an RFC against experimental features to make that change. From 836cd56afe512e8541233780f3cbaff1c6eea449 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Tue, 18 Jun 2019 17:19:51 -0300 Subject: [PATCH 08/15] incorporated feedback --- ...minating-Errors-Terminate-The-Right-Way.md | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/1-Draft/RFCNNNN-Make-Terminating-Errors-Terminate-The-Right-Way.md b/1-Draft/RFCNNNN-Make-Terminating-Errors-Terminate-The-Right-Way.md index 6697514b..0877408c 100644 --- a/1-Draft/RFCNNNN-Make-Terminating-Errors-Terminate-The-Right-Way.md +++ b/1-Draft/RFCNNNN-Make-Terminating-Errors-Terminate-The-Right-Way.md @@ -15,7 +15,8 @@ By default in PowerShell, terminating errors do not actually terminate. For exam ```PowerShell & { - 1/0 + $string = 'Hello' + $string.Substring(99) 'Why?' } ``` @@ -24,16 +25,32 @@ PowerShell has upheld this behaviour since version 1.0 of the language. You can ```PowerShell try { - 1/0 + $string = 'Hello' + $string.Substring(99) 'Why?' } catch { throw } ``` -In that example, the exception raised by dividing by zero properly terminates execution of the running command. +You can also convert the terminating-yet-handled-as-a-non-terminating error into an actual terminating error like this: -The difference between these two examples poses a risk to scripters who share scripts or modules with the community. The risk is that end users using a shared resource such as a script or module may see different behaviour from the logic within that module depending on whether or not they were inside of a `try` block when they invoked the script or a command exported by the module. That risk is very undesirable, and as a result many community members who share scripts/modules with the community wrap their logic in a `try/catch{throw}` (or similar) scaffolding to ensure that the behavior of their code is consistent no matter where or how it was invoked. +```PowerShell +& { + $ErrorActionPreference = 'Stop' + $string = 'Hello' + $string.Substring(99) + 'Why?' +} +``` + +In those last two examples, the exception raised by the .NET method terminates execution of the running command. + +The difference between the first example and the workarounds poses a risk to scripters who share scripts or modules with the community. + +In the first workaround, the risk is that end users using a shared resource such as a script or module may see different behaviour from the logic within that module depending on whether or not they were inside of a `try` block when they invoked the script or a command exported by the module. That risk is very undesirable, and as a result many community members who share scripts/modules with the community wrap their logic in a `try/catch{throw}` (or similar) scaffolding to ensure that the behavior of their code is consistent no matter where or how it was invoked. + +In the second workaround, if the shared script does not use `$ErrorActionPreference = 'Stop'`, a caller can get different behaviour by manipulating their `$ErrorActionPreference`. The caller should not be able to manipulate terminating error behavior in commands that they invoke -- that's up to the command author, and they shouldn't have to use extra handling to make terminating errors terminate. Now consider this code snippet: From 09a3b104e780bd643e4c70c35497ff43d350c610 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Fri, 21 Jun 2019 10:22:07 -0300 Subject: [PATCH 09/15] add job considerations --- 1-Draft/RFCNNNN-Optional-Features.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/1-Draft/RFCNNNN-Optional-Features.md b/1-Draft/RFCNNNN-Optional-Features.md index ca4372ae..4467d9f2 100644 --- a/1-Draft/RFCNNNN-Optional-Features.md +++ b/1-Draft/RFCNNNN-Optional-Features.md @@ -239,3 +239,18 @@ Optional features can be enabled or disabled in a session, module, script, or sc ### Extend experimental features to support the enhancements defined in this RFC At a glance, experimental features and optional features are very similar to one another, so it was proposed that it may make sense to have them both use the same functionality when it comes to enabling/disabling them in scripts and modules; however, experimental features have a completely different intent (to try out new functionality in a PowerShell session), are only for future PowerShell sessions, and they only have a single on/off state. On the other hand, optional features are for the current and future PowerShell sessions, for the current user or all users, and may be enabled or disabled in various scopes within those sessions. For that reason, this approach doesn't seem like a viable solution. If we want to change that, perhaps someone should file an RFC against experimental features to make that change. + +### Enable/disable optional features in jobs according to their current state when the job is launched + +Jobs run in the background have their own session, and therefore will not automatically have optional features enabled or disabled according to the current state when the job is launched. We should consider updating how jobs are launched in the engine such that they do "inherit" optional feature configuration to allow them to use optional features without additional code. + +You might think that you can accomplish this using `#requires` in a script block launched as a job, but that doesn't work -- the `#requires` statement is ignored when you do this at the moment. For example, the see the results of the following script when launched from PowerShell Core: + +```PowerShell +Start-Job { + #requires -PSEdition Desktop + $PSVersionTable +} | Receive-Job -Wait +``` + +The result of that command shows that the version of PowerShell where the job was run was Core, not Desktop, yet the job ran anyway despite the `#requires` statement. This may be a bug. If it is, and if that bug is corrected, then you could use #requires to enable/disable features, but regardless it would still be preferable (and more intuitive) for jobs to "inherit" the current optional feature configuration when they are invoked. From af54447cf96be71fb68303c400b54d726e256717 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Tue, 25 Jun 2019 10:38:33 -0300 Subject: [PATCH 10/15] add job detail --- 1-Draft/RFCNNNN-Optional-Features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/1-Draft/RFCNNNN-Optional-Features.md b/1-Draft/RFCNNNN-Optional-Features.md index 4467d9f2..5f8361bd 100644 --- a/1-Draft/RFCNNNN-Optional-Features.md +++ b/1-Draft/RFCNNNN-Optional-Features.md @@ -253,4 +253,4 @@ Start-Job { } | Receive-Job -Wait ``` -The result of that command shows that the version of PowerShell where the job was run was Core, not Desktop, yet the job ran anyway despite the `#requires` statement. This may be a bug. If it is, and if that bug is corrected, then you could use #requires to enable/disable features, but regardless it would still be preferable (and more intuitive) for jobs to "inherit" the current optional feature configuration when they are invoked. +The result of that command shows that the version of PowerShell where the job was run was Core, not Desktop, yet the job ran anyway despite the `#requires` statement. This may be a bug. If it is, and if that bug is corrected, then you could use #requires to enable/disable features, but regardless it would still be preferable (and more intuitive) for jobs to "inherit" the current optional feature configuration when they are invoked. This includes jobs launched with `Start-Job`, `Start-ThreadJob`, the `&` background operator, parallelized `foreach` statements or `ForEach-Object` commands, or the generic `-AsJob` parameter. From 9d0b313c76144e9af7b84294bba470da710810e8 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Fri, 12 Jul 2019 20:59:39 -0300 Subject: [PATCH 11/15] add comment to make example more clear --- .../RFCNNNN-Make-Terminating-Errors-Terminate-The-Right-Way.md | 1 + 1 file changed, 1 insertion(+) diff --git a/1-Draft/RFCNNNN-Make-Terminating-Errors-Terminate-The-Right-Way.md b/1-Draft/RFCNNNN-Make-Terminating-Errors-Terminate-The-Right-Way.md index 0877408c..b78ed3fe 100644 --- a/1-Draft/RFCNNNN-Make-Terminating-Errors-Terminate-The-Right-Way.md +++ b/1-Draft/RFCNNNN-Make-Terminating-Errors-Terminate-The-Right-Way.md @@ -56,6 +56,7 @@ Now consider this code snippet: ```PowerShell New-Module -Name ThisShouldNotImport { + # The next line uses "Generics" in the type name, when it should use "Generic" $myList = [System.Collections.Generics.List[string]]::new() function Test-RFC { From 3643092995b8d6fac1a175ca5ec5311011e54e8f Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Fri, 16 Aug 2019 00:06:37 -0300 Subject: [PATCH 12/15] move ScriptBlock action pref. RFC into PR #219 --- .../RFCNNNN-ScriptBlock-Action-Preference.md | 142 ------------------ 1 file changed, 142 deletions(-) delete mode 100644 1-Draft/RFCNNNN-ScriptBlock-Action-Preference.md diff --git a/1-Draft/RFCNNNN-ScriptBlock-Action-Preference.md b/1-Draft/RFCNNNN-ScriptBlock-Action-Preference.md deleted file mode 100644 index f9714e02..00000000 --- a/1-Draft/RFCNNNN-ScriptBlock-Action-Preference.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -RFC: RFCnnnn -Author: Kirk Munro -Status: Draft -SupercededBy: -Version: 1.0 -Area: Engine -Comments Due: July 15, 2019 -Plan to implement: Yes ---- - -# ScriptBlocks to handle non-terminating message processing - -@jpsnover suggested in [PowerShell Issue #6010](https://github.com/PowerShell/PowerShell/issues/6010) that an `[-OnError ]` is 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 (e.g. 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. Blurring the line between terminating and non-terminating errors 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, I propose instead that we extend what is allowed in `-*Action` common parameters, such that a `ScriptBlock` can be passed into those parameters. Further, I also propose 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 -$messageLog = [System.Collections.ArrayList]::new() - -function Write-MessageLog { - [CmdletBinding(DefaultParameterSetName='ErrorRecord')] - param( - [Parameter(Position=0, Mandatory=$true, ParameterSetName='ErrorRecord')] - [ValidateNotNull()] - [System.Management.Automation.ErrorRecord] - $ErrorRecord, - - [Parameter(Position=0, Mandatory=$true, ParameterSetName='InformationRecord')] - [ValidateNotNull()] - [System.Management.Automation.InformationRecord] - $InformationRecord - ) - $now = [System.DateTime]::UtcNow - if ($PSCmdlet.ParameterSetName -eq 'ErrorRecord') { - # Record the error however you would record it in a log file or database here - $message = $ErrorRecord | Out-String - } else { - # Record the information record however you record it in a log file or database here - $message = $InformationRecord.Message - } - $messageLog.Add([pscustomobject]@{ - Timestamp = $now - Message = $message - }) -} - -Set-StrictMode -Version Latest -$sb = { - [CmdletBinding()] - param([int]$Id = $PID) - Write-Verbose -Verbose -Message "Looking for process with ID ${Id}..." - $process = Get-Process -Id $Id -ErrorAction {WriteInternalErrorLog $_; [ActionPreference]::Ignore} - 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." - } -} - -# Run the script, recording all non-terminating errors that are not internally silenced -# or ignored in the error log, output them on the screen, and store them in $error -& $sb -Id 12345678 -ErrorAction {Write-MessageLog $_; [ActionPreference]]::Continue} - -# Run the script again, recording all messages, including verbose and debug, as well as -# any terminating error that occurs in the message log without showing them on screen. -# Errors will be stored in $error. -$ErrorActionPreference = $WarningPreference = $VerbosePreference = $DebugPreference = { - Write-MessageLog $_ - [ActionPreference]::SilentlyContinue -} -try { - & $sb -} catch { - Write-MessageLog $_ - throw -} -``` - -In the case of the first example, the message log will contain the first verbose message and the warning message, and the internal error message log (that may be from a module) will contain the internal errors that were silenced. - -In the case of the second example, the message log will contain both verbose messages. - -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 - -### Make the `ScriptBlock` an `EventHandler` - -The `ScriptBlock` implementation looks like event handlers, so an alternative approach would be to define a specific event handler type and having the `ScriptBlock` design conform to that event handler. For example, in PowerShell we could define a `StreamedMessageEventArgs` class that has a `Action` property of type `ActionPreference`, and require that the `ScriptBlock` take parameters `($MessageRecord, $EventArgs)`, where `$MessageRecord` is the message that was raised, and `$EventArgs` is an instance of `StreamedMessageEventArgs` used to define the `ActionPreference` to take once the message is processed. For this approach, `$_` would still be assigned to the message record to allow the `ScriptBlock` logic to remain as simple as possible. Scripters would need to assign a value to `$EventArgs.Action` in the `ScriptBlock` in order to change the default behavior (it would be assigned to the default behavior for the corresponding message type by default). - -The benefits of this alternative are as follows: - - * The `ScriptBlock` return `ActionPreference` 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 still leave the param block out and not bother with updating `$EventArgs.Action` in the `ScriptBlock`, keeping their code simple. - * There can only be one handler for each type of message at a time, so even using an event handler definition, scripters wouldn't have to worry about adding or removing event handlers. They just need to assign the `ScriptBlock` to the parameter or variable they want, and PowerShell will typecast it as an event handler appropriately. - -The downsides to this approach are as follows: - -* Scripters need to explicitly define params and work with those parameters in the `ScriptBlock` if they want to change the default `ActionPreference`, which may be a little more complicated than simply working with letting an `ActionPreference` enumeration value (which could even be a string) be returned from the `ScriptBlock`. - -### Add `-VerboseAction`, `-DebugAction` and `-ProgressAction` common parameters - -It is important to consider RFCNNNN-Propagate-Execution-Preferences-Beyond-Module-Scope here because it uses common parameters to pass execution preferences to other modules and/or scripts. In order for that to work properly for all message types, such that `ScriptBlock` action preferences for verbose, debug, and progress messages also propagate beyond module/script scope, we would need to add `-VerboseAction`, `-DebugAction`, and `-ProgressAction` to the common parameter lists. The implementation of these would simply be the same as `-WarningAction` or `-InformationAction`, but for their respective streams. - -The benefits of having these additional common parameters are as follows: - -* Users are provided a consistent mechanism for dealing with non-terminating messages of any type. -* Scripters can run scripts that leverage progress messages heavily unattended with logging so that users can see how far the script has made it after the fact, or they can silence progress messages since they are running scripts unattended and the display processing is not necessary. -* Tool makers and advanced scripters can display or log messages of any type however they need. -* With RFCNNNN-Propagate-Execution-Preferences-Beyond-Module-Scope implemented, even verbose, debug and progress `ActionPreference` values or `ScriptBlock` message handlers can propagate beyond the scope of modules or scripts, allowing them to function more like cmdlets do. - -The downsides to these common parameters are as follows: - -* We already have `-Verbose` and `-Debug` common parameters, so there is some overlap; however, the PowerShell engine would raise an error if both `-Verbose` and `-VerboseAction` were used in an invocation, or `-Debug` and `-DebugAction` were used in an invocation, so there would be no conflict in invocation. Scripters would simply choose one or the other. From 7fa90c370f7bac6779306911a90c1ab260a44f5f Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Fri, 16 Aug 2019 17:01:48 -0300 Subject: [PATCH 13/15] move optional feature RFC into PR #220 --- 1-Draft/RFCNNNN-Optional-Features.md | 256 --------------------------- 1 file changed, 256 deletions(-) delete mode 100644 1-Draft/RFCNNNN-Optional-Features.md diff --git a/1-Draft/RFCNNNN-Optional-Features.md b/1-Draft/RFCNNNN-Optional-Features.md deleted file mode 100644 index 5f8361bd..00000000 --- a/1-Draft/RFCNNNN-Optional-Features.md +++ /dev/null @@ -1,256 +0,0 @@ ---- -RFC: RFCnnnn -Author: Kirk Munro -Status: Draft -SupercededBy: -Version: 1.0 -Area: Engine -Comments Due: July 15, 2019 -Plan to implement: Yes ---- - -# Optional features in PowerShell - -There are several important issues in the PowerShell language that cannot be corrected without introducing breaking changes. At the same time, the number of breaking changes introduced in a new version of PowerShell needs to be as minimal as possible, so that there is a low barrier to adoption of new versions, allowing community members to transition scripts and modules across versions more easily. Given that those two statements are in conflict with one another, we need to consider how we can optionally introduce breaking changes into PowerShell over time. - -PowerShell has support for experimental features, which some may think covers this need; however, the intent of experimental features is to allow the community to try pre-release versions of PowerShell with breaking changes that are deemed necessary or new features that are not necessarily fully tested/polished so that they can more accurately assess the impact of those features. For release versions of PowerShell, an experimental feature has one of three possible outcomes: - -1. The the experimental feature is deemed necessary, adequately tested/polished and accepted by the community as not harmful to adoption of new versions, in which case the experimental feature is no longer marked as experimental. -1. The experimental feature is deemed necessary, and adequately tested/polished, but considered harmful to adoption of new versions, in which case the experimental feature is changed to an optional feature. -1. The experimental feature is deemed not useful enough, in which case the experimental feature is deprecated. - -In some cases a breaking change may be implemented immediately as an optional feature, when it is known up front that such a breaking change would be considered harmful to adoption of new versions of PowerShell if it was in place by default yet is still found important enough to implement as an optional feature. - -Given all of that, we need to add support for optional features in PowerShell so that what is described above becomes a reality. - -As an example of a feature that will be optional if implemented, see RFCNNNN-Propagate-Execution-Preferences-Beyond-Module-Scope or RFCNNNN-Make-Terminating-Errors-Terminate. - -## Motivation - -As a script, function, or module author, -I can enable optional features for specific users or in specific scopes, -so that I can leverage new functionality that may break existing scripts without risk. - -## User experience - -```powershell -# Create a module manifest, specifically enabling or disabling one or more optional features in the manifest -New-ModuleManifest -Path ./test.psd1 -OptionalFeatures @('OptionalFeature1',@{Name='OptionalFeature2';Enabled=$false}) -PassThru | Get-Content - -# Output: -# -# @{ -# -# -# -# # Optional features enabled or disabled in this module. -# OptionalFeatures = @( -# 'OptionalFeature1' -# @{Name='OptionalFeature2';Enabled=$false} -# ) -# -# } - -# Create a script file, enabling or disabling one or more optional features in the file -@' -#requires -OptionalFeature OptionalFeature1 -#requires -OptionalFeature OptionalFeature2 -Disabled - - -'@ | Out-File -FilePath ./test.ps1 - -# Get the current optional feature configuration for the current user and all users -Get-OptionalFeatureConfiguration - -# Output: -# -# For: AllUsers -# -# Name Session NewManifest -# ---- ------ ----------- -# OptionalFeature1 False True -# OptionalFeature2 True False -# OptionalFeature4 True True -# OptionalFeature5 False False -# -# -# For: CurrentUser -# -# Name Session NewManifest -# ---- ------ ----------- -# OptionalFeature2 False True -# OptionalFeature3 False True -# - -# Get a list of optional features, their source, and their descriptions -Get-OptionalFeature - -# Output: -# -# Name Source Description -# ---- ------ ----------- -# OptionalFeature1 PSEngine Description of optional feature 1 -# OptionalFeature2 PSEngine Description of optional feature 2 -# OptionalFeature3 PSEngine Description of optional feature 3 -# OptionalFeature4 PSEngine Description of optional feature 4 - -# Enable an optional feature in current and future PowerShell sessions for all -# users in PowerShell. -Enable-OptionalFeature -Name OptionalFeature1 -For AllUsers - -# Output: -# None - -# Disable an optional feature in current and future PowerShell sessions for the -# current user in PowerShell. -Disable-OptionalFeature -Name OptionalFeature1 -For CurrentUser - -# Output: -# None, unless the feature was explicitly enabled for all users and is being -# disabled only for the current user as an override, as is the case here, -# in which case they get prompted to confirm. This is described below. - -# Enable an optional feature in all new module manifests created with -# New-ModuleManifest in the current and future PowerShell sessions for any user -# in PowerShell. -Enable-OptionalFeature -Name OptionalFeature2 -For AllUsers -InNewModuleManifests - -# Output: -# None - -# Enable an optional feature in all new module manifests created with -# New-ModuleManifest in the current and future PowerShell sessions for the -# current user in PowerShell. -Disable-OptionalFeature -Name OptionalFeature3 -InNewModuleManifests - -# Output: -# None - -# Enable and disable an optional feature the duration of the script block being -# invoked. -Use-OptionalFeature -Enable OptionalFeature1 -Disable OptionalFeature2 -ScriptBlock { - # Do things using OptionalFeature1 here - # OptionalFeature2 cannot be used here -} -# If OptionalFeature1 was not enabled before this invocation, it is still no -# longerenabled here. If OptionalFeature2 was enabled before this invocation, -# it is still enabled here. All to say, their state before the call is -# preserved. -``` - -## Specification - -Unlike experimental features, which can only be enabled or disabled in PowerShell sessions created after enabling or disabling them, optional features can be enabled or disabled in the current PowerShell session as well as in future PowerShell sessions. This is necessary to allow certain functionality to be "lit up" in packaged modules or scripts. - -Below you will find details describing how this functionality will be implemented. - -### System and User powershell.config.json configuration files - -Enabling optional features automatically in future PowerShell sessions requires creating or updating one of two `powershell.config.json` configuration files that are read on startup of a new PowerShell session: - -* one in `$PSHOME`, which applies to all user sessions -* one in `$HOME\Documents\PowerShell\powershell.config.json` on Windows or `$HOME/.config/powershell/powershell.config.json` on Linux and macOS, which applies only to current user sessions. - -This RFC will enable optional feature defaults to be read from these configuration files, with current user configuration taking precedence over system (all users) configuration. System config is not policy so this should be acceptable and expected. - -### Add parameter to New-ModuleManifest - -`[-OptionalFeatures ]` - -This parameter would configure specific optional features in the new module manifest that is generated. - -The values provided to this parameter would be combined with optional features that are enabled or disabled by default according to the session configuration files, with the values specified in the `New-ModuleManifest` command overriding the settings for optional features with the same name that are configured in the configuration files. Entries in this collection would either be string (the name of the optional feature to enable) or a hashtable with two keys: `name` (a string) and `enabled` (a boolean value). The hashtable allows an optional feature to be specifically disabled instead of enabled within a module, which is necessary if an older module does not support a newer optional feature yet. - -A terminating error is generated if the same optional feature name is used twice in the collection passed into the `-OptionalFeatures` parameter. - -### Add parameter set to #requires statement - -`#requires -OptionalFeatures [-Disabled]` - -This parameter set would enable, or disable if `-Disabled` is used, optional features identified by `-Name` in the current script file. - -### New command: Get-OptionalFeatureConfiguration - -```none -Get-OptionalFeatureConfiguration [[-Name] ] [-For { CurrentUser | AllUsers }] [] -``` - -This command would return the current configuration of optional features that are available in PowerShell, read from the configuration files. - -The properties on the `S.M.A.OptionalFeatureConfiguration` object would be `Name`, `Session`, `NewManifest`, and `For`, defined as follows: - -|Property Name|Description| -|--|--| -|`Name`|A string value that identifies the optional feature name| -|`Session`|A boolean value that identifies whether the optional feature is enabled or disabled in the current and new PowerShell sessions| -|`NewManifest`|A boolean value that identifies whether the optional feature is enabled or disabled in manifests created by new module manifests in the current and new PowerShell sessions| -|`For`|An enumeration flag identifying whether the optional feature configuration was set up for the `CurrentUser` or `AllUsers`| - -The default output format is of type table with the properties `Name`, `Session`, and `NewManifest` with the results grouped by `For`. - -When this command is invoked with the `-For` parameter, the results are automatically filtered for that configuration file. The default value for `-For` is both flags, showing configuration values from both configuration files. - -### New command: Get-OptionalFeature - -```none -Get-OptionalFeature [[-Name] ] [] -``` - -This command will return a list of the optional features that are available in PowerShell, along with their source and description. - -The properties on the `S.M.A.OptionalFeature` object would be `Name`, `Source`, `Description`, defined as follows: - -|Property Name|Description| -|--|--| -|`Name`|A string value that identifies the optional feature name| -|`Source`|A string value that identifies the area of PowerShell that is affected by this optional feature| -|`Description`|A string value that describes the optional feature| - -The default output format would be of type table with the properties `Name`, `Source`, and `Description`. - -### Enabling and disabling optional features in current and future PowerShell sessions - -```none -Enable-OptionalFeature [-Name] [-For { CurrentUser | AllUsers }] [-InNewModuleManifests] [-WhatIf] [-Confirm] [] - -Disable-OptionalFeature [-Name] [-For { CurrentUser | AllUsers }] [-InNewModuleManifests] [-WhatIf] [-Confirm] [] -``` - -It is important to note up front that there are three default states for an optional feature: enabled by default, implicitly disabled by default, and explicitly disabled by default. The only time an optional feature needs to be explicitly disabled by default is if it is enabled by default in the AllUsers configuration file and a specific user wants to disable it for their sessions. This impacts how `Disable-OptionalFeature` works. - -The `Enable-OptionalFeature` command will enable an optional feature in current and future PowerShell sessions either globally (if the `-InNewModuleManifests` switch is not used) or only in manifests created by `New-ModuleManifest`. - -The `Disable-OptionalFeature` command will disable an optional feature in current and future PowerShell sessions either globally (if the `-InNewModuleManifests` switch is not used) or only in manifests created by `New-ModuleManifest`. If the feature is being disabled for `AllUsers` and the optional feature is completely disabled in that configuration file as a result of this command, the entry is removed from the configuration file. If the feature is being disabled for `AllUsers` and there is no entry in the system (all users) configuration file, nothing happens. If the feature is being disabled for the `CurrentUser` there is no entry in the system (all users) or current user configuration files, nothing happens. If the feature is being disabled for the `CurrentUser` and the optional feature is enabled in the `AllUsers` configuration file, users will be informed that this feature is enabled for all users and asked to confirm that they want to explicitly disable this feature for the current user in the current and future PowerShell sessions. They can always re-enable it later. - -### New command: Use-OptionalFeature - -```none -Use-OptionalFeature [[-Enable] ] [[-Disable] ] [-ScriptBlock] [-Confirm] [] -``` - -This command would enable or disable the optional features whose names are identified in the `-Enable` and `-Disable` parameters for the duration of the `ScriptBlock` identified in the `-ScriptBlock` parameter, and return the features to their previous state afterwards. This allows for easy use of optional features over a small section of code. If neither `-Enable` or `-Disable` are used, a terminating error is thrown. - -### Checking optional feature states within the PowerShell runtime - -Optional features can be enabled or disabled in a session, module, script, or script block. Since enabling or disabling an optional feature can happen at different levels, the current state of an optional feature should be maintained in a stack, where the validation logic simply peeks at the top of the stack to see if the feature is enabled or not, popping the top of the stack off when appropriate (when leaving the scope of the module, script, or script block where the feature is enabled). - -## Alternate proposals and considerations - -### Extend experimental features to support the enhancements defined in this RFC - -At a glance, experimental features and optional features are very similar to one another, so it was proposed that it may make sense to have them both use the same functionality when it comes to enabling/disabling them in scripts and modules; however, experimental features have a completely different intent (to try out new functionality in a PowerShell session), are only for future PowerShell sessions, and they only have a single on/off state. On the other hand, optional features are for the current and future PowerShell sessions, for the current user or all users, and may be enabled or disabled in various scopes within those sessions. For that reason, this approach doesn't seem like a viable solution. If we want to change that, perhaps someone should file an RFC against experimental features to make that change. - -### Enable/disable optional features in jobs according to their current state when the job is launched - -Jobs run in the background have their own session, and therefore will not automatically have optional features enabled or disabled according to the current state when the job is launched. We should consider updating how jobs are launched in the engine such that they do "inherit" optional feature configuration to allow them to use optional features without additional code. - -You might think that you can accomplish this using `#requires` in a script block launched as a job, but that doesn't work -- the `#requires` statement is ignored when you do this at the moment. For example, the see the results of the following script when launched from PowerShell Core: - -```PowerShell -Start-Job { - #requires -PSEdition Desktop - $PSVersionTable -} | Receive-Job -Wait -``` - -The result of that command shows that the version of PowerShell where the job was run was Core, not Desktop, yet the job ran anyway despite the `#requires` statement. This may be a bug. If it is, and if that bug is corrected, then you could use #requires to enable/disable features, but regardless it would still be preferable (and more intuitive) for jobs to "inherit" the current optional feature configuration when they are invoked. This includes jobs launched with `Start-Job`, `Start-ThreadJob`, the `&` background operator, parallelized `foreach` statements or `ForEach-Object` commands, or the generic `-AsJob` parameter. From af677343fc25bad3e1661db2e811c2cfcb2fbba3 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Fri, 16 Aug 2019 17:29:25 -0300 Subject: [PATCH 14/15] move propagate exec prefs RFC into PR #221 --- ...ecution-Preferences-Beyond-Module-Scope.md | 135 ------------------ 1 file changed, 135 deletions(-) delete mode 100644 1-Draft/RFCNNNN-Propagate-Execution-Preferences-Beyond-Module-Scope.md diff --git a/1-Draft/RFCNNNN-Propagate-Execution-Preferences-Beyond-Module-Scope.md b/1-Draft/RFCNNNN-Propagate-Execution-Preferences-Beyond-Module-Scope.md deleted file mode 100644 index ad1b7256..00000000 --- a/1-Draft/RFCNNNN-Propagate-Execution-Preferences-Beyond-Module-Scope.md +++ /dev/null @@ -1,135 +0,0 @@ ---- -RFC: RFCnnnn -Author: Kirk Munro -Status: Draft -SupercededBy: -Version: 1.0 -Area: Engine -Comments Due: July 15, 2019 -Plan to implement: Yes ---- - -# Execution preferences must persist beyond module or script scope - -PowerShell has a long-standing issue where execution preferences such as those defined by the `-ErrorAction`, `-WarningAction`, `-InformationAction`, `-Debug`, `-Verbose`, `-WhatIf` and `-Confirm` common parameters, or those defined by any of the `$*Preference` variables, do not persist from a module to another module or script, nor do they persist from a script to another module or script. This is a result of how modules and scripts are scoped within PowerShell. It impacts modules written by Microsoft as well as scripts and modules written by the community, and it is often identified as a bug when it shows up in various places. You can see some of the discussion around this in [PowerShell Issue #4568](https://github.com/PowerShell/PowerShell/issues/4568). - -Regardless of whether you are authoring a script, an advanced function, or a cmdlet, you should be able to do so knowing that all execution preferences will be carried through the entire invocation of a command, end to end, regardless of the implementation details of that command. Cmdlets already work this way today. You can invoke cmdlets within a script, or within an advanced function defined in a module, and the execution preferences used during the invocation of that script or advanced function will be respected by the cmdlets that are invoked. This RFC is about making it possible for scripts or advanced functions to work that way as well. - -It is important to note that the only way to implement this feature such that it is not a breaking change is by making it optional; however, even with it optional, it can be enabled by default for new modules to correct this problem going forward, and since it is optional, existing scripts and modules could be updated to support it as well, when they are ready to take on the responsibility of testing that out. That would allow this feature to be adopted more easily for new modules, while existing modules could be updated over time. While we have experimental feature support, those are for a different purpose so an additional RFC is being published at the same time as this RFC to add support for optional feature definition in PowerShell (see RFCNNNN-Optional-Features.md). - -## Motivation - -As a scripter or a command author, -I should never have to care what type of command (cmdlet vs advanced function) I am invoking, -so that I can can focus on my script without having to worry about the implementation details of commands I use. - -As a command author, -I should be able to change a published command from an advanced function to a cmdlet or vice versa, -so that as long as I keep the same functionality in the command, scripts and modules where that command is in use should behave the same way. - -## User experience - -```powershell -# First, create a folder for a new module -$moduleName = 'RFCPropagateExecPrefDemo' -$modulePath = Join-Path -Path $([Environment]::GetFolderPath('MyDocuments')) -ChildPath PowerShell/Modules/${moduleName} -New-Item -Path $modulePath -ItemType Directory -Force > $null - -# Then, create the manifest (which would have the PersistCommandExecutionPreferences -# optional feature enabled by default, to correct the behaviour moving forward in a -# non-breaking way; downlevel versions of PowerShell would ignore the optional feature -# flags) -$nmmParameters = @{ - Path = "${modulePath}/${moduleName}.psd1" - RootModule = "./${moduleName}.psm1" - FunctionsToExport = @('Test-1') - PassThru = $true -} -New-ModuleManifest @nmmParameters | Get-Content - -# Output: -# -# @{ -# -# RootModule = './RFCPropagateExecPrefDemo.psm1' -# -# # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. -# PrivateData = @{ -# -# -# -# PSData = @{ -# -# # Optional features enabled in this module. -# OptionalFeatures = @( -# 'PersistCommandExecutionPreferences' -# ) -# -# -# -# } # End of PSData hashtable -# -# -# -# } # End of PrivateData hashtable -# -# } - -# Then, create the script module file, along with a second module in memory that it invokes, -# and import both modules -$scriptModulePath = Join-Path -Path $modulePath -ChildPath ${moduleName}.psm1 -New-Item -Path $scriptModulePath -ItemType File | Set-Content -Encoding UTF8 -Value @' - function Test-1 { - [cmdletBinding()] - param() - Test-2 - } -'@ -Import-Module $moduleName -New-Module Name test2 { - function Test-2 { - [cmdletBinding()] - param() - Write-Verbose 'Verbose output' - } -} | Import-Module - -# When invoking the Test-2 command with -Verbose, it shows verbose output, as expected -Test-2 -Verbose - -# Output: -# -# VERBOSE: Verbose output - -# Thanks to this feature, when invoking the Test-1 command with -Verbose, it also shows -# verbose output. In PowerShell 6.2 and earlier, no verbose output would appear as a -# result of this command due to the issue preventing execution preferences from propagating -# beyond module/script scope -Test-1 -Verbose - -# Output: -# -# VERBOSE: Verbose output -``` - -## Specification - -To resolve this problem, a new optional feature called `PersistCommandExecutionPreferences` would be defined in PowerShell. When this feature is enabled in a script or module, it would change how common parameters work in that script or module. - -Today if you invoke a script or advanced function with `-ErrorAction`, `-WarningAction`, `-InformationAction`, `-Debug`, `-Verbose`, `-WhatIf`, or `-Confirm`, the corresponding `$*Preference` variable will be set within the scope of that command. That behaviour will remain the same; however when the optional feature is enabled, in addition to that behaviour, the names and values you supplied to those common parameters stored in a new `ExecutionPreferences` dictionary property of the `$PSCmdlet` instance. Once `$PSCmdlet.ExecutionPreferences` is set, any common parameters that are stored in `$PSCmdlet.ExecutionPreferences` that are not explicitly used in the invocation of another command within that command scope will be automatically passed through if the command being invoked supports common parameters. - -It is important to note that parameter/value pairs in `$PSCmdlet.ExecutionPreferences`, which represent command execution preferences, would take priority and be applied to a command invocation before values in `$PSDefaultParameterValues`, which represents user/module author parameter preferences (i.e. if both dictionaries have a value to be applied to a common parameter, only the value in `$PSCmdlet.ExecutionPreferences` would be applied). - -As per the optional feature specification, the optional feature can be enabled in a module manifest (see example above), or a script file via `#requires`. For more details on how that works, see RFCNNNN-Optional-Features. - -## Alternate proposals and considerations - -### Rip off the bandaid - -[Some members of the community](https://github.com/PowerShell/PowerShell/issues/6745#issuecomment-499740080) feel it would better to break compatibility here. On the plus side, not having to deal with this an an optional parameter would be ideal; however, to increase adoption of PowerShell 7, it would be better to make the transition from PowerShell 5.1 into 7 easier by having as few breaking changes as possible. - -One way to achieve this while supporting users who don't want the breaking change would be to inverse the optional feature, where the breaking change is in place and users opt out of the breaking change instead of opting into it. Another way would be to change the optional feature design such that users can turn them off in scripts/modules if those scripts/modules are not ready to use the breaking change. See the alternate proposals and considerations section of RFCNNNN-Optional-Features for more information. - -### Support `-DebugAction`, `-VerboseAction`, and `-ProgressAction` if those common parameters are added - -RFCNNNN-ScriptBlock-Action-Preference suggests that we consider adding `-DebugAction`, `-VerboseAction`, and `-ProgressAction` common parameters. These are important to consider adding, because beyond the `-Debug` and `-Verbose` switch common parameters (which only support `ActionPreference.Continue`), the new common parameters would be the only way to propagate execution preferences for debug, verbose, and progress messages to all commands that are invoked. See RFCNNNN-ScriptBlock-Action-Preference for more details. From 1afe4a1d2b3fc081a1f1e5dd3f99167257b7a7d2 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Fri, 16 Aug 2019 17:46:30 -0300 Subject: [PATCH 15/15] update text to link separate PRs --- ...minating-Errors-Terminate-The-Right-Way.md | 151 +++++++++++++----- 1 file changed, 112 insertions(+), 39 deletions(-) diff --git a/1-Draft/RFCNNNN-Make-Terminating-Errors-Terminate-The-Right-Way.md b/1-Draft/RFCNNNN-Make-Terminating-Errors-Terminate-The-Right-Way.md index b78ed3fe..aa4421c4 100644 --- a/1-Draft/RFCNNNN-Make-Terminating-Errors-Terminate-The-Right-Way.md +++ b/1-Draft/RFCNNNN-Make-Terminating-Errors-Terminate-The-Right-Way.md @@ -3,15 +3,17 @@ RFC: RFCnnnn Author: Kirk Munro Status: Draft SupercededBy: -Version: 1.0 +Version: 0.1 Area: Engine -Comments Due: July 15, 2019 +Comments Due: September 15, 2019 Plan to implement: Yes --- # Make terminating errors terminate the right way in PowerShell -By default in PowerShell, terminating errors do not actually terminate. For example, if you invoke this command in global scope, you will see the output "Why?" after the terminating error caused by the previous command: +By default in PowerShell, terminating errors do not actually terminate. For +example, if you invoke this command in global scope, you will see the output +"Why?" after the terminating error caused by the previous command: ```PowerShell & { @@ -21,7 +23,9 @@ By default in PowerShell, terminating errors do not actually terminate. For exam } ``` -PowerShell has upheld this behaviour since version 1.0 of the language. You can make the terminating error actually terminate execution of the command, by wrapping the command in try/catch, like this: +PowerShell has upheld this behaviour since version 1.0 of the language. You can +make the terminating error actually terminate execution of the command by +wrapping the command in try/catch, like this: ```PowerShell try { @@ -33,7 +37,8 @@ try { } ``` -You can also convert the terminating-yet-handled-as-a-non-terminating error into an actual terminating error like this: +You can also convert the terminating-yet-handled-as-a-non-terminating error +into an actual terminating error like this: ```PowerShell & { @@ -44,13 +49,27 @@ You can also convert the terminating-yet-handled-as-a-non-terminating error into } ``` -In those last two examples, the exception raised by the .NET method terminates execution of the running command. +In those last two examples, the exception raised by the .NET method terminates +execution of the running command. -The difference between the first example and the workarounds poses a risk to scripters who share scripts or modules with the community. +The difference between the first example and the workarounds poses a risk to +scripters who share scripts or modules with the community. -In the first workaround, the risk is that end users using a shared resource such as a script or module may see different behaviour from the logic within that module depending on whether or not they were inside of a `try` block when they invoked the script or a command exported by the module. That risk is very undesirable, and as a result many community members who share scripts/modules with the community wrap their logic in a `try/catch{throw}` (or similar) scaffolding to ensure that the behavior of their code is consistent no matter where or how it was invoked. +In the first workaround, the risk is that end users using a shared resource +such as a script or module may see different behaviour from the logic within +that module depending on whether or not they were inside of a `try` block when +they invoked the script or a command exported by the module. That risk is very +undesirable, and as a result many community members who share scripts/modules +with the community wrap their logic in a `try/catch{throw}` (or similar) +scaffolding to ensure that the behavior of their code is consistent no matter +where or how it was invoked. -In the second workaround, if the shared script does not use `$ErrorActionPreference = 'Stop'`, a caller can get different behaviour by manipulating their `$ErrorActionPreference`. The caller should not be able to manipulate terminating error behavior in commands that they invoke -- that's up to the command author, and they shouldn't have to use extra handling to make terminating errors terminate. +In the second workaround, if the shared script does not use +`$ErrorActionPreference = 'Stop'`, a caller can get different behaviour by +manipulating their `$ErrorActionPreference`. The caller should not be able to +manipulate terminating error behavior in commands that they invoke -- that's up +to the command author, and they shouldn't have to use extra handling to make +terminating errors terminate. Now consider this code snippet: @@ -71,14 +90,30 @@ New-Module -Name ThisShouldNotImport { There are several things wrong with this snippet: -1. If you invoke that snippet, the `ThisShouldNotImport` module imports successfully because the terminating error (`[System.Collections.Generics.List[string]]` is not a valid type name) does not actually terminate the loading of the module. This could cause your module to load in an unexpected state, which is a bad idea. -1. If you loaded your module by invoking a command defined with that module, you won't see the terminating error that was raised during the loading of the module (the terminating error that was raised during the loading of the module is not shown at all in that scenario!), so you could end up with some undesirable behaviour when that command executes even though the loading of the module generated a "terminating" error, and not have a clue why. -1. The Test-RFC command exported by this module produces a terminating error, yet continues to execute after that error. -1. If the caller either loads your module or invokes your command inside of a `try` block, they will see different behaviour. - -Any execution of code beyond a terminating error should be intentional, not accidental like it is in both of these cases, and it most certainly should not be influenced by whether or not the caller loaded the module or invoked the command inside of a `try` block. Binary modules do not behave this way. Why should script modules be any different? - -Now have a look at the same module definition, this time with some extra scaffolding in place to make sure that terminating errors actually terminate: +1. If you invoke that snippet, the `ThisShouldNotImport` module imports +successfully because the terminating error +(`[System.Collections.Generics.List[string]]` is not a valid type name) does +not actually terminate the loading of the module. This could cause your module +to load in an unexpected state, which is a bad idea. +1. If you loaded your module by invoking a command defined with that module, +you won't see the terminating error that was raised during the loading of the +module (the terminating error that was raised during the loading of the module +is not shown at all in that scenario!), so you could end up with some +undesirable behaviour when that command executes even though the loading of the +module generated a "terminating" error, and not have a clue why. +1. The Test-RFC command exported by this module produces a terminating error, +yet continues to execute after that error. +1. If the caller either loads your module or invokes your command inside of a +`try` block, they will see different behaviour. + +Any execution of code beyond a terminating error should be intentional, not +accidental like it is in both of these cases, and it most certainly should not +be influenced by whether or not the caller loaded the module or invoked the +command inside of a `try` block. Binary modules do not behave this way. Why +should script modules be any different? + +Now have a look at the same module definition, this time with some extra +scaffolding in place to make sure that terminating errors actually terminate: ```PowerShell New-Module -Name ThisShouldNotImport { @@ -102,25 +137,57 @@ New-Module -Name ThisShouldNotImport { } | Import-Module ``` -With this definition, if the script module generates a terminating error, the module will properly fail to load (note, however, that the type name has been corrected in case you want to try this out). Further, if the command encounters a terminating error, it will properly terminate execution and the error returned to the caller will properly indicate that the `Test-RFC` command encountered an error. This scaffolding is so helpful that members of the community apply it to every module and every function they define within that module, just to get things to work the right way in PowerShell. - -All of this is simply absurd. Any script module that generates a terminating error in the module body should fail to import without extra effort, with an appropriate error message indicating why it did not import. Any advanced function defined within a script module that encounters a terminating error should terminate gracefully, such that the error message indicates which function the error came from, without requiring extra scaffolding code to make it work that way. - -Between the issues identified above, and the workarounds that include anti-patterns (naked `try/catch` blocks and `trap{break}` statements are anti-patterns), the PowerShell community is clearly in need of a solution that automatically resolves these issues in a non-breaking way. +With this definition, if the script module generates a terminating error, the +module will properly fail to load (note, however, that the mispelled type name +has been corrected in case you want to try this out). Further, if the command +encounters a terminating error, it will properly terminate execution and the +error returned to the caller will properly indicate that the `Test-RFC` +command encountered an error. This scaffolding is so helpful that members of +the community apply it to every module and every begin, process and end block +within every function they define within that module, just to get things to +work the right way in PowerShell. + +All of this is simply absurd. + +Any script module that generates a terminating error in the module body should +fail to import without extra effort, with an appropriate error message +indicating why it did not import. + +Any advanced function defined within a script module that encounters a +terminating error should terminate gracefully, such that the error message +indicates which function the error came from, while respecting the callers +`-ErrorAction` preference, without requiring extra scaffolding code to make it +work that way. + +Between the issues identified above, and the workarounds that include +anti-patterns (naked `try/catch` blocks and `trap{break}` statements are +anti-patterns), the PowerShell community is clearly in need of a solution that +automatically resolves these issues in a non-breaking way. ## Motivation -As a script, function, or module author, -I can write scripts with confidence knowing that terminating errors will terminate those commands the right way, without needing to add any scaffolding to correct inappropriate behaviours in PowerShell +As a script, function, or module author,
+I can write scripts with confidence knowing that terminating errors will +terminate those commands the right way, without needing to add any scaffolding +to correct inappropriate behaviours in PowerShell
so that I can keep my logic focused on the work that needs to be done. ## User experience -The way forward for this issue is to add an optional feature (see: RFCNNNN-OptionalFeatures) that makes terminating errors terminate correctly. The script below demonstrates that a manifest can be generated with the `ImplicitTerminatingErrorHandling` optional feature enabled, and with that enabled the module author can write the script module and the advanced functions in that module knowing that terminating errors will be handled properly. No scaffolding is required once the optional feature is enabled, because it will correct the issues that need correcting to make this just work the right way, transparently. +The way forward for this issue is to add an optional feature (see: #220) that +makes terminating errors terminate correctly. The script below demonstrates +that a manifest can be generated with the `ImplicitTerminatingErrorHandling` +optional feature enabled, and with that enabled the module author can write the +script module and the advanced functions in that module knowing that +terminating errors will be handled properly. No scaffolding is required once +the optional feature is enabled, because it will correct the issues that need +correcting to make this just work the right way, transparently. ```powershell $moduleName = 'ModuleWithBetterErrorHandling' -$modulePath = Join-Path -Path $([Environment]::GetFolderPath('MyDocuments')) -ChildPath PowerShell/Modules/${moduleName} +$modulePath = Join-Path ` + -Path $([Environment]::GetFolderPath('MyDocuments')) ` + -ChildPath PowerShell/Modules/${moduleName} New-Item -Path $modulePath -ItemType Directory -Force > $null $nmmParameters = @{ Path = "${modulePath}/${moduleName}.psd1" @@ -129,7 +196,8 @@ $nmmParameters = @{ } # -# Create the module manifest, enabling the optional ImplicitTerminatingErrorHandling feature in the module it loads +# Create the module manifest, enabling the optional ImplicitTerminatingErrorHandling +# feature in the module it loads # New-ModuleManifest @nmmParameters -OptionalFeatures ImplicitTerminatingErrorHandling @@ -152,13 +220,15 @@ New-Item -Path $scriptModulePath -ItemType File | Set-Content -Encoding UTF8 -Va '@ ``` -Module authors who want this behaviour in every module they create can invoke the following command to make it default for module manifests created with `New-ModuleManifest`. +Module authors who want this behaviour in every module they create can use +`$PSDefaultParameterValues` to add the optional feature to the +`-OptionalFeatures` parameter of `New-ModuleManifest` by default. ```PowerShell -Enable-OptionalFeature -Name ImplicitTerminatingErrorHandling -NewModuleManifests +$PSDefaultParameterValues['New-ModuleManifest:OptionalFeatures'] = 'ImplicitTerminatingErrorHandling' ``` -Scripters wanting the behaviour in their scripts can use the #requires statement: +Scripters wanting the behaviour in their scripts can use #requires: ```PowerShell #requires -OptionalFeatures ImplicitTerminatingErrorHandling @@ -170,25 +240,28 @@ Implementation of this RFC would require the following: ### Implementation of optional feature support -See RFCNNNN-Optional-Features for more information. +See #220 for more information. ### Addition of the `ImplicitTerminatingErrorHandling` optional feature definition -This would require adding the feature name and description in the appropriate locations so that the feature can be discovered and enabled. +This would require adding the feature name and description in the appropriate +locations so that the feature can be discovered and enabled. ### PowerShell engine updates The PowerShell engine would have to be updated such that: -* scripts invoked with the optional feature enabled treat terminating errors as terminating -* scripts and functions with `CmdletBinding` attributes when this optional feature is enabled treat terminating errors as terminating and gracefully report errors back to the caller (i.e. these commands should not throw exceptions) +* scripts invoked with the optional feature enabled treat terminating errors as +terminating +* scripts and functions with `CmdletBinding` attributes when this optional +feature is enabled treat terminating errors as terminating and gracefully +report errors back to the caller (i.e. these commands should not throw +exceptions) ## Alternate proposals and considerations -### Make this optional feature on by default for new module manifests - -This feature is so useful that I would recommend it as a best practice. If making it just work this way globally wouldn't incur a breaking change in PowerShell, I would want it to always work that way by default. Since making it work this way globally would incur a breaking change, my recommendation is to make this optional feature on in new module manifests by default so that anyone not wanting it to work this way has to turn the optional feature off. That corrects the behaviour going forward while allowing authors of older modules/scripts can opt-in to the feature when they are ready. - ### Related issue -[PowerShell Issue #9855](https://github.com/PowerShell/PowerShell/issues/9855) is very closely related to this RFC, and it would be worth considering fixing that issue as part of this RFC if it is not already resolved at that time. +[PowerShell Issue #9855](https://github.com/PowerShell/PowerShell/issues/9855) +is very closely related to this RFC, and it would be worth considering fixing +that issue as part of this RFC if it is not already resolved at that time.