POWERSHELL - Handling Parameter Sets Gracefully [duplicate] - powershell

This question already has answers here:
Which parameter set has been used?
(2 answers)
Closed last month.
I have a function:
Function Get-CMAPICollection {
[CmdletBinding()]
param (
[parameter(ValueFromPipeline = $true, Mandatory = $True, ParameterSetName = "ByName")]
[string]$collectionName,
[parameter(ValueFromPipeline = $true, Mandatory = $True, ParameterSetName = "ById")]
[string]$collectionId
)
If ($collectionName) {
$URI = "https://$($CMAPI.cmServer)/AdminService/wmi/SMS_Collection?`$filter=Name eq '$collectionName'"
}
If ($collectionId) {
$URI = "https://$($CMAPI.cmServer)/AdminService/wmi/SMS_Collection?`$filter=CollectionId eq '$collectionId'"
}
(Send-CMAPIRequest -method GET -URI $uri).Value
}
Which all works fine.
I'm wondering if there is a better way to dynamically handle the parameters in the function than a bunch of 'If' statements.
My concern is that if I need to add a whole lot of parameter sets, the functions itself is going to have a whole bunch of 'If' statements cluttering up the place.
No matter how far I abstract it out, there is still an 'If' collecionName or 'If' collectionId.
Thanks!

As always, literally minutes after posting I have found the answer. Provided by this answer here:
Which parameter set has been used?
Using the variable:
$PSCmdlet.ParameterSetName
and then a switch statement will reduce the requirement for IF statements.
I suppose this is a duplicate, however I think the question and answer provide some good examples and information.
Fixed function:
Function Get-CMAPICollection {
[CmdletBinding()]
param (
[parameter(ValueFromPipeline = $true, Mandatory = $True, ParameterSetName = "ByName")]
[string]$collectionName,
[parameter(ValueFromPipeline = $true, Mandatory = $True, ParameterSetName = "ById")]
[string]$collectionId
)
switch ($PSCmdlet.ParameterSetName) {
byName { $URI = "https://$($CMAPI.cmServer)/AdminService/wmi/SMS_Collection?`$filter=Name eq '$collectionName'" }
ById { $URI = "https://$($CMAPI.cmServer)/AdminService/wmi/SMS_Collection?`$filter=CollectionId eq '$collectionId'" }
}
(Send-CMAPIRequest -method GET -URI $uri).Value
}

Related

Powershell function with $args and named parameter

I currently have the following powershell function:
function d { doppler run -- $args }
However, I would like to run something like d -Config dev ... and have that translate to
doppler run --config dev -- ...
How can I accomplish this?
If you want to add an optional parameter to your function you would no longer be able to use $args the same way you're currently using it. By adding a new parameter to your non-advanced function the same would be always bound positionally (-Config would be always bound hence wouldn't be optional).
What you could do instead to replace its functionality is turn your function into an advanced one and have a parameter that takes ValueFromRemainingArguments.
I haven't tested it but I believe this should do the trick.
function d {
[CmdletBinding(PositionalBinding = $false)]
param(
[Parameter(Position = 0, ValueFromRemainingArguments)]
[string[]] $Arguments,
[Parameter()]
[string] $Config
)
end {
doppler #(
'run'
if($PSBoundParameters.ContainsKey('Config')) {
'--config', $Config
}
'--'
$Arguments
)
}
}
Then both options should be available, with and without -Config:
PS ..\> d some arguments here
PS ..\> d -Config dev some arguments here

Running PowerShell script that leverages Parameter Sets without any parameters

How do I allow the option to run a PowerShell script that leverages parameter sets to run without passing any parameters? Is this possible?
param(
[Parameter(Mandatory=$true,HelpMessage="GenSecureFile used to generate secure password file.", ParameterSetName = 'GenSecureFile')]
[switch]$GenSecureFile,
[Parameter(Mandatory=$true,HelpMessage="GenSettingsFile used to generate settings file.", ParameterSetName = 'GenSettingsFile')]
[switch]$GenSettingsFile
)
Attempted to use a default parameter but this does not let you run the script with no parameters.
For this to work properly the parameter being declared as DefaultParameterSetName shouldn't be flagged as Mandatory and possibly be set to $true which wouldn't make sense for a switch Parameter. switch Parameters are meant to be optional (should not be Mandatory) and should not have a default value.
For Example:
function Test-Parameter {
[CmdletBinding(DefaultParameterSetName = 'GenSecureFile')]
param(
[Parameter(ParameterSetName = 'GenSecureFile')]
[switch] $GenSecureFile = $true,
[Parameter(Mandatory, ParameterSetName = 'GenSettingsFile')]
[switch] $GenSettingsFile
)
end {
$PSCmdlet.ParameterSetName
}
}
Test-Parameter
What you should do instead is have only one parameter that uses a ValidateSet Attribute Declaration, by doing so there wouldn't be a need for Parameter Sets.
For Example:
function Test-Parameter {
[CmdletBinding()]
param(
[Parameter()]
[ValidateSet('SecureFile', 'SettingsFile')]
[string] $Gen = 'SecureFile'
)
end {
if($Gen -eq 'SecureFile') {
# do something here when `SecureFile`
return
}
# do something here when `SettingsFile`
}
}
Test-Parameter

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 function receiving multiple parameters from pipeline

I'm writing a function as follows:
Function Display-ItemLocation {
Param(
[ Parameter (
Mandatory = $True,
Valuefrompipeline = $True ) ]
[ String ]$stringItem,
[ Parameter (
Mandatory = $False,
Valuefrompipeline = $True ) ]
[ String ]$stringLocation = 'unknown'
)
Echo "The location of item $stringItem is $stringLocation."
}
Display-ItemLocation 'Illudium Q-36 Explosive Space Modulator' 'Mars'
Display-ItemLocation 'Plumbus'
It works fine as written.
The location of item Illudium Q-36 Explosive Space Modulator is Mars.
The location of item Plumbus is unknown.
I'd like to be able to pre-load an array with multiple data pairs and send it via pipeline into the function.
$Data = #(
#('Bucket','Aisle 1'),
#('Spinach Pie','Freezer 4')
)
$Data | Display-ItemLocation
I can't find the magic syntax to get this to work. Can the function accept a pair of values at the same time from the pipeline?
Define your pipeline-binding parameters as binding by property name - ValuefromPipelineByPropertyName - and then pipe (custom) objects that have such properties:
Function Display-ItemLocation {
Param(
[ Parameter (
Mandatory,
ValuefromPipelineByPropertyName ) ]
[ String ]$stringItem,
[ Parameter (
Mandatory = $False,
ValuefromPipelineByPropertyName ) ]
[ String ]$stringLocation = 'unknown'
)
process { # !! You need a `process` block to process *all* input objects.
Echo "The location of item $stringItem is $stringLocation."
}
}
As an aside: Display is not an approved verb in PowerShell.
Now you can pipe to the function as follows; note that the property names must match the parameter names:
$Data = [pscustomobject] #{ stringItem = 'Bucket'; stringLocation = 'Aisle 1' },
[pscustomobject] #{ stringItem = 'Spinach Pie'; stringLocation = 'Freezer 4' }
$Data | Display-ItemLocation
The above yields:
The location of item Bucket is Aisle 1.
The location of item Spinach Pie is Freezer 4.
The above uses [pscustomobject] instances, which are easy to construct ad hoc.
Note that hash tables (e.g., just #{ stringItem = 'Bucket'; stringLocation = 'Aisle 1' }) do not work - although changing that is being discussed in this GitHub issue.
In PSv5+ you could alternatively define a custom class:
# Define the class.
class Product {
[string] $stringItem
[string] $stringLocation
Product([object[]] $itemAndLocation) {
$this.stringItem = $itemAndLocation[0]
$this.stringLocation = $itemAndLocation[1]
}
}
# Same output as above.
[Product[]] (
('Bucket', 'Aisle 1'),
('Spinach Pie', 'Freezer 4')
) | Display-ItemLocation
Thanks to #mklement0 for leading me to this solution. I came up with two options to resolve my dilemma using an array.
Option 1: Use .ForEach to pass the parameters in the usual way.
$Data = #(
#('Bucket','Aisle 1'),
#('Spinach Pie','Freezer 4')
)
$Data.ForEach({Format-ItemLocation "$($_[0])" "$($_[1])"})
Option 2: Using the pipeline (which is what I was after), I modified the function as mklement0 suggested to enable the ValuefromPipelineByPropertyName and to include a Process { } block.
Function Format-ItemLocation {
Param (
[ Parameter (
Mandatory = $True,
ValuefromPipelineByPropertyName = $True ) ]
[ String ]$stringItem,
[ Parameter (
Mandatory = $False,
ValuefromPipelineByPropertyName = $True ) ]
[ String ]$stringLocation = 'unknown'
)
Process {
"The location of item $stringItem is $stringLocation."
}
}
I pass the array via pipeline to a middle step to assign the parameter names to a [PSCustomObject]. This greatly reduces the amount of text that would bulk up the code, and it's the reason I was searching for a more elegant solution.
$Data = #(
#('Bucket','Aisle 1'),
#('Spinach Pie','Freezer 4')
)
$Data |
ForEach-Object { [PSCustomObject]#{stringItem=$_[0];stringLocation=$_[1]} } |
Format-ItemLocation
I changed the function name to Format-* as recommended.

Why DefaultParameterSetName restrain me to input the parameter?

function CTC {
[CmdletBinding(DefaultParameterSetName = "LocalBox")]
param
(
[Alias("l")]
[Parameter(ParameterSetName = "LocalBox", Mandatory)]
[Switch]
$Local,
[Alias("b")]
[Parameter(ParameterSetName = "LocalBox")]
[Parameter(ParameterSetName = "CloudTest")]
[string]
$BranchName = "head",
[Parameter(ParameterSetName = "CloudTest")]
[string]
$Tenant = "bingadsucmaz_cu"
)
process {
}
}
I have a function named "CTC", it has two parameter sets. The first one is "LocalBox" and I set it as default parameter set, the second one is "CloudTest". When I choose to run "CTC" without any parameters, it remind me to input the value like :
cmdlet CTC at command pipeline position 1
Supply values for the following parameters:
Local:
But suppose the meaningful action is the cmd choose to run "CloudTest" parameter set, right ?