Passing switch parameter thru pipeline in PowerShell
Problem
I am trying to make a function that has a switch parameter, but also I want to able to pass all function parameters thru pipeline in a script, and I don't know ho to do that. Is it that even possible? I my case I load parameters from .csv file in witch values are string values.
Exposition
To simplify my problem and to make it easier for others to use answers of this question, I am not going to use my code but an abstract version of my code. Let us call my function New-Function that has a -StringParameter, a -IntParameter and a -SwitchParameter parameters. And just to be clear in my .csv file all fields are named same as the New-Function parameters.
Using the function
Normally I you can use the New-Function this way:
New-Function -StringParameter "value" -IntParameter 123 -SwitchParameter
But I also want to use the New-Function this way:
$Data = Import-Csv -Path "$PSScriptRoot\Data.csv" -Delimiter ';'
$Data | New-Function
My attempts
I have tried to convert the string values in pipe line to boolean but it seems like the function's -SwitchParameter does not accept boolean($true, $false) values, because it skipping the process block completely when I debug it.
$Data | ForEach-Object -Process {
if ($_.SwitchParameter -eq "true") {
$_.SwitchParameter = $true
}
else {
$_.SwitchParameter = $false
}
} | New-Function
My temporary workaround
I have settled to use a string parameter instead of a switch parameter, so I can feed the New-Function with data thru pipeline from a .csv file with no problem.
function New-Function {
param (
[Parameter(Position = 0, Mandatory, ValueFromPipelineByPropertyName)]
[string]
$StringParameter,
[Parameter(Position = 1, Mandatory, ValueFromPipelineByPropertyName)]
[int]
$IntParameter,
[Parameter(Position = 2, ValueFromPipelineByPropertyName)]
[string]
$SwitchParameter = "false"
)
#----------------------------------------------------------------------------
}
You have to convert values for switch parameter to boolean type.
It works to me:
function Out-Test
{
param
(
[Parameter(ValueFromPipelineByPropertyName)]
[String]
$Label,
[Parameter(ValueFromPipelineByPropertyName)]
[Switch]
$Show
)
process
{
$Color = if ($Show) { 'Yellow' } else { 'Gray' }
Write-Host -ForegroundColor $Color $Label
}
}
$row1 = '' | select Label, Show
$row1.Label = 'First'
$row1.Show = 'True'
$row2 = '' | select Label, Show
$row2.Label = 'Second'
$row1.Show = 'False'
$rows = $row1, $row2
$rows |% { $_.Show = [bool]$_.Show }
$rows | Out-Test
Result:
You can convert your string to a Boolean object while leaving your parameter as type [switch] in your function. The Boolean type will be coerced into [switch] during binding.
$Data | Foreach-Object {
$_.SwitchParameter = [boolean]::Parse($_.SwitchParameter)
$_
} | New-Function
Alternatively, you can update all of your objects first and then pipe to your function. It matters how your function handles the input objects.
$Data | Foreach-Object {
$_.SwitchParameter = [boolean]::Parse($_.SwitchParameter)
}
$Data | New-Function
Part of the issue with your Foreach-Object attempt is that you never output the updated object $_ before piping into your function.
Related
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.
I would like to convert a log file to JSON format.
The content of the Log file is as below:
2021-07-13T14:32:00.197904 DDD client=10.4.35.4
2021-07-13T14:32:00.271923 BBB from=<josh.josh#test.com>
2021-07-13T14:32:01.350434 CCC from=<rob.roder#test.com>
2021-07-13T14:32:01.417904 DDD message-id=<1-2-3-a-a#A1>
2021-07-13T14:32:01.586494 DDD from=<Will.Smith#test.com>
2021-07-13T14:32:02.643101 DDD to=<Will.Smith#test.com>
2021-07-13T14:32:02.712803 AAA client=10.1.35.2
2021-07-13T14:32:03.832661 BBB client=2021:8bd::98e7:2e04:f94s
2021-07-13T14:32:03.920297 DDD status=sent
However the problem that occurs is that I need to match the IDs for each line to export to JSON that looks like:
{
"time": {
"start": "2021-07-13T14:32:01.417904",
"duration": "0:00:02.502393"
},
"sessionid": "DDD",
"client": "10.4.35.4",
"messageid": "<1-2-3-a-a#A1>",
"address": {
"from": "<Will.Smith#test.com>",
"to": "<Will.Smith#test.com>"
},
"status": "sent"
}
]
Next step is to import this data to analysis tool which only acceps JSON format. I've tried this with powershell and python, but got nowhere near the expected output.
The problems along the way:
How to link each row by session?
How to count 1st and last session duration?
How to link each session 3rd column results and how to convert them to json?
I would really appreciate any help, links, studies, etc.
You may do something similar to the following:
Get-Content a.log | Foreach-Object {
if ($_ -match '^(?<time>\S+)\s+(?<sessionid>\w+)\s+(?<key>[^=]+)=(?<data>.*)$') {
[pscustomobject]#{
time = $matches.time
sessionid = $matches.sessionid
key = $matches.key
data = $matches.data
}
}
} | Group sessionid | Foreach-Object {
$jsonTemplate = [pscustomobject]#{
time = [pscustomobject]#{ start = ''; duration = '' }
sessionid = ''
client = ''
messageid = ''
address = [pscustomobject]#{from = ''; to = ''}
status = ''
}
$start = ($_.Group | where key -eq 'message-id').time
$end = ($_.Group | where key -eq 'status').time -as [datetime]
$jsonTemplate.time.start = $start
$jsonTemplate.time.duration = ($end - ($start -as [datetime])).ToString()
$jsonTemplate.sessionid = $_.Name
$jsonTemplate.client = ($_.Group | where key -eq 'client').data
$jsonTemplate.messageid = ($_.Group | where key -eq 'message-id').data
$jsonTemplate.address.from = ($_.Group | where key -eq 'from').data
$jsonTemplate.address.to = ($_.Group | where key -eq 'to').data
$jsonTemplate.status = ($_.Group | where key -eq 'status').data
[regex]::Unescape(($jsonTemplate | convertTo-Json))
}
The general steps that are happening are the following:
Parse the log file to separate data elements
Group by sessionid to easily identify all event entries belonging to the session id
Create a custom object that contains the schema that'll easily convert to the desired JSON format.
The regex unescape is to remove the unicode escape codes for the < and > characters.
The $matches automatic variable updates when the -match operation returns $true. Since we are using named capture groups in the regex expression, capture groups are accessible as keys in the $matches hashtable.
Caveats:
Assumes sessionid only has one session per id.
Missing session data shows up as null in JSON format.
Alternative solutions may use ConvertFrom-String when reading the file. It is just simpler for me personally to do regex matching instead.
A solution based on a switch statement, which enables fast line-by-line processing with its -File parameter:
A nested (ordered) hashtable is used to compile session-specific information across lines.
The -split operator is used to split each line into fields, and to split the last field into property name and value.
Note:
The calculation of the session duration assumes that the first line for a given session ID marks the start of the session, and a line with a status= value the end.
$sessions = [ordered] #{}
switch -File file.log { # process file line by line
default {
$timestamp, $sessionId, $property = -split $_ # split the line into fields
$name, $value = $property -split '=', 2 # split the property into name an value
if ($session = $sessions.$sessionId) { # entry for session ID already exists
$session.$name = $value
# end of session? calculate the duration
if ($name -eq 'status') { $session.time.duration = ([datetime] $timestamp - [datetime] $session.time.start).ToString() }
}
else { # create new entry for this session ID
$sessions.$sessionId = [ordered] #{
$name = $value
time = [ordered] #{
start = $timestamp
duration = $null
}
}
}
}
}
# Convert the hashtable to JSON
$sessions | ConvertTo-Json
I created a function with some inspiration from here.
It's working as expected so far but only when an Option Switch is provided when calling the function.
What I want to do is just be able type "Get-ExternalIP" without providing a switch. The function should then automatically use the -All switch. Also I'm not sure why the -All switch even works...
I've tried to set the Parameter $All = $true it's not working and VSCode tells me that it's not recommended anyways.
Is there a way to automatically pass along the -All Option Switch when the function is called without any parameters?
Would somebody be able to explain why the function returns all info if the -All switch is specified?
Thanks!
Here's my code:
function Get-ExternalIP {
[CmdletBinding()]
param (
[parameter(Mandatory = $false)][switch]$Ip,
[parameter(Mandatory = $false)][switch]$HostName,
[parameter(Mandatory = $false)][switch]$City,
[parameter(Mandatory = $false)][switch]$Region,
[parameter(Mandatory = $false)][switch]$Country,
[parameter(Mandatory = $false)][switch]$Location,
[parameter(Mandatory = $false)][switch]$Provider,
[parameter(Mandatory = $false)][switch]$PostalCode,
[parameter(Mandatory = $false)][switch]$TimeZone,
[parameter(Mandatory = $false)][switch]$All
)
$IpInfo = Invoke-RestMethod https://ipinfo.io/json
switch ($PSBoundParameters.Values) {
$Ip { $IpInfo.ip }
$HostName { $IpInfo.hostname }
$City { $IpInfo.city }
$Region { $IpInfo.region }
$Country { $IpInfo.country }
$Location { $IpInfo.loc }
$Provider { $IpInfo.org }
$PostalCode { $IpInfo.postal }
$TimeZone { $IpInfo.timezone }
Default { $IpInfo }
}
}
Get-ExternalIP
I would use the $PSBoundParameters.Keys as your switches are properly named to match the returned info:
function Get-ExternalIP {
[CmdletBinding()]
param (
[parameter(Mandatory=$false)][switch]$Ip,
[parameter(Mandatory=$false)][switch]$HostName,
[parameter(Mandatory=$false)][switch]$City,
[parameter(Mandatory=$false)][switch]$Region,
[parameter(Mandatory=$false)][switch]$Country,
[parameter(Mandatory=$false)][switch]$Location,
[parameter(Mandatory=$false)][switch]$Provider,
[parameter(Mandatory=$false)][switch]$PostalCode,
[parameter(Mandatory=$false)][switch]$TimeZone,
[parameter(Mandatory=$false)][switch]$All
)
$IpInfo = Invoke-RestMethod https://ipinfo.io/json
if ($All) { return $IpInfo }
# exclude Common Parameters
$commonParams = 'Debug','ErrorAction','ErrorVariable','InformationAction','InformationVariable','OutVariable',
'OutBuffer','PipelineVariable','Verbose','WarningAction','WarningVariable','WhatIf','Confirm'
$items = (#($PSBoundParameters.Keys) | Where-Object { $commonParams -notcontains $_ }) -replace
'Location', 'loc' -replace 'Provider', 'org' -replace 'PostalCode', 'postal'
$IpInfo | Select-Object $items
}
As mklement0 commented, there is a much better way of retrieving the used switches than shown in the above code.
Instead of filtering out the known Common Parameters, it makes more sense to check the $PSBoundParameter keys against the properties returned in the $IpInfo object.
$items = ($PSBoundParameters.Keys -replace 'Location', 'loc' -replace 'Provider', 'org' -replace 'PostalCode', 'postal').Where({ $_ -in $IpInfo.psobject.Properties.Name })
You could use parameter sets to default to the All option:
[CmdletBinding(DefaultParameterSetName='Everything')]
param(
[Parameter(ParameterSetName='Something')][switch]$Ip,
[Parameter(ParameterSetName='Something')][switch]$HostName,
[Parameter(ParameterSetName='Something')][switch]$City,
[Parameter(ParameterSetName='Something')][switch]$Region,
[Parameter(ParameterSetName='Something')][switch]$Country,
[Parameter(ParameterSetName='Something')][switch]$Location,
[Parameter(ParameterSetName='Something')][switch]$Provider,
[Parameter(ParameterSetName='Something')][switch]$PostalCode,
[Parameter(ParameterSetName='Something')][switch]$TimeZone,
[Parameter(ParameterSetName='Everything')][switch]$All
)
# set up dictionary to hold the switch-name-to-ipinfo-names
$Options = [Ordered]#{
'Ip' = 'ip'
'HostName' = 'hostname'
'City' = 'city'
'Region' = 'region'
'Country' = 'country'
'Location' = 'loc'
'Provider' = 'org'
'PostalCode' = 'postal'
'TimeZone' = 'timezone'
}
$IpInfo = Invoke-RestMethod https://ipinfo.io/json
# Select all options
$selected = $Options.Keys
if($PSCmdlet.ParameterSetName -ne 'Everything'){
# 1 or more switches were passed, select only those options
$selected = $selected.Where({$PSBoundParameters[$_]})
}
# Create a new object with the selected options as properties
$properties = [ordered]#{}
foreach($prop in $selected){
$properties[$prop] = $IpInfo.($Options[$prop])
}
# return the new object
return [pscustomobject]$properties
By using the parameter set name to determine whether to output everything (and then return), the behavior is the same regardless of whether the user passes -All, or no switches at all.
Mathias R. Jessen's answer and Theo's answer provide elegant solutions; let me complement them with an answer to your specific question:
Would somebody be able to explain why the function returns all info if the -All switch is specified?
Your switch statement has a Default branch, which is invoked if none of the other branches' conditions are met; since you don't have a branch for the -All switch value, the Default handler kicks in if
-All was specified.
By contrast, if you don't pass any argument, the switch statement is never entered, because, for collection-valued input, the switch statement implicitly loops over the elements of that collection.
With no arguments passed, $PSBoundParameters.Values is an empty collection, so there is nothing to enumerate.
The parameter-set approach in Mathias' answer is the simplest solution to this problem, which has the added advantage of making the -All switch and the specific property-name switches mutually exclusive.
If you print out what $psboundparameters.values is, you'll see it's empty when you don't use "-all", otherwise it has one value of $true.
It seems that PowerShell adds an additional variable to the return value of a function.
The function subfoo2 itself delivers the correct values, but as soon as PowerShell jumps back to the postion where I called the function (in foo1), value contains the value of an other variable ($msg)
(Have a look at the comments in the code)
writeMessageLog($msg){
...
Add-Content $msg
...
}
subfoo2{
writeMessageLog($msg)
return $UserArrayWithValues #During Debug, $Array is fine (1)
}
foo1{
$var = subfoo2 $UserArray # $var has now the value of $msg and $UserArrayWithValues (2)
#do something with var
}
Realcode:
function WriteLog
{
param ( [string] $severity , $msgNumber, [string] $msg )
...
$msgOut = $date + ... + $msg
Add-Content $msgout ( $msgOut )
...
}
function getFeatures
{
writelog 'I' 1002 $true $true "Load Features"
$Features = importCsv -pPath $FeatureDefintionFilePath
Writelog 'I' 1000 $true $true "Features Loaded"
return $Features # $Features has value as expected (1)
}
function GetUserFeatures ($pUserObject)
{
$SfBFeatures = ""
$SfBFeatures = getFeatures #SfBFeaures has Value of $msg and $Features (2)
...
}
Do I use the functions/return values wrong? What could lead to such behavior? Is it an issue if i call a function within a function?
If I remove $msgOut = $date + ... + $msg in writeMessageLog, the values are fine.
I'm pretty lost right now, and have no ideas where this comes from. Any ideas welcome.
This is how powershell works, basically everything that you print out will be returned as the function output. So don't output extra stuff. To force something to not output stuff you can do:
$null = some-command_that_outputs_unwanted_things
since everybody is obsessed with Out-Null I'll add this link showing several other ways to do that.
Within a function, everything you don't assign or pipe to a consuming cmdlet will get put to the pipeline and returned from the function - even if you don't explicit return it. In fact the return keyword doesn't do anything in PowerShell so the following is equivalent:
function Test-Func
{
"Hello World"
}
function Test-Func
{
return "Hello World"
}
So it looks like your writeMessageLog puts anything on the pipeline thus you have to either assign the value to anything:
$notUsed = writeMessageLog($msg)
or (prefered) pipe it to the Out-Null cmdlet:
writeMessageLog($msg) | Out-Null
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))}
}