ValidatePattern in PowerShell: While value doesn't match - powershell

I want to perform a pattern validation of a string value I'm entering and then only continue with the script if it matches but feel like I'm missing something here:
Function Test-ValidatePattern {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[ValidatePattern(".co.uk|.com")]
[System.String]$DomainName
)
$DomainValid = "1"
Write-Output "$DomainName is a valid domain name format"
}
#Sets forwarding email address, checks what it's set to and outputs this to logfile.
If ($EmailForwarding) {
$ForwardDomain = Read-Host "What domain do you want to forward to? e.g. contoso.com`n"
Test-ValidatePattern -DomainName $ForwardDomain
While (!($DomainValid -eq "1")) {
Write-Host "$ForwardDomain isn't a valid domain. Try again"
$ForwardDomain = Read-Host "What domain do you want to forward to? e.g. contoso.com`n"
}
}
It returns an error as expected when entering a string that doesn't have a .co.uk or .com extension and no error when entering one that does but rather than presenting the message that the domain name is valid it loops back to the while block messages instead of continuing on with the rest of the script.
I've tried -eq, -notmatch, -notlike but still not working as intended.
Any ideas?

I don't understand why you think you need to write a validation script when that is the purpose of the ValidatePattern attribute. Here is an example function:
function sample {
param(
[Parameter(Mandatory=$true)]
[ValidatePattern('(\.co\.uk|\.com)$')]
[String] $DomainName
)
"Your domain name is $DomainName"
}
If you run
sample foo.bar
PowerShell will throw a ParameterBindingValidationException exception because the string you passed as the $DomainName parameter doesn't match the pattern. However, if you run
sample foo.com
You will see the expected output:
Your domain name is foo.com

You're not setting $DomainValid to any value. The scope of the function clears out $DomainValid when it finishes. You'll need to return a value from the Test-ValidatePattern function. For Example:
Function Test-ValidatePattern {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[ValidatePattern(".co.uk|.com")]
[System.String]$DomainName
)
#$DomainValid = "1"
Write-Output "$DomainName is a valid domain name format"
return 1
}
#Sets forwarding email address, checks what it's set to and outputs this to logfile.
If ($EmailForwarding) {
$ForwardDomain = Read-Host "What domain do you want to forward to? e.g. contoso.com`n"
$DomainValid = Test-ValidatePattern -DomainName $ForwardDomain
While (!($DomainValid -eq "1")) {
Write-Host "$ForwardDomain isn't a valid domain. Try again"
$ForwardDomain = Read-Host "What domain do you want to forward to? e.g. contoso.com`n"
}
}

I feel I've missed off a few things when explaining what I was after. I simply wanted to make sure the input had a dot present so single words couldn't be entered e.g. "foo.bar" is allowed but "foo" isn't. If not present then the user should be prompted again. Nothing too strict and doesn't stop invalid entries but more so to catch a lazy input.
After tinkering around a bit more the below appears to do what I require.
If ($EmailForwarding) {
$ForwardDomain = Read-Host "What domain do you want to forward to? e.g. contoso.com`n"
While (!($ForwardDomain -like "*.*"))
{Write-Host "$ForwardDomain isn't a valid domain. Try again"
$ForwardDomain = Read-Host "What domain do you want to forward to? e.g. contoso.com`n"}}
"rest of script"

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

Changing a variable based on output

Good Afternoon,
Just want to start by saying I am totally new with PS, and am really sorry if my approach to this is long winded or inaccurate.
I have a list of computernames that declared as individual Variables that my colleagues have provided me :
$Jon = "Abc1234"
$Mike = "Abc6789"
another 10 hostnames
I then ask the user if they want to send the files to another PC :
$Targetuser = Read-Host 'Who would you like to send it to?'
What I would like is the output from the above to change to the hostname, but I am unsure of how to do this.
Essentially, if the output was Mike, to change the $targetuser variable to $Abc6789
Thanks in advance
Dynamically named variables is almost always a bad idea. Instead, use a dictionary type, like a hashtable:
# Define user->host dictionary
$Hostnames = #{
Jon = "Abc1234"
Mike = "Abc6789"
}
# Ask user for target user
$TargetUser = Read-Host 'Who would you like to send it to?'
# Keep asking until the enter an actual user name
while(-not $Hostnames.ContainsKey($TargetUser)){
Write-Host "No hostname found for user '${targetUser}'!"
Write-Host "Choose one of:"
Write-Host ($Hostnames.Keys -join "`n")
$TargetUser = Read-Host 'Who would you like to send it to?'
}
# Get the associated hostname
$TargetHost = $Hostnames[$TargetUser]
# copy file to $TargetHost

How do I make a command line argument an integer but if given a non-numeric value ask again?

My script asks for a number of users $numUsers in the form of a command line argument. I then use that number to declare an array of size $numUsers.
It works fine when the user provides a number but when given a non-numeric character I need to ask for an it again.
Currently my code just fails and throws an ugly error message.
$numUsers = Read-Host -Prompt 'Specify how many users'
$userArray = New-Object string[] $numUsers
I have tried using a recursive function to convert the command line arg to an int and if that fails just ask for an arg again but it fails as well.
function makeInt {
param([string]$numString)
try { $newNum = [int]$numString }
catch { makeInt (Read-Host -Prompt 'try again') }
}
$numUsers = Read-Host -Prompt 'Specify how many users'
makeInt $numUsers
$userArray = New-Object string[] $numUsers
I tried several other ways as well but this is as close as I got in the last few days. Hopefully someone can help!
If the only constraint you want to impose is that the user input be convertible to an [int], you can use the [int]::TryParse() method in a loop:
[int] $number = -1
while (-not [int]::TryParse((Read-Host -Prompt 'Specify how many users'), [ref] $number)) {
Write-Warning "Not a valid number, please try again."
}
# You only get here if the user (ultimately) provided valid input.
# $number then contains the user's input as an [int].
[int]::TryParse() attempts a conversion and if it succeeds, stores the [int] value in the variable passed by reference ([ref]) and returns $True;
if conversion isn't possible, $False is returned.
The while loop keeps prompting until valid input is provided, after which $number contains the [int] equivalent of the number string entered by the user.
To validate if $numUsers is an [int] just put [int] in front of your Read-Host variable
try{
[int]$numUsers = Read-Host "Enter an number"
}
catch
{
Write-Host "Please enter a valid number"
}
However, if you want to continue to prompt the user until they enter a valid input, you would want to put it in a do{}while()
Ex. Solution:
function promptForUsers{
[int](Read-Host 'Enter Number' )
}
do{
try
{
$numUsers = promptForUsers; break
}
catch
{
Write-Warning "Not a number; please try again."
}
}while($true)

Is there a one-liner for using default values with Read-Host?

I've written something like this to specify default values for prompts.
$defaultValue = 'default'
$prompt = Read-Host "Press enter to accept the default [$($defaultValue)]"
if ($prompt -eq "") {} else {
$defaultValue = $prompt
}
Can it be shortened further?
Here is my attempt.
$defaultValue = 'default'
$prompt = Read-Host "Press enter to accept the default [$($defaultValue)]"
if (!$prompt -eq "") {$defaultValue = $prompt}
I want a one-liner, so I'm gonna hold out accepting an answer until then.
N.b. $defaultValue should be stored independently of the one liner. Similar to the example above.
I've accepted the answer which lead me to the solution I was looking for.
$defaultValue = 'default'
if (($result = Read-Host "Press enter to accept default value $defaultValue") -eq '') {$defaultValue} else {$result}
And for those of you asking why. The reason is because it is easier on the eyes of whoever comes after me. Less is always more, when clarity is not sacrificed. IMHO.
EDIT;
Instead of a single line, perhaps I should have said a single phrase?
I've added this edit clarify whilst a few answers I have seen use are using a semi-colon.
$defaultValue = 'default'
$prompt = Read-Host "Press enter to accept the default [$($defaultValue)]"
$prompt = ($defaultValue,$prompt)[[bool]$prompt]
If you absolutely have to have it in one line:
$defaultValue = 'default'
($defaultValue,(Read-Host "Press enter to accept the default [$($defaultValue)]")) -match '\S' |% {$prompt = $_}
Shortest Version I could came up with:
if (!($value = Read-Host "Value [$default]")) { $value = $default }
This version doesn't have to use else.
if(($result = Read-Host "Press enter to accept default value [default]") -eq ''){"default"}else{$result}
$DefaultValue="Foobar"
.... (Optional other code) ....
$Value=if($Value=(Read-Host "Enter value [$DefaultValue]")){$Value}else{$DefaultValue}
Just threw this together to re-use a previously entered config value while still allowing user to change it if needed... The accepted answer is missing the assignment portion and uses a hardcoded "Default" value...
There is a function (from other languages) called "Ternary Operator" or "Binary Operator"(https://en.wikipedia.org/wiki/%3F:) (the 'xx : yy ? zz' style one) and there are several people who have submitted functions to implement its behavior in Powershell.
For me this is the shortest, easiest to use, and doesn't repeat the variable value or name:
$ChosenValue = 'Default Value' | %{ If($Entry = Read-Host "Enter a value ($_)"){$Entry} Else {$_} }
The magic sauce is the |%{} that pipes the default value to ForEach, allowing repeating the default value (aliased as $_) in the message and return value. It also depends on the fact that the assignment operator returns the value assigned for the purposes of the If statement. This assigned value will be treated as $true when an entry is provided, or $false(empty) when the [Enter] key is pressed instead.
Thanks go to #ojk's excellent answer which I considered the easiest to use and read, but I agree with #bluekeys that it's better to avoid repeating the default value and assigned variable name. Having the intermediate $Entry variable hasn't proven to be an inconvenience.
I use this method frequently to define function or script parameters with defaults that can be overridden at run time:
param(
$hostname = ('google.com' | %{ If($Entry = Read-Host "Host name ($_)"){$Entry} Else {$_} })
)
Write-Host $hostname
Running:
Host name (google.com):
google.com
Host name (google.com): facebook.com
facebook.com
Also, as #E.Sundin pointed out, Powershell 7 finally has a ternary operator that makes this syntax much simpler. Unfortunately many of us work on legacy systems that won't get version 7 in the near future.
EDIT: here's a slightly shorter version without an intermediate $Entry variable:
$hostname = 'google.com' | %{(Read-Host "Host name ($_)"),$_} | ?{$_} | Select -First 1
It makes an array containing the Read-Host response and the default value, then filters to exclude empty values, and selects the first (there would be two if the user responded).
$prompt = ( ($defaultValue='a_default_value'), (Read-Host "Please enter something [$defaultValue]")) -match '\S' | select -last 1
The given answers doesn't satisfy me (and don't work for me) but gave me enough input to came up with the shortest one-liner.
if (($prompt = Read-Host -Prompt "Your Text [your default]") -eq "") {$prompt= "your default"}
Why adding an else? If user inputs nothing it does $prompt="your default" (could be a variable of course). If he adds something, its stored in %prompt already and you can leave the statement.
I am using a function for that:
Function Read-HostDefault($Prompt, $Default) {
if ($default) {
$prompt = "$prompt [$default]"
}
$val = Read-Host $prompt
($default,$val)[[bool]$val]
}
Which gives:
PS C:\Windows\system32> Read-HostDefault "Enter port" 5432
Enter port [5432] :
5432
PS C:\Windows\system32> Read-HostDefault "Enter port" 5432
Enter port [5432] : 1234
1234
PS C:\Windows\system32> Read-HostDefault "Enter port"
Enter port : 2468
2468
You could also use a switch statement in a single line like this:
param([string]$myVariable = $($($val = $(Read-Host "Enter value")); $(Switch -regex ($val) { ".+" { $val } default { "my default value" } })))
The -regex .+ will match one or more characters, this would include any white space characters so maybe you want to strip out white space when doing the switch match i.e. \S.+.
Assuming you already have $defaultValue:
$newValue = if ($value = Read-Host -Prompt "Please enter a value ($defaultValue)") { $value } else { $defaultValue }
This should work

Alternative to Throwing Param Exceptions in PowerShell?

Bottom Line Up Front
I'm looking for a method to validate powershell (v1) command line parameters without propagating exceptions back to the command line.
Details
I have a powershell script that currently uses param in conjunction with [ValidateNotNullOrEmpty] to validate command line paramaters:
param(
[string]
[ValidateNotNullOrEmpty()]$domain = $(throw "Domain (-d) param required.")
)
We're changing the paradigm of error handling where we no longer want to pass exceptions back to the command line, but rather provide custom error messages. Since the param block can not be wrapped in a try catch block, i've resorted to something like the following:
param(
[string]$domain = $("")
)
Try{
if($domain -like $("")){
throw "Domain (-d) param required."
}
...
}Catch{
#output error message
}
My concern is that we're bypassing all of the built-in validation that is available with using param. Is my new technique a reasonable solution? Is there a better way to validate command line params while encapsulating exceptions within the script? I'm very much interested in see how PowerShell professionals would handle this situation.
Any advice would be appreciated.
You can write a custom validation script. Give this parameter a try.
Param(
[ValidateScript({
If ($_ -eq $Null -or $_ -eq "") {
Throw "Domain (-d) param required."
}
Else {
$True
}
})][string]$Domain
)
As I mentioned in a comment: more I read your description, more I come to the conclusion that you should not worry about "bypassing all built-in validation". Why? Because that's exactly your target. You want to bypass it's default behavior, so if that's what you need and have to do - than just do it. ;)
One way is to use default parameters like this [from msdn] -
Function CheckIfKeyExists
{
Param(
[Parameter(Mandatory=$false,ValueFromPipeline=$true)]
[String]
$Key = 'HKLM:\Software\DoesNotExist'
)
Process
{
Try
{
Get-ItemProperty -Path $Key -EA 'Stop'
}
Catch
{
write-warning "Error accessing $Key $($_.Exception.Message)"
}
}
}
So, here, if you try calling the function without passing any parameters, you will get warning what you have defined in your try/catch block. And, you are not using any default validation attributes for that. You should always assume that you will encounter an error, and write code that can survive the error. But the lesson here is if you implement a default value, remember that it is not being validated.
Read more here