Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Combine a default parameter value with validation via the ValidateScript attribute

Tags:

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] 
like image 886
Harvey Lin Avatar asked Feb 28 '17 20:02

Harvey Lin


2 Answers

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:

      • assign to the parameter variable directly
      • contain any other output statements such as 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:

      • aborts invocation of the function
      • reports a non-terminating error
    • 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

Why your default parameter value didn't take effect:

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.


Optional reading: using splatting to (selectively) pass arguments through to another command:

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
like image 114
mklement0 Avatar answered Sep 22 '22 15:09

mklement0


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{
like image 45
Mark Wragg Avatar answered Sep 24 '22 15:09

Mark Wragg