Environmental note: I'm currently targetting PowerShell 5.1 because 6 has unrelated limitations I can't work around yet.
In the Powershell module I'm writing, there is one main function that's sort of a conglomeration of a bunch of the smaller functions. The main function has a superset of the smaller function's parameters. The idea is that calling the main function will call each smaller function with the necessary parameters specified on the main. So for example:
function Main { [CmdletBinding()] param($A,$B,$C,$D)
Sub1 -A $A -B $B
Sub2 -C $C -D $D
}
function Sub1 { [CmdletBinding()] param($A,$B)
"$A $B"
}
function Sub2 { [CmdletBinding()] param($C,$D)
"$C $D"
}
Explicitly specifying the sub-function parameters is both tedious and error prone particularly with things like [switch] parameters. So I wanted to use splatting to make things easier. Instead of specifying each parameter on the sub-function, I'll just splat $PSBoundParameters from the parent onto each sub-function like this:
function Main { [CmdletBinding()] param($A,$B,$C,$D)
Sub1 #PSBoundParameters
Sub2 #PSBoundParameters
}
The immediate problem with doing this is that the sub-functions then start throwing an error for any parameter they don't have defined such as, "Sub1 : A parameter cannot be found that matches parameter name 'C'." If I remove the [CmdletBinding()] declaration, things work but I lose all the benefits of those subs being advanced functions.
So my current workaround is to add and additional parameter on each sub-function that uses the ValueFromRemainingArguments parameter attribute like this:
function Sub1 { [CmdletBinding()]
param($A,$B,[Parameter(ValueFromRemainingArguments)]$Extra)
"$A $B"
}
function Sub2 { [CmdletBinding()]
param($C,$D,[Parameter(ValueFromRemainingArguments)]$Extra)
"$C $D"
}
Technically, this works well enough. The sub-functions get their specific params and the extras just get ignored. If I was writing this just for me, I'd move on with my life and be done with it.
But for a module intended for public consumption, there's an annoyance factor with that -Extra parameter being there. Primarily, it shows up in Get-Help output which means I have to document it even if just to say, "Ignore this."
Is there an extra step I can take to make that extra parameter effectively invisible to end users? Or am I going about this all wrong and there's a better way to allow for extra parameters on an advanced function?
My usual approach is to export only "wrapper" functions that call internal (i.e., not user-facing) functions in the module.
Related
I've seen this, but neither answer actually answers the question using named argument in the call to the function
This is for Powershell 7. No workflow. And really Named Parameters this time! Also not covered in the standard documentation here .. Starting to think that this isn't possible.
this works, but it is requires brackets with positional arguments, not named arguments
Function MyFunction ([ref][string]$MyRefParam) {
$MyRefParam.Value = "newValue";
}
$myLocal = "oldValue"
Write-Host $myLocal # Outputs: oldValue
MyFunction ([ref]$myLocal);
Write-Host $myLocal # Outputs: newValue
Of course we all know that the best way to call a Powershell function is with named argument MyFunction -Arg1 23 rather than positional arguments MyFunction 23 or MyFunction(23) . These would not get through our Pull Requests!
But this doesn't work
Function MyFunction ([ref][string]$MyRefParam) {
$MyRefParam.Value = "newValue";
}
$myLocal = "oldValue"
Write-Host $myLocal # Outputs: oldValue
MyFunction -MyRefParam ([ref]$myLocal) # Outputs Cannot process argument transformation on parameter 'MyRefParam'. Reference type is expected in argument.
Write-Host $myLocal # Outputs: oldValue
Is there another way to provide the ref type in this syntax? So far I've tried combinations of [ref] and [ref][string] in both the param definition and in the call - I can't get anything to let Powershell see that I am really passing a Ref
Thanks for any help!
Use only a [ref] type constraint in your parameter declaration, i.e. remove the additional [string] type constraint (it doesn't do anything, and arguably shouldn't even be allowed - see bottom section):
Function MyFunction ([ref] $MyRefParam) {
$MyRefParam.Value = "newValue"
}
$myLocal = 'oldVaue'
MyFunction -MyRefParam ([ref] $myLocal)
$myLocal # -> 'newvalue'
You cannot type a [ref] instance: [ref] isn't a keyword that modifies a parameter declaration (as ref would be in C#), it is a type in its own right, System.Management.Automation.PSReference, and its value-holding property, .Value is of type object, i.e. it can hold any type of object.
The upshot:
You cannot enforce a specific data type when a value is passed via [ref].[1]
Conversely, you're free to assign a value of any type to the .Value property of the [ref] instance received.
Taking a step back:
[ref] is primarily intended for supporting calls to .NET APIs with ref / out parameters.
Its use in pure PowerShell code is unusual and best avoided, not least due to the awkward invocation syntax.
That said, the difference in behavior with your double type constraint between positional and named parameter binding is certainly curious - see Mathias R. Jessen's excellent explanation in the comments.
However, the real problem is that you're even allowed to define a multi-type constraint (unusual in itself) that involves [ref], because there appears to be an intentional check to prevent that, yet it doesn't surface in parameter declarations.
You can make it surface as follows, however (run this directly at the command line):
# ERROR, because [ref] cannot be combined with other type constraints.
PS> [ref] [string] $foo = 'bar'
Cannot use [ref] with other types in a type constraint
That the same check isn't enforced in parameter declarations has been reported in GitHub issue #16146.
[1] Behind the scenes, a generic type that is non-public is used when an actual value is passed with a [ref] cast (System.Management.Automation.PSReference<T>), which is instantiated with whatever type the value being cast is. However, this generic type's .Value property is still [object]-typed, allowing for modifications to assign any type.
I've made a powershell script which validates some parameters. In the process of validation I need to create some strings. I also need these strings later in the script.
To avoid rebuilding the same strings again, can I reuse variables defined within validation blocks? Perhaps I can use functions in validation blocks somehow? Or maybe global variables? I'm not sure what's possible here, or what's good practice.
Example:
Test.ps1
Function Test {
param(
[string]
[Parameter(Mandatory=$true)]
$thing1
[string]
[Parameter(Mandatory=$true)]
$thing2
[string]
[Parameter(Mandatory=$true)]
[ValidateScript({
$a = Get-A $thing1
$b = Get-B $thing2
$c = $a + $b
$d = Get-D $c
if(-not($d -contains $_)) {
throw "$_ is not a valid value for the thing3 parameter."
}
return $true
})]
$thing3
)
# Here I'd like to use $c
# At worst, calling Get-A and Get-B again may be expensive
# Or it could just be annoying duplication of code
}
Bonus question, if this is possible, could I reuse those variables in a subsequent validation block?
You could use a byref varliable.
This will affect the variable being passed to it so you could both have a return value and a parameter affected by the execution of your function.
About Ref
You can pass variables to functions by reference or by value.
When you pass a variable by value, you are passing a copy of the data.
In the following example, the function changes the value of the
variable passed to it. In PowerShell, integers are value types so they
are passed by value. Therefore, the value of $var is unchanged outside
the scope of the function.
Function Test{
Param($thing1,$thing2,[ref]$c)
$c.Value = new-guid
return $true
}
#$ThisIsC = $null
test -c ([ref] $ThisIsC)
Write-Host $ThisIsC -ForegroundColor Green
Alternatively, you can use the $script or the $global scope.
For a simple script to quickly expose your variable, the $scriptscope will do just that. A byref parameter might be easier for the end-user if you intend to distribute your function by making it clear you need to pass a reference parameter.
See About Scopes documentation.
Scopes in PowerShell have both names and numbers. The named scopes
specify an absolute scope. The numbers are relative and reflect the
relationship between scopes.
Global: The scope that is in effect when PowerShell starts. Variables
and functions that are present when PowerShell starts have been
created in the global scope, such as automatic variables and
preference variables. The variables, aliases, and functions in your
PowerShell profiles are also created in the global scope.
Local: The current scope. The local scope can be the global scope or
any other scope.
Script: The scope that is created while a script file runs. Only the
commands in the script run in the script scope. To the commands in a
script, the script scope is the local scope.
Private: Items in private scope cannot be seen outside of the current
scope. You can use private scope to create a private version of an
item with the same name in another scope.
Numbered Scopes: You can refer to scopes by name or by a number that
describes the relative position of one scope to another. Scope 0
represents the current, or local, scope. Scope 1 indicates the
immediate parent scope. Scope 2 indicates the parent of the parent
scope, and so on. Numbered scopes are useful if you have created many
recursive scopes.
I've got some fairly complex functions that I'm writing for a library module, with lots of different ways it can be called. However, it is in fact possible to default all of them, but when I try to call my function with no parameters the call fails because the parameter set cannot be determined.
I would like to define a parameter set that contains no parameters whatsoever, such that calls with no parameters succeed. This is difficult to do since ParameterSetName is a property of the Parameter attribute though, and it's not possible to attribute nothing.
I experimented with the DefaultParameterSet property of the CmdletBinding attribute that is placed on the param block, however nothing seemed to work. It seems that the parameter set name defined there must actually exist in order for Powershell to default to it.
Currently my best approximation of this use case is to define one of the parameter sets to have no Mandatory parameters, however these fail when empty strings or nulls are piped in, and I would like for this not to be the case.
Is this possible?
Sure is. Just specify a default parameter set name that isn't used otherwise:
function Foo {
[CmdletBinding(DefaultParameterSetName='x')]
Param(
[Parameter(Mandatory=$true, ParameterSetName='y')]$a,
[Parameter(Mandatory=$false, ParameterSetName='y')]$b,
[Parameter(Mandatory=$true, ParameterSetName='z')]$c,
[Parameter(Mandatory=$false)]$d
)
"`$a: $a"
"`$b: $b"
"`$c: $c"
"`$d: $d"
}
Suppose I have a function into which a dependency and some parameters are injected like the following:
function Invoke-ACommandLaterOn
{
param
(
# ...
[string] $CommandName,
[object] $PipelineParams,
[object[]] $PositionalParams,
[hashtable]$NamedParams
# ...
)
Assert-ParameterBinding #PSBoundParameters
# ...
# Some complicated long-running call tree that eventually invokes
# something like
# $PipelineParams | & $CommandName #PositionalParams #NamedParams
# ...
}
I would like to immediately assert that binding of the parameters to $CommandName succeeds. That's what Assert-ParameterBinding is meant to do. I'm not exactly sure how to implement Assert-ParameterBinding, however.
Of course I could try to invoke $CommandName immediately, but in this case doing so has side-effects that cannot occur until a bunch of other long-running things are completed first.
How can I assert parameter binding to a function will succeed without invoking the function?
What if you did something like this (inside the Assert- function):
$cmd = Get-Command $CommandName
$meta = [System.Management.Automation.CommandMetadata]::new($cmd)
$proxy = [System.Management.Automation.ProxyCommand]::Create($meta)
$code = $proxy -ireplace '(?sm)(?:begin|process|end)\s*\{.*','begin{}process{}end{}'
$sb = [scriptblock]::Create($code)
$PipeLineParams | & $sb #PositionalParams #NamedParams
I'm actually not sure if it will work with the positional params or with splatting two different sets, off the top of my head (and I didn't do much testing).
Explanation
I had a few thoughts. For one, parameter binding can be very complex. And in the case of a pipeline call, binding happens differently as different blocks are hit.
So it's probably a good idea to let PowerShell handle this, by essentially recreating the same function but with a body that does nothing.
So I went with the built in way to generate a proxy function since it takes care of all that messy work, then brutally replaced the body so that it doesn't actually call the original.
Ideally then, you'll be making a call that follows all the regular parameter binding process but in the end accomplishes nothing.
Wrapping that in a try/catch or otherwise testing for errors should be a pretty good test of whether this was a successful call or not.
This even handles dynamic parameters.
There are probably edge cases where this won't quite work, but I think they will be rare.
Additionally, ValidateScript attributes and dynamic parameters could conceivably create side effects.
I am sure I read somewhere that there is an easy way to pass named parameters from a calling function to a called function without explicitly naming and specifying each parameter.
This is more than just reusing the position; I'm interested in the case where the name of the passed parameters is the same in some cases, but not in others.
I also think there is a way that is not dependent on position.
function called-func {
param([string]$foo, [string]$baz, [string]$bar)
write-debug $baz
write-host $foo,$bar
}
function calling-func {
param([int]$rep = 1, [string]$foo, [string]$bar)
1..$rep | %{
called-func -foo $foo -bar $bar -baz $rep ## <---- Should this be simpler?
}
}
calling-func -rep 10 -foo "Hello" -bar "World"
What would the method be, and is there a link?
I thought it might have been Jeffrey Snover, but I'm not sure.
In PowerShell v2 (which admittedly you may not be ready to move to yet) allows you to pass along parameters without knowing about them ahead of time:
called-func $PSBoundParameters
PSBoundParameters is a dictionary of all the parameters that were actually provided to your function. You can remove parameters you don't want (or add I suppose).
Well, I think I was confusing a blog post I read about switch parameters. As far as I can tell the best way is to just reuse the parameters like so:
called-func -foo:$foo -bar:$bar
How about
called-func $foo $bar