Using ValidateSet with functions or List - powershell

I'm trying to do something like this.
The Get-SomeOtherParameter returns a system.Array type list from a database.
I don't want to hardcode my ValidateSet in case the list changes overTime in the database
function Get-SomeItems {
param (
[Parameter(Mandatory = $true)]
[ValidateSet(Get-SomeOtherParameter)]
[string]$filter,
[Parameter(Mandatory = $true)]
[ValidateSet('abc', 'def', 'ghi')]
[String]$filter2
)
}

To complement Start-Automating's helpful answer by spelling out the [ValidateScript({ ... }] and [ArgumentCompleter({ ... }) approaches:
# Function that returns the valid values for the -filter parameter below.
function Get-ValidFilterValues {
# Sample, hard-coded values. This is where your database lookup would happen.
'foo', 'bar'
}
function Get-SomeItems {
param (
[Parameter(Mandatory)]
[ValidateScript({
$validValues = Get-ValidFilterValues
if ($_ -in $validValues) { return $true } # OK
throw "'$_' is not a valid value. Use one of the following: '$($validValues -join ', ')'"
})]
[ArgumentCompleter({
param($cmd, $param, $wordToComplete)
(Get-ValidFilterValues) -like "$wordToComplete*"
})]
[string]$filter,
[Parameter(Mandatory)]
[ValidateSet('abc', 'def', 'ghi')]
[String]$filter2
)
$filter, $filter2 # sample output.
}
A simpler PowerShell (Core) 7+ alternative is to implement validation via a custom class that implements the System.Management.Automation.IValidateSetValuesGenerator interface, which automatically also provides tab-completion:
# Custom class that implements the IValidateSetValuesGenerator interface
# in order to return the valid values for the -filter parameter below.
class ValidFilterValues : System.Management.Automation.IValidateSetValuesGenerator {
[string[]] GetValidValues() {
# Sample, hard-coded values. This is where your database lookup would happen.
return 'foo', 'bar'
}
}
function Get-SomeItems {
param (
[Parameter(Mandatory)]
[ValidateSet([ValidFilterValues])] # Pass the custom class defined above.
[string]$filter,
[Parameter(Mandatory)]
[ValidateSet('abc', 'def', 'ghi')]
[String]$filter2
)
$filter, $filter2 # sample output.
}

There's two aspects to what you're trying to do:
Making sure the parameter validation is correct
Making the PowerShell experience around it "good" (aka supporting tab completion).
Parameter Validation :
As you might have already noticed [ValidateSet] is a hard-coded list. It's not really possible to soft code this (it is possible to dynamically build your script every time using some other modules, lemme know if you want more of an explainer for this).
To make the Validation work without [ValidateSet], I'd suggest [ValidateScript({})]. [ValidateScript] will run whatever script is in ValidateScript to ensure the script is valid. If the [ValidateScript()] throws, the user will see that message when they pass an invalid value in.
Tab-Completion :
To make it feel easy, you'll also want to add support for tab completion.
This is fairly straightforward using the [ArgumentCompleter] attribute.
Here's an example copied / pasted from a module called LightScript
[ArgumentCompleter({
param ( $commandName,
$parameterName,
$wordToComplete,
$commandAst,
$fakeBoundParameters )
$effectNames = #(Get-NanoLeaf -ListEffectName |
Select-Object -Unique)
if ($wordToComplete) {
$toComplete = $wordToComplete -replace "^'" -replace "'$"
return #($effectNames -like "$toComplete*" -replace '^', "'" -replace '$',"'")
} else {
return #($effectNames -replace '^', "'" -replace '$',"'")
}
})]
This ArgumentCompleter does a few things:
Calls some other command to get a list of effects
If $wordToComplete was passed, finds all potential completions (while stripping off whitespace and enclosing in quotes)
If $WordToComplete was not passed, puts each potential completion in quotes
Basically, all you should need to change are the command names / variables to make this work.
Hope this Helps

Related

Powershell: Improve class constructor for IntelliSense support

I worte this class with two custom constructors:
class ExtensionApp {
[String] $name
[String] $version
[String] $path
[switch] $isContainerPath
[switch] $useNugetDownloader
[switch] $force
[switch] $skipVerification
ExtensionApp() {}
ExtensionApp(
[String] $name,
[String] $version,
[switch] $useNugetDownloader,
[switch] $force,
[switch] $skipVerification
) {
$this.name = $name
$this.version = $version
$this.useNugetDownloader = $useNugetDownloader
$this.force = $force
$this.skipVerification = $skipVerification
if (($this.version -eq '') -or ($null -eq $this.version)) {
#do something
}
}
ExtensionApp(
[String] $name,
[switch] $isContainerPath,
[String] $path,
[switch] $force,
[switch] $skipVerification
) {
$this.name = $name
$this.path = $path
$this.isContainerPath = $isContainerPath
$this.force = $force
$this.skipVerification = $skipVerification
}
}
I'm using those Objects to fill a list i want to process later which looks something like this atm:
$CustApps = New-Object Collections.Generic.List[ExtensionApp]
$CustApps.Add([ExtensionApp]::new('Extension A', '10.0.2554.0', $true , $false, $false)) #first constructor
$CustApps.Add([ExtensionApp]::new('Extension b', $false, '\\server\folder\file.app' , $false, $false)) #second constructor
Thing is: I don't like the way i have to use the constuctors because you dont' get any Intellisense support, and if you have to mix up the constructors while filling the same List, it gets messy.
I could just define a function like this:
function CreateExtensionApp {
param(
[String] $name,
[String] $version,
[String] $path,
[switch] $isContainerPath,
[switch] $useNugetDownloader,
[switch] $force,
[switch] $skipVerification
)
$new = [ExtensionApp]::new()
$new.name = $name
$new.version = $version
$new.path = $path
$new.isContainerPath = $isContainerPath
$new.useNugetDownloader = $useNugetDownloader
$new.force = $force
$new.skipVerification = $skipVerification
if (($true -eq $new.useNugetDownloader) -and (($new.version -eq '') -or ($null -eq $new.version))) {
Get-LogiVersion -PackageId $this.name
}
return $new
}
and use that one like this:
$CustApps = New-Object Collections.Generic.List[ExtensionApp]
$CustApps.Add((New-ExtensionAppObj -name 'Extension A' -version '10.0.2554.0' -useNugetDownloader))
$CustApps.Add((New-ExtensionAppObj -name 'Extension B' -path '\\server\folder\file.app' -force -skipVerification))
but i'm kind of a perfectionist and in my opinion, this function should be a static method belonging to the class itself. - I also found a way to do that but it would deprive me of the parameter-names and intellisense support again.
is there a solution that would allow me to use the parameter-names AND have all the code in the class definition?
I can't offer a solution, but I'll try to shed some light on the challenges involved:
IntelliSense:
As of v2022.6.3 of the PowerShell extension for Visual Studio Code, IntelliSense support for calling methods is limited to read-only display of all overloads, and is only shown when the method name has been typed, before typing (
In other words, there is currently no support for:
(a) showing an overload signature (parameter names) as arguments are being typed.
(b) allowing selecting one of the overloads so as to auto-complete a call to that overload with argument placeholders named for the parameters.
GitHub issue #1356 discusses potentially improving method-call IntelliSense in the future:
(a) is problematic, given PowerShell's lack of static typing, though it's conceivable to let the user manually cycle through available overloads in the same way that the C# extension does, albeit without type awareness.
(b) may be feasible, if the IntelliSense feature allows calling a Visual Studio Code snippet in response to a user selection.
Self-documenting code:
Since all (declared) parameters in PowerShell cmdlets / functions / scripts have names, you always have the option to spell out the target parameter for each argument (though there's often also support for positional arguments for a subset of parameters).
Unlike C#, PowerShell, as of PowerShell 7.2.x, does not support named arguments in method calls:
That is, the following C# sample call currently has no PowerShell equivalent:
C#: string.Compare(strA: "foo", strB: "FOO", ignoreCase: true)
PowerShell: [string]::Compare(strA: "foo", strB: "FOO", ignoreCase: $true)
GitHub issue #13307 proposes adding support for named arguments.

What is the proper way to define a dynamic ValidateSet in a PowerShell script?

I have a PowerShell 7.1 helper script that I use to copy projects from subversion to my local device. I'd like to make this script easier for me to use by enabling PowerShell to auto-complete parameters into this script. After some research, it looks like I can implement an interface to provide valid parameters via a ValidateSet.
Based on Microsoft's documentation, I attempted to do this like so:
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[ValidateSet([ProjectNames])]
[String]
$ProjectName,
#Other params
)
Class ProjectNames : System.Management.Automation.IValidateSetValuesGenerator {
[string[]] GetValidValues() {
# logic to return projects here.
}
}
When I run this, it does not auto-complete and I get the following error:
❯ Copy-ProjectFromSubversion.ps1 my-project
InvalidOperation: C:\OneDrive\Powershell-Scripts\Copy-ProjectFromSubversion.ps1:4
Line |
4 | [ValidateSet([ProjectNames])]
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Unable to find type [ProjectNames].
This makes sense since the class isn't defined until after the parameters. So I moved the class above the parameters. Obviously this is a syntax error. So how do I do this? Is it not possible in a simple PowerShell script?
Indeed, you've hit a catch-22: for the parameter declaration to work during the script-parsing phase, class [ProjectNames] must already be defined, yet you're not allowed to place the class definition before the parameter declaration.
The closest approximation of your intent using a stand-alone script file (.ps1) is to use the ValidateScript attribute instead:
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[ValidateScript(
{ $_ -in (Get-ChildItem -Directory).Name },
ErrorMessage = 'Please specify the name of a subdirectory in the current directory.'
)]
[String] $ProjectName # ...
)
Limitations:
[ValidateScript] does not and cannot provide tab-completion: the script block, { ... }, providing the validation is only expected to return a Boolean, and there's no guarantee that a discrete set of values is even involved.
Similarly, you can't reference the dynamically generated set of valid values (as generated inside the script block) in the ErrorMessage property value.
The only way around these limitations would be to duplicate that part of the script block that calculates the valid values, but that can become a maintenance headache.
To get tab-completion you'll have to duplicate the relevant part of the code in an [ArgumentCompleter] attribute:
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[ValidateScript(
{ $_ -in (Get-ChildItem -Directory).Name },
ErrorMessage = 'Please specify the name of a subdirectory in the current directory.'
)]
[ArgumentCompleter(
{
param($cmd, $param, $wordToComplete)
# This is the duplicated part of the code in the [ValidateScipt] attribute.
[array] $validValues = (Get-ChildItem -Directory).Name
$validValues -like "$wordToComplete*"
}
)]
[String] $ProjectName # ...
)

Couldn't use predefined array inside Validateset - Powershell

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

Powershell multiple function params from ValidateSet

I'm writing a script with PowerShell and at some point I needed to use ValidateSet on function params. It's a very good feature, but what I need is something more than that.
For example
Function JustAnExample
{
param(
[Parameter(Mandatory=$false)][ValidateSet("IPAddress","Timezone","Cluster")]
[String]$Fields
)
write-host $Fields
}
So this code snippet allows me to choose one item from the list like that
JustAnExample -Fields IPAddress
and then prints it to the screen.
I wonder if there is a possibility to allow to choose multiple values and pass them to function from one Validation set like so
JustAnExample -Fields IPAddress Cluster
Maybe there is a library for that or maybe I just missed something but I really can't find a solution for this.
If you want to pass multiple string arguments to the -Fields parameter, change it to an array type ([String[]]):
param(
[Parameter(Mandatory=$false)]
[ValidateSet("IPAddress","Timezone","Cluster")]
[String[]]$Fields
)
And separate the arguments with , instead of space:
JustAnExample -Fields IPAddress,Cluster
#SokIsKedu, I had the same issue and could not find an answer. So I use the following inside the function:
function Test-Function {
param (
[ValidateSet('ValueA', 'ValueB')]
[string[]] $TestParam
)
$duplicate = $TestParam | Group-Object | Where-Object -Property Count -gt 1
if ($null -ne $duplicate) {
throw "TestParam : The following values are duplicated: $($duplicate.Name -join ', ')"
}
...
}
It does not prevent duplicates being passed in, but it does catch them.

Pass an unspecified set of parameters into a function and thru to a cmdlet

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