powershell workflow - parameter splatting not supported? - powershell

I'm scratching my head trying to figure out why parameter splatting won't work for a set of workflow runbooks executed in Azure Automation.
I've got 3 runbooks (super, sub, child) that are called in a nested fashion.
They all share (generally) the same parameters. In my real-world scenario, I have multiple sub & child runbooks and I want to use splatting so I could define a hashtable once and pass it as needed, without having to list out all the params.
I've also tried using the -PSParameterCollection parameter. However, in Azure Automation, this appears only to be available for the topmost runbook (which already works), as it throws a parameter not found error for it in the children. The Note section here says Workflows that are nested three levels deep do not support any common parameters, including workflow common parameters, which seems like it could be a problem, depending on how these runbooks are compiled.. but I don't think I can avoid that without limiting the reusability of the runbooks. I wouldn't expect called workflows to be compiled as nested workflows within the calling one.
The error I get is:
$newnumber = sub #splatter
~~~~~~~~~~
Could not find a parameter named '0'. Supported parameters are: Debug, ErrorAction, ....
Oddly enough, unlike what I'm seeing with these test runbooks, in my 'real-world' workflows, I don't get this error in the top-level runbook - only when calling the 'child' runbooks from within 'sub'.
workflow child
{
Param ([int]$val)
Write-Verbose "child: val ($val)"
$NewNumber = $val*2
$NewNumber
}
workflow childtwo
{
Param ([int]$val, [int]$valtwo)
Write-Verbose "childtwo: val ($val) valtwo ($valtwo)"
$NewNumber = $val * $valtwo
$NewNumber
}
workflow sub
{
Param ([int]$val)
Write-Verbose "sub: val ($val)"
$splatter = #{ val = $val }
# works
$NewNumber = child -val $splatter.val
# fail
$NewNumber = child #splatter
# works
$NewNumber = childtwo -val $splatter.val -valtwo 2
# fail
$NewNumber = childtwo ($splatter + #{ valtwo = 2 })
$mergeSplatter = ($splatter + #{ valtwo = 2 })
$NewNumber = childtwo #mergeSplatter
$twoSplatter = #{ val = $val; valtwo = 2 }
$NewNumber = childtwo #twoSplatter
$NewNumber
}
workflow super
{
Param([int]$val)
Write-Verbose "super: id is $val"
$splatter = #{ val = $val }
$newnumber = sub -val $val
$newnumber = sub #splatter # # or $ doesn't matter - fails the same
Write-Verbose "super: newnumber is $newnumber"
$newnumber
}
super 2

Please see https://technet.microsoft.com/en-us/library/jj574140.aspx?f=255&MSPPError=-21472173969:
Splatting is not permitted in workflow activities or in calls to workflows.
You will need to either not use splatting or switch the PowerShell Workflow runbook to a native PowerShell runbook.

Related

Create a wrapper for functions in powershell

I want to wrap a function, that is to create a new function such that it would automatically pass some arguments to the old function, like python's partial functions. The arguments passed are the ones defined in the callee and not the caller. The important thing is that I don't want to refer to each of them explicitly (define them twice).
That is really done to save typing-in the same flags to complicated functions while allowing customization.
For example, in python, I would do:
call_with_x=partial(call,x=1)
or maybe use **kw and pass it to the callee in some cases .
This is my best try (based on Wrapper function in PowerShell: Pass remaining parameters) :
function Let
{
[CmdletBinding()]
Param([parameter(mandatory=$true, position=0)][string]$Option,
[parameter(mandatory=$false, position=1, ValueFromRemainingArguments=$true)]$Remaining)
Get #Remaining
}
function Get
{
[CmdletBinding()]
Param([parameter(mandatory=$false, position=0)][string]$OptionA,
[parameter(mandatory=$true, position=1)][string]$OptionB)
Write-Host $OptionA, $OptionB
}
But Let -Option c -OptionA 1
Prints -OptionA 1
which is obviously not what I intended.
If you don't require "advanced" function features from CmdletBinding(), then you can get away with using $args for this:
# simple function parameters are positional and named, but not mandatory
function let {
Param($OptionA,$OptionB)
write-host "OptionA=$OptionA"
write-host "OptionB=$OptionB"
get #args
}
function get {
Param($OptionC,$OptionD)
write-host "OptionC=$OptionC"
write-host "OptionD=$OptionD"
}
# not necessary to name or include parameters
let -OptionA A B -OptionC C D
OptionA=A
OptionB=B
OptionC=C
OptionD=D
# named parameters get assigned first, so the order is not too big a deal either
# these produce the same results as above:
let B -OptionA A D -OptionC C
let -OptionD D -OptionC C A B
Any named parameters will not get positionally assigned to let
Any additional parameters, named or otherwise, will be forwarded to get
That was hard!
For this you would need to add a bit of code in DynmaicParams and begin section of the function.
It might be possible to do it in an Attribute
function x{
[WrapperFor(y)]
Might do it Later.
function Get
{
[CmdLetBinding()]
Param([parameter(mandatory=$false, position=0)][string]$OptionA,
[parameter(mandatory=$false, position=1)][string]$OptionB)
Write-Host "opta",$OptionA
Write-Host "optb",$OptionB
}
function Let
{
[CmdletBinding()]
Param([parameter(mandatory=$true, position=0)][string]$Option,[parameter(mandatory=$false, position=0)][string]$OptionB)
DynamicParam {
AddWrapper -For "Get" -To "Let"
}
Begin {
$params = GetRestOfParams "Get" $PSBoundParameters
}
Process {
Get #params
}
}
Needed code:
using namespace System.Management.Automation
function Empt
{
[CmdletBinding()]
Param([parameter(mandatory=$true, position=0)][string]$aaaa)
1
}
function AddWrapper([parameter(mandatory=$true, position=0)][string]$For,[parameter(mandatory=$true, position=1)][string]$To)
{
$paramDictionary = [RuntimeDefinedParameterDictionary]::new()
$paramset= $(Get-Command $For).Parameters.Values | %{[System.Management.Automation.RuntimeDefinedParameter]::new($_.Name,$_.ParameterType,$_.Attributes)}
$paramsetlet= $(Get-Command empt).Parameters.Keys
$paramsetlet+= $(Get-Command $To).ScriptBlock.Ast.Body.ParamBlock.Parameters.Name | %{ $_.VariablePath.UserPath }
$paramset | %{ if ( -not ($paramsetlet -contains $_.Name) ) {$paramDictionary.Add($_.Name,$_)}}
return $paramDictionary
}
function GetRestOfParams($dst,$params)
{
$dstorgparams=$(Get-Command $dst).Parameters.Keys
$z= $params
$z.Keys | %{ if ( -not ($dstorgparams -contains $_) ) {$z.Remove($_)} } | Out-Null
return $z
}

Defining parameters common to all functions within a PowerShell module

I am writing a PowerShell module, the functions inside this module have some parameters which will be re-used across all functions. Rather than copy-pasting the function definition each time I add a new function, I would like to define them at the top like a script variable and then insert them into each function, giving me a single place to update if they need to be changed.
Looking at how dynamic parameters are defined it seems like I should be able to define an object of that type and then reference it in the function definitions, but I can't find anything online giving me the correct syntax to do this.
Using PowerShell version 7.2
$Script:ReUsedParameters = param(
[Parameter()]
[String]$Name,
[Parameter()]
[Int]$Id
)
Function New-Command {
Param ($ReUsedParameters)
Write-Output "Name: $Name, ID: $ID"
}
For the sake of answering, you can store the runtime parameters definitions in a script block and then call it & inside the function's dynamicparam block.
I do not think this is a good idea nor I recommend using this. All functions should have their own repeated param blocks if needed.
$reusedParameters = {
$paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
# Since both parameters don't have any arguments (Mandatory, Position, ValueFromPipeline, etc..)
# you can use this one for both, otherwise, each dynamic parameter should have their own
# Parameter Declaration
[Parameter[]] $paramAttribute = [Parameter]::new()
$paramDictionary['Name'] = [System.Management.Automation.RuntimeDefinedParameter]::new('Name', [string], $paramAttribute)
$paramDictionary['Id'] = [System.Management.Automation.RuntimeDefinedParameter]::new('Id', [int], $paramAttribute)
return $paramDictionary
}
Function New-Command {
[CmdletBinding()] # `CmdletBinding` is Mandataroy here
param() # if the `param` block is empty
dynamicparam {
& $reusedParameters
}
end {
# Caveat: you can reference these parameters via $PSBoundParameters
# $Name and $Id are not part of the `param` block
# hence that wouldn't work here
"Name: {0}, ID: {1}" -f $PSBoundParameters['Name'], $PSBoundParameters['ID']
}
}
New-Command -Name asd -Id 123
As a declarative approach, you may turn the common parameters into class properties and have a single function parameter of the class type.
class MyReUsedParameters {
[String] $Name
[Int] $Id = 23
}
Function New-Command {
Param (
[MyReUsedParameters] $ReUsedParameters,
$AnotherParam
)
Write-Output "Name: $($ReUsedParameters.Name), ID: $($ReUsedParameters.ID)"
}
# Pass the common parameters as a hashtable which gets converted to
# MyReUsedParameters automatically.
New-Command -ReUsedParameters #{ Name = 'foo'; Id = 42 } -AnotherParam bar
# Alternatively pass the common parameters as a (typed) variable.
# PowerShell is able to deduce the argument name from the type.
$commonArgs = [MyReUsedParameters] #{ Name = 'Foo'; Id = 42 }
New-Command $commonArgs -AnotherParam bar
When passing a hashtable or PSCustomObject that has matching properties, it will automatically be converted to the class type.
You may even validate class properties similar to regular parameters. Most parameter validation attributes can be specified for class properties as well.
class MyReUsedParameters {
[ValidateNotNullOrEmpty()] [String] $Name
[Int] $Id = 23
# Constructor - required to apply validation
MyReUsedParameters( [Hashtable] $ht ) {
$this.Name = $ht.Name
$this.Id = $ht.Id
}
}
Function New-Command {
Param (
[Parameter(Mandatory)]
[MyReUsedParameters] $ReUsedParameters
)
Write-Output "Name: $($ReUsedParameters.Name), ID: $($ReUsedParameters.ID)"
}
# Causes an error (as expected), because Name property is missing
New-Command -ReUsedParameters #{ Id = 42 }

PowerShell Classifier with File Server Resource Manager

I'm trying to use the Windows PowerShell Classifier in FSRM on Server 2019. I need it to look for files that start with "~$" and classify them with a Yes or No property I created.
I would also be fine with a REGEX code as well.
This is what I have but it's not working:
# Global variables available:
# $ModuleDefinition (IFsrmPipelineModuleDefinition)
# $Rule (IFsrmClassificationRule)
# $PropertyDefinition (IFsrmPropertyDefinition)
#
# And (optionally) any parameters you provide in the Script parameters box below,
# i.e. "$a = 1; $b = 2" . The string you enter is treated as a script and executed so the
# variables you define become globally available
# optional function to specify when the behavior of this script was last modified
# if it consumes additional files. emit one value of type DateTime
#
# function LastModified
# {
# }
# required function that outputs a value to be assigned to the specified property for each file classified
# emitting no value is allowed, which causes no value to be assigned for the property
# emitting more than one value will result in errors during classification
# begin and end are optional; process is required
#
function GetPropertyValueToApply
{
# this parameter is of type IFsrmPropertyBag
# it also has an additional method, GetStream, which returns a IO.Stream object to use for
# reading the contents of the file. Make sure to close the stream after you are done reading
# from the file
param
(
[Parameter(Position = 0)] $PropertyBag
)
Process
{
$FileName = $_.Name
If($FileName -like "~$*")
{
$True
}
Else
{
$False
}
}

Reuse parameters among multiple powershell functions

Consider two powershell functions:
function A {
params(
[Parameter()]$x,
[Parameter()]$y
)
Write-Host $x $y
}
function B {
params(
[Parameter()]$x,
[Parameter()]$z
)
Write-Host $x $z
}
I'd like to define a single parameter $x once (which could have fairly complex attributes that must be kept identical for both functions) and re-use it in both functions, so something like:
$x = {[Parameter()]$x}
function A {
params(
$x,
[Parameter()]$y
)
Write-Host $x $y
}
function B {
params(
$x,
[Parameter()]$z
)
Write-Host $x $z
}
(How) is this possible?
To reuse parameter declarations across functions - as requested in your question - see the following section.
To reuse parameter values (arguments) across functions, by way of presets (default value), see the bottom section.
In order to reuse parameter declarations - short of using design-time templating to generate source code - you need to to define a script block that creates a dynamic parameter that can be passed to the dynamicparam block of multiple advanced functions:
using namespace System.Management.Automation
# The script block that declares the dynamic parameter to be shared
# across functions.
$sharedDynParam = {
# Define the -x parameter dynamically.
$paramName = 'x'
$dict = [RuntimeDefinedParameterDictionary]::new()
$dict.Add(
$paramName,
[RuntimeDefinedParameter]::new(
$paramName,
[datetime], # Type the parameter [datetime]. for instance.
[ParameterAttribute] #{
Mandatory = $true # Make the parameter mandatory, for instance.
# ParameterSetName = 'default' # Assign it to a parameter set, if neeeded.
}
)
)
# Return the dictionary
return $dict
}
function A {
[CmdletBinding()]
param(
$y
)
# Assign the shared dynamic parameter.
dynamicparam { & $sharedDynParam }
# The use of `dynamicparam { ... }` requires use of an explicit
# `process { ... }` block (and optionally `begin { ... }` and
# `end { ... }`, as needed).
process {
# Note: A dynamic -x parameter cannot be accessed as $x
# Instead, it must be accessed via the $PSBoundParameters dictionary.
"[$($PSBoundParameters['x'])] - [$y]"
}
}
function B {
[CmdletBinding()]
param(
$z
)
# Assign the shared dynamic parameter.
dynamicparam { & $sharedDynParam }
process {
"[$($PSBoundParameters['x'])] - [$z]"
}
}
# Sample calls,
A -x '1970-01-01' -y yval
B -x '1970-01-02' -z zval
Output:
[01/01/1970 00:00:00] - [yval]
[01/02/1970 00:00:00] - [zval]
To preset the value of a parameter by a given name across commands, use the $PSDefaultParameterValues preference variable:
# Preset a parameter value for all commands ('*') that have
# an -x ('x') parameter.
$PSDefaultParameterValues = #{ '*:x' = [pscustomobject] #{ foo = 1; bar = 2 } }
function A {
[CmdletBinding()] # This makes the function an *advanced* one, which respects
# $PSDefaultParameterValues; similarly, at least one
# parameter-individual [Parameter()] attribute does the same.
param(
$x,
$y
)
"[$x] - [$y]"
}
function B {
[CmdletBinding()]
param(
$x,
$z
)
"[$x] - [$z]"
}
# Sample calls, without an -x argument, relying on
# $PSDefaultParameterValues to provide it automatically.
A -y yval
B -z zval
Output, showing that parameter -x was automatically bound via $PSDefaultParameterValues:
[#{foo=1; bar=2}] - [yval]
[#{foo=1; bar=2}] - [zval]

Pester: Use variables in -ForEach data driven tests with rich content

I write a module used for powershell-wrapping a commandline tool.
For my tests I had some variables defined in BeforeAll. Those vars should help in feeding the command with desired params and check them against the expected result, f.i.
BeforeAll {
$PORead=#{
Name = "Paul"
Permission ="Read"
Inheritance = "ThisFolderSubfoldersAndFiles"
}
$POReadExp="-bla bla (bla:bla;fra:fra;tra:tra)"
}
Now I want to use these in an it segment to feed the function and check the result, like:
It "checks result" -ForEach (
#{Param=$PORead;Expected=$POReadExp}
) {
$result=FunctionCall -Parameter1 $Param
$result | Should -Be $Expected
}
Note: This is actually no working code - just to make clear what I want.
The problem is, that $Param and $Expected are $null, thus the current value of $PORead and $POReadExp are not assigned to $Param and $Expected.
I made it easy to demonstrate the problem - in my code I use arrays in the -ForEach hash assigned to $Param and $Expected.
Using plain strings, there is no problem and everything works fine.
I could use plain text and than use som switch construct to match my strings with the according variables from BeforeAll{}, but I thought there must be a more elegant way.
Using standard hashtables, the variable substition works fine.
I'd appreciate any idea.
To clarify some things:
The solution of Mark works somehow, but it needs the $PORead and $POReadExp to be set outside the BeforeAll-block (if you want to use -ForEach), or within the BeforeAll-block, but in that case you only can use one argument.
My plan was a bit different, so I'm making my point a bit clearer:
In the BeforeAll-block I wanted a bunch of definitions for params and outcomes
BeforeAll {
$PORead = #{
Name = "Paul"
Permission ="Read"
Inheritance = "ThisFolderSubfoldersAndFiles"
$PODelete = #{
Name = "Paul"
Permission ="Delete"
Inheritance = "ThisFolderOnly"
}
$POReadResult = "add parameters"
$PODeleteResult = "delete parameters"
}
Now I want to test a whole bunch of combinations, feeding different combinations to the function and checking, if the combination of results match:
It "Test if all combinations works" -ForEach(
#{FunctionParam=$PORead;Expected=$POReadExp}
#{FunctionParam=$PORead,$PORead;Expected=$POReadExp,$POReadExp}
#{FunctionParam=$PORead,$PODelete,$PORead,$PORead;Expected=$PORead,$PORead,$PORead,$PODelete}
) {
$result = Function-Call -Param $FunctionParam
$wanted = $expected -join(" ")
$result | Should -Be $wanted
}
I can realize this by placing the definitions out of any block, but it feels wrong in terms of pester coding guidelines.
Greets
Usul
The scopes for how to set test data with Pester v5 are quite confusing. What I've found is that anything you set in a BeforeAll or BeforeEach is available within the It. However if you want to set values to the -ForEach (or its alias -TestData) you need to do it as part of or before the It, outside of the BeforeEach or BeforeAll.
As such I think your tests will work if you do this:
$PORead = #{
Name = "Paul"
Permission ="Read"
Inheritance = "ThisFolderSubfoldersAndFiles"
}
$POReadExp = "-bla bla (bla:bla;fra:fra;tra:tra)"
It "checks result" -ForEach (
#{Param=$PORead;Expected=$POReadExp}
) {
$result=FunctionCall -Parameter1 $Param
$result | Should -Be $Expected
}
Or alternatively (if you just need to perform a single test):
BeforeAll {
$PORead=#{
Name = "Paul"
Permission ="Read"
Inheritance = "ThisFolderSubfoldersAndFiles"
}
$POReadExp="-bla bla (bla:bla;fra:fra;tra:tra)"
}
It "checks result" {
$result = FunctionCall -Parameter1 $PORead
$result | Should -Be $POReadExp
}
If you want to avoid using a variable that is not within one of the Pester code blocks, then you should do this (which is in line with the examples given in the Pester documentation on Data Driven Tests):
It "checks result" -ForEach (
#{
Param = #{
Name = "Paul"
Permission = "Read"
Inheritance = "ThisFolderSubfoldersAndFiles"
}
Expected = "-bla bla (bla:bla;fra:fra;tra:tra)"
}) {
$result=FunctionCall -Parameter1 $Param
$result | Should -Be $Expected
}