I am wondering if it is possible (and if so how) to create repeatable (and hopefully linked) parameters in a PowerShell function. This is how am looking for this to work:
function foo()
{
[CmdletBinding()]
Params(
[Parameter(Mandatory=$true,ParameterSetName="Default")]
[Parameter(Mandatory=$true,ParameterSetName="Set1")]
[Parameter(Mandatory=$true,ParameterSetName="Set2")]
[string]$SomeParam1,
[Parameter(Mandatory=$true,ParameterSetName="Set1")]
[Parameter(Mandatory=$true,ParameterSetName="Set2")]
*some magic here, likely, to make this repeatable*
[string]$SomeRepeatableParam,
[Parameter(Mandatory=$true,ParameterSetName="Set1")]
[string]$SomeLinkedParam1,
[Parameter(Mandatory=$true,ParameterSetName="Set2")]
[string]$SomeLinkedParam2
)
Begin
{
*some code here*
}
Process
{
foreach ($val in $SomeRepeateableParam)
{
*some code here using param and its linked param*
}
}
End
{
*some code here*
}
}
And then call this function like so:
foo -SomeParam "MyParam" -SomeRepeatableParam "MyProperty1" -SomeLinkedParam1 "Tall" -SomeRepeatableParam "MyProperty2" -SomeLinkedParam2 "Wide"
and so on, being able to use the repeatable parameter as many times in a single call as I feel like it.
Can this be done? And if so how?
Thanks for your time.
EDIT: For clarity, I don't mean an array parameter, but a repeatable parameter in which the linked parameter sets can be matched to each instance of the repeatable parameter.
Since PowerShell supports arrays as parameter values, there is generally no need to repeat a parameter.
There is no syntactic way to enforce the pairing (linking) of parameter values the way you intend, with repeating instances of the same parameter name, because parameter names must be unique (and even they didn't have to be unique, that alone wouldn't enforce the desired pairing).
You can, however, use parallel array parameters, and enforce their symmetry inside the function, e.g.:
function foo
{
[CmdletBinding()]
Param(
[string] $SomeParam1,
[string[]] $SomeRepeatableParam,
[string[]] $SomeLinkedParam
)
if ($SomeRepeatableParam.Count -ne $SomeLinkedParam.Count) {
Throw "Please specify paired values for -SomeRepeatableParam and -SomeLinkedParam"
}
for ($i = 0; $i -lt $SomeRepeatableParam.Count; ++$i) {
$SomeRepeatableParam[$i] + ': ' + $SomeLinkedParam[$i]
}
}
You would then call it as follows (note the , to separate the array elements):
foo -SomeParam1 "MyParam" `
-SomeRepeatableParam "MyProperty1", "MyProperty2" `
-SomeLinkedParam "Tall", "Wide"
Related
Consider the following simple function:
function Write-HostIfNotVerbose()
{
if ($VerbosePreference -eq 'SilentlyContinue')
{
Write-Host #args
}
}
And it works fine:
Now I want to make it an advanced function, because I want it to inherit the verbosity preference:
function Write-HostIfNotVerbose([Parameter(ValueFromRemainingArguments)]$MyArgs)
{
if ($VerbosePreference -eq 'SilentlyContinue')
{
Write-Host #MyArgs
}
}
But it does not work:
And what drives me nuts is that I am unable to identify how $args in the first example is different from $args in the second.
I know that the native #args splatting does not work for advanced functions by default - https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_splatting?view=powershell-7.2#notes
But I hoped it could be simulated, yet it does not work either. My question is - what is wrong with the way I am trying to simulate it and whether it is possible to fix my code without surfacing all the Write-Host parameters at Write-HostIfNotVerbose
Santiago Squarzon's helpful answer contains some excellent sleuthing that reveals the hidden magic behind #args, i.e. splatting using the automatic $args variable, which is available in simple (non-advanced) functions only.
The solution in Santiago's answer isn't just complex, it also isn't fully robust, as it wouldn't be able to distinguish -ForegroundColor (a parameter name) from '-ForegroundColor' a parameter value that happens to look like a parameter name, but is distinguished from it by quoting.
As an aside: even the built-in #args magic has a limitation: it doesn't correctly pass a [switch] parameter specified with an explicit value through, such as
-NoNewLine:$false[1]
A robust solution requires splatting via the automatic $PSBoundParameters variable, which in turn requires that the wrapping function itself also declare all potential pass-through parameters.
Such a wrapping function is called a proxy function, and the PowerShell SDK facilitates scaffolding such functions via the PowerShell SDK, as explained in this answer.
In your case, you'd have to define your function as follows:
function Write-HostIfNotVerbose {
[CmdletBinding()]
param(
[Parameter(Position = 0, ValueFromPipeline, ValueFromRemainingArguments)]
[Alias('Msg', 'Message')]
$Object,
[switch] $NoNewline,
$Separator,
[System.ConsoleColor] $ForegroundColor,
[System.ConsoleColor] $BackgroundColor
)
begin {
$scriptCmd =
if ($VerbosePreference -eq 'SilentlyContinue') { { Write-Host #PSBoundParameters } }
else { { Out-Null } }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
}
process {
$steppablePipeline.Process($_)
}
end {
$steppablePipeline.End()
}
}
[1] Such an argument is invariably passed through as two arguments, namely as parameter name -NoNewLine by itself, followed by a separate argument, $false. The problem is that at the time the original arguments are parsed into $args, it isn't yet known what formally declared parameters they will bind to. The NoteProperty tagging applied to $args for marking elements as parameter names doesn't preserve the information as to whether the subsequent argument was separated from the parameter name with :, which for a [switch] parameter is necessary to identify that argument as belonging to the switch. In the absence of this information, two separate arguments are always passed during splatting.
This is too obscure for me to explain, but for the sake of answering what PowerShell could be doing with $args you can test this:
function Write-HostIfNotVerbose {
param(
[parameter(ValueFromRemainingArguments)]
[object[]]$MagicArgs
)
$params = #{
NotePropertyName = '<CommandParameterName>'
PassThru = $true
InputObject = ''
}
$z = foreach($i in $MagicArgs) {
if($i.StartsWith('-')) {
$params.NotePropertyValue = $i
Add-Member #params
continue
}
$i
}
if ($VerbosePreference -eq 'SilentlyContinue') {
Write-Host #z
}
}
Write-HostIfNotVerbose -ForegroundColor Green Hello world! -BackgroundColor Yellow
A way of seeing what $args is doing automatically for us could be to serialize the variable:
function Test-Args {
[System.Management.Automation.PSSerializer]::Serialize($args)
}
Test-Args -Argument1 Hello -Argument2 World
Above would give us the serialized representation of $args where we would observe the following:
<LST>
<Obj RefId="1">
<S>-Argument1</S>
<MS>
<S N="<CommandParameterName>">Argument1</S>
</MS>
</Obj>
<S>Hello</S>
<Obj RefId="2">
<S>-Argument2</S>
<MS>
<S N="<CommandParameterName>">Argument2</S>
</MS>
</Obj>
<S>World</S>
</LST>
I am trying to have a function that can count jobs based on the LatestStatus value that I would pass a parameter. So far what I got:
Function JobCountStatus {
Write-Output (Get-VBRJob | ?{$_.Info.LatestStatus -eq $args} | Measure-Object).Count
}
The issue is that as I've read somewhere there will be a subshell(?) executing the where so the argument is not passed.
If I replace the $args with a specific string like "Failed" it will work.
Is there a way to overcome this? I do not want to write separate functions for all possible values.
I would appreciate any comments - Thanks
Well you can just name the value when you run the function as $args is an Automatic Variable
JobCountStatus "Failed"
You can use an advanced function with a parameter, named or not:
function JobCountStatus {
[CmdletBinding()]
Param (
[Parameter(Mandatory = $true, Position = 0)]
[System.String]
$Status
)
Process {
(Get-VBRJob | Where-Object { $_.Info.LatestStatus -eq $Status } | Measure-Object).Count
}
}
And call it like so:
JobCountStatus -Status "Failed"
# OR
JobCountStatus "Failed"
The latter having the same end result as just using $args. The only possible advantage to specifying your own parameter here is you could define a ValidateSet of statuses or an Enum of Status values so you can tab through them. An example of the former would be like so:
function JobCountStatus {
[CmdletBinding()]
Param (
[Parameter(Mandatory = $true, Position = 0)]
[ValidateSet("Failed", "Running", "Successful", "Unknown")]
[System.String]
$Status
)
Process {
(Get-VBRJob | Where-Object { $_.Info.LatestStatus -eq $Status } | Measure-Object).Count
}
}
$args is an array, not a single value. In any case, a ScriptBlock {} is an unnamed function and $args has it's own meaning within it, so you can't use it without some modification in something like Where-Object. You would have to store the elements of $args as another variable or multiple variables to reference within a child ScriptBlock. Not a huge change for this function, but for one where more parameters are expected this can result in a lot of unnecessary code which can be difficult to maintain.
I prefer to recommend defining named parameters in most cases, and this would be a simpler change than making the parent $args work in a child ScriptBlock:
Function JobCountStatus {
Param(
[string]$Status
)
( Get-VBRJob | Where-Object { $_.Info.LatestStatus -eq $Status } ).Count
}
I've also made a few more changes in this function, I'll explain below:
Use Param() to strongly define parameters by name. You could use the simpler syntax of function JobCountStatus([string]$Status) {} but for this case it's really a matter of preference for which technique to use. Using Param() is something I recommend as a matter of convention, but you'll need to use either technique to get a named parameter.
I replaced the $args reference with $Status.
Your use of Measure-Object is extraneous and so I've removed it. Where-Object returns a collection which already has the Count property.
You can use ? if you want but it's considered best practice to omit aliases and use full cmdlet names in scripts and modules, so I've replaced ? with Where-Object.
Note that you can invoke the function/cmdlet (the difference is minimal for PowerShell-defined cmdlets) with or without the parameter, as when you don't define a positional order, the order is automatically determined in the order of declaration:
# Either will work
JobCountStatus -Status Running
JobCountStatus Running
Here is some more documentation you may find useful:
About Functions
About Advanced Functions
#Ash's answer gives some more advanced examples of what you can do with param() which are mentioned in the the Advanced Functions link above. You cannot use advanced parameter attributes with the simple function syntax I mentioned in the first bullet point.
In TCL is it possible to have the default values of parameters be the return value of a function call?
proc GetParameterValue { } {
# calculation for value...
return value
}
proc TestFunction { {paramVal [GetParameterValue]} } {
puts $paramVal
}
TestFunction
This results in printing "[GetParameterValue]". Rather than calling the procedure GetParameterValue. Is this possible to do in TCL or do I need to redesign this bit of code?
The default values of parameters can only be constants that you compute at the time of declaration of the procedure (most commonly, they're literals which means you don't need to use list to do the construction):
proc TestFunction [list [list paramVal [GetParameterValue]]] {
...
}
To compute a default value at procedure call time, you have to move the calculation into the body of the procedure. There's a few ways to do the detection of whether to do the calculation, but they come down to three options: using a marker value, getting a count of words in the call, and taking full control of parsing.
Using a marker value
The trick to this is to find some value that is really unlikely to be passed in. For example, if this is to be a piece of text shown to the user, a value with nothing but an ASCII NUL in it is not going to occur; put that in the default then you can tell whether you've got the default and can substitute with what the complex code provides.
proc TestFunction {{paramVal "\u0000"}} {
if {$paramVal eq "\u0000"} {
set paramVal [GetParameterValue]
}
...
}
Getting a count of words in the call
This relies on the capabilities of the info level introspection command. In particular, info level 0 reports the full list of actual arguments to the current procedure. A bit of counting, and we can can know whether a real value was passed.
proc TestFunction {{paramVal "dummy"}} {
if {[llength [info level 0]] < 2} {
# Note that the command name itself is always present
set paramVal [GetParameterValue]
}
...
}
It's a totally general approach, so there's no worry about the case where someone provides an unexpected edge case, but it's more complicated when you have multiple arguments as you need to work out how many arguments should be present and so on yourself. That is simple in this case, but gets progressively more difficult as you have more arguments.
Taking full control of parsing
Ultimately, you can also decide to make a procedure that takes full control of the parsing of its arguments. You do that by giving it a single argument, args, and then you can use any approach you want to handle the actual argument list. (I tend to not put the formal argument list in parentheses in this case only but that's just my own style.)
proc TestFunction args {
if {[llength $args] == 0} {
set paramVal [GetParameterValue]
} elseif {[llength $args] == 1} {
set paramVal [lindex $args 0]
} else {
# IMPORTANT! Let users discover how to use the command!
return -code error -errorcode {TCL WRONGARGS} \
"wrong # args: should be \"TestFunction ?paramVal?\""
}
...
}
This is currently the only way to do anything truly advanced, e.g., to have optional arguments before mandatory ones. It's also pretty much what you'd have to do in C if you implemented the command there, though adjusted for a different language. The downside is that it is definitely more work than using the built-in basic argument parsing support code provided by the implementation of the proc command.
This is meant as a complement to Donal's thorough answer. In the past, I sometimes resorted to the assistance of [subst] for computed defaults:
proc GetParameterValue {} { return computedDefault }
proc TestFunction {{paramVal [GetParameterValue]}} {
puts [subst -novariables $paramVal]
}
TestFunction; # returns "computedDefault"
TestFunction "providedValue"
TestFunction {$test}
This avoids the need for (implementing) full control over arguments, and is piggybacking onto the built-in argument handler. It also allows for using anonymous procs rather than explicitly named ones for computing the defaults:
proc TestFunction {{paramVal "[apply {{} { return computedValue }}]"}} {
puts [subst -novariables ${paramVal}]
}
TestFunction; # returns "computedDefault"
TestFunction "providedValue"
TestFunction {$test}
It goes without saying that there are also some assumptions behind, which turn into important restrictions depending on one's application case:
You must keep some discipline in using brackets for the defaults in the argument lists, and placing [subst] at the usage sites of the argument variable.
It assumes that you have some control over the arguments, or that you can guarantee that certain special-purpose characters are not valid members of the arguments' value domain.
Watch:
TestFunction {[xxx]}
throws
invalid command name "xxx"
and must be sanitized to
TestFunction {\[xxx\]}
I'm trying to write a custom validateParameter function with Perl.
I have the following code which also works:
sub validateParameter {
my ($args, $list) = #_;
if ( ref($list) eq "ARRAY" ) {
foreach my $key (#$list) {
if ( not defined $args->{$key} ) {
die "no $key given!";
}
}
}
#elsif ( check if string ) {
#}
}
I want to call my function the following way:
validateParameter({ hallo => "Welt", test => "Blup"}, ["hallo", "test"]);
But I also want to call my function like this:
validateParameter({ hallo => "Welt", test => "Blup"}, "hallo");
I know that Perl only has the following three data-types (scalars, hashes, arrays). But maybe there is a smart way to check if a variable is a string.
How can I check if the given arg is a string?
Update: I somehow missed the end of the question. Just testing ref($list) eq 'ARRAY' will work most of the time, but to properly allow even overloaded objects, you should just try dereferencing the parameter:
if ( eval { \#$list } ) {
# it was an array
}
else {
# assume it is a string
}
Original answer:
You can check a number of things about a parameter:
if ( ! defined $param ) {
# undefined
}
elsif ( defined Scalar::Util::blessed($param) ) {
# object
}
elsif ( ref $param ) {
# reference (return of ref will give the type)
}
elsif ( length do { no warnings "numeric"; $param & '' } ) {
# number
}
else {
# string
}
But all of that (except perhaps the defined check) kind of defeats the purpose of Perl's automatically converting to your desired type and will limit what can be passed (for instance, a string or dualvar where a number is wanted, or an overloaded object where a string, number, or reference is wanted, or tied variables or magic variables such as $!).
You may want to also just look at what Params::Validate can do.
Don't base behaviour on the "type" of arguments because there really isn't such a thing. You will run into problems if you use type-base polymorphism because Perl values often have more than one type.
For example,
The scalar produced by "123" is stored as as string, but Perl doesn't distinguish it from the scalar produced by 123 which isn't stored as a string.
Scalars can contain both a number and a cached stringification of that number. (e.g. my $i = 123; "".$i;)
Scalars can contain both a number and a string (that isn't a stringification of the number). Common examples of these are $! and !1.
A reference to an object that overloads stringification is also a useful string. (e.g. DateTime->now)
A reference to an array may overload %{} to usable as a hash reference. A reference to an hash may overload #{} to usable as an array reference.
And more.
No, there is no way to check if a scalar is a string, as Perl does implicit type conversions depending on context. If you give a number as the second argument to your function, and you use it in a context that requires a string, it will be automatically converted to a string. So, just check if ref($list) is empty - in such case, $list is not a reference, and therefore it is a string or a number, and you don't need to distinguish between those two.
I have an Advanced Function with a number of helper functions in a .ps1 script file.
How do organize my functions so I can call AdvFunc from the command line and not break the ability of AdvFunc to use the helper functions?
Abbreviated contents of script.ps1:
Function AdvFunc {
[cmdletbinding(DefaultParameterSetName='Scheduled')]
Param (
[Parameter(ValueFromPipeline=$true, ParameterSetName='Scheduled')]$SomeValue
)
Begin {
$value = Helper1 $stuff
}
Process {
# do stuff
}
End {
# call Helper2
}
}
Helper1 {
Param ($stuff)
# do stuff
Return $valueForAdvFunc
}
Helper2 {
# do other stuff
}
# Entry Point
$collection | AdvFunc
script.ps1 is currently launched by a scheduler and processes pre-defined $collection as expected.
The problem is I need to call AdvFunc from the command like with a different parameter set. I've add the AdHoc Parameter Set below. This will be used to send a different collection to AdvFunc. As I understand things, this means the first lines of script.ps1 will now need to be:
Param (
[Parameter(ValueFromPipeline=$true, ParameterSetName='Scheduled')]$SomeValue,
[Parameter(ParameterSetName='AdHoc')][string]$OtherValue1,
[Parameter(ParameterSetName='AdHoc')][string]$OtherValue2
)
Obviously this means the helper functions can no longer be in the same .ps1 file.
Does this mean I will now need 3 script files, each dot-sourcing the other as needed?
Should I use: script.ps1 (containing only AdvFunc), helpers.ps1 (containing the several helper functions), and collection.ps1 (with only $collection being piped to script.ps1) ?
Are there any alternatives?
Proposed solution: Use a launcher script that sources script.ps1. All functions (AdvFunc and all helper functions) reside in script.ps1.
# launcher.ps1
[cmdletbinding(DefaultParameterSetName='Scheduled')]
Param (
[Parameter(ParameterSetName='AdHoc', Mandatory=$true)][ValidateNotNullOrEmpty()][string]$Param1,
[Parameter(ParameterSetName='AdHoc', Mandatory=$true)][ValidateNotNullOrEmpty()][string]$Param2,
[Parameter(ParameterSetName='AdHoc', Mandatory=$true)][ValidateNotNullOrEmpty()][string]$Param3
)
. .\script.ps1
if ($PSBoundParameters.ContainsKey('param1')) {
AdvFunc -Param1 $Param1 -Param2 $Param2 -Param3 $Param3
}
else {
$collection | AdvFunc
}
The idea is to accommodate either no parameter (sending $collection to AdvFunc) or a full 'AdHoc' set of parameters (sending the command-line defined collection to AdvFunc). The empty 'Scheduled' Parameter Set may not be necessary to accommodate the no parameter option.