Skip to content

Commit 3263667

Browse files
committed
first draft
1 parent 65cdf61 commit 3263667

4 files changed

+609
-0
lines changed
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
---
2+
RFC: RFCnnnn
3+
Author: Kirk Munro
4+
Status: Draft
5+
SupercededBy:
6+
Version: 1.0
7+
Area: Engine
8+
Comments Due: July 15, 2019
9+
Plan to implement: Yes
10+
---
11+
12+
# Make terminating errors terminate the right way in PowerShell
13+
14+
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:
15+
16+
```PowerShell
17+
& {
18+
1/0
19+
'Why?'
20+
}
21+
```
22+
23+
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:
24+
25+
```PowerShell
26+
try {
27+
1/0
28+
'Why?'
29+
} catch {
30+
throw
31+
}
32+
```
33+
34+
In that example, the exception raised by dividing by zero properly terminates execution of the running command.
35+
36+
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.
37+
38+
Now consider this code snippet:
39+
40+
```PowerShell
41+
New-Module -Name ThisShouldNotImport {
42+
$myList = [System.Collections.Generics.List[string]]::new()
43+
44+
function Test-RFC {
45+
[CmdletBinding()]
46+
param()
47+
'Some logic'
48+
1 / 0 # Oops
49+
'Some more logic'
50+
}
51+
} | Import-Module
52+
```
53+
54+
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?
55+
56+
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:
57+
58+
```PowerShell
59+
New-Module -Name ThisShouldNotImport {
60+
trap {
61+
break
62+
}
63+
$myList = [System.Collections.Generic.List[string]]::new()
64+
65+
function Test-RFC {
66+
[CmdletBinding()]
67+
param()
68+
$callerEAP = $ErrorActionPreference
69+
try {
70+
'Some logic'
71+
1 / 0 # Oops
72+
'Some more logic'
73+
} catch {
74+
Write-Error -ErrorRecord $_ -ErrorAction $callerEAP
75+
}
76+
}
77+
} | Import-Module
78+
```
79+
80+
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.
81+
82+
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.
83+
84+
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.
85+
86+
## Motivation
87+
88+
As a script, function, or module author,
89+
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
90+
so that I can keep my logic focused on the work that needs to be done.
91+
92+
## User experience
93+
94+
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.
95+
96+
```powershell
97+
$moduleName = 'ModuleWithBetterErrorHandling'
98+
$modulePath = Join-Path -Path $([Environment]::GetFolderPath('MyDocuments')) -ChildPath PowerShell/Modules/${moduleName}
99+
New-Item -Path $modulePath -ItemType Directory -Force > $null
100+
$nmmParameters = @{
101+
Path = "${modulePath}/${moduleName}.psd1"
102+
RootModule = "./${moduleName}.psm1"
103+
FunctionsToExport = @('Test-ErrorHandling')
104+
}
105+
106+
#
107+
# Create the module manifest, enabling the optional ImplicitTerminatingErrorHandling feature in the module it loads
108+
#
109+
New-ModuleManifest @nmmParameters -OptionalFeatures ImplicitTerminatingErrorHandling
110+
111+
$scriptModulePath = Join-Path -Path $modulePath -ChildPath "${moduleName}.psm1"
112+
New-Item -Path $scriptModulePath -ItemType File | Set-Content -Encoding UTF8 -Value @'
113+
# If the next command is uncommented, Import-Module would fail when trying to load
114+
# this module due to the terminating error actually terminating like it should
115+
# 1/0 # Oops!
116+
function Test-ErrorHandling {
117+
[cmdletBinding()]
118+
param()
119+
'Some logic'
120+
# The next command generates a terminating error, which will be treated as
121+
# terminating and Test-ErrorHandling will fail properly, with the error text
122+
# showing details about the Test-ErrorHandling invocation rather than details
123+
# about the internals of Test-ErrorHandling.
124+
Get-Process -Id 12345678 -ErrorAction Stop
125+
'Some more logic'
126+
}
127+
'@
128+
```
129+
130+
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`.
131+
132+
```PowerShell
133+
Enable-OptionalFeature -Name ImplicitTerminatingErrorHandling -NewModuleManifests
134+
```
135+
136+
Scripters wanting the behaviour in their scripts can use the #requires statement:
137+
138+
```PowerShell
139+
#requires -OptionalFeatures ImplicitTerminatingErrorHandling
140+
```
141+
142+
## Specification
143+
144+
Implementation of this RFC would require the following:
145+
146+
### Implementation of optional feature support
147+
148+
See RFCNNNN-Optional-Features for more information.
149+
150+
### Addition of the `ImplicitTerminatingErrorHandling` optional feature definition
151+
152+
This would require adding the feature name and description in the appropriate locations so that the feature can be discovered and enabled.
153+
154+
### PowerShell engine updates
155+
156+
The PowerShell engine would have to be updated such that:
157+
158+
* scripts invoked with the optional feature enabled treat terminating errors as terminating
159+
* 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)
160+
161+
## Alternate proposals and considerations
162+
163+
### Make this optional feature on by default for new module manifests
164+
165+
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.
166+
167+
### Related issue
168+
169+
[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.

1-Draft/RFCNNNN-Optional-Features.md

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
---
2+
RFC: RFCnnnn
3+
Author: Kirk Munro
4+
Status: Draft
5+
SupercededBy:
6+
Version: 1.0
7+
Area: Engine
8+
Comments Due: July 15, 2019
9+
Plan to implement: Yes
10+
---
11+
12+
# Optional features in PowerShell
13+
14+
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.
15+
16+
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:
17+
18+
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.
19+
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.
20+
1. The breaking change in the experimental feature is deemed not useful enough, in which case the experimental feature is deprecated.
21+
22+
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.
23+
24+
Given all of that, we need to add support for optional features in PowerShell so that what is described above becomes a reality.
25+
26+
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.
27+
28+
## Motivation
29+
30+
As a script, function, or module author,
31+
I can enable optional features in my scripts or modules,
32+
so that I can leverage new functionality that could break existing scripts.
33+
34+
## User experience
35+
36+
```powershell
37+
# Create a module manifest, specifically enabling one or more optional features in the manifest
38+
New-ModuleManifest -Path ./test.psd1 -OptionalFeatures @('OptionalFeature1','OptionalFeature2') -PassThru | Get-Content
39+
40+
# Output:
41+
#
42+
# @{
43+
#
44+
# <snip>
45+
#
46+
# # 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.
47+
# PrivateData = @{
48+
#
49+
# <snip>
50+
#
51+
# PSData = @{
52+
#
53+
# # Optional features enabled in this module.
54+
# OptionalFeatures = @(
55+
# 'OptionalFeature1'
56+
# 'OptionalFeature2'
57+
# )
58+
#
59+
# <snip>
60+
#
61+
# } # End of PSData hashtable
62+
#
63+
# <snip>
64+
#
65+
# } # End of PrivateData hashtable
66+
#
67+
# }
68+
69+
# Create a script file, enabling one or more optional features in the file
70+
@'
71+
#requires -OptionalFeature OptionalFeature1,OptionalFeature2
72+
73+
<snip>
74+
'@ | Out-File -FilePath ./test.ps1
75+
76+
# Get a list of optional features that are available
77+
Get-OptionalFeature
78+
79+
# Output:
80+
#
81+
# Name EnabledIn Source Description
82+
# ---- ------- ------ -----------
83+
# OptionalFeature1 Manifest PSEngine Description of optional feature 1
84+
# OptionalFeature2 Session PSEngine Description of optional feature 2
85+
86+
# Enable an optional feature by default in PowerShell
87+
Enable-OptionalFeature -Name OptionalFeature1
88+
89+
# Output:
90+
# This works just like Enable-ExperimentalFeature, turning the optional
91+
# feature on by default for all future sessions in PowerShell.
92+
93+
# Disable an optional feature by default in PowerShell
94+
Disable-OptionalFeature -Name OptionalFeature1
95+
96+
# Output:
97+
# This works ust like Disable-ExperimentalFeature, turning the optional
98+
# feature off by default for all future sessions in PowerShell.
99+
100+
# Enable an optional feature by default in all new module manifests
101+
# created with New-ModuleManifest in all future sessions in PowerShell.
102+
Enable-OptionalFeature -Name OptionalFeature1 -NewModuleManifests
103+
104+
# Disable an optional feature by default in all new module manifests
105+
# created with New-ModuleManifest in all future sessions in PowerShell.
106+
Disable-OptionalFeature -Name OptionalFeature1 -NewModuleManifests
107+
```
108+
109+
## Specification
110+
111+
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:
112+
113+
### Add parameter to New-ModuleManifest
114+
115+
`[-OptionalFeatures <string[]>]`
116+
117+
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`.
118+
119+
### Add parameter to #requires statement
120+
121+
`#requires -OptionalFeatures <string[]>`
122+
123+
This new parameter would enable optional features in the current script file.
124+
125+
### New command: Get-OptionalFeature
126+
127+
```none
128+
Get-OptionalFeature [[-Name] <string[]>] [<CommonParameters>]
129+
```
130+
131+
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`.
132+
133+
### New command: Enable-OptionalFeature
134+
135+
```none
136+
Enable-OptionalFeature [-Name] <string[]> [-NewModuleManifests] [-WhatIf] [-Confirm] [<CommonParameters>]
137+
```
138+
139+
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`.
140+
141+
### New command: Disable-OptionalFeature
142+
143+
```none
144+
Disable-OptionalFeature [-Name] <string[]> [-NewModuleManifests] [-WhatIf] [-Confirm] [<CommonParameters>]
145+
```
146+
147+
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.
148+
149+
### New command: Use-OptionalFeature
150+
151+
```none
152+
Use-OptionalFeature [-Name] <string[]> [-ScriptBlock] <ScriptBlock> [-Confirm] [<CommonParameters>]
153+
```
154+
155+
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.
156+
157+
## Alternate proposals and considerations
158+
159+
### Extend experimental features to support the enhancements defined in this RFC
160+
161+
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.
162+
163+
### Supporting a `-Scope` parameter like the experimental feature cmdlets do
164+
165+
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.

0 commit comments

Comments
 (0)