I have the following PowerShell code to validate user input as a path, if the user didn't enter anything, I am attempting to assign a default value to them. However when I run this, the $filePath
variable does not gets assigned any value.
Is there anyway I can change this to have it assigned a default value while the validation is going on?
Code below:
function validatePath {
Param
(
[ValidateScript({
If ($_ -eq "" -or $_ -eq [String]::Empty) {
$_ = "C:\Install"
$True
}
ElseIf ($_ -match "^([a-z]:\\(?:[-\\w\\.\\d])*)") {
$True
}
Else {
Write-Host "Please enter a valid path,$_ is not a valid path."
Write-debug $_.Exception
}
})]
[string]$filePath = "C:\Install"
)
Process
{
Write-Host "The path is "$filePath
}
}
validatePath -filePath $args[0]
This answer first discusses the correct use of the ValidateScript
attribute.
The unrelated default-value issue is discussed afterward, followed by an optional section on parameter splatting.
Matt provides good pointers in his comment on the question:
A ValidateScript
script block should output a Boolean only.
That Boolean tells PowerShell whether the parameter value is considered valid or not, and it takes action accordingly.
Notably, the script block is not meant to:
Write-Host
(which you shouldn't use to report errors anyway).If the script block outputs (effective) $False
or the script block throws an exception, PowerShell:
If the script block outputs $False
, you get a generic error message that includes the literal contents of your script block (excluding the enclosing {
and }
) - which may be too technical for end users.
PowerShell Core introduced an optional ErrorMessage = "..."
field for both the ValidateScript
and ValidatePattern
attributes; e.g.,[ValidateScript({ $_ % 2 -eq 0 }, ErrorMessage = "{0} is not an even number.")]
In Windows Powershell, it is advisable to throw an exception with a user-friendly error message**, in which case PowerShell includes the exception text in its error message.
A parameter's default value is by design not checked against the validation script - you as the function creator assume the responsibility of defaulting to a value that is valid - see this blog post.
Applied to your example:
Note that I'm using '^[a-z]:\\[-\w\d\\]*$'
as the regex, because that's what I think you actually meant to use.
function validatePath {
Param
(
[ValidateScript({
if ($_ -match '^[a-z]:\\[-.\w\d\\]*$') { return $True }
Throw "'$_' is not a valid local path."
})]
[string] $filePath = "C:\Install"
)
Process
{
"The path is: $filePath"
}
}
Now all 3 invocation scenarios should work as intended:
> validatePath # use default value
The path is: C:\Install
> validatePath -filePath C:\MyInstall # valid path
The path is: C:\MyInstall
> validatePath -filePath NotAFullPath # invalid path -> error with custom message
validatePath : Cannot validate argument on parameter 'filePath'.
'NotAFullPath' is not a valid local path.
At line:1 char:24
+ validatePath -filePath NotAFullPath # invalid path
+ ~~~~~~~~~~~~
+ CategoryInfo : InvalidData: (:) [validatePath], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,validatePath
This issue is unrelated to validation, and stems from the fact that you're passing $args[0]
in your validatePath
invocation:
If the script itself received no arguments, $args[0]
is $null
, but it is still being passed as an explicit value, so it binds to parameter $filePath
by coercion to an empty string.
Since an explicit parameter value was passed, the default value wasn't used, and $filePath
ended up containing the empty string.
Since this is how parameter binding works in PowerShell, I suggest not trying to work around it inside your function, but to instead fix the problem on invocation:
if ([string] $args[0]) { # only true if $args[0] is neither $null nor the empty string
validatePath -filePath $args[0]
} else {
validatePath
}
Note that it's usually preferable to declare your parameters explicitly rather than using $args
.
As an alternative to using two separate invocations in the conditional above, consider using parameter splatting, which allows you to pass multiple parameters using a single collection variable, prefixed with @
:
an array that represents multiple positional parameters.
more commonly and more robustly, a hashtable that represents multiple named parameters.
This allows you to dynamically build the collection of parameters ahead of time, and the pass the collection as a whole to a single invocation of the target command.
A quick and dirty workaround in your case would be to use splatting with all parameters, i.e. to pass $args
through (note the @
sigil instead of $
):
validatePath @args
This would simply pass all arguments, if any, passed to the script through to validatePath
as if they had been specified separately; if no argument is passed to the script, nothing is passed through, and the -filePath
default value inside validatePath
does take effect.
Parameter-individual splatting is another option, which is a robust technique for passing select parameters through to another command:
# Define a hashtable to hold the parameters, if any, to pass through
# to validatePath() via splatting.
$htPassthruParams = @{}
# If the first script argument is neither $null nor the empty string,
# add a hashtable entry for it that will bind to the -filePath parameter.
if ([string] $args[0]) { $htPassthruParams.Add('filePath', $args[0]) }
# Pass the hashtable with `@`, the splatting operator, to validatePath()
validatePath @htPassthruParams
If you declare your script with explicit parameters as well (using its own param(...)
block), the approach can be generalized by using the automatic $PSBoundParameters
dictionary to determine if a parameter was bound, which obviates the need to check for a specific value:
# Define a hashtable to hold the parameters, if any, to pass through
# to validatePath() via splatting.
$htPassthruParams = @{}
# Using a list of parameters, pass their values through only if they are
# *bound*, i.e., only if they received values when the enclosing script/function
# itself was called.
# Assume that the enclosing script declared a -filePath parameter too.
foreach($paramName in , 'filePath') {
if ($PSBoundParameters.ContainsKey($paramName)) {
$htPassthruParams.Add($paramName, $PSBoundParameters[$paramName])
}
}
# Pass the hashtable with `@`, the splatting operator, to validatePath()
validatePath @htPassthruParams
I think you could drop the validate script and instead do this in a Begin block:
Begin{
If ($filepath -eq "") {
$filepath = "C:\Install"
}
ElseIf ($filepath -notmatch "^([a-z]:\\(?:[-\\w\\.\\d])*)") {
Write-Error "Please enter a valid path,$filepath is not a valid path."
}
}
Process{
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With