Consider the following function:
function f {param($x = 42)}
$x has a default value of 42. Suppose I have a bunch of functions whose parameters I want to test programmatically for, among other things, their default values. Those other things I am able to discover using the objects returned using one of the following commands:
Get-Item function:/f | % Parameters | % x | % Attributes
Get-Help f | % Parameters | % parameter
Those commands output the following:
Position : 0
ParameterSetName : __AllParameterSets
Mandatory : False
ValueFromPipeline : False
ValueFromPipelineByPropertyName : False
ValueFromRemainingArguments : False
HelpMessage :
HelpMessageBaseName :
HelpMessageResourceId :
DontShow : False
TypeId : System.Management.Automation.ParameterAttribute
name : x
required : false
pipelineInput : false
isDynamic : false
parameterSetName : (All)
parameterValue : Object
type : #{name=Object}
position : 0
aliases : None
There doesn't seem to be any clue as to the default value.
How can I programmatically determine the default value of a function parameter?
You can use syntax tree to find default value expression for parameter.
function f {
param(
$x = 42,
$y = 6*7,
$z = (Get-Random)
)
}
$Parameters = (Get-Item function:\f).ScriptBlock.Ast.Body.ParamBlock.Parameters
$xDefaultValue = $($Parameters.Where{$_.Name.VariablePath.UserPath -eq 'x'}).DefaultValue
$yDefaultValue = $($Parameters.Where{$_.Name.VariablePath.UserPath -eq 'y'}).DefaultValue
$zDefaultValue = $($Parameters.Where{$_.Name.VariablePath.UserPath -eq 'z'}).DefaultValue
You can than use SafeGetValue() method of syntax tree node to retrieve constant value, but it does not work with expressions.
$xDefaultValue.SafeGetValue()
$yDefaultValue.SafeGetValue()
$zDefaultValue.SafeGetValue()
Related
I am trying to write a PowerShell function that I can invoke using a switch and passing one value or not using the switch and passing two values:
option-or-value -opt one
option-or-value two three
I have assumed that I need parameter sets and need to define the param() section of the function as follows:
function option-or-value {
param (
[parameter (
parameterSetName = 'using_option',
mandatory = $true
)][switch ] $opt,
[parameter (
parameterSetName = 'using_value',
mandatory = $true
)][string ] $val_0,
[parameter (
mandatory = $true
)][string ] $val_1
)
if ($using_option) {
$val_0 = '...'
}
write-host "val_0 = $val_0, val_1 = $val_1"
}
However, I am doing something wrong and when I try to invoke the function, I get the error A positional parameter cannot be found that accepts argument.
This surprises me because I thought the entire point of parameter sets is that functions can differentiate between different parameter protocols.
So, where is my assumption wrong and how can I achieve such a function?
I agree with Mathias that it would be better to go with multiple Parameter Sets rather than using switches here. He has the right answer but I think misinterpreted your question slightly which became more clear with your additional comment.
You would like to supply a switch and one value or not supply a switch and be forced to enter two values? And you would like to have more than one switch which will determine what the function actually does. To this point I don't agree as functions should typically be written to do one thing and do it well. If you have multiple work that needs to be done then you should have different functions.
Anyways, I think instead of multiple switches I would recommend having only one parameter where you could specify the switch/action. You could use ValidateSet() in order to restrict the input and provide autocompletion. To the code!
function Test-OptionOrValue {
[CmdletBinding(DefaultParameterSetName = 'Non-Option')]
param (
[Parameter(Mandatory = $true, ParameterSetName = 'Options')]
[ValidateSet( 'DoThis', 'DoThat', 'Other')]
$Action,
[Parameter(Mandatory = $true, ParameterSetName = 'Options')]
[Parameter(Mandatory = $true, ParameterSetName = 'Non-Option')]
[string]
$FirstValue,
[Parameter(Mandatory = $true, ParameterSetName = 'Non-Option')]
[string]
$SecondValue
)
if ($PSCmdlet.ParameterSetName -eq 'Non-Option') {
Write-Host "Both FirstValue and SecondValue required"
Write-Host "FirstValue: $FirstValue"
Write-Host "SecondValue: $SecondValue"
} elseif ($PSCmdlet.ParameterSetName -eq 'Options') {
Write-Host "Action argument provided: $Action"
Write-Host "Only FirstValue required"
Write-Host "FirstValue: $FirstValue"
Write-Host "SecondValue: $SecondValue"
switch ($action) {
'DoThis' { Write-Host "Performing 'DoThis' Action using FirstValue [$firstValue]" }
'DoThat' { Write-Host "Performing 'DoThat' Action using FirstValue [$firstValue]" }
'Other' { Write-Host "Performing 'Other' Action using FirstValue [$firstValue]" }
Default {}
}
}
}
Usage Examples
PS > Test-OptionOrValue -Action DoThis -FirstValue 32
Action argument provided: DoThis
Only FirstValue required
FirstValue: 32
SecondValue:
Performing 'DoThis' Action using FirstValue [32]
PS > Test-OptionOrValue -Action DoThat -FirstValue 32
Action argument provided: DoThat
Only FirstValue required
FirstValue: 32
SecondValue:
Performing 'DoThat' Action using FirstValue [32]
PS > Test-OptionOrValue -FirstValue 12 -SecondValue 23
Both FirstValue and SecondValue required
FirstValue: 12
SecondValue: 23
Forget about switches, make a third [string] parameter and declare it mandatory, as well as part of its own non-default parameter set instead:
function Test-Option
{
[CmdletBinding(DefaultParameterSetName = 'UsingValue')]
param(
[Parameter(Mandatory = $true, ParameterSetName = 'UsingValue', Position = 0)]
[string]$FirstValue,
[Parameter(Mandatory = $true, ParameterSetName = 'UsingValue', Position = 1)]
[string]$SecondValue = '...',
[Parameter(Mandatory = $true, ParameterSetName = 'UsingOption')]
[string]$Option
)
if($PSCmdlet.ParameterSetName -eq 'UsingOption'){
$FirstValue = $Option
}
Write-Host "First: $FirstValue, Second: $SecondValue"
}
Giving you the desired behavior:
PS ~> Test-Option 123 456
First: 123, Second: 456
PS ~> Test-Option -Option 123
First: 123, Second: ...
PS ~> Test-Option -Option 123 456
Test-Optional : A positional parameter cannot be found that accepts argument '456'.
At line:1 char:1
+ Test-Option -Option 123 456
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Test-Option], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : PositionalParameterNotFound,Test-Option
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.
If param One is set to bbb then I want to make param Two mandatory. Is it possible to do this with parameter sets somehow or will I just have to add this logic to the script itself?
Example idea of what I need:
param (
[Parameter(Mandatory, ParameterSetName = 'default')]
[ValidateSet(“aaa”, ”bbb”, ”ccc”, "ddd")]
[String]
$One,
[Parameter(ParameterSetName = 'default')]
[ValidateScript( { $One -eq 'bbb'; THEN MAKE THIS PARAM MANDATORY! })]
[String]
$Two
)
The value of $One seems to not be set yet as I tried doing this and one.txt is empty
[ValidateScript( { $One > One.txt; $true })]
Edit
While there is DynamicParam{} that looks like its only if you have begin,process,end, etc setup. This is a simple function that I don't want to add that to it. Also DynamicParam seems to require a bizarre amount of boilerplate code to work
Edit
It looks like DynamicParam is the only way, but I think it crazy. It's weird and not readable, but I'd still prefer Powershell to handle validation for me.
Doing it myself though is still pretty simple:
if ($One -eq 'bbb' -and -not $Two) {
ThrowError Param Two required when One set to $One
}
Use the default value for parameter -Two to enforce the desired logic, via an if statement that uses Throw:
param (
[Parameter(Mandatory, ParameterSetName = 'default')]
[ValidateSet('aaa', 'bbb', 'ccc', 'ddd')]
[String]
$One,
[Parameter(ParameterSetName = 'default')]
[String]
$Two = $(if ($One -eq 'bbb') { Throw "-Two must be passed if -One equals 'bbb'." })
)
"-One: $One; -Two: $Two"
Note: Emulating a (conditionally) mandatory parameter with Throw means that the behavior differs from a regular Mandatory parameter in that the latter prompts when a value isn't given.
A solution based on validation attributes would be preferable, but validation attributes aren't designed for cross-parameter validation, and no particular order of their evaluation is guaranteed.
The above solution relies on the fact that default values are evaluated after explicitly passed arguments have been bound.
The much more verbose alternative is to use a dynamic parameter, as shown in Wasif Hasan's answer, relies on the same timing, though it does have the advantage of exhibiting normal prompt-for-a-missing-mandatory-value behavior.
As of this writing, Wasif's answer doesn't work as posted, so here's a working solution; note how the use of DynamicParam syntactically requires (at least one of) the begin, process and end blocks.
# Syntax requires PSv5+:
using namespace System.Management.Automation
param(
[Parameter(Mandatory, ParameterSetName='default')]
[ValidateSet('aaa', 'bbb', 'ccc', 'ddd')]
[String]$One
)
# Define parameter -Two *dynamically*, so that its Mandatory property
# can be based on the specific value of the already-bound static -One parameter.
DynamicParam {
# Create a the dictionary of dynamic parameters.
$dict = [RuntimeDefinedParameterDictionary]::new()
# Define and add the -Two parameter
$paramName = 'Two'
$dict.Add(
$paramName,
[RuntimeDefinedParameter]::new(
$paramName,
[string],
[ParameterAttribute] #{
ParameterSetName = 'default'
# *Conditionally* make the parameter mandatory, depending on the value
# of the already-bound static -One parameter.
Mandatory = $One -eq 'bbb'
}
)
)
# Return the dictionary
return $dict
}
begin {
# NOTE: Dynamic parameter values do not become local variables the way
# they do for static parameters; the value must be accessed via the
# automatic $PSBoundParameters dictionary.
$Two = $PSBoundParameters['Two']
"-One: $One; -Two: $Two"
}
You are looking for dynamic parameters: Conditional PowerShell parameters
param(
[String]$One
)
DynamicParam {
# Create a parameter dictionary
$runtimeParams = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
# Populate it with parameters, with optional attributes
$attribs = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$mandatoryAttrib = New-Object System.Management.Automation.ParameterAttribute
$mandatoryAttrib.Mandatory = ($one -eq "bbb")
$attribs.Add($mandatoryAttrib)
$param = New-Object System.Management.Automation.RuntimeDefinedParameter('Two', String, $attribs)
}
Begin {
# If desired, move dynamic parameter values to variables
$ParameterName = $PSBoundParameters['Two']
}
# Do like regular
Without process block it will work.
I am trying to run a piece of remote code using powershell remoting and getting some strange behavior which I am unable to explain. This is the sequence of commands I run.
$sb1 = {$r1 = 1; $r2 = 2; $r3 = Get-Culture; return $r3}
$sb2 = {1; 2; $r3 = Get-Culture; return $r3}
$session = New-PSSession -ComputerName $comp -Credential $creds
$ret1 = Invoke-Command -Session $Session -ScriptBlock $sb1
$ret2 = Invoke-Command -Session $Session -ScriptBlock $sb2
$ret1
>>> en-US
$ret2
>>> 1
Does anyone know a reason for this behavior? I find it very odd. The return statement is ignored, and the scriptblock is evaluated to the first 'uncaptured' expression. Hmmm?
Also, if I did want this block to always evaluate to the return statement, or even the last statement, does anyone know how I might accomplish that?
The entire script block is executed and the results are returned. $ret2 will contain three answers. The first is "1", the second is "2" and the third is the output of Get-Culture. You can explore these by looking at $ret2[0], $ret2[1], and $ret[2]. You can find out how many results are returned with $ret2.count.
Below shows everything in $ret2 on my computer.
PS C:\Users\user\Documents\PowerShell> $ret2 | select * | fl
#{PSComputerName=MyComputer; RunspaceId=b9568f5d-88a0-4346-be1a-827b8ba2f29d; PSShowComputerName=True}
#{PSComputerName=MyComputer; RunspaceId=b9568f5d-88a0-4346-be1a-827b8ba2f29d; PSShowComputerName=True}
PSComputerName : MyComputer
RunspaceId : b9568f5d-88a0-4346-be1a-827b8ba2f29d
Parent : en
LCID : 1033
KeyboardLayoutId : 1033
Name : en-US
IetfLanguageTag : en-US
DisplayName : English (United States)
NativeName : English (United States)
EnglishName : English (United States)
TwoLetterISOLanguageName : en
ThreeLetterISOLanguageName : eng
ThreeLetterWindowsLanguageName : ENU
CompareInfo : CompareInfo - en-US
TextInfo : TextInfo - en-US
IsNeutralCulture : False
CultureTypes : SpecificCultures, InstalledWin32Cultures, FrameworkCultures
NumberFormat : System.Globalization.NumberFormatInfo
DateTimeFormat : System.Globalization.DateTimeFormatInfo
Calendar : System.Globalization.GregorianCalendar
OptionalCalendars : {System.Globalization.GregorianCalendar, System.Globalization.GregorianCalendar}
UseUserOverride : True
IsReadOnly : False
Can you overload functions in PowerShell?
I want to my function to accept a string, array or some switch.
An example of what I want:
Backup-UsersData singleUser
Backup-UsersData #('Alice', 'Bob',
'Joe')
Backup-UsersData -all
In PowerShell functions are not overloaded. The last definition overrides the previous in the same scope or hides the previous in a parent scope. Thus, you should create a single function and provide a way to distinguish its call mode by arguments.
In V2 you may use an advanced function, see help about_Functions_Advanced_Parameters and avoid some manual coding on resolving parameter set ambiguities:
# advanced function with 3 parameter sets
function Backup-UsersData
(
[Parameter(Position=0, ParameterSetName="user")]
[string]$user,
[Parameter(Position=0, ParameterSetName="array")]
[object[]]$array,
[Parameter(Position=0, ParameterSetName="all")]
[switch]$all
)
{
# use this to get the parameter set name
$PSCmdlet.ParameterSetName
}
# test
Backup-UsersData -user 'John'
Backup-UsersData 1, 2
Backup-UsersData -all
# OUTPUT:
# user
# array
# all
Note that this mechanism is sometimes strange. For example in the first test we have to specify parameter name -user explicitly. Otherwise:
Backup-UsersData : Parameter set cannot be resolved using the specified named parameters.
At C:\TEMP\_101015_110059\try2.ps1:21 char:17
+ Backup-UsersData <<<< 'John'
+ CategoryInfo : InvalidArgument: (:) [Backup-UsersData], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : AmbiguousParameterSet,Backup-UsersData
In many cases standard, not advanced, function with mixed parameters will do:
function Backup-UsersData
(
[string]$user,
[object[]]$array,
[switch]$all
)
{
if ($user) {'user'}
elseif ($array) {'array'}
elseif ($all) {'all'}
else {'may be'}
}
Backup-UsersData -user 'John'
Backup-UsersData -array 1, 2
Backup-UsersData -all
Backup-UsersData
But in this case you should resolve (or accept and ignore) ambiguities, e.g. to decide what to do if, say:
Backup-UsersData -user 'John' -array 1, 2 -all
Here is a variant of Roman's answer that I think is a little more flexible:
function Backup
{
[CmdletBinding(DefaultParameterSetName='Users')]
Param (
[parameter(mandatory=$true, ParameterSetName='Users', position=0, ValueFromPipeline=$true)][string[]]$User,
[parameter(mandatory=$true, ParameterSetName='AllUsers')][switch]$All
)
Begin
{
if ($All) { $User = #('User1', 'User2', 'User3') }
}
Process
{
foreach ($u in $User)
{
echo "Backup $u"
}
}
}
1) Build a class...
class c1 {
[int]f1( [string]$x ){ return 1 }
[int]f1( [int ]$x ){ return 2 }
}
1+) Use STATIC METHODS if you prefer to call them without instantiation...
class c1 {
static [int] f1( [string]$x ){ return 1 }
static [int] f1( [int]$x ){ return 2 }
}
2) Call the methods in class or object... overload works OK
$o1 = [c1]::new()
o1.f1( "abc" ) ~> returns 1
o1.f1( 123 ) ~> returns 2
-OR-
[c1]::f1( "abc" ) ~> returns 1
[c1]::f1( 123 ) ~> returns 2
3)
If (like me)
you want to have "Overloaded Functions" placed in a libraries...
so your users can use them transparently...
from code or from Interactive Command Line (REPL)...
the closest I could came to
"Overloading functions in Powershell"
was something like this:
function Alert-String() { [c1]::f1( "abc" ) }
function Alert-Strings(){ [c1]::f1( 123 ) }
function Alert-Stringn(){ [c1]::f1( 123 ) }
Maybe in PS-Core v8??? ;-)
Hope it helps...
If you use PSObject instead of Object to define your parameter type, it should work.
For example, The function Get-Control, know's how to overload based on type string or template and can be called using the positional value:
Get-Control "A-Name-Of-A-Control"
Get-Control $template
To make the overload work, use PSObject as follows:
Function Get-Control {
Param(
[Parameter(Mandatory=$False,ParameterSetName="ByTemplate",Position=0)]
[PSObject]
$Template,
[Parameter(Mandatory=$False,ParameterSetName="ByName",Position=0)]
[String]
$Name,
[Parameter(Mandatory=$False)]
[Switch]
$List
)
... # remaining code removed for brevity