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
Related
I'm having troubles wrapping my head around the code logic needed to get my Param block functioning as I want in PowerShell 5.1. A very simplified (but usable for my request) script with the Param block is here and this works exactly as I want:
[CmdletBinding(DefaultParameterSetName = 'All')]
Param (
[Parameter(Position=0,Mandatory=$True)]
[String]$Name,
[Parameter(ParameterSetName = 'All')]
[Switch]$All,
[Parameter(ParameterSetName = 'Individual')]
[Switch]$P1,
[Parameter(ParameterSetName = 'Individual')]
[Switch]$P2
)
If ($All -Or ($P1.ToBool() + $P2.ToBool() -eq 0)) {
$All = $True
$P1 = $True
$P2 = $True
}
"`$Name is $Name"
"`$All is $All"
"`$P1 is $P1"
"`$P2 is $P2"
The auto-completion of parameters when running this script above works as intended. If I use the "-All" switch, then -P# are not available. If I use -P# switche, -All is not available. If I omit the -Name, then it prompts me to put in a name. If I use the -All switch, I want all of the individual -P# options to later be set to $True. If I use no switches at all, it prompts me for a Name then sets all options to $True.
The first problem is that when using DefaultParameterSetName = 'All' (which I had to do in order to make the script work without any switches on the command line), then the $All variable is NOT actually being set to $True when it is not present on the command line. I had to make the "If" block in order to overcome that behavior. This makes the next problem come up because the actual script I'm trying to use this in will have fifteen or more -P# switches. That will make the "If" test more complex and ugly.
Is there a better way I can do this? Maybe something in the layout of my Parameter Sets? I could even eliminate the "-All" switch entirely if there's an easier way to evaluate that none of the -P# switches are used. Is there an easier way to add up the boolean value of all Parameters named P#? I've stumbled across the $MyInvocation variable and $MyInvocation.MyCommand.Parameters seems promising but I'm not sure exactly how to process that either.
Update after answer found:
Here is my new, simplified working code sample which I arrived at thanks to all the suggestions here. The "-All" Switch was unnecessary. I decided to go with the Get-Variable method here for now due to its simplicity and scalability, but it does require a common prefix on the Switch variables. The If() block will remain the same no matter how many variables are used.
Param (
[Parameter(Position=0,Mandatory=$True)]
[String]$Name,
[Alias('Blue')]
[Switch]$OptionBlue,
[Alias('Red')]
[Switch]$OptionRed,
[Alias('Yellow')]
[Switch]$OptionYellow
)
$AllOptions = Get-Variable -Name 'Option*'
If (-Not $AllOptions.Value.Contains($True)) {
"None of the Option Switches were used, setting all Option Variables to $True"
ForEach ($Option In $AllOptions) {$Option.Value = $True}
}
"`$Name: $Name"
"`$OptionBlue: $OptionBlue"
"`$OptionRed: $OptionRed"
"`$OptionYellow: $OptionYellow"
and here's how it works:
PS C:\Scripts\PowerShell> .\Test-Params.ps1
cmdlet Test-Params.ps1 at command pipeline position 1
Supply values for the following parameters:
Name: Testing
None of the Option Switches were used, setting all Option Variables to True
$Name: Testing
$OptionBlue: True
$OptionRed: True
$OptionYellow: True
PS C:\Scripts\PowerShell> .\Test-Params.ps1 -Name Testing -OptionRed
$Name: Testing
$OptionBlue: False
$OptionRed: True
$OptionYellow: False
PS C:\Scripts\PowerShell> .\Test-Params.ps1 -Name Testing -Blue -Yellow
$Name: Testing
$OptionBlue: True
$OptionRed: False
$OptionYellow: True
PS C:\Scripts\PowerShell> .\Test-Params.ps1 -Yellow -OptionRed
cmdlet Test-Params.ps1 at command pipeline position 1
Supply values for the following parameters:
Name: Testing
$Name: Testing
$OptionBlue: False
$OptionRed: True
$OptionYellow: True
Final Update
I figured out the way to avoid any issue where the script's switch parameters might match an already existing variable in the scope. Pulling the script's parameters that match the proper prefix/suffix used in the script's parameter names using $MyInvocation and then passing those specific names to Get-Variable avoids the issue. The switches also work correctly both when they are not present, or if they are explicitly set to $False. If more switches are needed, simply add them in the Param block with the proper prefix/suffix in the name and an alias for the simpler version. I think this bit of the code is bulletproof now...
Param (
[Parameter(Position=0,Mandatory=$True)]
[String]$Name,
[Alias('Blue')]
[Switch]$OptionBlue,
[Alias('Red')]
[Switch]$OptionRed,
[Alias('Yellow')]
[Switch]$OptionYellow
)
$OptionNotAnOption = $True
$OptionSwitches = ForEach ($Option In ($MyInvocation.MyCommand.Parameters.Keys | Where {$_ -Like "Option*"})) {Get-Variable $Option}
If ($OptionSwitches.Value.IsPresent -NotContains $True) {
"All options are `$False or not present: Enabling all options"
ForEach ($Switch In $OptionSwitches) {
Get-Variable -Name ($Switch.Name) | Set-Variable -Value $True
}
}
"`$Name: $Name"
"`$OptionBlue: $OptionBlue"
"`$OptionRed: $OptionRed"
"`$OptionYellow: $OptionYellow"
"`$OptionNotAnOption: $OptionNotAnOption"
You can use the $PSBoundParameters "automatic variable" to access the parameters specified in the call to the script / function and alter the function's behaviour accordingly.
For example:
function Invoke-MyFunction
{
param
(
[string] $Name,
[switch] $P1,
[switch] $P2
)
# how many $Pn parameters were specified in the call to the function?
$count = #( $PSBoundParameters.GetEnumerator()
| where-object { $_.Key.StartsWith("P") }
).Length;
# if *none* specified then enable *all*
$all = $count -eq 0;
if( $all )
{
$P1 = $true;
$P2 = $true;
}
"`$Name is $Name"
"`$all is $all"
"`$P1 is $P1"
"`$P2 is $P2"
}
And some tests:
PS> Invoke-MyFunction
$Name is
$all is True
$P1 is True
$P2 is True
PS> Invoke-MyFunction -Name "aaa"
$Name is aaa
$all is True
$P1 is True
$P2 is True
PS> Invoke-MyFunction -Name "aaa" -P1
$Name is aaa
$all is False
$P1 is True
$P2 is False
PS> Invoke-MyFunction -Name "aaa" -P2
$Name is aaa
$all is False
$P1 is False
$P2 is True
PS> Invoke-MyFunction -Name "aaa" -P1 -P2
$Name is aaa
$all is False
$P1 is True
$P2 is True
but watch out because the way I've evaluated $count means that specifying -P1:$false or -P2:$false makes $all = $false:
PS> Invoke-MyFunction -Name "aaa" -P1:$false
$Name is aaa
$all is False
$P1 is False
$P2 is False
so you might need to refine the expression to suit whatever you want to happen in this edge case...
Update
If your parameter names don't follow a simple pattern you can do something like this instead:
$names = #( "SomeParam", "AnotherParam", "Param3" );
$count = #( $PSBoundParameters.GetEnumerator()
| where-object { $_.Key -in $names }
).Length;
Assuming those are all switches and they belong to the parameter set of 'Individual', you can set the unset parameters to $true if $All is specified like so:
if ($All.IsPresent)
{
(Get-Command -Name $MyInvocation.MyCommand.Name).Parameters.Values |
Where-Object -FilterScript { $_.ParameterSets.Keys -eq 'Individual' } |
Foreach-Object -Process {
Set-Variable -Name $_.Name -Value $true -PassThru # remove -PassThru to silence the output
}
}
Referencing the current executing command with $MyInvocation.MyCommand.Name, you can pass it to Get-Command for more detailed info. on its parameters. This will allow you to filter by ParameterSets grabbing just the ones falling under Individual. Finally, it will pass it to Set-Variable setting all the switches to $true in a dynamic sense.
Note: I typed this up on my phone so there may be some typos that should be easy to correct.
I'm looking for a way to make a cmdlet which receives parameter and while typing, it prompts suggestions for completion from a predefined array of options.
I was trying something like this:
$vf = #('Veg', 'Fruit')
function Test-ArgumentCompleter {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[ValidateSet($vf)]
$Arg
)
}
The expected result should be:
When writing 'Test-ArgumentCompleter F', after clicking the tub button, the F autocompleted to Fruit.
To complement the answers from #mklement0 and #Mathias, using dynamic parameters:
$vf = 'Veg', 'Fruit'
function Test-ArgumentCompleter {
[CmdletBinding()]
param ()
DynamicParam {
$RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
$AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
$ParameterAttribute.Mandatory = $true
$AttributeCollection.Add($ParameterAttribute)
$ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($vf)
$AttributeCollection.Add($ValidateSetAttribute)
$RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter('Arg', [string], $AttributeCollection)
$RuntimeParameterDictionary.Add('Arg', $RuntimeParameter)
return $RuntimeParameterDictionary
}
}
Depending on how you want to predefine you argument values, you might also use dynamic validateSet values:
Class vfValues : System.Management.Automation.IValidateSetValuesGenerator {
[String[]] GetValidValues() { return 'Veg', 'Fruit' }
}
function Test-ArgumentCompleter {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[ValidateSet([vfValues])]$Arg
)
}
note: The IValidateSetValuesGenerator class [read: interface] was introduced in PowerShell 6.0
In addition to mklement0's excellent answer, I feel obligated to point out that in version 5 and up you have a slightly simpler alternative available: enum's
An enum, or an "enumeration type", is a static list of labels (strings) associated with an underlying integral value (a number) - and by constraining a parameter to an enum type, PowerShell will automatically validate the input value against it AND provide argument completion:
enum MyParameterType
{
Veg
Fruit
}
function Test-ArgumentCompleter {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[MyParameterType]$Arg
)
}
Trying to tab complete the argument for -Arg will now cycle throw matching valid enum labels of MyParameterType:
PS ~> Test-ArgumentCompleter -Arg v[<TAB>]
# gives you
PS ~> Test-ArgumentCompleter -Arg Veg
PowerShell generally requires that attribute properties be literals (e.g., 'Veg') or constants (e.g., $true).
Dynamic functionality requires use of a script block (itself specified as a literal, { ... }) or, in specific cases, a type literal.
However, the [ValidateSet()] attribute only accepts an array of string(ified-on-demand) literals or - in PowerShell (Core) v6 and above - a type literal (see below).
Update:
If you're using PowerShell (Core) v6+, there's a simpler solution based on defining a custom class that implements the System.Management.Automation.IValidateSetValuesGenerator interface - see the 2nd solution in iRon's helpful answer.
Even in Windows PowerShell a simpler solution is possible if your validation values can be defined as an enum type - see Mathias R. Jessen's helpful answer.
To get the desired functionality based on a non-literal array of values, you need to combine two other attributes:
[ArgumentCompleter()] for dynamic tab-completion.
[ValidateScript()] for ensuring on command submission that the argument is indeed a value from the array, using a script block.
# The array to use for tab-completion and validation.
[string[]] $vf = 'Veg', 'Fruit'
function Test-ArgumentCompleter {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
# Tab-complete based on array $vf
[ArgumentCompleter({
param($cmd, $param, $wordToComplete) $vf -like "$wordToComplete*"
})]
# Validate based on array $vf.
# NOTE: If validation fails, the (default) error message is unhelpful.
# You can work around that in *Windows PowerShell* with `throw`, and in
# PowerShell (Core) 7+, you can add an `ErrorMessage` property:
# [ValidateScript({ $_ -in $vf }, ErrorMessage = 'Unknown value: {0}')]
[ValidateScript({
if ($_ -in $vf) { return $true }
throw "'$_' is not in the set of the supported values: $($vf -join ', ')"
})]
$Arg
)
"Arg passed: $Arg"
}
To add to the other helpful answers, I use something similiar for a script I made for work:
$vf = #('Veg', 'Fruit','Apple','orange')
$ScriptBlock = {
Foreach($v in $vf){
New-Object -Type System.Management.Automation.CompletionResult -ArgumentList $v,
$v,
"ParameterValue",
"This is the description for $v"
}
}
Register-ArgumentCompleter -CommandName Test-ArgumentCompleter -ParameterName Arg -ScriptBlock $ScriptBlock
function Test-ArgumentCompleter {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[String]$Arg )
}
Documentation for Register-ArgumentCompleter is well explained on Microsoft Docs. I personally don't like to use the enum statement as it didnt allow me to uses spaces in my Intellisense; same for the Validate parameter along with nice features to add a description.
Output:
EDIT:
#Mklement made a good point in validating the argument supplied to the parameter. This alone doesnt allow you to do so without using a little more powershell logic to do the validating for you (unfortunately, it would be done in the body of the function).
function Test-ArgumentCompleter {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
$Arg )
if($PSBoundParameters.ContainsKey('Arg')){
if($VF -contains $PSBoundParameters.Values){ "It work:)" }
else { "It no work:("}
}
}
I think it's worth sharing another alternative that complements the helpful answers from mklement0, Mathias, iRon and Abraham. This answer attempts to show the possibilities that PowerShell can offer when it comes to customization of a Class.
The Class used for this example offers:
Compatibility with Windows PowerShell 5.1 and PowerShell Core.
Validation, Completion and custom Error Message.
For the example below I'll be using completion and validation on values from a current directory, the values are fed dynamically at runtime with Get-ChildItem -Name.
Class
When referring to the custom validation set I've decided to use the variable $this, however that can be easily change for a variable name of one's choice:
[psvariable]::new('this', (& $this.CompletionSet))
The completion set could be also a hardcoded set, i.e.:
[string[]] $CompletionSet = 'foo', 'bar', 'baz'
However that would also require some modifications in the class logic itself.
using namespace System.Management.Automation
using namespace System.Management.Automation.Language
using namespace System.Collections
using namespace System.Collections.Generic
class CustomValidationCompletion : ValidateEnumeratedArgumentsAttribute, IArgumentCompleter {
[scriptblock] $CompletionSet = { Get-ChildItem -Name }
[scriptblock] $Validation
[scriptblock] $ErrorMessage
CustomValidationCompletion() { }
CustomValidationCompletion([scriptblock] $Validation, [scriptblock] $ErrorMessage) {
$this.Validation = $Validation
$this.ErrorMessage = $ErrorMessage
}
[void] ValidateElement([object] $Element) {
$context = #(
[psvariable]::new('_', $Element)
[psvariable]::new('this', (& $this.CompletionSet))
)
if(-not $this.Validation.InvokeWithContext($null, $context)) {
throw [MetadataException]::new(
[string] $this.ErrorMessage.InvokeWithContext($null, $context)
)
}
}
[IEnumerable[CompletionResult]] CompleteArgument(
[string] $CommandName,
[string] $ParameterName,
[string] $WordToComplete,
[CommandAst] $CommandAst,
[IDictionary] $FakeBoundParameters
) {
[List[CompletionResult]] $result = foreach($item in & $this.CompletionSet) {
if(-not $item.StartsWith($wordToComplete)) {
continue
}
[CompletionResult]::new("'$item'", $item, [CompletionResultType]::ParameterValue, $item)
}
return $result
}
}
Implementation
function Test-CompletionValidation {
[alias('tcv')]
[CmdletBinding()]
param(
[CustomValidationCompletion(
Validation = { $_ -in $this },
ErrorMessage = { "Not in set! Must be one of these: $($this -join ', ')" }
)]
[ArgumentCompleter([CustomValidationCompletion])]
[string] $Argument
)
$Argument
}
Demo
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/','') }
How do I add dynamic argument tab completion to a PowerShell Cmdlet?
When I type this and hit tab, I'd like for it to do tab completion.
PM> Paket-Add -NuGet FSharp.Co
These are the values I'd like to use in this example:
PM> Paket-FindPackages -SearchText FSharp.Co
FSharp.Core
FSharp.Core.3
FSharp.Configuration
FSharp.Core.Fluent-3.1
FSharp.Core.Fluent-4.0
FSharp.Compiler.Tools
FSharp.Compatibility.Scala
FSharp.Compatibility.OCaml
FSharp.Compiler.CodeDom
FSharp.Compiler.Service
FSharp.Control.Reactive
FSharp.Compatibility.Haskell
FSharp.Compatibility.OCaml.Numerics
FSharp.Compatibility.OCaml.Format
FSharp.Compatibility.OCaml.System
FSharp.Collections.ParallelSeq
FSharp.Compatibility.StandardML
FSharp.Compatibility.OCaml.LexYacc
FSharp.Control.AsyncSeq
I found this answer that gave a couple of helpful links and said I should run Get-Content function:TabExpansion2:
It looks like CommandCompletion.CompleteInput needs to implemented. I thought I read somewhere that there is a Hashtable of commands to functions. If so, where is it and how do I install custom ones? I'm using Chocolatey to distribute Paket.PowerShell. Here is the Cmdlet code.
UPDATE 2015-06-20:
I ended up getting it to work with the code here:
https://github.com/fsprojects/Paket/blob/76de1c44853ce09029ba157855525f435d951b85/src/Paket.PowerShell/ArgumentTabCompletion.ps1
# https://github.com/mariuszwojcik/RabbitMQTools/blob/master/TabExpansions.ps1
function createCompletionResult([string]$text, [string]$value, [string]$tooltip) {
if ([string]::IsNullOrEmpty($value)) { return }
if ([string]::IsNullOrEmpty($text)) { $text = $value }
if ([string]::IsNullOrEmpty($tooltip)) { $tooltip = $value }
$completionText = #{$true="'$value'"; $false=$value }[$value -match "\W"]
$completionText = $completionText -replace '\[', '``[' -replace '\]', '``]'
New-Object System.Management.Automation.CompletionResult $completionText, $text, 'ParameterValue', $tooltip | write
}
$findPackages = {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
Paket-FindPackages -SearchText $wordToComplete -Max 100 | % {
createCompletionResult $_ $_ $_ | write
}
}
$findPackageVersions = {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
if (-not $fakeBoundParameter.NuGet){ return }
Paket-FindPackageVersions -Name $fakeBoundParameter.NuGet -Max 100 | % {
createCompletionResult $_ $_ $_ | write
}
}
# create and add $global:options to the list of completers
# http://www.powertheshell.com/dynamicargumentcompletion/
if (-not $global:options) { $global:options = #{CustomArgumentCompleters = #{};NativeArgumentCompleters = #{}}}
$global:options['CustomArgumentCompleters']['Paket-Add:NuGet'] = $findPackages
$global:options['CustomArgumentCompleters']['Paket-Add:Version'] = $findPackageVersions
$function:tabexpansion2 = $function:tabexpansion2 -replace 'End\r\n{','End { if ($null -ne $options) { $options += $global:options} else {$options = $global:options}'
The completer param names are important. Renaming them will make it not work.
You may want to look at the TabExpansion++ module, which was designed to make extending tab completion easier.
I just played with it for few minutes, and I think you want something like this based on the example:
Import-Module TabExpansion++
function PaketAddNugetCompletion
{
[ArgumentCompleter(Parameter = 'Nuget', Command = 'Paket-Add')]
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
Paket-FindPackages -SearchText $wordToComplete |
ForEach-Object {
# not quite sure what property to use off the result, but this might work.
New-CompletionResult -CompletionText $_
}
}
These are called Dynamic parameters and are described in about_Functions_Advanced_Parameters.
The following example shows a sample function with standard
parameters named Name and Path, and an optional dynamic parameter
named DP1.The DP1 parameter is in the PSet1 parameter set and has a
type of Int32. The DP1 parameter is available in the Sample function
only when the value of the Path parameter contains "HKLM:", indicating
that it is being used in the HKEY_LOCAL_MACHINE registry drive.
function Get-Sample {
[CmdletBinding()]
Param ([String]$Name, [String]$Path)
DynamicParam
{
if ($path -match ".*HKLM.*:")
{
$attributes = new-object System.Management.Automation.ParameterAttribute
$attributes.ParameterSetName = "__AllParameterSets"
$attributes.Mandatory = $false
$attributeCollection = new-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
$attributeCollection.Add($attributes)
$dynParam1 = new-object -Type System.Management.Automation.RuntimeDefinedParameter("dp1", [Int32], $attributeCollection)
$paramDictionary = new-object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
$paramDictionary.Add("dp1", $dynParam1)
return $paramDictionary
}
}
}
Here's another example that does validation sets dynamically.
I've re-read your question, and it looks like maybe you just want a static, pre-defined list of tab-completed values for a specific parameter. If that's the case, then you can simply use the [ValidateSet()] attribute:
function Get-Something {
[CmdletBinding()]
param(
[ValidateSet('One','Two','Three')]
[String]
$MyParam
)
}
But if the values need to be determined at runtime, then see the above section on dynamic parameters instead.
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