Skip to content

Instantly share code, notes, and snippets.

@alx9r
Created February 28, 2018 17:35
Show Gist options
  • Save alx9r/a021854c06efdae411f7ae80e6f13d0f to your computer and use it in GitHub Desktop.
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.
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