Powershell non-positional, optional params - powershell

I'm trying to create a powershell (2.0) script that will accept arguments that follow this basic pattern:
.\{script name} [options] PATH
Where options are any number of optional parameters - think along the lines of '-v' for verbose. The PATH argument will simply be whatever argument is passed in last, and is mandatory. One could call the script with no options and only one argument, and that argument would be assumed to be the Path. I'm having trouble setting up a parameters list that contains only optional parameters but is also non-positional.
This quick script demonstrates the problem I am having:
#param test script
Param(
$firstArg,
$secondArg,
[switch]$thirdArg,
[Parameter(ValueFromRemainingArguments = $true)]
$remainingArgs)
write-host "first arg is $firstArg"
write-host "second arg is $secondArg"
write-host "third arg is $thirdArg"
write-host "remaining: $remainingArgs"
When called like so:
.\param-test.ps1 firstValue secondValue
The script outputs:
first arg is firstValue
second arg is secondValue
third arg is False
remaining:
The behavior I am trying to create would have both arguments fall through the optional params and end up in the remainingArgs variable.
This question/answer helpfully provided a way to achieve the desired behavior, but it only seems to work if there is at least one mandatory parameter, and only if it comes before all of the other arguments.
I can demonstrate this behavior by making firstArg mandatory and specifying a position of 0:
#param test script
Param(
[Parameter(Mandatory=$true, Position = 0)]
$firstArg,
$secondArg,
[switch]$thirdArg,
[Parameter(ValueFromRemainingArguments = $true)]
$remainingArgs)
write-host "first arg is $firstArg"
write-host "second arg is $secondArg"
write-host "third arg is $thirdArg"
write-host "remaining: $remainingArgs"
Run with the same input as before:
.\param-test.ps1 firstValue secondValue
The output is as follows:
first arg is firstValue
second arg is
third arg is False
remaining: secondValue
The first, mandatory argument is assigned, and everything left falls all the way through.
The question is this: How can I set up a params list such that all of the params are optional, and none of them is positional?

How about this?
function test
{
param(
[string] $One,
[string] $Two,
[Parameter(Mandatory = $true, Position = 0)]
[string] $Three
)
"One = [$one] Two = [$two] Three = [$three]"
}
One and Two are optional, and may only be specified by name. Three is mandatory, and may be provided without a name.
These work:
test 'foo'
One = [] Two = [] Three = [foo]
test -One 'foo' 'bar'
One = [foo] Two = [] Three = [bar]
test 'foo' -Two 'bar'
One = [] Two = [bar] Three = [foo]
This will fail:
test 'foo' 'bar'
test : A positional parameter cannot be found that accepts argument 'bar'.
At line:1 char:1
+ test 'foo' 'bar'
+ ~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [test], ParameterBindingException
+ FullyQualifiedErrorId : PositionalParameterNotFound,test
This doesn't enforce that your mandatory arg is placed last, or that it's not named. But it allows for the basic usage pattern you want.
It also does not allow for more than one value in $Three. This might be what you want. But, if you want to treat multiple non-named params as being part of $Three, then add the ValueFromRemainingArguments attribute.
function test
{
param(
[string] $One,
[string] $Two,
[Parameter(Mandatory = $true, Position = 0, ValueFromRemainingArguments = $true)]
[string] $Three
)
"One = [$one] Two = [$two] Three = [$three]"
}
Now things like this work:
test -one 'foo' 'bar' 'baz'
One = [foo] Two = [] Three = [bar baz]
Or even
test 'foo' -one 'bar' 'baz'
One = [bar] Two = [] Three = [foo baz]

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

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 }

Possible to repeat an alias among different parameter sets in a powershell function?

Suppose I have:
function f {
[CmdletBinding(DefaultParameterSetName='x')]
param(
[Parameter(Mandatory,ParameterSetName='x')]
[Alias('a')]
[int]$Apple,
[Parameter(Mandatory,ParameterSetName='y')]
[Alias('b')]
[int]$Banana,
[Parameter(Mandatory,ParameterSetName='x')]
[Alias('b')]
[int]$Cherry
)
"Apple $Apple"
"Banana $Banana"
"Cherry $Cherry"
}
I'd like to be able to call f -Apple 1 -b 3 because including Apple means I'm certainly using Parameter Set x, but powershell complains that the alias b is declared multiple times.
Is it entirely impossible, or am I just missing a trick?
The non-trivial function I'm trying to write is a convenience wrapper for multiple external functions that have their own aliases, some of which can be the same for different named parameters, but the set of mandatory parameters would never be ambiguous.
I couldn't get it to work using regular params, but I found a workaround by defining Banana and Cherry as dynamic params. This way the alias b is only defined once, so PowerShell won't complain.
function f {
[CmdletBinding(DefaultParameterSetName='x')]
param(
[Parameter(Mandatory, ParameterSetName='x')]
[Alias('a')]
[int]$Apple
)
DynamicParam {
# If param 'Apple' exists, define dynamic param 'Cherry',
# else define dynamic param 'Banana', both using alias 'b'.
if( $PSBoundParameters.ContainsKey('Apple') ) {
$paramName = 'Cherry'
$paramSetName = 'x'
} else {
$paramName = 'Banana'
$paramSetName = 'y'
}
$aliasName = 'b'
$parameterAttribute = [System.Management.Automation.ParameterAttribute]#{
ParameterSetName = $paramSetName
Mandatory = $true
}
$aliasAttribute = [System.Management.Automation.AliasAttribute]::new( $aliasName )
$attributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
$attributeCollection.Add( $parameterAttribute )
$attributeCollection.Add( $aliasAttribute )
$dynParam = [System.Management.Automation.RuntimeDefinedParameter]::new(
$paramName, [Int32], $attributeCollection
)
$paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
$paramDictionary.Add($paramName, $dynParam)
$paramDictionary
}
process {
"--- Parameter Set '$($PSCmdlet.ParameterSetName)' ---"
if( $PSBoundParameters.ContainsKey('Apple') ) {
"Apple $Apple"
}
if( $PSBoundParameters.ContainsKey('Banana') ) {
# Dynamic params require special syntax to read
$Banana = $PSBoundParameters.Banana
"Banana $Banana"
}
if( $PSBoundParameters.ContainsKey('Cherry') ) {
# Dynamic params require special syntax to read
$Cherry = $PSBoundParameters.Cherry
"Cherry $Cherry"
}
}
}
Calling the function:
f -Apple 1 -b 3
f -b 2
Output:
--- Parameter Set 'x' ---
Apple 1
Cherry 3
--- Parameter Set 'y' ---
Banana 2

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 ?