Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

RFC Proposal: Allow execution preferences to persist beyond module or script scope #198

Closed
KirkMunro opened this issue Jun 19, 2019 · 4 comments

Comments

@KirkMunro
Copy link
Contributor

KirkMunro commented Jun 19, 2019

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.

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 the RFC proposal for optional features in PowerShell).

Motivation

As a scripter or a command author,
I can invoke commands without having 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 can change a published command from an advanced function to a cmdlet or vice versa if needed without worrying about invocation nuances in PowerShell,
So that I can focus on what is the best way to code my commands for myself and my team.

User experience

# 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 = @{
#
#     <snip>
#
#     PSData = @{
#
#         # Optional features enabled in this module.
#         OptionalFeatures = @(
#             'PersistCommandExecutionPreferences'
#         )
#
#         <snip>
#
#     } # End of PSData hashtable
#
#     <snip>
#
# } # 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 the RFC proposal for optional features in PowerShell.

Alternate proposals and considerations

Rip off the bandaid

Some members of the community 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 the RFC proposal for optional features in PowerShell for more information.

Support -DebugAction, -VerboseAction, and -ProgressAction if those common parameters are added

The RFC proposal for ScriptBlocks to handle non-terminating message processing 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.

@jtmoree-github-com
Copy link

In my own scripts I have found that passing these flags as parameters leads to problems. A better approach is to use the environment variable and code scripts to use those. By using parameters we have to explicitly define and deal with those at the top of the script AND SET DEFAULTS. Yes we can set the default to the environment variable but then we have created more work for ourselves to get the same result.

@KirkMunro
Copy link
Contributor Author

KirkMunro commented Jul 9, 2019

@jtmoree-kahalamgmt-com: That's actually not true.

At the top of your script file, make sure you add the CmdletBinding attribute, and then you'll get the common parameters with their respective defaults added automatically.

For example:

@'
[CmdletBinding(SupportsShouldProcess)]
param()
'@ | Out-File .\t.ps1
Get-Command .\t.ps1 -Syntax
Remove-Item .\t.ps1

The output of that snippet will show you that the t.ps1 script file has the -WhatIf and -Confirm parameters with the appropriate defaults because of the SupportsShouldProcess setting in the CmdletBinding attribute definition, and the script also has all common parameters with their respective defaults as shown by [<CommonParameters>] in the syntax of the command.

Given this is how PowerShell already works today, I don't see any benefit to using environment variables instead.

@jtmoree-github-com
Copy link

One benefit of using env vars is that I don't have to put extra code at the top of my scripts. ;-)

My OP may have been ambiguous. When I say 'have to set defaults' I mean that any defined parameter is defaulted to NULL. I don't want defaults in the local scope. I want values to come from the parent.

For example, as we automate our processes and deploy the same code to dev/stage/live I am removing the 'parameters' and setting environment variables for each platform and prefs. This includes the debug and verbose prefs, etc. IMHO it takes more code use parameters by defining them in the scripts, setting whatever binding options, and then passing them on the calling command.

Thanks for the tip on CmdletBinding. Might come in handy.

@KirkMunro
Copy link
Contributor Author

Moved into PR #221.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants