Vadilate True or False arguments in function

Hello everyone,

I will try to describe what I am trying to achieve.
I have a script for deploying virtual machines on vmware.
I need to validate each value entered befor prociding with another step in script.

In most cases was quite easy to validate see example below

# Getting info about clusters Write-Host "Select cluster to deploy new virtual machine" -ForegroundColor Green Get-Datacenter | Get-Cluster | Select-Object Name | Format-Table -AutoSize do { $ClusterToDeployVM = Read-Host "Enter cluster name" } while (($ClusterToDeployVM -like "") -or ($ClusterToDeployVM.StartsWith(" ")) -or ($ClusterToDeployVM.EndsWith(" ")) -or ((Get-Cluster).Name -notcontains $ClusterToDeployVM)

In case of IP configuration, there is much more values at the same time and also there is more things to be checked.
So I was trying to write function with built-in parameter validation but I do not know, how to extract error codes
from these built-in validation parameters.
Here is example (sorry for split but there is some limitation in editor)

function Test-VMipConfiguration { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [ValidateNotNull()] [ValidateScript({-not($_.StartsWith(" "))})] [ValidateScript({-not($_.EndsWith(" "))})] [ValidateScript({($_.StartsWith("10"))})] [string]$VMip, [Parameter(Mandatory=$true)] [ValidateNotNull()] [ValidateScript({-not($_.StartsWith(" "))})] [ValidateScript({-not($_.EndsWith(" "))})] [ValidateScript({($_.StartsWith("255"))})] [string]$VMnetmask, [Parameter(Mandatory=$true)] [ValidateNotNull()] [ValidateScript({-not($_.StartsWith(" "))})] [ValidateScript({-not($_.EndsWith(" "))})] [ValidateScript({($_.EndsWith("254"))})] [string]$VMgw, [Parameter(Mandatory=$true)] [ValidateNotNull()] [ValidateScript({-not($_.StartsWith(" "))})] [ValidateScript({-not($_.EndsWith(" "))})] [ValidateScript({($_.StartsWith("10"))})] [string]$VMdns1, [Parameter(Mandatory=$true)] [ValidateNotNull()] [ValidateScript({-not($_.StartsWith(" "))})] [ValidateScript({-not($_.EndsWith(" "))})] [ValidateScript({($_.StartsWith("10"))})] [string]$VMdns2, [Parameter(Mandatory=$true)] [ValidateNotNull()] [ValidateRange(1, 4094)] [int]$VMvlan ) begin { $VMipOctets = @($VMip.Split('.')) if ($VMipOctets.Length -gt 4) { Write-Error -Message "Invalid number of octets in IP address" -TargetObject $VMip -Category InvalidArgument exit } if ($VMipOctets.Length -lt 4) { Write-Error -Message "Invalid number of octets in IP address" -TargetObject $VMip -Category InvalidArgument exit } $VMnetmaskOctets = @($VMnetmask.Split('.')) if ($VMnetmaskOctets.Length -gt 4) { Write-Error -Message "Invalid number of octets in mask address" -TargetObject $VMnetmask -Category InvalidArgument exit } if ($VMnetmaskOctets.Length -lt 4) { Write-Error -Message "Invalid number of octets in mask address" -TargetObject $VMnetmask -Category InvalidArgument exit } $VMgwOctets = @($VMgw.Split('.')) if ($VMgwOctets.Length -gt 4) { Write-Error -Message "Invalid number of octets in gateway address" -TargetObject $VMgw -Category InvalidArgument exit } if ($VMgwOctets.Length -lt 4) { Write-Error -Message "Invalid number of octets in gateway address" -TargetObject $VMgw -Category InvalidArgument exit } $VMdns1Octets = @($VMdns1.Split('.')) if ($VMdns1Octets.Length -gt 4) { Write-Error -Message "Invalid number of octets in IP address" -TargetObject $VMdns1 -Category InvalidArgument exit } if ($VMdns1Octets.Length -lt 4) { Write-Error -Message "Invalid number of octets in IP address" -TargetObject $VMdns1 -Category InvalidArgument exit } $VMdns2Octets = @($VMdns2.Split('.')) if ($VMdns2Octets.Length -gt 4) { Write-Error -Message "Invalid number of octets in IP address" -TargetObject $VMdns2 -Category InvalidArgument exit } if ($VMdns2Octets.Length -lt 4) { Write-Error -Message "Invalid number of octets in IP address" -TargetObject $VMdns2 -Category InvalidArgument exit } } process { } end { } } The problem is, that I need to ask user for input, if the input is not valid, I need to repeat user's input to variables again and do the validation process again. So the only option that I figured out so far, is to write function with my own validation (no built-in agruments validation)

Does anyone has an idea for better/other solution?
Thank you for any feedback

When I need to request a decision from the user I’m used to limit the possible input choices to a predefined list of choices. Either I add an index to a given list and the user picks just the index or I use a gridview wtih the parameter -Passthru.

In any case, I would avoid giving the user the opportunity to input arbitrary text.

Use Trim to remove the extra whitespace when prompting:

# Getting info about clusters
Write-Host "Select cluster to deploy new virtual machine" -ForegroundColor Green
#Collect the names one time
$vms = Get-Datacenter | Get-Cluster | Select-Object Name
#Show the data
$vms | Format-Table -AutoSize

do {
    #Wrap your call with Trim to remove any whitespace before or after the input
    $ClusterToDeployVM = (Read-Host "Enter cluster name").Trim()

    #Provide context on why you are re-prompting for input
    if ( $vms -contains $ClusterToDeployVM ) {
        '{0} vm already exists, choose another name' -f $ClusterToDeployVM
    }

} while ( $vms -notcontains $ClusterToDeployVM )

For the function params, try to use a [type] so that you are not trying to validate a string with a complex pattern. For something like IP addresses, another option is a regex and use validate pattern for something more specific like a subnet mask. There isn’t a need to validate null because the type will most likely cover that, but even listing it as mandatory will ensure a value is passed.

PS C:\WINDOWS\system32> function Test-It {
    param (
        [Parameter(Mandatory=$true)]
        [System.Net.IPAddress]$ip
    )
    begin {}
    process {}
    end {}
}

Test-It -Ip one
Test-It : Cannot process argument transformation on parameter 'ip'. Cannot convert value "one" to type "System.Net.IPAddress". Error: "An invalid IP address was specified."
At line:11 char:13
+ Test-It -Ip one
+             ~~~
    + CategoryInfo          : InvalidData: (:) [Test-It], ParameterBindingArgumentTransformationException
    + FullyQualifiedErrorId : ParameterArgumentTransformationError,Test-It
 

PS C:\WINDOWS\system32> Test-It -Ip 1.1.1.1

Hello Olaf, I was trying to make script as flexible as possible (in current state even someone in other company could use it with minimum changes to do).
I agree that predefined list would be a solid solution to avoid wrong inputs but in environment where clusters, folders and other resources are created and revoked frequently, it would require a lot of effort.
Thank you for your input.

Hello Rob, thanks for tip! I have completely forgot about Trim option. I see only one problem,
if variable is empty, it throws an error. I could set $ErrorActionPreference = “SilentlyContinue” but then I would
lost ability to see all error messages (unless I would probably set it only in parts of script where I would need to
suppress it and then set it back to default)

Regarding IP configuration test, I need something that will do the test and based on “True” or “False” statement it will
continue.
So I have created one-purpose function see below.

function Test-VmIpConfiguration {
    [CmdletBinding()]
    param (
        [string]$VMip,
        [string]$VMnetmask,
        [string]$VMgw,
        [string]$VMdns1,
        [string]$VMdns2,
        [int]$VMvlan
    )
    
    begin {
        $VMdns1Value = $VMdns1
        $VMdns2Value = $VMdns2
        $VMipOctets = @($VMip.Split('.'))
        $VMnetmaskOctets = @($VMnetmask.Split('.'))
        $VMgwOctets = @($VMgw.Split('.'))
        $VMdns1Octets = @($VMdns1.Split('.'))
        $VMdns2Octets = @($VMdns2.Split('.'))
    }
    
    process {
        # $VMip test
        if ($VMip -like "") {
            Write-Error -Message "Argument ""VMip"" is empty" -TargetObject $VMip -Category NotSpecified
            $VMip = 0
        } elseif (($VMip.StartsWith(" ")) -or ($VMip.EndsWith(" "))) {
            Write-Error -Message "Argument ""VMip"" contains empty character" -TargetObject $VMip -Category InvalidArgument
            $VMip = 0
        } elseif (-not $VMip.StartsWith("10")) {
            Write-Error -Message "Argument ""VMip"" does not start with number ""10"" in first octet" -TargetObject $VMip -Category InvalidArgument
            $VMip = 0
        } elseif (($VMipOctets.Length -lt 4) -or ($VMipOctets.Length -gt 4)) {
            Write-Error -Message "Invalid number of octets in IP address" -TargetObject $VMip -Category InvalidArgument
            $VMip = 0
        } else {
            $VMip = 1
        }

        # $VMnetmask test
        if ($VMnetmask -like "") {
            Write-Error -Message "Argument ""VMnetmask"" is empty" -TargetObject $VMnetmask -Category NotSpecified
            $VMnetmask = 0
        } elseif (($VMnetmask.StartsWith(" ")) -or ($VMnetmask.EndsWith(" "))) {
            Write-Error -Message "Argument ""$VMnetmask"" contains empty character" -TargetObject $VMnetmask -Category InvalidArgument
            $VMnetmask = 0
        } elseif (-not $VMnetmask.StartsWith("255")) {
            Write-Error -Message "Argument ""VMnetmask"" does not start with number ""255"" in first octet" -TargetObject $VMnetmask -Category InvalidArgument
            $VMnetmask = 0
        } elseif (($VMnetmaskOctets.Length -lt 4) -or ($VMnetmaskOctets.Length -gt 4)) {
            Write-Error -Message "Invalid number of octets in network mask" -TargetObject $VMnetmask -Category InvalidArgument
            $VMnetmask = 0
        } else {
            $VMnetmask = 1
        }

        # $VMgw test
        if ($VMgw -like "") {
            Write-Error -Message "Argument ""VMgw"" is empty" -TargetObject $VMgw -Category NotSpecified
            $VMgw = 0
        } elseif (($VMgw.StartsWith(" ")) -or ($VMgw.EndsWith(" "))) {
            Write-Error -Message "Argument ""VMgw"" contains empty character" -TargetObject $VMgw -Category InvalidArgument
            $VMgw = 0
        } elseif (-not $VMgw.EndsWith("254")) {
            Write-Error -Message "Argument ""VMgw"" does not end with number ""254"" in last octet" -TargetObject $VMgw -Category InvalidArgument
            $VMgw = 0
        } elseif (($VMgwOctets.Length -lt 4) -or ($VMgwOctets.Length -gt 4)) {
            Write-Error -Message "Invalid number of octets in gateway address" -TargetObject $VMgw -Category InvalidArgument
            $VMgw = 0
        } else {
            $VMgw = 1
        }

        # $VMdns1 test
        if ($VMdns1 -like "") {
            Write-Error -Message "Argument ""VMdns1"" is empty" -TargetObject $VMdns1 -Category NotSpecified
            $VMdns1 = 0
        } elseif (($VMdns1.StartsWith(" ")) -or ($VMdns1.EndsWith(" "))) {
            Write-Error -Message "Argument ""VMdns1"" contains empty character" -TargetObject $VMdns1 -Category InvalidArgument
            $VMdns1 = 0
        } elseif (-not $VMdns1.StartsWith("10")) {
            Write-Error -Message "Argument ""VMdns1"" does not start with number ""10"" in first octet" -TargetObject $VMdns1 -Category InvalidArgument
            $VMdns1 = 0
        } elseif (($VMdns1Octets.Length -lt 4) -or ($VMdns1Octets.Length -gt 4)) {
            Write-Error -Message "Invalid number of octets in DNS address" -TargetObject $VMip -Category InvalidArgument
            $VMdns1 = 0
        } else {
            $VMdns1 = 1
        }

        # $VMdns2 test
        if ($VMdns2 -like "") {
            Write-Error -Message "Argument ""VMdns2"" is empty" -TargetObject $VMdns2 -Category NotSpecified
            $VMdns2 = 0
        } elseif (($VMdns2.StartsWith(" ")) -or ($VMdns2.EndsWith(" "))) {
            Write-Error -Message "Argument ""VMdns2"" contains empty character" -TargetObject $VMdns2 -Category InvalidArgument
            $VMdns2 = 0
        } elseif (-not $VMdns2.StartsWith("10")) {
            Write-Error -Message "Argument ""VMdns2"" does not start with number ""10"" in first octet" -TargetObject $VMdns2 -Category InvalidArgument
            $VMdns2 = 0
        } elseif (($VMdns2Octets.Length -lt 4) -or ($VMdns2Octets.Length -gt 4)) {
            Write-Error -Message "Invalid number of octets in DNS address" -TargetObject $VMip -Category InvalidArgument
            $VMdns2 = 0
        } else {
            $VMdns2 = 1
        }

        # DNS duplicity test
        if ($VMdns1Value -like $VMdns2Value) {
            Write-Error -Message "Argument ""VMdns1"" and ""VMdns2"" cannot be same" -TargetObject $VMdns1 -Category InvalidResult
            $VMdnsDuplicity = 0
        } else {
            $VMdnsDuplicity = 1
        }

        # VLAN test
        if ($VMvlan -like "") {
            Write-Error -Message "Argument ""VMvlan"" is empty" -TargetObject $VMvlan -Category NotSpecified
            $VMvlan = 0
        } elseif (($VMvlan -lt 1) -or ($VMvlan -gt 4094)) {
            Write-Error -Message "Argument ""VMvlan"" range is not between 1 and 4096" -TargetObject $VMvlan -Category LimitsExceeded
            $VMvlan = 0
        } else {
            $VMvlan = 1
        }
    }
    
    end {
        if (($VMip -eq 0) -or ($VMnetmask -eq 0) -or ($VMgw -eq 0) -or ($VMdns1 -eq 0) -or ($VMdns2 -eq 0) -or ($VMvlan -eq 0) -or ($VMdnsDuplicity -eq 0)) {
            return $false
        } else {
            return $true
        }        
    }
}

So then I can ask user for input until it is correct

do {
    $NewVMip = Read-Host "Enter new IP address"
    $NewVMmask = Read-Host "Enter subnet mask"
    $NewVMgw = Read-Host "Enter gateway address"
    $NewVMdns1 = Read-Host "Enter DNS1 server address"
    $NewVMdns2 = Read-Host "Enter DNS2 server address"
    $NewVMvlan = Read-Host "Enter VLAN number"
} until (Test-VmIpConfiguration -VMip $NewVMip -VMnetmask $NewVMmask -VMgw $NewVMgw -VMdns1 $NewVMdns1 -VMdns2 $NewVMdns2 -VMvlan $NewVMvlan)

Get-OSCustomizationSpec $NewVMcustomSpec | Get-OSCustomizationNicMapping | Set-OSCustomizationNicMapping -IpMode UseStaticIP -IpAddress $NewVMip -SubnetMask $NewVMmask -DefaultGateway $NewVMgw -Dns $NewVMdns1, $NewVMdns2

I know there is missing other validation and I guess this is your point to use [System.Net.IPAddress]$ip , to avoid writing validation for number of octets, “0” as first number in octet, etc.
I will test it and rewrite it
Thank you very much for answer, it will make script much more clear and reliable.

Just to make clear what I meant: I do create this indexed lists to choose from dynamically from arrays created during the runtime of the script. I do not create them by hand. :wink: … of course that’s a little more effort in advance. But it saves effort you spend to validate the user input.