Multiple powershell switch parameters - can it be optimized? - powershell

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.

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
}

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

Adding two options to a Powershell script

Forgive me as I'm still learning powershell so this might be a silly question, but how do I add an options to a .ps1?
For example, I have currently have a script that downloads a file and runs it, but if it cant run it, it will look for the file locally and run it then. How do I separate these two so the user can pick either to download or just run locally?
e.g.:
'./script.ps1 local '
Will look for the file locally and run it
'./script.ps1 external'
will download the file and run it
I'm not sure if functions will be appropriate for this because the point of the script isn't to import it into the modules, I just want it so you run the .ps1.
At the top of your file add;
Param(
[Parameter(Position=1)][string]$option
)
Switch ($option)
{
'local' { RunLocal }
'other' { RunOther }
default { RunDefault }
}
Would look something like
Param(
[Parameter(Position=1)][string]$option
)
function RunLocal {
Write-Host "RunLocal"
}
function RunOther {
Write-Host "RunOther"
}
function RunDefault {
Write-Host "RunDefault"
}
Switch ($option)
{
'local' { RunLocal }
'other' { RunOther }
default { RunDefault }
}
If you need to constrain the values passed to a parameter to a fixed set of values, use the [ValidateSet(...)] parameter attribute:
[CmdletBinding()]
Param(
[ValidateSet('Local', 'External')]
[string] $Option = 'Local'
)
Switch ($Option)
{
'local' {
# ...
break
}
'external' {
# ...
break
}
}
The above defaults -Option (and thus parameter variable $Option) to 'Local', while allowing to pass either Local or External explicitly to -Option - no other values are permitted.

How to run if inline on PowerShell command

Suppose I have this function;
Function SomeCommand{
param(
[string]$Var1,
[string]$Var2
)
# Do Something
}
I could set it up like this;
Function SomeCommand{
param(
[string]$Var1,
[string]$Var2
)
if ($Var2){
Do-SomeOtherCommand -SomeParam1 $Var1 -SomeParam2 $Var2
} else {
Do-SomeOtherCommand -SomeParam1 $Var1
}
This works fine if I only have one optional parameter, but if I have two it gets harry. I would like to do something like this;
Function SomeCommand{
param(
[string]$Var1,
[string]$Var2,
[string]$Var3
)
Do-SomeOtherCommand -SomeParam1 $Var1 (if($Var2){-SomeParam2 $Var2}) (if($Var3){-SomeParam3 $Var3})
}
Is there a way to accomplish this?
You are probably looking for splatting. You can build up a hashtable with the parameters you wish to pass (and their values), then specify the whole thing in one shot:
function FuncB($param1, $param2)
{
"FuncB -- param1:[$param1] param2:[$param2]"
}
function FuncA($paramA, $paramB)
{
$args = #{}
if ($paramA){ $args['param1'] = $paramA }
if ($paramB){ $args['param2'] = $paramB }
FuncB #args
}
Test
FuncA 'first' 'second'
FuncA 'OnlyFirst'
FuncA -paramB 'OnlySecond'
# results
# FuncB -- param1:[first] param2:[second]
# FuncB -- param1:[OnlyFirst] param2:[]
# FuncB -- param1:[] param2:[OnlySecond]
Semicolons. PowerShell allows you to use semicolons as line terminators.
Write-Output 1;Write-Output 2;Write-Output 3;
Personally, I think it should be mandatory.
Also note that you can build up an arbitrary expression as a simple string, then use Invoke-Expression (alias iex) to invoke it inline.
function FuncB($param1, $param2)
{
"FuncB -- param1:[$param1] param2:[$param2]"
}
function FuncA($paramA, $paramB)
{
$funcBCall = "FuncB $(if($paramA){ "-param1 '$paramA'" }) $(if($paramB){ "-param2 '$paramB'" })"
iex $funcBCall
}
This approach is very hacky and brittle, though, so I wouldn't recommend it.

Pipeline parameter interferes with $args array

I'm trying to make use of the $args array with a pipeline parameter.
The function expects an arbitrary number of parameters (e.g. param0) following the first, pipelined parameter:
function rpt-params {
param (
[Parameter(ValueFromPipeline=$true,Position=0,Mandatory=$true)][CrystalDecisions.CrystalReports.Engine.ReportDocument]$reportDocument
)
try {
write-host "count: " $args.count
#TODO process args
}
catch [Exception] {
write-host $_.Exception
}
finally {
return $reportDocument
}
}
Attempts to call the function produce an error that reads "rpt-params : A parameter cannot be found that matches parameter name 'param0'.":
...
# syntax 0
rpt-params $rpt -param0 "mb-1" -param1 "me-1"
...
...
# syntax 1; explicitly naming the first parameter
rpt-params -reportDocument $rpt -param0 "mb-1" -param1 "me-1"
...
Is my syntax the issue or is it related to using a pipelined parameter?
Create another parameter, called it something like $rest and decorate it with [Parameter(ValueFromRemainingArguments = $true)].
When you use "[cmdletbinding()]" or "[Parameter()]", which is the case here, your Function turns into an Advanced Function. An Advanced Function can only take the Arguments that are specified under "Param" and no more. To make your Function act like before, like Keith recommends, you'll need to add [Parameter(ValueFromRemainingArguments = $true)]
For Example:
function rpt-params {
param (
[Parameter(ValueFromPipeline=$true,Position=0,Mandatory=$true)]
[CrystalDecisions.CrystalReports.Engine.ReportDocument]$reportDocument,
[Parameter(ValueFromRemainingArguments=$true)]$args
)
try {
write-host "count: " $args.count
#TODO Now args can have all remaining values
}
catch [Exception] {
write-host $_.Exception
}
finally {
return $reportDocument
}
}