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.
Related
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
)
I'm basically building my own parallel foreach pipeline function, using runspaces.
My problem is: I call my function like this:
somePipeline | MyNewForeachFunction { scriptBlockHere } | pipelineGoesOn...
How can I pass the $_ parameter correctly into the ScriptBlock? It works when the ScriptBlock contains as first line
param($_)
But as you might have noticed, the powershell built-in ForEach-Object and Where-Object do not need such a parameter declaration in every ScriptBlock that is passed to them.
Thanks for your answers in advance
fjf2002
EDIT:
The goal is: I want comfort for the users of function MyNewForeachFunction - they shoudln't need to write a line param($_) in their script blocks.
Inside MyNewForeachFunction, The ScriptBlock is currently called via
$PSInstance = [powershell]::Create().AddScript($ScriptBlock).AddParameter('_', $_)
$PSInstance.BeginInvoke()
EDIT2:
The point is, how does for example the implementation of the built-in function ForEach-Object achieve that $_ need't be declared as a parameter in its ScriptBlock parameter, and can I use that functionality, too?
(If the answer is, ForEach-Object is a built-in function and uses some magic I can't use, then this would disqualify the language PowerShell as a whole in my opinion)
EDIT3:
Thanks to mklement0, I could finally build my general foreach loop. Here's the code:
function ForEachParallel {
[CmdletBinding()]
Param(
[Parameter(Mandatory)] [ScriptBlock] $ScriptBlock,
[Parameter(Mandatory=$false)] [int] $PoolSize = 20,
[Parameter(ValueFromPipeline)] $PipelineObject
)
Begin {
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $poolSize)
$RunspacePool.Open()
$Runspaces = #()
}
Process {
$PSInstance = [powershell]::Create().
AddCommand('Set-Variable').AddParameter('Name', '_').AddParameter('Value', $PipelineObject).
AddCommand('Set-Variable').AddParameter('Name', 'ErrorActionPreference').AddParameter('Value', 'Stop').
AddScript($ScriptBlock)
$PSInstance.RunspacePool = $RunspacePool
$Runspaces += New-Object PSObject -Property #{
Instance = $PSInstance
IAResult = $PSInstance.BeginInvoke()
Argument = $PipelineObject
}
}
End {
while($True) {
$completedRunspaces = #($Runspaces | where {$_.IAResult.IsCompleted})
$completedRunspaces | foreach {
Write-Output $_.Instance.EndInvoke($_.IAResult)
$_.Instance.Dispose()
}
if($completedRunspaces.Count -eq $Runspaces.Count) {
break
}
$Runspaces = #($Runspaces | where { $completedRunspaces -notcontains $_ })
Start-Sleep -Milliseconds 250
}
$RunspacePool.Close()
$RunspacePool.Dispose()
}
}
Code partly from MathiasR.Jessen, Why PowerShell workflow is significantly slower than non-workflow script for XML file analysis
The key is to define $_ as a variable that your script block can see, via a call to Set-Variable.
Here's a simple example:
function MyNewForeachFunction {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[scriptblock] $ScriptBlock
,
[Parameter(ValueFromPipeline)]
$InputObject
)
process {
$PSInstance = [powershell]::Create()
# Add a call to define $_ based on the current pipeline input object
$null = $PSInstance.
AddCommand('Set-Variable').
AddParameter('Name', '_').
AddParameter('Value', $InputObject).
AddScript($ScriptBlock)
$PSInstance.Invoke()
}
}
# Invoke with sample values.
1, (Get-Date) | MyNewForeachFunction { "[$_]" }
The above yields something like:
[1]
[10/26/2018 00:17:37]
What I think you're looking for (and what I was looking for) is to support a "delay-bind" script block, supported in PowerShell 5.1+. The Microsoft documentation tells a bit about what's required, but doesn't provide any user-script examples (currently).
The gist is that PowerShell will implicitly detect that your function can accept a delay-bind script block if it defines an explicitly typed pipeline parameter (either by Value or by PropertyName), as long as it's not of type [scriptblock] or type [object].
function Test-DelayedBinding {
param(
# this is our typed pipeline parameter
# per doc this cannot be of type [scriptblock] or [object],
# but testing shows that type [object] may be permitted
[Parameter(ValueFromPipeline, Mandatory)][string]$string,
# this is our scriptblock parameter
[Parameter(Position=0)][scriptblock]$filter
)
Process {
if (&$filter $string) {
Write-Output $string
}
}
}
# sample invocation
>'foo', 'fi', 'foofoo', 'fib' | Test-DelayedBinding { return $_ -match 'foo' }
foo
foofoo
Note that the delay-bind will only be applied if input is piped into the function, and that the script block must use named parameters (not $args) if additional parameters are desired.
The frustrating part is that there is no way to explicitly specify that delay-bind should be used, and errors resulting from incorrectly structuring your function may be non-obvious.
Maybe this can help.
I'd normally run auto-generated jobs in parallel this way:
Get-Job | Remove-Job
foreach ($param in #(3,4,5)) {
Start-Job -ScriptBlock {param($lag); sleep $lag; Write-Output "slept for $lag seconds" } -ArgumentList #($param)
}
Get-Job | Wait-Job | Receive-Job
If I understand you correctly, you are trying to get rid of param() inside the scriptblock. You may try to wrap that SB with another one. Below is the workaround for my sample:
Get-Job | Remove-Job
#scriptblock with no parameter
$job = { sleep $lag; Write-Output "slept for $lag seconds" }
foreach ($param in #(3,4,5)) {
Start-Job -ScriptBlock {param($param, $job)
$lag = $param
$script = [string]$job
Invoke-Command -ScriptBlock ([Scriptblock]::Create($script))
} -ArgumentList #($param, $job)
}
Get-Job | Wait-Job | Receive-Job
# I was looking for an easy way to do this in a scripted function,
# and the below worked for me in PSVersion 5.1.17134.590
function Test-ScriptBlock {
param(
[string]$Value,
[ScriptBlock]$FilterScript={$_}
)
$_ = $Value
& $FilterScript
}
Test-ScriptBlock -Value 'unimportant/long/path/to/foo.bar' -FilterScript { [Regex]::Replace($_,'unimportant/','') }
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
I'm trying to find a way to get all parameter information from a powershell script. Ex script:
function test()
{
Param(
[string]$foo,
[string]$bar,
[string]$baz = "baz"
)
foreach ($key in $MyInvocation.BoundParameters.keys)
{
write-host "Parameter: $($key) -> $($MyInvocation.BoundParameters[$key])"
}
}
test -foo "foo!"
I'd like to get the values of $bar and $baz in a dynamic way without knowing the names of the parameters ahead of time.
I've looked through $MyInvocation properties and methods but I don't see anything besides parameters that are set/passed.
Update 1:
I'm close to getting it with:
function test()
{
Param(
[string]$foo,
[string]$bar,
[string]$baz = "baz"
)
foreach($var in (get-variable -scope private))
{
write-host "$($var.name) -> $($var.value)"
}
}
test -foo "foo!"
If i could filter out the script parameters vs the default parameters I would be good to go.
Update 2:
The final working solution looks like this:
function test {
param (
[string] $Bar = 'test'
, [string] $Baz
, [string] $Asdf
)
$ParameterList = (Get-Command -Name $MyInvocation.InvocationName).Parameters;
foreach ($key in $ParameterList.keys)
{
$var = Get-Variable -Name $key -ErrorAction SilentlyContinue;
if($var)
{
write-host "$($var.name) > $($var.value)"
}
}
}
test -asdf blah;
Check this solution out. This uses the CmdletBinding() attribute, which provides some additional metadata through the use of the $PSCmdlet built-in variable. You can:
Dynamically retrieve the command's name, using $PSCmdlet
Get a list of the parameter for the command, using Get-Command
Examine the value of each parameter, using the Get-Variable cmdlet
Code:
function test {
[CmdletBinding()]
param (
[string] $Bar = 'test'
, [string] $Baz
, [string] $Asdf
)
# Get the command name
$CommandName = $PSCmdlet.MyInvocation.InvocationName;
# Get the list of parameters for the command
$ParameterList = (Get-Command -Name $CommandName).Parameters;
# Grab each parameter value, using Get-Variable
foreach ($Parameter in $ParameterList) {
Get-Variable -Name $Parameter.Values.Name -ErrorAction SilentlyContinue;
#Get-Variable -Name $ParameterList;
}
}
test -asdf blah;
Output
The output from the command looks like this:
Name Value
---- -----
Bar test
Baz
Asdf blah
To read the value dynamically use the get-variable function / cmdlet
write-host (get-variable "foo")
To print out all of the parameters do the following
foreach ($key in $MyInvocation.BoundParameters.keys)
{
$value = (get-variable $key).Value
write-host "$key -> $value"
}
Hopefully, some may find this one-liner useful:
function test()
{
Param(
[string]$foo,
[string]$bar,
[string]$baz = "baz"
)
$MyInvocation.MyCommand.Parameters | Format-Table -AutoSize #{ Label = "Key"; Expression={$_.Key}; }, #{ Label = "Value"; Expression={(Get-Variable -Name $_.Key -EA SilentlyContinue).Value}; }
}
test -foo "foo!"
Result
Keys Value
---- -----
foo foo!
bar
baz baz
I found this most useful for PS4 (Windows 2012 R2) - it includes default values / optional parameters:
$cmdName = $MyInvocation.InvocationName
$paramList = (Get-Command -Name $cmdName).Parameters
foreach ( $key in $paramList.Keys ) {
$value = (Get-Variable $key -ErrorAction SilentlyContinue).Value
if ( $value -or $value -eq 0 ) {
Write-Host "$key -> $value"
}
}
For those of you who do not want to use cmdletbinding() here's a variation on the one liner I found above:
(Get-Command -Name $PSCommandPath).Parameters | Format-Table -AutoSize #{ Label = "Key"; Expression={$_.Key}; }, #{ Label = "Value"; Expression={(Get-Variable -Name $_.Key -EA SilentlyContinue).Value}; }
$PSCommandPath is always available
I played with the 2 solutions i liked in this thread, they both work.
however I needed to produce an error out on missing parameter for a build script
$cmdName = $MyInvocation.InvocationName
$paramList = (Get-Command -Name $cmdName).Parameters
foreach ( $key in $paramList.Keys ) {
$value = (Get-Variable $key -ErrorAction Stop)
#Write-Host $value.Value #remove comment for error checking
if ([string]::IsNullOrEmpty($value.Value)){
$(throw ("$key is a mandatory value please declare with -$key <Required value> " ))
}
}
What if I don't know what arguments or how many will be passed? For example, the function could be called with:
test -foo "value1" -bar "value2" -baz "value3"
and someone else might call it with:
test -variable1 "somevalue" -variable2 "somevalue2" -variable3 "somevalue3" -variable4 "value4"
I looked at $MyInvocation and the arguments come across as UnboundArguments, so theoretically I could "pair up" argument 0 with argument 1, 2 with 3, etc and error out if there's an odd number of UnboundArguments.
Stumbled upon this trying to do something similar and figured out my preferred option.
Function ParamTest {
Param (
$t1 = '1234',
$t2,
[switch]$3
)
$MyInvocation |Add-Member -Name:'Param' -MemberType:'NoteProperty' -Value:(
(Get-Variable -Scope:'Local' -Include:#($MyInvocation.MyCommand.Parameters.keys)|
ForEach-Object -begin:{$h=#{}} -process:{$h.add($_.Name,$_.Value)} -end:{$h}
))
$MyInvocation.Param
}
Result
Name Value
---- -----
t1 1234
3 False
t2
PSVersionTable
Name Value
---- -----
PSVersion 5.1.19041.1320
PSEdition Desktop
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
BuildVersion 10.0.19041.1320
CLRVersion 4.0.30319.42000
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
A streamlined solution that:
builds on your own approach now shown at the bottom of the question,
while also including an additional piece of information, namely whether each parameter was bound on invocation, i.e. whether an argument was passed or not, based on its presence in the automatic $PSBoundParameter variable, which is a dictionary containing the bound parameters and their values (arguments).
Note: $PSBoundParameters does not include parameters bound by a default value, only those parameters to which an argument was explicitly passed; for your use case, that is arguably desirable in order to distinguish between a default value and an explicit one, but in scenarios where arguments should be passed through it may be desirable to have default-value parameters included - see GitHub issue #3285.
function test {
param (
[string]$foo,
[string]$bar,
[string]$baz = "baz"
)
foreach ($paramName in $MyInvocation.MyCommand.Parameters.Keys) {
$bound = $PSBoundParameters.ContainsKey($paramName)
[pscustomobject] #{
ParameterName = $paramName
ParameterValue = if ($bound) { $PSBoundParameters[$paramName] }
else { Get-Variable -Scope Local -ErrorAction Ignore -ValueOnly $paramName }
Bound = $bound
}
}
}
test -foo "foo!"
The above yields the following:
ParameterName ParameterValue Bound
------------- -------------- -----
foo foo! True
bar False
baz baz False
Note: This solution also handles dynamic parameters correctly:
Such parameters are reflected in $MyInvocation.MyCommand.Parameters if they situationally apply, but - unlike regular static parameters - are never reflected in scope-local variables. If they apply and are also bound, they and their values are reflected in $PSBoundParameters.
Thus - given that an applicable dynamic parameter may not be bound - the Get-Variable call to look for scope-local variables representing static parameters:
Must explicitly be limited to the current scope with -Scope Local so as not to accidentally pick up unrelated variables of the same name from ancestral scopes for unbound dynamic parameters.
Must ignore errors from the potentially resulting failure to find a variable in the current scope, using -ErrorAction Ignore.
I wanted a compact string of parameter key/value pairs that I can write out when catching error. Use content of the other answers, I came up with this:
$parameters = (Get-Variable -Scope:'Local' -Include:#($MyInvocation.MyCommand.Parameters.keys) |
Select-Object Name, Value | ForEach-Object { "$($_.Name) : $($_.Value)" }) -join ' | '
Sample output:
ComputerName : SomePC | Directory : C:\Tools\LogExpert | UserName : Hans Wurst | YesOrNo : False
Thanks to all other posters with great & helpful answers above!
I'm cobbling together some of the above posts to try to meet my requirements.
I'd like to show Params in a Powershell-language-compatible format, so that one could easily see & display these params, and then also copy+pasta back into either a script as Param() Defaults (ParamsFormat1) or into a commandline/function call (CommandFormat2). I struggled with this for a while, so I hope this saves someone some time.
Collection of various Scripts from above:
# Note: there are some subtle differences between these versions below
# Specifically in the first line for *which* object you're checking for Parameters, and subsequently what type of Parameters you're looking at.
# PSCmdlet version REQUIRES [CmdletBinding()] on your function!!!
# $($(Get-Command -Name $($PSCmdlet.MyInvocation.InvocationName)).Parameters) | `
# %{ Get-Variable -Name $_.Values.Name -ErrorAction SilentlyContinue; }
# -Scope:'Local' FILTERS out any global params
# (Get-Variable -Scope:'Local' -Include:#($MyInvocation.MyCommand.Parameters.keys)) | ft
# PSCommandPath supposedly available/usable "in all situations" - so this may be the most useful of all of them, BUT it shows global params [Debug, ErrorAction, etc...]
# (Get-Command -Name $PSCommandPath).Parameters | `
# %{ Get-Variable -Name $_.Values.Name -ErrorAction SilentlyContinue; }
ParamsFormat1 / HYBRID VERSION: To output "Default Params" as you would see them in a function definition. Combines the -Scope:"Local" and "always available" versions to get BOTH param TYPES as well as Name, Value
Write-Host "Params("
# TBD Figure out how to expand #{} of System.Collections.Hashtable
# HYBRID VERSION: Combine the -Scope:"Local" and "always available" versions to get BOTH param TYPES as well as Name, Value
# If you remove LocalList Filter here, it will also show "Global" function properties like Debug, ErrorAction, etc...
$LocalList = $(Get-Variable -Scope:'Local' -Include:#($MyInvocation.MyCommand.Parameters.keys) | Select-Object -ExpandProperty Name) -join "|"
# VERSION: Wrapper script with DEFAULTS : normally DOES include Global vars [Debug, ErrorAction, etc]. but DOES preserve param order as-written.
((Get-Command -Name $PSCommandPath).Parameters | `
Select -ExpandProperty Values | `
Where-Object { $_.Name -Match $LocalList } | `
Format-Table -HideTableHeaders -AutoSize `
#{ Label="Type"; Expression={"[$($_.ParameterType )]"}; }, `
#{ Label="Name"; Expression={"`t`$$($_.Name)"}; }, `
#{ Label="Equals"; Expression={"="}; }, `
#{ Label="Value"; Expression={ If( $_.ParameterType -Match "String" ) { "`"$((Get-Variable -Name $_.Name -EA SilentlyContinue).Value)`"" } Else{ $((Get-Variable -Name $_.Name -EA SilentlyContinue).Value)}; }; }, `
#{ Label="RowEndComma"; Expression={ "," }; }
##{ Label="Value"; Expression={ $((Get-Variable -Name $_.Name -EA SilentlyContinue).Value) }; } # (OPTIONAL) Values only, no wrapping quotes
)
Write-Host ")";
CommandFormat2 / SIMPLER VERSION: Call with CommandLine Args (TYPES not needed). This filters out Global vars, does NOT preserve param ordering. (sorted alphabetically?)
# VERSION: Call with CommandLine Args (TYPES not available - TBD needs more work to display String, Array, and Hashmap/PsCustomObject ["", #() and {}] types better): filters out Global vars, does NOT preserve param ordering.
# If needed, cut+paste OPTIONAL lines down above Format-Table line.
# Where-Object { $_.Value } | ` # (Optional) remove any Null items.
# Sort-Object -Property Name | ` # (Optional) sort output
(Get-Variable -Scope:'Local' -Include:#($MyInvocation.MyCommand.Parameters.keys) | `
Format-Table -HideTableHeaders -AutoSize `
#{ Label="Name"; Expression={"`t-$($_.Name)"}; }, `
#{ Label="Equals"; Expression ={":"}; }, `
#{ Label="Value"; Expression={ If( $_.ParameterType -NotMatch "String" ) { $_.Value; } Else {"`"$($_.Value)`"";} }; Alignment="left"; }
)
How do you implement the parameter tab completion for PowerShell functions or cmdlets like Get-Service and Get-Process in PowerShell 3.0?
I realise ValidateSet works for a known list, but I want to generate the list on demand.
Adam Driscoll hints that it is possible for cmdlets but unfortunately hasn't elaborated.
Trevor Sullivan shows a technique for functions, but as I understand it, his code only generates the list at the time the function is defined.
I puzzled over this for a while, because I wanted to do the same thing. I put together something that I'm really happy with.
You can add ValidateSet attributes from a DynamicParam. Here's an example of where I've generated my ValidateSet on-the-fly from an xml file. See the "ValidateSetAttribute" in the following code:
function Foo() {
[CmdletBinding()]
Param ()
DynamicParam {
#
# The "modules" param
#
$modulesAttributeCollection = new-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
# [parameter(mandatory=...,
# ...
# )]
$modulesParameterAttribute = new-object System.Management.Automation.ParameterAttribute
$modulesParameterAttribute.Mandatory = $true
$modulesParameterAttribute.HelpMessage = "Enter one or more module names, separated by commas"
$modulesAttributeCollection.Add($modulesParameterAttribute)
# [ValidateSet[(...)]
$moduleNames = #()
foreach($moduleXmlInfo in Select-Xml -Path "C:\Path\to\my\xmlFile.xml" -XPath "//enlistment[#name=""wp""]/module") {
$moduleNames += $moduleXmlInfo.Node.Attributes["name"].Value
}
$modulesValidateSetAttribute = New-Object -type System.Management.Automation.ValidateSetAttribute($moduleNames)
$modulesAttributeCollection.Add($modulesValidateSetAttribute)
# Remaining boilerplate
$modulesRuntimeDefinedParam = new-object -Type System.Management.Automation.RuntimeDefinedParameter("modules", [String[]], $modulesAttributeCollection)
$paramDictionary = new-object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
$paramDictionary.Add("modules", $modulesRuntimeDefinedParam)
return $paramDictionary
}
process {
# Do stuff
}
}
With that, I can type
Foo -modules M<press tab>
and it will tab-complete "MarcusModule" if that module was in the XML file. Furthermore, I can edit the XML file and the tab-completion behavior will immediately change; you don't have to re-import the function.
Check the TabExpansionPlusPlus module on github, written by a former PowerShell team magician.
https://github.com/lzybkr/TabExpansionPlusPlus#readme
Classically, I used regex.
for example,
function TabExpansion {
param($line, $lastWord)
if ( $line -match '(-(\w+))\s+([^-]*$)' )
{
### Resolve Command name & parameter name
$_param = $matches[2] + '*'
$_opt = $Matches[3].Split(" ,")[-1] + '*'
$_base = $Matches[3].Substring(0,$Matches[3].Length-$Matches[3].Split(" ,")[-1].length)
$_cmdlet = [regex]::Split($line, '[|;=]')[-1]
if ($_cmdlet -match '\{([^\{\}]*)$')
{
$_cmdlet = $matches[1]
}
if ($_cmdlet -match '\(([^()]*)$')
{
$_cmdlet = $matches[1]
}
$_cmdlet = $_cmdlet.Trim().Split()[0]
$_cmdlet = #(Get-Command -type 'Cmdlet,Alias,Function,Filter,ExternalScript' $_cmdlet)[0]
while ($_cmdlet.CommandType -eq 'alias')
{
$_cmdlet = #(Get-Command -type 'Cmdlet,Alias,Function,Filter,ExternalScript' $_cmdlet.Definition)[0]
}
### Currently target is Get-Alias & "-Name" parameter
if ( "Get-Alias" -eq $_cmdlet.Name -and "Name" -like $_param )
{
Get-Alias -Name $_opt | % { $_.Name } | sort | % { $_base + ($_ -replace '\s','` ') }
break;
}
}
}
Reference
http://gallery.technet.microsoft.com/scriptcenter/005d8bc7-5163-4a25-ad0d-25cffa90faf5
Posh-git renames TabExpansion to TabExpansionBackup in GitTabExpansion.ps1.
And posh-git's redifined TabExpansion calls original TabExpansion(TabExpansionBackup) when completions don't match with git commands.
So all you have to do is redefine TabExpansionBackup.
(cat .\GitTabExpansion.ps1 | select -last 18)
============================== GitTabExpansion.ps1 ==============================
if (Test-Path Function:\TabExpansion) {
Rename-Item Function:\TabExpansion TabExpansionBackup
}
function TabExpansion($line, $lastWord) {
$lastBlock = [regex]::Split($line, '[|;]')[-1].TrimStart()
switch -regex ($lastBlock) {
# Execute git tab completion for all git-related commands
"^$(Get-AliasPattern git) (.*)" { GitTabExpansion $lastBlock }
"^$(Get-AliasPattern tgit) (.*)" { GitTabExpansion $lastBlock }
# Fall back on existing tab expansion
default { if (Test-Path Function:\TabExpansionBackup) { TabExpansionBackup $line $lastWord } }
}
}
===============================================================================
Redefine TabExpansionBackup(original TabExpansion)
function TabExpansionBackup {
...
### Resolve Command name & parameter name
...
### Currently target is Get-Alias & "-Name" parameter
...
}