Why are string passed as ArrayLists [duplicate] - powershell

Is there a resource on how to pass in a Object[] as a parameter within a PowerShell function?
Both of these functions are cmdlets and they are being exported correctly, but I cannot see the $Return object in my second function.
Is something like the following needed?
ParameterAttribute.ValueFromPipeline Property (System.Management.Automation)
# Within PowerShell code
$Return = My-Function -Param "value" # $Return is of type Object[]
$ModifiedReturn = My-SecondFunction -Input $Return
Where this is my function definition:
function My-SecondFunction
{
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True)]
[Object[]]$Input
)
begin {}
process
{
Write-Host "test: $Input" # Does not return anything
}
end {}
}

$Input is the name of an automatic variable. Use a different name.
I recommend $InputObject as that is in common usage so it has a well-understood meaning, but usually that means you are accepting pipeline input as well.
Of course if there's a name that's more descriptive for this parameter, you should use that.
I have submitted this issue on the PowerShell GitHub project suggesting that Set-StrictMode be modified to check for automatic variable assignment.

Related

Correct parameters values not passed on to the function

I have a file called TestFunc.ps1. Its contents are as follows
Function TestFunc([string]$param1, [string]$param2)
{
Write-Host "------------------"
Write-Host $param1
Write-Host $param2
Write-Host "------------------"
}
TestFunc $param1 $param2
I called it as follows
C:\Test\TestFunc.ps1 "Hello" "World"
The output is as follows
------------------
------------------
I expect the output as
------------------
Hello
World
------------------
What am I doing wrong here?
The parameters are defined for the scope of the function, and not the script.
What you want is a Param section:
param ([string]$param1, [string]$param2)
function TestFunc([string]$param1, [string]$param2) {
Write-Host "------------------"
Write-Host $param1
Write-Host $param2
Write-Host "------------------"
}
TestFunc $param1 $param2
Of course, having duplicate variable names is misleading, but I it's just a test function. In your case, you wouldn't even need a function at all:
param ([string]$param1, [string]$param2)
Write-Host "------------------"
Write-Host $param1
Write-Host $param2
Write-Host "------------------"
Or alternatively:
param ([string]$param1, [string]$param2)
function TestFunc {
Write-Host "------------------"
Write-Host $param1
Write-Host $param2
Write-Host "------------------"
}
TestFunc
Or use the $args automatic variable, without defining any parameters at all:
function TestFunc {
Write-Host "------------------"
Write-Host $args[0]
Write-Host $args[1]
Write-Host "------------------"
}
TestFunc foo bar
To complement marsze's helpful and effective answer:
PowerShell has two largely equivalent syntax forms for defining parameters - leaving PSv5+ class definitions aside[1]:
Note: For brevity, the parameter lists are placed on a single line below; however, both syntax forms allow placing individual parameters on their own line.
For functions only:
C/C#-like: a ,-separated list of parameter-variable declarations inside (...) after the function name and before the opening {; e.g.:
function foo ($bar, $baz) {
# ...
}
For scripts and functions too, as well as script blocks ({ ... }, which are like anonymous functions):
PowerShell-specific: A ,-separated list of parameter-variable declarations inside param(...), which must be the first statement inside the body (apart from comments and using directives):
# --- Script foo.ps1
param($bar, $baz)
# ...
# --- Function
# This example is fully equivalent to `foo ($bar, $baz) { ...` above.
# Note that no () is needed after the function name.
function foo {
param($bar, $baz)
# ...
}
# --- Script block
& {
param($bar, $baz)
# ...
} # arguments...
For brevity, the following optional elements were omitted above:
On individual parameter declarations:
Typing; e.g., to declare parameter $foo as type [int] (System.Int32):
[int] $foo
Parameter attributes, typically, but not exclusively via the [Parameter()] attribute; among other things, the latter determines whether the associated parameter is mandatory; e.g.:
[Parameter(Mandatory=$true)] [int] $foo
Above the param(...) statement only:
The [CmdletBinding()] attribute, which makes a function or script an advanced one, with behaviors on par with (compiled) PowerShell cmdlets - see about_Functions_Advanced
In simple (non-advanced) scripts and functions it is also an option not to declare any parameters at all, in which any arguments passed are contained in the automatic $args variable, which is a regular PowerShell array ([object[]]).
You can even combine $args with declared parameters: $args then contains only those arguments that didn't bind to declared ones.
By contrast, in advanced scripts and functions, you are fundamentally only permitted to pass arguments that bind to declared parameters.
When to choose which syntax form:
Script files and script blocks must use a param(...) statement - the C#-like syntax isn't available.
Functions can technically use the C#-like syntax and the param(...) interchangeably, except if the [CmdletBinding()] attribute is needed, in which case only the param(...) syntax works.
That said, for consistency and easier extensibility (making a function an advanced one later), the param(...) syntax is generally preferable.
Also, using the C#-like syntax frequently can more easily lead to syntax confusion when invoking a function, given that PowerShell cmdlets and functions are invoked like shell commands (no parentheses, whitespace-separated arguments), not like C# methods; e.g., foo 1 2 (or foo -bar 1 -baz 2) rather than foo(1, 2)
[1] The method declarations in class definitions must use C#-like syntax, and no parameter attributes are supported (they're only supported on properties). Just like methods on native .NET types, class methods must also be called with method syntax - see this answer and help topic about_Classes.

When my powershell cmdlet parameter accepts ValueFromPipelineByPropertyName and I have an alias, how can I get the original property name?

How can a function tell if a parameter was passed in as an alias, or an object in the pipeline's property was matched as an alias? How can it get the original name?
Suppose my Powershell cmdlet accepts pipeline input and I want to use ValueFromPipelineByPropertyName. I have an alias set up because I might be getting a few different types of objects, and I want to be able to do something slightly different depending on what I receive.
This does not work
function Test-DogOrCitizenOrComputer
{
[CmdletBinding()]
Param
(
# Way Overloaded Example
[Parameter(Mandatory=$true,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true,
Position=0)]
[Alias("Country", "Manufacturer")]
[string]$DogBreed,
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=1)]
[string]$Name
)
# For debugging purposes, since the debugger clobbers stuff
$foo = $MyInvocation
$bar = $PSBoundParameters
# This always matches.
if ($MyInvocation.BoundParameters.ContainsKey('DogBreed')) {
"Greetings, $Name, you are a good dog, you cute little $DogBreed"
}
# These never do.
if ($MyInvocation.BoundParameters.ContainsKey('Country')) {
"Greetings, $Name, proud citizen of $Country"
}
if ($MyInvocation.BoundParameters.ContainsKey('Manufacturer')) {
"Greetings, $Name, future ruler of earth, created by $Manufacturer"
}
}
Executing it, we see problems
At first, it seems to work:
PS> Test-DogOrCitizenOrComputer -Name Keith -DogBreed Basset
Greetings, Keith, you are a good dog, you cute little Basset
The problem is apparent when we try an Alias:
PS> Test-DogOrCitizenOrComputer -Name Calculon -Manufacturer HP
Greetings, Calculon, you are a good dog, you cute little HP
Bonus fail, doesn't work via pipeline:
PS> New-Object PSObject -Property #{'Name'='Fred'; 'Country'='USA'} | Test-DogOrCitizenOrComputer
Greetings, Fred, you are a good dog, you cute little USA
PS> New-Object PSObject -Property #{'Name'='HAL'; 'Manufacturer'='IBM'} | Test-DogOrCitizenOrComputer
Greetings, HAL, you are a good dog, you cute little IBM
Both $MyInvocation.BoundParameters and $PSBoundParameters contain the defined parameter names, not any aliases that were matched. I don't see a way to get the real names of arguments matched via alias.
It seems PowerShell is not only being 'helpful' to the user by silently massaging arguments to the right parameters via aliases, but it's also being 'helpful' to the programmer by folding all aliased inputs into the main parameter name. That's fine, but I can't figure out how to determine the actual original parameter passed to the Cmdlet (or the object property passed in via pipeline)
How can a function tell if a parameter was passed in as an alias, or an object in the pipeline's property was matched as an alias? How can it get the original name?
I don't think there is any way for a Function to know if an Alias has been used, but the point is it shouldn't matter. Inside the function you should always refer to the parameter as if its used by it's primary name.
If you need the parameter to act different depending on whether it's used an Alias that is not what an Alias is for and you should instead use different parameters, or a second parameter that acts as a switch.
By the way, if you're doing this because you want to use multiple parameters as ValueFromPipelineByPropertyName, you already can with individual parameters and you don't need to use Aliases to achieve this.
Accepting value from the pipeline by Value does need to be unique, for each different input type (e.g only one string can be by value, one int by value etc.). But accepting pipeline by Name can be enabled for every parameter (because each parameter name is unique).
I banged my head quite hard on this, so I'd like to write down the state of my understanding. The solution is at the bottom (such as it is).
First, quickly: if you alias the command, you can get the alias easily with $MyInvocation.InvocationName. But that doesn't help with parameter aliases.
Works in some cases
You can get some joy by pulling the commandline that invoked you:
function Do-Stuff {
[CmdletBinding()]param(
[Alias('AliasedParam')]$Param
)
$InvocationLine = $MyInvocation.Line.Substring($MyInvocation.OffsetInLine - 1)
return $InvocationLine
}
$a = 42; Do-Stuff -AliasedParam $a; $b = 23
# Do-Stuff -AliasedParam $a; $b = 23
This will show the alias names. You could parse them with regex, but I'd suggest using the language parser:
$InvocationAst = [Management.Automation.Language.Parser]::ParseInput($InvocationLine, [ref]$null, [ref]$null)
$InvocationAst.EndBlock.Statements[0].PipelineElements[0].CommandElements.ParameterName
That will get you a list of parameters as they were called. However, it's flimsy:
Doesn't work for splats
Doesn't work for ValueFromPipelineByPropertyName
Abbreviated param names will cause extra headache
Only works in the function body; in a dynamicparam block, the $MyInvocation properties are not yet populated
Doesn't work
I did a deep dive into ParameterBinderController - thanks to Rohn Edwards for some reflection snippets.
This is not going to get you anywhere. Why not? Because the relevant method has no side effects - it just moves seamlessly from canonical param names to aliases. Reflection ain't enough; you would need to attach a debugger, which I do not consider to be a code solution.
This is why Trace-Command never shows the alias resolution. If it did, you might be able to hook the trace provider.
Doesn't work
Register-ArgumentCompleter takes a scriptblock which accepts a CommandAst. This AST holds the aliased param names as tokens. But you won't get far in a script, because argument completers are only invoked when you interactively tab-complete an argument.
There are several completer classes that you could hook into; this limitation applies to them all.
Doesn't work
I messed about with custom parameter attributes, e.g. class HookAttribute : System.Management.Automation.ArgumentTransformationAttribute. These receive an EngineIntrinsics argument. Unfortunately, you get no new context; parameter binding has already been done when attributes are invoked, and the bindings you'll find with reflection are all referring to the canonical parameter name.
The Alias attribute itself is a sealed class.
Works
Where you can get joy is with the PreCommandLookupAction hook. This lets you intercept command resolution. At that point, you have the args as they were written.
This sample returns the string AliasedParam whenever you use the param alias. It works with abbreviated param names, colon syntax, and splatting.
$ExecutionContext.InvokeCommand.PreCommandLookupAction = {
param ($CommandName, $EventArgs)
if ($CommandName -eq 'Do-Stuff' -and $EventArgs.CommandOrigin -eq 'Runspace')
{
$EventArgs.CommandScriptBlock = {
# not sure why, but Global seems to be required
$Global:_args = $args
& $CommandName #args
Remove-Variable _args -Scope Global
}.GetNewClosure()
$EventArgs.StopSearch = $true
}
}
function Do-Stuff
{
[CmdletBinding()]
param
(
[Parameter()]
[Alias('AliasedParam')]
$Param
)
$CalledParamNames = #($_args) -match '^-' -replace '^-' -replace ':$'
$CanonParamNames = $MyInvocation.BoundParameters.Keys
$AliasParamNames = $CanonParamNames | ForEach-Object {$MyInvocation.MyCommand.Parameters[$_].Aliases}
# Filter out abbreviations that could match canonical param names (they take precedence over aliases)
$CalledParamNames = $CalledParamNames | Where-Object {
$CalledParamName = $_
-not ($CanonParamNames | Where-Object {$_.StartsWith($CalledParamName)} | Select-Object -First 1)
}
# Param aliases that would bind, so we infer that they were used
$BoundAliases = $AliasParamNames | Where-Object {
$AliasParamName = $_
$CalledParamNames | Where-Object {$AliasParamName.StartsWith($_)} | Select-Object -First 1
}
$BoundAliases
}
# Do-Stuff -AliasP 42
# AliasedParam
If the Global variable offends you, you could use a helper parameter instead:
$EventArgs.CommandScriptBlock = {
& $CommandName #args -_args $args
}.GetNewClosure()
[Parameter(DontShow)]
$_args
The drawback is that some fool might actually use the helper parameter, even though it's hidden with DontShow.
You could develop this approach further by doing a dry-run call of the parameter binding mechanism in the function body or the CommandScriptBlock.

Default value of parameter is not used in function

I have a very basic PowerShell script:
Param(
[string]$MyWord
)
function myfunc([string] $MyWord) {
Write-Host "$MyWord"
}
myfunc #PSBoundParameters
This is how I execute it:
PS C:\> .\test.ps1 -MyWord 'hello'
hello
All fine. But I want to set a default value if -MyWord isn't specified.
I tried this:
Param(
[string]$MyWord='hi'
)
function myfunc([string] $MyWord) {
Write-Host "$MyWord"
}
myfunc #PSBoundParameters
But than the output of my script was just empty. It was printing nothing when I did not describe my parameter. (it only showed 'hello' if I specified the parameter).
I also tried:
Param(
[string]$MyWord
)
function myfunc([string] $MyWord) {
[string]$MyWord='hi'
Write-Host "$MyWord"
}
myfunc #PSBoundParameters
But than the output was of course always 'hi' and never 'hello'. Even when I executed the script with the parameter -MyWord 'hello'
Can someone explaining what I'm doing wrong?
When I'm not using the function it is working as expected:
Param(
[string]$MyWord='hi'
)
Write-Host $MyWord
Output:
PS C:\> .\test.ps1 -MyWord 'hallo'
hallo
PS C:\> .\test.ps1
hi
Automatic variable $PSBoundParameters, as the name suggests, contains only bound parameters, where bound means that an actual value was supplied by the caller.
Therefore, a parameter default value does not qualify as binding the associated parameter, so $MyWord with its default value of 'hi' does not become part of $PSBoundParameters.
Note: Arguably, a parameter with a default value should also be considered bound (it is bound by its default value, as opposed to by a caller-supplied value). Either way, it would be convenient to have an automatic variable that includes default values too, so as to enable simple and comprehensive passing through of arguments. A suggestion has been submitted to the PowerShell repository as GitHub issue #3285.
Workarounds
The following solutions assume that you want to pass the default value through, and don't want to simply duplicate the default value in function myfunc (as demonstrated in Ansgar Wiecher's helpful answer), because that creates a maintenance burden.
Regarding function syntax: The following two forms are equivalent (in this case), though you may prefer the latter for consistency and readability.[1]
function myfunc([string] $MyWord = 'hi') { ... }
parameter declaration inside (...) after the function name.
function myfunc { param([string] $MyWord = 'hi') ... }
parameter declaration inside a param(...) block inside the function body.
A simple fix would be to add the default value explicitly to $PSBoundParameters:
Param(
[string]$MyWord = 'hi'
)
function myfunc ([string] $MyWord){
Write-Host "$MyWord"
}
# Add the $MyWord default value to PSBoundParameters.
# If $MyWord was actually bound, this is effectively a no-op.
$PSBoundParameters.MyWord = $MyWord
myfunc #PSBoundParameters
To achieve what you want generically, you must use reflection (introspection):
param(
[alias('foop')]
[string]$MyWord = 'hi'
)
function myfunc ([string] $MyWord) {
Write-Host "$MyWord"
}
# Add all unbound parameters that have default values.
foreach ($paramName in $MyInvocation.MyCommand.Parameters.Keys) {
if (-not $PSBoundParameters.ContainsKey($paramName)) {
$defaultVal = Get-Variable -Scope Local $paramName -ValueOnly
# A default value is identified by either being non-$null or
# by being a [switch] parameter that defaults to $true (which is bad practice).
if (-not ($null -eq $defaultVal -or ($defaultVal -is [switch] -and -not $defaultVal))) {
$PSBoundParameters[$paramName] = $defaultVal
}
}
}
myfunc #PSBoundParameters
[1] The param(...) form is required if you need to use the [CmdletBinding()] attribute with non-default values, as well as in scripts (.ps1). See this answer.
A parameter is bound only if you actually pass it a value, meaning that a parameter's default value does not show up in $PSBoundParameters. If you want to pass script parameters into a function, you must replicate the script parameter set in the function parameter set:
Param(
[string]$MyWord = 'hi'
)
function myfunc([string]$MyWord = 'hi') {
Write-Host "$MyWord"
}
myfunc #PSBoundParameters
Maintaining something like this is easier if you define both parameter sets the same way, though, so I'd put the function parameter definition in a Param() block as well:
Param(
[string]$MyWord = 'hi'
)
function myfunc {
Param(
[string]$MyWord = 'hi'
)
Write-Host "$MyWord"
}
If you want to use "Param" enclose it in the function like this:
function myfunc {
Param(
[string]$MyWord='hi'
)
Write-Host "$MyWord"
}
Very simple way is,
function myfunc([string]$MyWord = "hi") {
Write-Output $MyWord
}

Pass object[] into a function in PowerShell

Is there a resource on how to pass in a Object[] as a parameter within a PowerShell function?
Both of these functions are cmdlets and they are being exported correctly, but I cannot see the $Return object in my second function.
Is something like the following needed?
ParameterAttribute.ValueFromPipeline Property (System.Management.Automation)
# Within PowerShell code
$Return = My-Function -Param "value" # $Return is of type Object[]
$ModifiedReturn = My-SecondFunction -Input $Return
Where this is my function definition:
function My-SecondFunction
{
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True)]
[Object[]]$Input
)
begin {}
process
{
Write-Host "test: $Input" # Does not return anything
}
end {}
}
$Input is the name of an automatic variable. Use a different name.
I recommend $InputObject as that is in common usage so it has a well-understood meaning, but usually that means you are accepting pipeline input as well.
Of course if there's a name that's more descriptive for this parameter, you should use that.
I have submitted this issue on the PowerShell GitHub project suggesting that Set-StrictMode be modified to check for automatic variable assignment.

How to pass a switch parameter to another PowerShell script?

I have two PowerShell scripts, which have switch parameters:
compile-tool1.ps1:
[CmdletBinding()]
param(
[switch]$VHDL2008
)
Write-Host "VHDL-2008 is enabled: $VHDL2008"
compile.ps1:
[CmdletBinding()]
param(
[switch]$VHDL2008
)
if (-not $VHDL2008)
{ compile-tool1.ps1 }
else
{ compile-tool1.ps1 -VHDL2008 }
How can I pass a switch parameter to another PowerShell script, without writing big if..then..else or case statements?
I don't want to convert the parameter $VHDL2008 of compile-tool1.ps1 to type bool, because, both scripts are front-end scripts (used by users). The latter one is a high-level wrapper for multiple compile-tool*.ps1 scripts.
You can specify $true or $false on a switch using the colon-syntax:
compile-tool1.ps1 -VHDL2008:$true
compile-tool1.ps1 -VHDL2008:$false
So just pass the actual value:
compile-tool1.ps1 -VHDL2008:$VHDL2008
Try
compile-tool1.ps1 -VHDL2008:$VHDL2008.IsPresent
Assuming you were iterating on development, it is highly likely that at some point you are going to add other switches and parameters to your main script that are going to be passed down to the next called script. Using the previous responses, you would have to go find each call and rewrite the line each time you add a parameter. In such case, you can avoid the overhead by doing the following,
.\compile-tool1.ps1 $($PSBoundParameters.GetEnumerator() | ForEach-Object {"-$($_.Key) $($_.Value)"})
The automatic variable $PSBoundParameters is a hashtable containing the parameters explicitly passed to the script.
Please note that script.ps1 -SomeSwitch is equivalent to script.ps1 -SomeSwitch $true and script.ps1 is equivalent to script.ps1 -SomeSwitch $false. Hence, including the switch set to false is equivalent to not including it.
According to a power shell team's blog (link below,) since V2 there is a technique called splatting. Basically, you use the automatic variable #PsBoundParameters to forward all the parameters. Details about splatting and the difference between # and $ are explained in the Microsoft Docs article (link below.)
Example:
parent.ps1
#Begin of parent.ps1
param(
[Switch] $MySwitch
)
Import-Module .\child.psm1
Call-Child #psBoundParameters
#End of parent.ps1
child.psm1
# Begin of child.psm1
function Call-Child {
param(
[switch] $MySwitch
)
if ($MySwitch){
Write-Output "`$MySwitch was specified"
} else {
Write-Output "`$MySwitch is missing"
}
}
#End of child.psm1
Now we can call the parent script with or without the switch
PS V:\sof\splatting> .\parent.ps1
$MySwitch is missing
PS V:\sof\splatting> .\parent.ps1 -MySwitch
$MySwitch was specified
PS V:\sof\splatting>
Update
In my original answer, I sourced the children instead of importing it as a module. It appears sourcing another script into the original just makes the parent's variables visible to all children so this will also work:
# Begin of child.ps1
function Call-Child {
if ($MySwitch){
Write-Output "`$MySwitch was specified"
} else {
Write-Output "`$MySwitch is missing"
}
}
#End of child.ps1
with
#Begin of parent.ps1
param(
[Switch] $MySwitch
)
. .\child.ps1
Call-Child # Not even specifying #psBoundParameters
#End of parent.ps1
Maybe, this is not the best way to make a program, nevertheless, this is the way it works.
About Splatting(Microsoft Docs)
How and Why to Use Splatting (passing [switch] parameters)
Another solution. If you declare your parameter with a default value of $false:
[switch] $VHDL2008 = $false
Then the following (the -VHDL2008 option with no value) will set $VHDL2008 to $true:
compile-tool1.ps1 -VHDL2008
If instead you omit the -VHDL2008 option, then this forces $VHDL2008 to use the default $false value:
compile-tool1.ps1
These examples are useful when calling a Powershell script from a bat script, as it is tricky to pass a $true/$false bool from bat to Powershell, because the bat will try to convert the bool to a string, resulting in the error:
Cannot process argument transformation on parameter 'VHDL2008'.
Cannot convert value "System.String" to type "System.Management.Automation.SwitchParameter".
Boolean parameters accept only Boolean values and numbers, such as $True, $False, 1 or 0.