Is there a way in powershell to catch ALL named parameters - function

Consider this:
Function Foo
{
param(
#????
)
}
I want to call Foo like this:
Foo -Bar "test"
Without it blowing up that I don't have a $bar param specified. Is that possible? :)
Update:
I want this to work:
Function IfFunctionExistsExecute
{
param ([parameter(Mandatory=$true)][string]$func, [parameter(Mandatory=$false)][string]$args)
begin
{
# ...
}
process
{
if(Get-Command $func -ea SilentlyContinue)
{
& $func $args # the amperersand invokes the function instead of just printing the variable
}
else
{
# ignore
}
}
end
{
# ...
}
}
Function Foo
{
param([string]$anotherParam)
process
{
$anotherParam
}
}
IfFunctionExistsExecute Foo -Test "bar"
This gives me:
IfFunctionExistsExecute : A parameter cannot be found that matches parameter name 'Test'.
At C:\PSTests\Test.ps1:35 char:34
+ IfFunctionExistsExecute Foo -Test <<<< "bar"
+ CategoryInfo : InvalidArgument: (:) [IfFunctionExistsExecute], ParameterBindingException
+ FullyQualifiedErrorId : NamedParameterNotFound,IfFunctionExistsExecute

I would suggest two alternatives.
First: you may want to consider passing whole function + it's parameters as scriptblock parameter to your ifFunction...
OR: use ValueFromRemainingArguments:
function Test-SelfBound {
param (
[Parameter(
Mandatory = $true,
HelpMessage = 'Help!'
)]
[string]$First,
[Parameter(
ValueFromRemainingArguments = $true
)]
[Object[]]$MyArgs
)
$Arguments = foreach ($Argument in $MyArgs) {
if ($Argument -match '^-([a-z]+)$') {
$Name = $Matches[1]
$foreach.MoveNext() | Out-Null
$Value = $foreach.Current
New-Variable -Name $Name -Value $Value
$PSBoundParameters.Add($Name,$Value) | Out-Null
} else {
$Argument
}
}
$PSBoundParameters | Out-Default
"Positional"
$Arguments
}
Test-SelfBound -First Test -Next Foo -Last Bar Alfa Beta Gamma
In this case I use $MyArgs to store everything besides my mandatory parameter 'First'. Than some simple if will tell me if it's named parameter (-Next, -Last) or positional (Alfa, Beta, Gamma). This way you can have both advantages of advanced functions binding (whole [Parameter()] decoration) AND leave room for $args-style parameters too.

You could use the $args variable in your function which is an array of the arguments passed into the function, e.g.
function Foo()
{
Write-Output $args[0];
Write-Output $args[1];
}
Foo -Bar "test"
Outputs:
-Bar
test

Related

PowerShell - How do I iterate a PSCustomObject nested object?

I feel like this is something simple and I'm just not getting it, and I'm not sure if my explanation is great.
I have this below JSON file, and I want to get "each App" (App1, App2, App3) under the "New" object
In this script line below I'm essentially trying to replace "TestApp2" with some variable. I guess I'm trying to get TestApp2 as an object without knowing the name.
And I realize that the foreach loop doesn't do anything right now
Write-Host $object.Value.TestApp2.reply_urls
JSON:
{
"New": {
"App1": {
"reply_urls": [
"https://testapp1url1"
]
},
"App2": {
"reply_urls": [
"https://testapp2url1",
"https://testapp2url2"
]
},
"App3": {
"reply_urls": [
"https://testapp3url1",
"https://testapp3url2",
"https://testapp3url3"
]
}
},
"Remove": {
"object_id": [
""
]
}
}
Script:
$inputFile = Get-Content -Path $inputFilePath -Raw | ConvertFrom-Json
foreach ($object in $inputFile.PsObject.Properties)
{
switch ($object.Name)
{
New
{
foreach ($app in $object.Value)
{
Write-Host $object.Value.TestApp2.reply_urls
# essentially want to replace this line with something like
# Write-Host $app.reply_urls
}
}
Remove
{
}
}
}
Output:
https://testapp2url1 https://testapp2url2
You can access the object's PSObject.Properties to get the property Names and property Values, which you can use to iterate over.
For example:
foreach($obj in $json.New.PSObject.Properties) {
$out = [ordered]#{ App = $obj.Name }
foreach($url in $obj.Value.PSObject.Properties) {
$out[$url.Name] = $url.Value
}
[pscustomobject] $out
}
Produces the following output:
App reply_urls
--- ----------
App1 {https://testapp1url1}
App2 {https://testapp2url1, https://testapp2url2}
App3 {https://testapp3url1, https://testapp3url2, https://testapp3url3}
If you just want to output the URL you can skip the construction of the PSCustomObject:
foreach($obj in $json.New.PSObject.Properties) {
foreach($url in $obj.Value.PSObject.Properties) {
$url.Value
}
}
Complementing Santiago Squarzon's helpful answer, I've looked for a generalized approach to get JSON properties without knowing the names of their parents in advance.
Pure PowerShell solution
I wrote a little helper function Expand-JsonProperties that "flattens" the JSON. This allows us to use simple non-recursive Where-Object queries for finding properties, regardless how deeply nested they are.
Function Expand-JsonProperties {
[CmdletBinding()]
param (
[Parameter(Mandatory, ValueFromPipeline)] [PSCustomObject] $Json,
[Parameter()] [string] $Path,
[Parameter()] [string] $Separator = '/'
)
process {
$Json.PSObject.Properties.ForEach{
$propertyPath = if( $Path ) { "$Path$Separator$($_.Name)" } else { $_.Name }
if( $_.Value -is [PSCustomObject] ) {
Expand-JsonProperties $_.Value $propertyPath
}
else {
[PSCustomObject]#{
Path = $propertyPath
Value = $_.Value
}
}
}
}
}
Given your XML sample we can now write:
$inputFile = Get-Content -Path $inputFilePath -Raw | ConvertFrom-Json
$inputFile | Expand-JsonProperties | Where-Object Path -like '*/reply_urls'
Output:
Path Value
---- -----
New/App1/reply_urls {https://testapp1url1}
New/App2/reply_urls {https://testapp2url1, https://testapp2url2}
New/App3/reply_urls {https://testapp3url1, https://testapp3url2, https://testapp3url3}
Optimized solution using inline C#
Out of curiosity I've tried out a few different algorithms, including ones that don't require recursion.
One of the fastest algorithms is written in inline C# but can be called through an easy to use PowerShell wrapper cmdlet (see below). The C# code basically works the same as the pure PowerShell function but turned out to be more than 9 times faster!
This requires at least PowerShell 7.x.
# Define inline C# class that does most of the work.
Add-Type -TypeDefinition #'
using System;
using System.Collections.Generic;
using System.Management.Automation;
public class ExpandPSObjectOptions {
public bool IncludeObjects = false;
public bool IncludeLeafs = true;
public string Separator = "/";
}
public class ExpandPSObjectRecursive {
public static IEnumerable< KeyValuePair< string, object > > Expand(
PSObject inputObject, string parentPath, ExpandPSObjectOptions options ) {
foreach( var property in inputObject.Properties ) {
var propertyPath = parentPath + options.Separator + property.Name;
if( property.Value is PSObject ) {
if( options.IncludeObjects ) {
yield return new KeyValuePair< string, object >( propertyPath, property.Value );
}
// Recursion
foreach( var prop in Expand( (PSObject) property.Value, propertyPath, options ) ) {
yield return prop;
}
continue;
}
if( options.IncludeLeafs ) {
yield return new KeyValuePair< string, object >( propertyPath, property.Value );
}
}
}
}
'#
# A PowerShell cmdlet that wraps the C# class.
Function Expand-PSObjectRecursive {
[CmdletBinding()]
param (
[Parameter(Mandatory, ValueFromPipeline)] [PSObject] $InputObject,
[Parameter()] [string] $Separator = '/',
[Parameter()] [switch] $IncludeObjects,
[Parameter()] [switch] $ExcludeLeafs
)
process {
$options = [ExpandPSObjectOptions]::new()
$options.IncludeObjects = $IncludeObjects.ToBool()
$options.IncludeLeafs = -not $ExcludeLeafs.ToBool()
$options.Separator = $Separator
[ExpandPSObjectRecursive]::Expand( $InputObject, '', $options )
}
}
The C# code is wrapped by a normal PowerShell cmdlet, so you can basically use it in the same way as the pure PowerShell function, with minor syntactic differences:
$inputFile | Expand-PSObjectRecursive | Where-Object Key -like '*/reply_urls'
I've added some other useful parameters that allows you to define the kind of elements that the cmdlet should output:
$inputFile |
Expand-PSObjectRecursive -IncludeObjects -ExcludeLeafs |
Where-Object Key -like '*/App*'
Parameter -IncludeObjects also includes PSObject properties from the input, while -ExcludeLeafs excludes the value-type properties, resulting in this output:
Key Value
--- -----
/New/App1 #{reply_urls=System.Object[]}
/New/App2 #{reply_urls=System.Object[]}
/New/App3 #{reply_urls=System.Object[]}
While the table format output in itself is not too useful, you could use the output objects for further processing, e. g.:
$apps = $inputFile |
Expand-PSObjectRecursive -IncludeObjects -ExcludeLeafs |
Where-Object Key -like '*/App*'
$apps.Value.reply_urls
Prints:
https://testapp1url1
https://testapp2url1
https://testapp2url2
https://testapp3url1
https://testapp3url2
https://testapp3url3
Implementation notes:
The C# code uses the yield return statement to return properties one-by-one, similar to what we are used from PowerShell pipelines. In inline C# code we can't use the pipeline directly (and it wouldn't be advisable for performance reasons), but yield return allows us to smoothly interface with PowerShell code that can be part of a pipeline.
PowerShell automatically inserts the return values of the C# function into the success output stream, one by one. The difference to returning an array from C# is that we may exit early at any point without having to process the whole input object first (e. g. using Select -First n). This would actually cancel the C# function, something that would otherwise only be possible using multithreading (with all its complexities)! But all of this is just single-threaded code.

Powershell Start-job invoke function argument with parameter

I have a function mainFunction that gets 2 parameters - $name will be just a regular string, and $moveFunction will be some function.
I want to start a job of a ScriptBlock ($SB) that will invoke $moveFunction with $name as his argument.
function foo($a){
Write-Output "In function foo with the argument => $a"
}
$SB = {
param($C, $fooFunction)
$fooFunction.Invoke($C)
}
function mainFunction($name, $moveFunction){
Start-Job -Name "currentJob" -ArgumentList $name, ${Function:$moveFunction} -ScriptBlock $SB
}
$j1 = mainFunction -name "output!" -moveFunction $Function:foo
I checked that $moveFunction exists in mainFunction already ($moveFunction.invoke(5) at mainFunction)
I can't find the problem in passing the function as argument in the start-job.
and from Get-Job -Name "CurrentJob" | Receive-Job I get:
You cannot call a method on a null-valued expression.
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
+ PSComputerName : localhost
Any help would be appreciated.
edit:
The problem is most likely the way I pass the function as an argument (${Function:$moveFunction}
Just a rehash of my previous comment plus code example. Similar issue here. Essentially, arguments passed to Jobs and Remote commands are serialized. During the de-serialization process, functions and script blocks come out as strings instead of their original type. Fortunately it's a simple process to transform these into invokable scriptblocks using [ScriptBlock]::Create("string").
function foo {
write-host "foo"
}
function bar {
# This argument comes in as a string
param($func)
write-host "bar"
# Create scriptblock from string
$func = [ScriptBlock]::Create($func)
$func.invoke()
}
Start-Job -ArgumentList $Function:Foo -ScriptBlock $Function:Bar
Get-Job | Wait-job
Get-Job | Receive-job
You passing the same function and invoking it. You can directly use the function in the job.
Start-Job -Name "currentJob" -ArgumentList $name - ScriptBlock ${function:foo}

When using Powershell's Dynamic parameters can I suppress Errors?

When using Powershell's Dynamic parameters can I suppress Errors?
Specifically the error being:
f foo
Search-FrequentDirectory : Cannot validate argument on parameter 'dirSearch'. The argument "foo" does not belong to the set
"bin,omega,ehiller,psmodules,deploy,gh.riotgames.com,build-go,vim74,cmder,dzr,vimfiles,src,openssh,git" specified by the ValidateSet attribute. Supply an argument
that is in the set and then try the command again.
At line:1 char:3
+ f foo
+ ~~~
+ CategoryInfo : InvalidData: (:) [Search-FrequentDirectory], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Search-FrequentDirectory
Dynamic parameters being:
DynamicParam {
$dirSearch = new-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
# [parameter(mandatory=...,
# ...
# )]
$dirSearchParamAttribute = new-object System.Management.Automation.ParameterAttribute
$dirSearchParamAttribute.Mandatory = $true
$dirSearchParamAttribute.Position = 1
$dirSearchParamAttribute.HelpMessage = "Enter one or more module names, separated by commas"
$dirSearch.Add($dirSearchParamAttribute)
# [ValidateSet[(...)]
$dirPossibles = #()
$historyFile = (Get-PSReadlineOption).HistorySavePath
# directory Seperating character for the os; \ (escaped to \\) for windows (as C:\Users\); / for linux (as in /var/www/);
# a catch all would be \\\/ ; but this invalidates the whitespace escape character that may be used mid-drectory.
$dirSep = "\\"
# Group[1] = Directory , Group[length-1] = lowest folder
$regex = "^[[:blank:]]*cd ([a-zA-Z\~:]+([$dirSep][^$dirSep]+)*[$dirSep]([^$dirSep]+)[$dirSep]?)$"
# original: ^[[:blank:]]*cd [a-zA-Z\~:\\\/]+([^\\\/]+[\\\/]?)*[\\\/]([^\\\/]+)[\/\\]?$
# test for historyFile existance
if( -not (Test-Path $historyFile )){
Write-Warning "File $historyFile not found, unable to load command history. Exiting.";
return 1;
}
$historyLines = Get-Content $historyFile
# create a hash table, format of ;;; [directory path] = [lowest directory]
$searchHistory = #{}
# create a hash table for the count (number of times the command has been run)
$searchCount = #{}
ForEach ( $line in $historyLines ) {
if( $line -match $regex ){
try {
# since the matches index can change, and a hashtable.count is not a valid way to find the index...
# I need this to figure out the highest integer index
$lowestDirectory = $matches[($matches.keys | sort -Descending | Select-Object -First 1)]
$fullPath = $matches[1]
if($searchHistory.keys -notcontains $matches[1]){
$searchHistory.Add($matches[1],$lowestDirectory)
}
$searchCount[$fullPath] = 1
} catch {
$searchCount[$fullPath]++
}
}
}
# this helps with hashtables
# https://www.simple-talk.com/sysadmin/powershell/powershell-one-liners-collections-hashtables-arrays-and-strings/
$dirPossibles = ( $searchHistory.values | Select -Unique )
$modulesValidated_SetAttribute = New-Object -type System.Management.Automation.ValidateSetAttribute($dirPossibles)
$dirSearch.Add($modulesValidated_SetAttribute)
# Remaining boilerplate
$dirSearchDefinition = new-object -Type System.Management.Automation.RuntimeDefinedParameter("dirSearch", [String[]], $dirSearch)
$paramDictionary = new-object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
$paramDictionary.Add("dirSearch", $dirSearchDefinition)
return $paramDictionary
}
The function works, when I'm in the set everything is great. When I miskey or whatever, I get a rather unpleasant looking (non-user friendly error) - which I would like to style.
Is there a way to do this? To suppress the error? I tried try / catch and it was a no go, and I haven't been able to find much else on this - that is, error suppression in dynamic parameters.
I found a way to do it, but not sure I really recommend its use, and it has some downsides of duplicated code. Maybe there is a way to solve this better, but I did this more as an exercise of if it could be done.
Code Get-DynamicParamTestCustom.ps1
<#
Reference: http://blog.enowsoftware.com/solutions-engine/bid/185867/Powershell-Upping-your-Parameter-Validation-Game-with-Dynamic-Parameters-Part-II
#>
[CmdletBinding()]
param (
)
DynamicParam {
function New-ValidationDynamicParam {
[CmdletBinding()]
[OutputType('System.Management.Automation.RuntimeDefinedParameter')]
param (
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$Name,
[ValidateNotNullOrEmpty()]
[Parameter(Mandatory)]
[array]$ValidateSetOptions,
[Parameter()]
[switch]$Mandatory = $false,
[Parameter()]
[string]$ParameterSetName = '__AllParameterSets',
[Parameter()]
[switch]$ValueFromPipeline = $false,
[Parameter()]
[switch]$ValueFromPipelineByPropertyName = $false
)
$AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$ParamAttrib = New-Object System.Management.Automation.ParameterAttribute
$ParamAttrib.Mandatory = $Mandatory.IsPresent
$ParamAttrib.ParameterSetName = $ParameterSetName
$ParamAttrib.ValueFromPipeline = $ValueFromPipeline.IsPresent
$ParamAttrib.ValueFromPipelineByPropertyName = $ValueFromPipelineByPropertyName.IsPresent
$AttribColl.Add($ParamAttrib)
$AttribColl.Add((New-Object System.Management.Automation.ValidateSetAttribute($Param.ValidateSetOptions)))
$RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter($Param.Name, [string], $AttribColl)
$RuntimeParam
}
function Get-ValidValues
{
# get list of valid values
$validValues = #()
$validValues += 'a'
$validValues += 'b'
$validValues += 'c'
$validValues += $global:dynamic1Value
$validValues
}
# coerce the current passed value into our list, and we detect later
# to customize message
# the heart of this problem is getting the param from the Call Stack
# and stashing it away (a hack, but it IS a solution).
$line = (Get-PSCallStack | Select -First 1 | Select *).InvocationInfo.Line
# parse this for the command line arg
# TODO: make this more robust
$null = $line -match "-Dynamic1 (.*?)(\s+|$)"
$global:dynamic1Value = $Matches[1]
$ParamOptions = #(
#{
'Name' = 'Dynamic1';
'Mandatory' = $true;
'ValidateSetOptions' = Get-ValidValues
}
)
$RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
foreach ($Param in $ParamOptions)
{
$RuntimeParam = New-ValidationDynamicParam #Param
$RuntimeParamDic.Add($Param.Name, $RuntimeParam)
}
return $RuntimeParamDic
}
begin
{
$PsBoundParameters.GetEnumerator() | foreach { New-Variable -Name $_.Key -Value $_.Value -ea 'SilentlyContinue'}
}
process
{
# not sure how else to write this because the function needs to be inside
# DynamicParam{} block for its usage, and here for 'process' usage.
function Get-ValidValuesReal
{
# get list of valid values
$validValues = #()
$validValues += 'a'
$validValues += 'b'
$validValues += 'c'
$validValues
}
function foo
{
}
Write-Output "global:dynamic1Value is: '$($global:dynamic1Value)'."
Write-Output "Dynamic1 is: '$($Dynamic1)'."
$realValues = Get-ValidValuesReal
if ($global:dynamic1Value -notin $realValues)
{
Write-Error "Hey, '$global:dynamic1Value' is not allowed."
}
else
{
Write-Output "Dynamic1 is: '$($Dynamic1)' and is cool."
}
}
end {}
Test Cases
.\Get-DynamicParamTestCustom.ps1 -Dynamic1 t
.\Get-DynamicParamTestCustom.ps1 -Dynamic1 test
.\Get-DynamicParamTestCustom.ps1 -Dynamic1 test -Verbpse
.\Get-DynamicParamTestCustom.ps1 -Dynamic1 a

Function overloading in PowerShell

Can you overload functions in PowerShell?
I want to my function to accept a string, array or some switch.
An example of what I want:
Backup-UsersData singleUser
Backup-UsersData #('Alice', 'Bob',
'Joe')
Backup-UsersData -all
In PowerShell functions are not overloaded. The last definition overrides the previous in the same scope or hides the previous in a parent scope. Thus, you should create a single function and provide a way to distinguish its call mode by arguments.
In V2 you may use an advanced function, see help about_Functions_Advanced_Parameters and avoid some manual coding on resolving parameter set ambiguities:
# advanced function with 3 parameter sets
function Backup-UsersData
(
[Parameter(Position=0, ParameterSetName="user")]
[string]$user,
[Parameter(Position=0, ParameterSetName="array")]
[object[]]$array,
[Parameter(Position=0, ParameterSetName="all")]
[switch]$all
)
{
# use this to get the parameter set name
$PSCmdlet.ParameterSetName
}
# test
Backup-UsersData -user 'John'
Backup-UsersData 1, 2
Backup-UsersData -all
# OUTPUT:
# user
# array
# all
Note that this mechanism is sometimes strange. For example in the first test we have to specify parameter name -user explicitly. Otherwise:
Backup-UsersData : Parameter set cannot be resolved using the specified named parameters.
At C:\TEMP\_101015_110059\try2.ps1:21 char:17
+ Backup-UsersData <<<< 'John'
+ CategoryInfo : InvalidArgument: (:) [Backup-UsersData], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : AmbiguousParameterSet,Backup-UsersData
In many cases standard, not advanced, function with mixed parameters will do:
function Backup-UsersData
(
[string]$user,
[object[]]$array,
[switch]$all
)
{
if ($user) {'user'}
elseif ($array) {'array'}
elseif ($all) {'all'}
else {'may be'}
}
Backup-UsersData -user 'John'
Backup-UsersData -array 1, 2
Backup-UsersData -all
Backup-UsersData
But in this case you should resolve (or accept and ignore) ambiguities, e.g. to decide what to do if, say:
Backup-UsersData -user 'John' -array 1, 2 -all
Here is a variant of Roman's answer that I think is a little more flexible:
function Backup
{
[CmdletBinding(DefaultParameterSetName='Users')]
Param (
[parameter(mandatory=$true, ParameterSetName='Users', position=0, ValueFromPipeline=$true)][string[]]$User,
[parameter(mandatory=$true, ParameterSetName='AllUsers')][switch]$All
)
Begin
{
if ($All) { $User = #('User1', 'User2', 'User3') }
}
Process
{
foreach ($u in $User)
{
echo "Backup $u"
}
}
}
1) Build a class...
class c1 {
[int]f1( [string]$x ){ return 1 }
[int]f1( [int ]$x ){ return 2 }
}
1+) Use STATIC METHODS if you prefer to call them without instantiation...
class c1 {
static [int] f1( [string]$x ){ return 1 }
static [int] f1( [int]$x ){ return 2 }
}
2) Call the methods in class or object... overload works OK
$o1 = [c1]::new()
o1.f1( "abc" ) ~> returns 1
o1.f1( 123 ) ~> returns 2
-OR-
[c1]::f1( "abc" ) ~> returns 1
[c1]::f1( 123 ) ~> returns 2
3)
If (like me)
you want to have "Overloaded Functions" placed in a libraries...
so your users can use them transparently...
from code or from Interactive Command Line (REPL)...
the closest I could came to
"Overloading functions in Powershell"
was something like this:
function Alert-String() { [c1]::f1( "abc" ) }
function Alert-Strings(){ [c1]::f1( 123 ) }
function Alert-Stringn(){ [c1]::f1( 123 ) }
Maybe in PS-Core v8??? ;-)
Hope it helps...
If you use PSObject instead of Object to define your parameter type, it should work.
For example, The function Get-Control, know's how to overload based on type string or template and can be called using the positional value:
Get-Control "A-Name-Of-A-Control"
Get-Control $template
To make the overload work, use PSObject as follows:
Function Get-Control {
Param(
[Parameter(Mandatory=$False,ParameterSetName="ByTemplate",Position=0)]
[PSObject]
$Template,
[Parameter(Mandatory=$False,ParameterSetName="ByName",Position=0)]
[String]
$Name,
[Parameter(Mandatory=$False)]
[Switch]
$List
)
... # remaining code removed for brevity

Parameters with default value not in PsBoundParameters?

General code
Consider this code:
PS> function Test { param($p='default value') $PsBoundParameters }
PS> Test 'some value'
Key Value
--- -----
p some value
PS> Test
# nothing
I would expect that $PsBoundParameters would contain record for $p variable on both cases. Is that correct behaviour?
Question
I'd like to use splatting that would work like this for a lot of functions:
function SomeFuncWithManyRequiredParams {
param(
[Parameter(Mandatory=$true)][string]$p1,
[Parameter(Mandatory=$true)][string]$p2,
[Parameter(Mandatory=$true)][string]$p3,
...other parameters
)
...
}
function SimplifiedFuncWithDefaultValues {
param(
[Parameter(Mandatory=$false)][string]$p1='default for p1',
[Parameter(Mandatory=$false)][string]$p2='default for p2',
[Parameter(Mandatory=$false)][string]$p3='default for p3',
...other parameters
)
SomeFuncWithManyRequiredParams #PsBoundParameters
}
I don't want to call SomeFuncWithManyRequiredParams with all the params enumerated:
SomeFuncWithManyRequiredParams -p1 $p1 -p2 $p2 -p3 $p3 ...
Is it possible?
I know this question is very old, but I had a need for something like this recently (wanted to do splatting with a lot of default parameters). I came up with this and it worked very well:
$params = #{}
foreach($h in $MyInvocation.MyCommand.Parameters.GetEnumerator()) {
try {
$key = $h.Key
$val = Get-Variable -Name $key -ErrorAction Stop | Select-Object -ExpandProperty Value -ErrorAction Stop
if (([String]::IsNullOrEmpty($val) -and (!$PSBoundParameters.ContainsKey($key)))) {
throw "A blank value that wasn't supplied by the user."
}
Write-Verbose "$key => '$val'"
$params[$key] = $val
} catch {}
}
Shameless plug ahead: I decided to turn this into a blog post which has more explanation and a sample usage script.
It depends on how you define "bound" I guess i.e. is the value bound from a user supplied value or a default value supplied by the function? Honestly, it doesn't surprise me that it behaves the way it does as I view "bound" to mean the former - bound from user input. Anyway, you can solve this by patching the $PSBoundParameters variable e.g.:
function SimplifiedFuncWithDefaultValues {
param(
[Parameter(Mandatory=$false)][string]$p1='default for p1',
[Parameter(Mandatory=$false)][string]$p2='default for p2',
[Parameter(Mandatory=$false)][string]$p3='default for p3',
...other parameters
)
if (!$PSBoundParameters.ContainsKey(p1))
{
$PSBoundParameters.p1 = 'default for p1'
}
# rinse and repeat for other default parameters.
SomeFuncWithManyRequiredParams #PSBoundParameters
}
This is what I like to do:
foreach($localP in $MyInvocation.MyCommand.Parameters.Keys)
{
if(!$PSBoundParameters.ContainsKey($localP))
{
$PSBoundParameters.Add($localP, (Get-Variable -Name $localP -ValueOnly))
}
}
You could use a helper function similar to the Add-Variable function below:
function SimplifiedFuncWithDefaultValues {
param(
[Parameter(Mandatory=$false)][string]$p1='default for p1',
[Parameter(Mandatory=$false)][string]$p2='default for p2',
[Parameter(Mandatory=$false)][string]$p3='default for p3',
...other parameters
)
$PSBoundParameters | Add-Variable p1, p2, p3
SomeFuncWithManyRequiredParams #PSBoundParameters
}
function Add-Variable {
param(
[Parameter(Position = 0)]
[AllowEmptyCollection()]
[string[]] $Name = #(),
[Parameter(Position = 1, ValueFromPipeline, Mandatory)]
$InputObject
)
$Name |
? {-not $InputObject.ContainsKey($_)} |
% {$InputObject.Add($_, (gv $_ -Scope 1 -ValueOnly))}
}