I have a script that calls other scripts that other people manage. It's essentially a CI/CD script that gives users the ability to tap into the pipeline.
The issue I'm running into now is that I would like this calling script to implement a couple new parameters. However, the old scripts don't always implement those parameters.
If I call their script that doesn't implement the parameters, I get an error "A parameter cannot be found that matches parameter name 'newparameter'".
Is there a way to dynamically pass in a parameter so that it doesn't fail if the parameter doesn't exist? I don't mind if they don't implement it. It's a bonus parameter that they don't need to use.
Alternately, can I do something like a Get-Command for a custom .ps1 script, to get a list of accepted parameters? With that, I could confirm that a parameter is implemented before I pass it.
This might help you get started, you could use the Parser Class
to get all functions and it's parameters from a script, this answer shows a minimal reproduction. I'll leave it to you to investigate further.
Given myScript.ps1 that has these 3 functions:
function ExampleFunc {
param([int] $param1 = 123, [string] $param2)
}
function ExampleFunc2 {
param([object] $param3, [switch] $param4)
}
function ExampleFunc3 ($param5, [hashtable] $param6 = #{foo = 'var'}) {
}
You can use the ParseFile Method to get the AST, then you can use the .FindAll method to filter for all FunctionDefinitionAst and subsequently find all parameters filtering for all ParameterAst.
using namespace System.Management.Automation.Language
$ast = [Parser]::ParseFile('path\to\myScript.ps1', [ref] $null, [ref] $null)
$ast.FindAll({ $args[0] -is [FunctionDefinitionAst] }, $true) | ForEach-Object {
$out = [ordered]#{ Function = $_.Name }
$_.FindAll({ $args[0] -is [ParameterAst] }, $true) | ForEach-Object {
$out['ParameterName'] = $_.Name.VariablePath
$out['Type'] = $_.StaticType
$out['DefaultValue'] = $_.DefaultValue
[pscustomobject] $out
}
} | Format-Table
Above code would result in the following for myScript.ps1:
Function ParameterName Type DefaultValue
-------- ------------- ---- ------------
ExampleFunc param1 System.Int32 123
ExampleFunc param2 System.String
ExampleFunc2 param3 System.Object
ExampleFunc2 param4 System.Management.Automation.SwitchParameter
ExampleFunc3 param5 System.Object
ExampleFunc3 param6 System.Collections.Hashtable #{foo = 'var'}
The same could be accomplished using Get-Command:
(Get-Command 'fullpath\to\myScript.ps1').ScriptBlock.Ast.FindAll({
... same syntax as before ... }, $true # or $false for non-recursive search
)
Related
I'm trying out writing test-driven PowerShell code using Pester. Is there a way to test if a function has a certain CmdletBinding attribute, e.g. SupportsShouldProcess and ConfirmImpact:
function Remove-Something {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param ()
...
Is that kind of metadata available from the result of e.g. Get-Command?
$functionUnderTest = Get-Command Remove-Something
# How to test if $functionUnderTest has SupportsShouldProcesses defined?
It's a bit of a mouthful, but you can try something like this to get the attributes:
function Invoke-MyFunction {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param ()
}
(get-command Invoke-MyFunction).ScriptBlock.Ast.Body.ParamBlock.Attributes
#PositionalArguments : {}
#NamedArguments : {SupportsShouldProcess, ConfirmImpact}
#TypeName : CmdletBinding
#Extent : [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
#Parent : param ()
and then you'll need to filter that to find an attribute called "CmdletBinding" and check the arguments specified on it.
$command = Get-Command -Name "Invoke-MyFunction";
$attributes = $command.ScriptBlock.Ast.Body.ParamBlock.Attributes;
$cmdletBinding = $attributes | where-object { $_.TypeName.FullName -eq "CmdletBinding" };
$supportsShouldProcess = $cmdletBinding.NamedArguments | where-object { $_.ArgumentName -eq "SupportsShouldProcess" };
$confirmImpact = $cmdletBinding.NamedArguments | where-object { $_.ArgumentName -eq "ConfirmImpact" };
From there you can check whatever other parts of the definition you want to confirm exist...
Note - error handling left as an exercise for the reader :-)
In case someone else is interested in Test-driven development with PowerShell using Pester. Based on mclayton's answer above, I have created a set of test functions and published them in a PowerShell module called TDDUtils.
It has functions like Test-TDDCmdletBinding and Test-TDDOutputType that allows you to write Pester tests like:
function My-Command
{
[CmdletBinding()]
...
}
It "Should be an advanced function" {
$c = Get-Command -Name My-Command
Test-TDDCmdletBinding $c | Should -BeTrue
}
And
function My-Command
{
[OutputType([Bool])]
...
}
It "Should have Output type Bool" {
$c = Get-Command -Name My-Command
Test-TDDOutputType $c -TypeName 'Bool' | Should -BeTrue
}
Install it from a PowerShell administrator shell:
Install-Module TDDUtils
https://github.com/hansEricG/TDDUtils for complete source and more information.
How to pass value along with parameter? Something like ./test.ps1 -controllers 01. I want the script to use hyphen and also a value is passed along for the parameter.
Here is the part of the script I wrote. But if I call the script with hyphen (.\test.ps1 -Controllers) it says A parameter cannot be found that matches parameter name 'Controllers'.
param(
# [Parameter(Mandatory=$false, Position=0)]
[ValidateSet('Controllers','test2','test3')]
[String]$options
)
Also I need to pass a value to it which is then used for a property.
if ($options -eq "controllers")
{
$callsomething.$arg1 | where {$_ -eq "$arg2" }
}
Lets talk about why it does not work
function Test()
param(
[Parameter(Mandatory=$false, Position=0)]
[ValidateSet('Controllers','test2','test3')]
[String]$options
)
}
Parameters are Variables that are created and filled out at the start of the script
ValidateSet will only allow the script to run if $Options equals one of the three choices 'Controllers','test2','test3'
Lets talk about what exactly all the [] are doing
Mandatory=$false means that $options doesnt have to be anything in order for the script to run.
Position=0 means that if you entered the script without using the -options then the very first thing you put would still be options
Example
#If Position=0 then this would work
Test "Controllers"
#Also this would work
Test -options Controllers
[ValidateSet('Controllers','test2','test3')] means that if Option is used or is Mandatory then it has to equal 'Controllers','test2','test3'
It sounds like you are trying to create parameters at runtime. Well that is possible using DynamicParam.
function Test{
[CmdletBinding()]
param()
DynamicParam {
$Parameters = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
'Controllers','test2','test3' | Foreach-object{
$Param = New-Object System.Management.Automation.ParameterAttribute
$Param.Mandatory = $false
$AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$AttribColl.Add($Param)
$RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter("$_", [string], $AttribColl)
$Parameters.Add("$_", $RuntimeParam)
}
return $Parameters
}
begin{
$PSBoundParameters.GetEnumerator() | ForEach-Object{
Set-Variable $_.Key -Value $_.Value
}
}
process {
"$Controllers $Test2 $Test3"
}
}
DynamicParam allows you to create parameters in code.
The example above turns the array 'Controllers','test2','test3' into 3 separate parameters.
Test -Controllers "Hello" -test2 "Hey" -test3 "Awesome"
returns
Hello Hey Awesome
But you said you wanted to keep the hypen and the parameter
So the line
$PSBoundParameters.GetEnumerator() | ForEach-Object{
Set-Variable $_.Key -Value $_.Value
}
allows you to define each parameter value. a slight change like :
$PSBoundParameters.GetEnumerator() | ForEach-Object{
Set-Variable $_.Key -Value "-$($_.Key) $($_.Value)"
}
Would return
-Controllers Hello -test2 Hey -test3 Awesome
In a PowerShell script, I want to read a CSV file that contains something like this:
Type Title Param1 Param2
---- ----- ------ ------
Type1 Foo type 1 ValueForType1
Type2 Foo type 2 ValueForType2
When type is Type1, I have to call a function named New-FooType1, when type is Type2, the funcation is named New-FooType2, and so on:
function New-FooType1{
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Title,
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Param1
)
Write-Host "New-FooType1 $Title with $Param1"
}
function New-FooType2{
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Title,
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Param2
)
Write-Host "New-FooType2 $Title with $Param2"
}
I'm trying to route the call to either of the functions, using a dynamic invocation:
$csv | % {
$cmdName = "New-Foo$($_.Type)"
Invoke-Command (gcm $cmdName) -InputObject $_
}
However, I always get an error:
Parameter set cannot be resolved using the specified named parameters
As you can see, different type mean different parameters set.
How can I solve this? I would like to avoid manipulating properties manually, because in my real life script, I have a dozen of different types, with up to 6 parameters.
Here is a complete repro sample of the issue:
$csvData = "Type;Title;Param1;Param2`nType1;Foo type 1;ValueForType1;;`nType2;Foo type 2;;ValueForType2"
$csv = ConvertFrom-csv $csvData -Delimiter ';'
$csv | ft -AutoSize
function New-FooType1{
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Title,
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Param1
)
Write-Host "New-FooType1 $Title with $Param1"
}
function New-FooType2{
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Title,
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Param2
)
Write-Host "New-FooType2 $Title with $Param2"
}
$csv | % {
$cmdName = "New-Foo$($_.Type)"
Invoke-Command (gcm $cmdName) -InputObject $_
}
The expected output of this script is:
New-FooType1 Foo type 1 with ValueForType1
New-FooType2 Foo type 2 with ValueForType2
Use the call operator &:
$CmdName = "New-FooType1"
$Arguments = "type1"
& $CmdName $Arguments
the call operator also supports splatting if you want the arguments bound to specific named parameters:
$Arguments = #{
"title" = "type1"
}
& $CmdName #Arguments
To invoke command by name you should use invoke operator &. Invoke-Command cmdlet support only ScriptBlock and file invocation, and file invocation only supported for remote calls.
For dynamic parameter binding you can use spatting, but in that case you have to convert PSCustomObjects, returned by ConvertFrom-Csv cmdlet, to Hashtable. You also have to strip any extra parameters from Hashtable because splatting will fail if you try to bind non-existing parameter.
Another approach for dynamic parameter binding would be to use binding from pipeline object. It looks like it is what you want to do, since you mark all your parameters with ValueFromPipelineByPropertyName option. And this approach will just ignore any extra property it can not bind to parameter. I recommend you to remove ValueFromPipeline option, because with this option in case of absence of property with parameter name PowerShell will just convert PSCustomObject to string (or to whatever type you use for parameter) and pass it as value for parameter.
So, all you need is to pass object by pipeline and use invoke operator for invocation of command with dynamic name:
$_ | & "New-Foo$($_.Type)"
dont know exactly what your trying to do, but
Invoke-Command (gcm $cmdName) ?
Try invoke-expression $cmdname
Consider the following function:
function f1{
param(
$sb = {},
$s = ''
)
if ($sb -isnot [scriptblock]) { 'scriptblock' }
if ($s -isnot [string] ) { 'string' }
}
Now invoke it with a splat parameter:
PS C:\> $splat = #{foo='bar'}
PS C:\> f1 #splat
As expected, nothing is returned. Now try it again with a $null splat parameter:
PS C:\> $splat = $null
PS C:\> f1 #splat
scriptblock
Oddly, scriptblock is returned. Clearly, at least for the [scriptblock] parameter, powershell is not honoring the default value when a $null splat parameter is used. But powershell does honor the default value for the [string]. What is going on here?
For what types does Powershell honour default values when using $null splat parameters?
Isn't this just normal application of positional parameters? You are splatting a single $null which is being applied to $sb.
Compare:
> function f{ param($sb = {}, $s = '') $PSBoundParameters }
> $splat = #(1,2)
> f #splat
Key Value
--- -----
sb 1
s 2
> f #flkejlkfja
Key Value
--- -----
sb
> function f{ param($aaa = 5, $sb = {}, $s = '') $PSBoundParameters }
> f #splat
Key Value
--- -----
aaa 1
sb 2
It's an old question but if it is still interesting...
As others have written with $splat = $null calling f1 #splat the first parameters will get the value $null instead it's default value.
If you want the parameters use their default value in this case you have to use $splat = #{} or $splat = #().
Here's a demonstration to help understand what's happening
$splat = #{foo='bar'}
"$(&{$args}#splat)"
-foo: bar
When you splat the hash table, it gets converted to -Key: Value string pairs that become the parameters to your function.
Now try:
$splat = $null
"$(&{$args}#splat)"
Nothing is returned. There are no keys to generate the parameter string from, so the end result is the same as not passing any parameters at all.
To complement Etan Reisner's helpful answer with a more direct demonstration that splatting $null indeed passes $null as the first (and only) positional argument:
$splat = $null
& { [CmdletBinding(PositionalBinding=$False)] param($dummy) } #splat
The above yields the following error:
A positional parameter cannot be found that accepts argument '$null'.
...
Decorating the param() block with [CmdletBinding(PositionalBinding=$False)] ensures that only named parameter values can be passed, causing the positional passing of $null from splatting to trigger the error above.
Note that using the special "null collection" value ([System.Management.Automation.Internal.AutomationNull]::Value) that you get from commands that produce no output for splatting is effectively the same as splatting $null, because that "null collection" value is converted to $null during parameter binding.
VargaJoe's helpful answer explains how to construct a variable for splatting so that no arguments are passed, so that the callee's default parameter values are honored.
Let's say I want to write a helper function that wraps Read-Host. This function will enhance Read-Host by changing the prompt color, calling Read-Host, then changing the color back (simple example for illustrative purposes - not actually trying to solve for this).
Since this is a wrapper around Read-Host, I don't want to repeat the all of the parameters of Read-Host (i.e. Prompt and AsSecureString) in the function header. Is there a way for a function to take an unspecified set of parameters and then pass those parameters directly into a cmdlet call within the function? I'm not sure if Powershell has such a facility.
for example...
function MyFunc( [string] $MyFuncParam1, [int] $MyFuncParam2 , Some Thing Here For Cmdlet Params that I want to pass to Cmdlet )
{
# ...Do some work...
Read-Host Passthru Parameters Here
# ...Do some work...
}
It sounds like you're interested in the 'ValueFromRemainingArguments' parameter attribute. To use it, you'll need to create an advanced function. See the about_Functions_Advanced and about_Functions_Advanced_Parameters help topics for more info.
When you use that attribute, any extra unbound parameters will be assigned to that parameter. I don't think they're usable as-is, though, so I made a little function that will parse them (see below). After parsing them, two variables are returned: one for any unnamed, positional parameters, and one for named parameters. Those two variables can then be splatted to the command you want to run. Here's the helper function that can parse the parameters:
function ParseExtraParameters {
[CmdletBinding()]
param(
[Parameter(ValueFromRemainingArguments=$true)]
$ExtraParameters
)
$ParamHashTable = #{}
$UnnamedParams = #()
$CurrentParamName = $null
$ExtraParameters | ForEach-Object -Process {
if ($_ -match "^-") {
# Parameter names start with '-'
if ($CurrentParamName) {
# Have a param name w/o a value; assume it's a switch
# If a value had been found, $CurrentParamName would have
# been nulled out again
$ParamHashTable.$CurrentParamName = $true
}
$CurrentParamName = $_ -replace "^-|:$"
}
else {
# Parameter value
if ($CurrentParamName) {
$ParamHashTable.$CurrentParamName += $_
$CurrentParamName = $null
}
else {
$UnnamedParams += $_
}
}
} -End {
if ($CurrentParamName) {
$ParamHashTable.$CurrentParamName = $true
}
}
,$UnnamedParams
$ParamHashTable
}
You could use it like this:
PS C:\> ParseExtraParameters -NamedParam1 1,2,3 -switchparam -switchparam2:$false UnnamedParam1
UnnamedParam1
Name Value
---- -----
switchparam True
switchparam2 False
NamedParam1 {1, 2, 3}
Here are two functions that can use the helper function (one is your example):
function MyFunc {
[CmdletBinding()]
param(
[string] $MyFuncParam1,
[int] $MyFuncParam2,
[Parameter(Position=0, ValueFromRemainingArguments=$true)]
$ExtraParameters
)
# ...Do some work...
$UnnamedParams, $NamedParams = ParseExtraParameters #ExtraParameters
Read-Host #UnnamedParams #NamedParams
# ...Do some work...
}
function Invoke-Something {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, Position=0)]
[string] $CommandName,
[Parameter(ValueFromRemainingArguments=$true)]
$ExtraParameters
)
$UnnamedParameters, $NamedParameters = ParseExtraParameters #ExtraParameters
&$CommandName #UnnamedParameters #NamedParameters
}
After importing all three functions, try these commands:
MyFunc -MyFuncParam1 Param1Here "PromptText" -assecure
Invoke-Something -CommandName Write-Host -Fore Green "Some text" -Back Red
One word: splatting.
Few more words: you can use combination of $PSBoundParameters and splatting to pass parameters from external command, to internal command (assuming names match). You would need to remove any parameter that you don't want to use though from $PSBoundParameters first:
$PSBoundParameters.Remove('MyFuncParam1')
$PSBoundParameters.Remove('MyFuncParam2')
Read-Host #PSBoundParameters
EDIT
Sample function body:
function Read-Data {
param (
[string]$First,
[string]$Second,
[string]$Prompt,
[switch]$AsSecureString
)
$PSBoundParameters.Remove('First') | Out-Null
$PSBoundParameters.Remove('Second') | Out-Null
$Result = Read-Host #PSBoundParameters
"First: $First Second: $Second Result: $Result"
}
Read-Data -First Test -Prompt This-is-my-prompt-for-read-host