Created
February 28, 2018 17:35
-
-
Save alx9r/a021854c06efdae411f7ae80e6f13d0f to your computer and use it in GitHub Desktop.
Proof-of-concept of a utility module that simplifies honoring user preferences and common parameters throughout a module.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
New-Module HonorCallerPrefs { | |
# Utility module for honoring user preference variables | |
# and common parameters. | |
<# | |
This ErrorActionPrefrence affects only execution in this module. | |
All errors arising in this utility module represent bugs and | |
should therefore throw exceptions and their cause fixed. | |
#> | |
$ErrorActionPreference = 'Stop' | |
function Get-CallerVariable | |
{ | |
<# | |
Non-public utility function to invoke Get-Variable in the | |
caller's SessionState. This is needed so that *Preference | |
variables can be collected for later injection into the call site | |
of commands that use them but are called within a module whose | |
author wants to honor those preferences. | |
#> | |
[OutputType([psvariable])] | |
param | |
( | |
[Parameter(ValueFromPipeline, | |
ValueFromPipelineByPropertyName, | |
Mandatory)] | |
[System.Management.Automation.SessionState] | |
[Alias('SessionState')] | |
$CallerSessionState, | |
[Parameter(Position=1)] | |
$Name | |
) | |
process | |
{ | |
# BREAKING_CHANGES_WARNING | |
# This is currently the least-bad strategy for invoking | |
# a scriptblock in a given SessionState. This might be | |
# vulnerable to breaking changes. | |
# See https://github.com/PowerShell/PowerShell/issues/6147 | |
$module = [psmoduleinfo]::new($false) | |
$module.SessionState = $CallerSessionState | |
,$Name | & $module { process { Get-Variable $_ } } | |
} | |
} | |
function Get-BoundParameter | |
{ | |
<# | |
Non-public utility function to get a hashtable containing a subset | |
of bound parameters. This is needed so that common parameters | |
(like -ErrorAction, -WhatIf, etc) can be collected for later | |
use at the call site of commands that use them but are deep | |
within a module whose author wants to forward common parameters | |
to the command. | |
#> | |
[OutputType([hashtable])] | |
param | |
( | |
[Parameter(ValueFromPipeline, | |
ValueFromPipelineByPropertyName, | |
Mandatory)] | |
[System.Collections.Generic.Dictionary[string,object]] | |
$BoundParameters, | |
[Parameter(Position=1)] | |
[string[]] | |
$Name | |
) | |
process | |
{ | |
$output = @{} | |
$BoundParameters.get_Keys() | | |
? { $_ -in $Name } | | |
% { $output[$_] = $BoundParameters[$_] } | |
$output | |
} | |
} | |
function HonorCallerPrefs | |
{ | |
<# | |
Public utility function used at the entry point to | |
a module whose author wants to honor caller | |
preferences. | |
This function prepares the necessary information | |
from the call site of the entry point for later use. | |
#> | |
param | |
( | |
# Everything we need to know about the call site | |
# is available in the entry point function's $PSCmdlet | |
# automatic variable. | |
[System.Management.Automation.PSCmdlet] | |
$EntryPSCmdlet=$PSCmdlet.SessionState.PSVariable.Get('PSCmdlet').Value, | |
# The scriptblock where all the calls that might | |
# need to honor caller preferences are called from. | |
# When InvokeWithCallerPrefs are called from descendent | |
# scopes of this ScriptBlock, it has access to the | |
# caller's *Preference variables and common parameters | |
# so that those values can be honor when making calls | |
# to commands that use them. | |
[Parameter(Position=1,Mandatory)] | |
[scriptblock] | |
$ScriptBlock | |
) | |
[pscustomobject]@{ | |
ScriptBlock = $ScriptBlock | |
EntryPSCmdlet = $EntryPSCmdlet | |
} | | |
& $ScriptBlock.Module { | |
process | |
{ | |
# inject the $EntryPSCmdlet for later use by InvokeWithCallerPrefs | |
Set-Variable EntryPSCmdlet $_.EntryPSCmdlet | |
# call the user scriptblock | |
& $_.ScriptBlock | |
} | |
} | |
} | |
function InvokeWithCallerPrefs { | |
<# | |
Public utility function used inside a module for calls | |
to commands outside a module whose author wants to honor | |
caller preferences. | |
This function injects two things into ScriptBlock: | |
* All the *Preference variables from the call site of | |
the entry point to the module (where HonorCallPrefs | |
was used). | |
* $CallerCommonArgs which is a hashtable containing | |
all of the common parameters used at the call site | |
of the entry point to the module (where HonorCallPrefs | |
was used). | |
#> | |
param | |
( | |
# The scriptblock from which calls made to commands outside | |
# a module should be made when the module author wants to | |
# honor user preferences. | |
[Parameter(Position=1)] | |
[scriptblock] | |
$ScriptBlock | |
) | |
[pscustomobject]@{ | |
ScriptBlock = $ScriptBlock | |
VariableGetter = Get-Command Get-CallerVariable | |
BoundParameterGetter = Get-Command Get-BoundParameter | |
} | | |
& $ScriptBlock.Module { | |
process | |
{ $in = $_ | |
# make all the caller preference variables available | |
# to $ScriptBlock | |
$EntryPSCmdlet | | |
& $in.VariableGetter *Preference | | |
% { Set-Variable $_.Name $_.Value } | |
# make all the common parameters from the call site available | |
# as the hashtable $CallerCommonArgs in $ScriptBlock | |
,@( | |
[System.Management.Automation.PSCmdlet]::CommonParameters | |
[System.Management.Automation.PSCmdlet]::OptionalCommonParameters | |
) | | |
% { | |
$EntryPSCmdlet.MyInvocation | | |
& $in.BoundParameterGetter $_ | | |
% { Set-Variable CallerCommonArgs $_ } | |
} | |
# invoke the user scriptblock | |
& $_.ScriptBlock | |
} | |
} | |
} | |
Export-ModuleMember HonorCallerPrefs,InvokeWithCallerPrefs | |
} | Out-Null | |
New-Module MyModule { | |
# Example module where the caller's intentions expressed using | |
# preference variables and common parameters are honored when | |
# the module makes calls to commands outside the module that use | |
# preference variables and common parameters. | |
<# | |
This ErrorActionPreference affects only execution in this module that | |
is outside an InvokeWithCallerPrefs{} block. In general, an error | |
that occurs in the domain-specific code internal to this module should | |
not generate errors. All errors arising outside InvokeWithCallerPrefs{} | |
in this module represent bugs and should therefore throw exceptions | |
and their cause fixed. | |
The behavior of errors arising from commands inside InvokeWithCallerPrefs{} | |
is affected by a shadow of $ErrorActionPreference whose value is a copy | |
of the value at the call site of the entry point of this module. | |
#> | |
$ErrorActionPreference = 'Stop' | |
# import the utility module | |
Get-Module HonorCallerPrefs | Import-Module | |
function Get-MyItem | |
{ | |
# This is an entry point to the module. | |
# This function eventually results in a call | |
# to Get-Item $Path from within this module. | |
param | |
( | |
[Parameter(Position=1)] | |
$Path | |
) | |
HonorCallerPrefs { | |
# Any code in this module whose execution descends | |
# from here can honor caller preferences by using | |
# InvokeWithCallerPrefs {}. | |
Get-MyItemImpl $Path | |
} | |
} | |
function Get-MyItemImpl | |
{ | |
param( | |
[Parameter(Position=1)] | |
$Path | |
) | |
# Get-Item is outside this module and uses preference variables | |
# and common arguments that we would like to honor. So we call | |
# it from InvokeWithCallerPrefs | |
InvokeWithCallerPrefs { | |
# Preference variables appearing here have the same value as the call | |
# site of Get-MyItem | |
# $CallerCommonArgs is a hashtable containing the common arguments | |
# passed to Get-MyItem. By splatting $CallerCommonArgs to Get-Item, | |
# parameters like -ErrorAction are forwarded from the call site of | |
# Get-MyItem to this call to Get-Item. | |
# This call doesn't see the module's value of $ErrorActionPreference. | |
# Instead it sees the value of $ErrorActionPreference from the call site | |
# of Get-MyItem | |
Get-Item $Path @CallerCommonArgs | |
} | |
} | |
function New-MyItem | |
{ | |
param( | |
[Parameter(Position=1)] | |
$Path, | |
# These optional common parameters are implemented here | |
# so that it becomes available at the call to | |
# New-Item | |
[switch] $WhatIf, | |
[switch] $Confirm | |
) | |
HonorCallerPrefs { | |
New-MyItemImpl $Path | |
} | |
} | |
function New-MyItemImpl | |
{ | |
param( | |
[Parameter(Position=1)] | |
$Path | |
) | |
InvokeWithCallerPrefs { | |
# Invoking New-MyItem -WhatIf causes $WhatIf to be | |
# true for this call by way of $CallerCommonArgs. | |
New-Item $Path @CallerCommonArgs | |
} | |
} | |
# export only the public API | |
Export-ModuleMember Get-MyItem,New-MyItem | |
} | Import-Module | |
# The following tests demonstrate calls to MyModule that use | |
# HonorCallerPrefs to forward preference variables and | |
# common parameters to where they are needed inside MyModule. | |
& { | |
'==== eap=Continue; Get-MyItem bogus' | |
$ErrorActionPreference = 'Continue' | |
Get-MyItem bogus | |
'reaches statement after' | |
} | |
& { | |
'==== eap=Continue; Get-MyItem bogus -ErrorAction Stop' | |
$ErrorActionPreference = 'Continue' | |
try | |
{ | |
Get-MyItem bogus -ErrorAction Stop | |
} | |
catch | |
{ | |
'throws this error' | |
$_ | |
} | |
} | |
& { | |
'==== eap=Stop; Get-MyItem bogus' | |
$ErrorActionPreference = 'Stop' | |
try | |
{ | |
Get-MyItem bogus | |
} | |
catch | |
{ | |
'throws this error' | |
$_ | |
} | |
} | |
& { | |
'==== New-MyItem bogus -WhatIf' | |
New-MyItem bogus -WhatIf | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment