Using ValidateSet() and ValidatePattern() to allow new values? - powershell

I would like to write a script with a parameter that has an existing set of values, but also allows the user to enter an unknown value. I am hoping that it will allow tab-completion from the known set, but not reject a value not yet in the known set.
In this case, there is a list of known servers. A new server might be added, so I want to allow the new server name to be entered. However, the ValidateSet() will reject anything it does not know.
This code does not work.
[cmdletbinding()]
Param (
[Parameter(Mandatory=$true)]
[Validatepattern('.*')]
[ValidateSet('server1', 'server2', 'bazooka')]
[string]$dbhost
)
Write-Host $dbhost
Running this for a known host works well. Automatic tab-completion works with the known list of hosts. But, a new hostname will be rejected.
>.\qd.ps1 -dbname server2
server2
>.\qd.ps1 -dbname spock
C:\src\t\qd.ps1 : Cannot validate argument on parameter 'dbname'. The argument "spock" does not belong to the set "server1,server2,bazooka" specified by the
ValidateSet attribute. Supply an argument that is in the set and then try the command again.

You can use a ArgumentCompleter script block for this purpose. See https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters
Example:
function Test-ArgumentCompleter {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[ArgumentCompleter({
$possibleValues = #('server1', 'server2', 'bazooka')
return $possibleValues | ForEach-Object { $_ }
})]
[String] $DbHost
)
}

Related

ValidateScript and order of operations

I am not understanding why ValidateScript is not erroring out until the end..
param (
[Parameter(
Mandatory = $true,
HelpMessage = "Specifies the email address that will manage the share drive to be created.")]
[ValidateScript({
if ($_.host -eq "domain.com") {
return $true
}
else {
Throw [System.Management.Automation.ValidationMetadataException] "Enter an email address ending with #domain.com"
}
})][Net.Mail.MailAddress]$EmailAddress,
[Parameter(
Mandatory = $true,
HelpMessage = "Specifies the name of the share drive name to be created. Enter a shared drive name like prgm- and at least 2 letters. Example prgm-hi")]
[ValidateScript({
if ($_ -like "prgm-??*") {
return $true
}
else {
Throw [System.Management.Automation.ValidationMetadataException] "Enter a shared drive name like prgm- and at least 2 letters. Example prgm-hi"
}
})][string]$SharedDriveName
)
In this code snip sample above if the script is run the person will get past the emailaddress variable if it is wrong and than get to the error after all the variables are answered.
I am not understanding how to have the script stop right as someone makes a bad entry.
I assume, I will have to use something other than validate script but I am not sure what?
Perhaps surprisingly, when PowerShell automatically prompts for values for missing mandatory parameters (written as of PowerShell 7.3.2):
it enforces the parameter data type after each prompt.
but performs validation - based on attributes such as ValidateScript() - only after ALL prompts have been answered.
This makes for an awkward interactive experience, given that as a user you won't learn whether any of your inputs are valid until all of them have been provided.
I can't speak to the design intent - a conceivable reason for this evaluation order is to allow validation script blocks to also consider other parameter values; however, in practice you can not refer to other parameters inside a [ValidateScript()] script block.
On a general note, the automatic prompting mechanism has fundamental limitations - see GitHub issue #7093.
Workaround:
You can move prompting and validating into the body of your function (as well), where you get to control the execution flow - obviously, this requires extra work:
param (
[Parameter(
# Do NOT make the parameter mandatory, so that it can be prompted for
# in the function body, but DO define a HelpMessage.
HelpMessage = "Specify the email address that will manage the share drive to be created (must end in #sonos.com)"
)]
[ValidateScript({
if ($_.host -eq 'sonos.com') {
return $true
}
else {
Throw "Enter an email address ending with #sonos.com"
}
})]
[Net.Mail.MailAddress] $EmailAddress,
[Parameter(
# Do NOT make the parameter mandatory, so that it can be prompted for
# in the function body, but DO define a HelpMessage.
HelpMessage = "Specify the name of the share drive name to be created. Enter a shared drive name like prgm- and at least 2 letters. Example prgm-hi"
)]
[ValidateScript({
if ($_ -like "prgm-??*") {
return $true
}
else {
Throw "Enter a shared drive name like prgm- and at least 2 letters. Example prgm-hi"
}
})]
[string]$SharedDriveName
)
# If no values were provided any of the conceptually mandatory parameters,
# prompt for and validate them now.
# Infer which parameters are conceptually mandatory from the presence of a
# .HelpMessage property.
$MyInvocation.MyCommand.Parameters.GetEnumerator() | ForEach-Object {
# See if there's a .HelpMessage property value and, if so,
# check if the parameter hasn't already been bound.
$helpMessage = $_.Value.Attributes.Where({ $_ -is [Parameter] }).HelpMessage
if ($helpMessage -and -not $PSBoundParameters.ContainsKey($_.Key)) {
# Prompt now and validate right away, which happens implicitly when
# the parameter variable is assigned to.
while ($true) {
try {
Set-Variable -Name $_.Key -Value (Read-Host $helpMessage)
break # Getting here means that validation succeeded -> exit loop.
}
catch {
# Validation failed: write the error message as a warning, and keep prompting.
Write-Warning "$_"
}
}
}
}
# ... all required arguments are now present and validated.
Note:
The upside of this approach is that you can keep prompting until valid input is received (or the user aborts with Ctrl-C).

Using ValidateSet with functions or List

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

how to use both parameters and hard-code the servers list through powershell

I am trying to create a script which should run using parameter and without parameter should result for for multiple list:
Ex: I have a function under which I gave param as servername which is working fine for single server ad fetching the results.
(RestoreFileList -servername XXXXXXXXXXX1)
If I do not want to give a parameter as servername and output should result full set of servers list data
Ex : Restorefilecount
Am i missing something between parameter and serverlist condition which should fetch the results, Any help on this?
Script *************
function Restorefilecount()
{
[cmdletbinding()]
param(
[Parameter(position = 0,mandatory = $false)]
[string] $Servername
)
$servername = #()
$servername = ('XXXXXXXX1', 'XXXXXXXXX2', 'XXXXXXXXX3', 'XXXXXXXXXX4')
$result = Invoke-Client -ComputerName $Servername -ScriptBlock {
$Server = HOSTNAME.EXE
$Query = #'
Select ##ServerName AS ServerName , name,(SUBSTRING(NAME ,15,3) * 100 ) / 100 AS
file,state_desc,user_access_desc FROM master.sys.databases where name like 'TKT' ORDER BY 2
'#
Invoke-Sqlcmd -ServerInstance $Server -Database master -Query $Query
}
Building on Abraham Zinala's helpful comments:
It looks like you're simply looking to define a parameter's default value:
function Restore-FileCount {
[CmdletBinding()]
param(
[string[]] $Servername = #('XXXXXXXX1', 'XXXXXXXXX2', 'XXXXXXXXX3', 'XXXXXXXXXX4')
)
# ...
$Servername # Output for demo purposes
}
Note how the parameter type had to be changed from [string] to [string[]] in order to support an array of values.
Incidental changes:
Since you're using a param(...) block to define your parameters (which is generally preferable), there is no need to place () after the function name (Restorefilecount()) - while doing so doesn't cause a syntax error as long as there's nothing or only whitespace between ( and ), note that you declare parameters either via a param(...) block or via function foo(...); also, in parameter-less functions () is never needed - see the conceptual about_Functions help topic.
I've inserted a hyphen (-) in your function name, to make it conform to PowerShell's naming convention.
I've omitted [Parameter(position = 0, mandatory = $false)], because what this attribute instance specifies amounts to the default behavior (all parameters are non-mandatory by default, and Position=0 is implied by $ServerName being the first (and only) parameter).

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 # ...
)

How to check Number of arguments in powershell?

param (
[string]$Name = $args[0],#First argument will be the adapter name
[IPAddress]$IP = $args[1],#Second argument will be the IP address
[string]$InterfaceId = $args[3],#Second argument will be the IP address
[string]$VlanId = $args[4], #Fourth argument will be vlanid
[string]$SubnetIP = $args[5],#subnet mask
[string]$IPType = "IPv4",
[string]$Type = "Static"
)
Write-Host $Args.Count
I want to check if command line arguments are supplied to the powershell script or not and if its not supplied then i want to show the usage by write. I am running the script in admin mode. I found one method after searching that using $Args.Count we can get the arguments count while running the script but its always zero for me. what am i doing wrong?
enter image description here
Get rid of the $args[x] assignments and add [cmdletbinding()] on top.
[CmdLetbinding()]
param (
[string]$Name, #First argument will be the adapter name
[IPAddress]$IP, # etc...
[string]$InterfaceId,
[string]$VlanId,
[string]$SubnetIP,
[string]$IPType = "IPv4",
[string]$Type = "Static"
)
Then you can use $PSBoundParameters.Count to get the argument count.
$args is a special variable that is used when named parameter are not present.
Therefore, since you have named parameter, it will always give you a count of zero (except maybe if you add more arguments than there is named parameters)
If you use a param block, then you don't need to assign $args[0] and others. In fact, this is totally useless as they will be $null.
The other approach, although I recommend you to keep the param block, is to not use any named parameters at all. In that case, $args will work as you expect it to.
[string]$Name = $args[0]
[IPAddress]$IP = $args[1]
[string]$InterfaceId = $args[3]
[string]$VlanId = $args[4]
[string]$SubnetIP = $args[5]
[string]$IPType = "IPv4"
[string]$Type = "Static"
The main difference is that if you have a param block, you can call your script in the following ways:
.\MyScript.ps1 -Name "Hello" -Ip 127.0.0.1
.\MyScript.ps1 "Hello" 127.0.0.1
Without the param block, you have only option #2 available to call the script.