Powershell function with $args and named parameter - powershell

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

Related

Create a wrapper for functions in powershell

I want to wrap a function, that is to create a new function such that it would automatically pass some arguments to the old function, like python's partial functions. The arguments passed are the ones defined in the callee and not the caller. The important thing is that I don't want to refer to each of them explicitly (define them twice).
That is really done to save typing-in the same flags to complicated functions while allowing customization.
For example, in python, I would do:
call_with_x=partial(call,x=1)
or maybe use **kw and pass it to the callee in some cases .
This is my best try (based on Wrapper function in PowerShell: Pass remaining parameters) :
function Let
{
[CmdletBinding()]
Param([parameter(mandatory=$true, position=0)][string]$Option,
[parameter(mandatory=$false, position=1, ValueFromRemainingArguments=$true)]$Remaining)
Get #Remaining
}
function Get
{
[CmdletBinding()]
Param([parameter(mandatory=$false, position=0)][string]$OptionA,
[parameter(mandatory=$true, position=1)][string]$OptionB)
Write-Host $OptionA, $OptionB
}
But Let -Option c -OptionA 1
Prints -OptionA 1
which is obviously not what I intended.
If you don't require "advanced" function features from CmdletBinding(), then you can get away with using $args for this:
# simple function parameters are positional and named, but not mandatory
function let {
Param($OptionA,$OptionB)
write-host "OptionA=$OptionA"
write-host "OptionB=$OptionB"
get #args
}
function get {
Param($OptionC,$OptionD)
write-host "OptionC=$OptionC"
write-host "OptionD=$OptionD"
}
# not necessary to name or include parameters
let -OptionA A B -OptionC C D
OptionA=A
OptionB=B
OptionC=C
OptionD=D
# named parameters get assigned first, so the order is not too big a deal either
# these produce the same results as above:
let B -OptionA A D -OptionC C
let -OptionD D -OptionC C A B
Any named parameters will not get positionally assigned to let
Any additional parameters, named or otherwise, will be forwarded to get
That was hard!
For this you would need to add a bit of code in DynmaicParams and begin section of the function.
It might be possible to do it in an Attribute
function x{
[WrapperFor(y)]
Might do it Later.
function Get
{
[CmdLetBinding()]
Param([parameter(mandatory=$false, position=0)][string]$OptionA,
[parameter(mandatory=$false, position=1)][string]$OptionB)
Write-Host "opta",$OptionA
Write-Host "optb",$OptionB
}
function Let
{
[CmdletBinding()]
Param([parameter(mandatory=$true, position=0)][string]$Option,[parameter(mandatory=$false, position=0)][string]$OptionB)
DynamicParam {
AddWrapper -For "Get" -To "Let"
}
Begin {
$params = GetRestOfParams "Get" $PSBoundParameters
}
Process {
Get #params
}
}
Needed code:
using namespace System.Management.Automation
function Empt
{
[CmdletBinding()]
Param([parameter(mandatory=$true, position=0)][string]$aaaa)
1
}
function AddWrapper([parameter(mandatory=$true, position=0)][string]$For,[parameter(mandatory=$true, position=1)][string]$To)
{
$paramDictionary = [RuntimeDefinedParameterDictionary]::new()
$paramset= $(Get-Command $For).Parameters.Values | %{[System.Management.Automation.RuntimeDefinedParameter]::new($_.Name,$_.ParameterType,$_.Attributes)}
$paramsetlet= $(Get-Command empt).Parameters.Keys
$paramsetlet+= $(Get-Command $To).ScriptBlock.Ast.Body.ParamBlock.Parameters.Name | %{ $_.VariablePath.UserPath }
$paramset | %{ if ( -not ($paramsetlet -contains $_.Name) ) {$paramDictionary.Add($_.Name,$_)}}
return $paramDictionary
}
function GetRestOfParams($dst,$params)
{
$dstorgparams=$(Get-Command $dst).Parameters.Keys
$z= $params
$z.Keys | %{ if ( -not ($dstorgparams -contains $_) ) {$z.Remove($_)} } | Out-Null
return $z
}

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 5.1 Can receiving function signature contain splatting parameter?

PowerShell 5.1
Can you define a splatting parameter in a receiving function's signature?
function main {
$test = #{p1='a';p2='b'}
DoWork #test
}
function DoWork {
param([?] #test) # line in question
# Maybe call another function with it
DoWork2 #test
}
main
What I believe you're looking for is to pass the same arguments used for DoWork to DoWork2, if that is the case, you can use the automatic variable $PSBoundParameters for this.
function DoWork {
[cmdletbinding()]
param($p1, $p2, $p3, $p4)
DoWork2 #PSBoundParameters
}
function DoWork2 {
[cmdletbinding()]
param($p1, $p2, $p3, $p4)
"'{0}' was called with the following parameters:" -f $MyInvocation.MyCommand.Name
$PSBoundParameters.Keys | ForEach-Object {
'Parameter: {0} // Argument: {1}' -f $_, $PSBoundParameters[$_]
}
}
Below there is a minimal example of how this works:
PS /> DoWork a b c d
'DoWork2' was called with the following parameters:
Parameter: p1 // Argument: a
Parameter: p2 // Argument: b
Parameter: p3 // Argument: c
Parameter: p4 // Argument: d
PS /> DoWork -p4 hello -p2 world
'DoWork2' was called with the following parameters:
Parameter: p4 // Argument: hello
Parameter: p2 // Argument: world
It's important to understand that for this to work properly, both functions must share the same parameter names or an Alias Attribute Declaration that matches the same parameter from the caller, i.e.:
param($p1, $p2, $p3, [alias('p4')] $somethingelse)
It's also worth noting that [cmdletbinding()] attribute and / or [parameter()] attribute declaration will make your function act as an Advanced Function. If you attempt to bind to a non existent parameter of an advanced function you would get a ParameterBindingException as a result.

Multiple powershell switch parameters - can it be optimized?

I am trying to write a simple wrapper that accept one parameter for the output.
This is how it looks now
function Get-data{
param (
[switch]$network,
[switch]$profile,
[switch]$server,
[switch]$devicebay
)
if ($network.IsPresent) { $item = "network"}
elseif ($profile.IsPresent) {$item = "profile"}
elseif ($server.IsPresent) {$item = "server"}
elseif ($devicebay.IsPresent){$item = "devicebay"}
$command = "show $item -output=script2"
}
Clearly this could be optimize but I am struggling to wrap my head around on how I can achieve it .Is there some easy way to ensure only single parameter is accepted and used without resorting to multiple elseif statements?
Also I would like to provide array of paramters instead doing it the way it is done at the moment.
Another thing you could do instead of all those switch parameters is to use a [ValidateSet]
function Get-Data{
[cmdletbinding()]
param(
[Parameter(Mandatory=$true)]
[ValidateSet('Network','Profile','Server','DeviceBay')]
[string]$Item
)
Switch ($Item){
'network' {'Do network stuff'}
'profile' {'Do profile stuff'}
'server' {'Do server stuff'}
'devicebay' {'Do devicebay stuff'}
}
}
Probably not the most elegant solution, but using parametersets makes powershell do some of the work for you:
#requires -version 2.0
function Get-data {
[cmdletbinding()]
param(
[parameter(parametersetname="network")]
[switch]$network,
[parameter(parametersetname="profile")]
[switch]$profile,
[parameter(parametersetname="server")]
[switch]$server,
[parameter(parametersetname="devicebay")]
[switch]$devicebay
)
$item = $PsCmdlet.ParameterSetName
$command = "show $item -output=script2"
}
This example will error out if you don't provide one of the switches, but you could probably provide an extra switch that does nothing or errors more gracefully if you want to account for that case...
You can add the [cmdletbinding()] keyword so you get $PSBoundParameters, and use that for a switch pipeline:
function Get-data{
[cmdletbinding()]
param (
[switch]$network,
[switch]$profile,
[switch]$server,
[switch]$devicebay
)
Switch ($PSBoundParameters.GetEnumerator().
Where({$_.Value -eq $true}).Key)
{
'network' { 'Do network stuff' }
'profile' { 'Do profile stuff' }
'server' { 'Do server stuff' }
'devicebay' { 'Do devicebay stuff' }
}
}
Since you want only one switch to be enabled, an enum might help you.
This way, you're not using a switch but a standard parameter - still, the user of the cmdlet can use TAB to autocomplete the values that may be entered.
Just set the type of the parameter to your enum.