ValidateSet, ValidatePattern, ValidateRange, ValidateScript, and Safer Input
Validate PowerShell script inputs at the boundary with readable attributes, actionable failures, and clear separation between validation and sanitization.
Learning objectives
- Explain why input validation belongs near the automation boundary.
- Apply ValidateSet, ValidatePattern, ValidateRange, null/empty validators, and ValidateScript appropriately.
- Prefer the simplest validation attribute that expresses the intended contract.
- Differentiate validation, normalization, and context-specific escaping/serialization.
- Handle cross-parameter requirements with clear explicit logic at a beginner level.
- Exercise both accepted and rejected inputs and diagnose validation errors.
1. Validate input at the boundary before unsafe values spread through the script
A script boundary is the point where outside values enter your logic: command-line parameters, environment variables, files, or API responses. Parameter validation attributes let PowerShell reject invalid parameter values before normal processing continues. Centralizing simple rules here keeps later code focused on the actual operation.
param(
[Parameter(Mandatory)]
[ValidateSet('dev','test','prod')]
[string]$Environment
)Validation answers “is this value acceptable for this parameter?” It does not automatically make hostile text safe for every future context. Escaping, encoding, and safe API/native-command construction remain separate responsibilities.
2. ValidateSet is the clearest choice for a small closed vocabulary
ValidateSet accepts only values from an explicit list and gives callers tab completion. It is ideal when the domain really is closed: deployment environment, log level, output format, or mode.
param(
[ValidateSet('dev','test','prod')]
[string]$Environment = 'test'
)
"Environment: $Environment"
# Accepted: -Environment prod
# Rejected: -Environment productionDo not use a closed set if valid values are expected to change independently of the script, such as server names retrieved from inventory. That turns an operational data source into a code release problem.
3. ValidatePattern and ValidateRange express shape and numeric bounds
ValidatePattern compares text with a regular expression. ValidateRange constrains numeric values. Keep patterns narrow and understandable; a validation regex that nobody can safely maintain is worse than a few clear checks.
param(
[ValidatePattern('^[a-z][a-z0-9-]{2,31}$')]
[string]$ServiceName,
[ValidateRange(1, 300)]
[int]$TimeoutSeconds = 30
)The pattern above allows a lowercase letter followed by lowercase letters, digits, or hyphens, with a total length of 3–32 characters. The range rejects zero, negative timeouts, and unrealistic large values before the operation starts.
4. Mandatory does not mean every concept of “non-empty”
Beginners often assume [Parameter(Mandatory)] means “useful non-empty value.” PowerShell has separate validation attributes for null, empty strings, empty collections, and whitespace. Choose the rule that expresses your contract instead of layering scattered if statements later.
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$ConfigPath,
[Parameter(Mandatory)]
[ValidateNotNullOrWhiteSpace()]
[string]$Owner
)If whitespace is meaningful input for a specialized tool, do not reject it blindly. Validation should reflect actual semantics, not a universal style rule.
5. ValidateScript runs a rule, but prefer simpler attributes when they can express the contract
ValidateScript evaluates a script block for each supplied value. Inside the block, $_ is the candidate value. The rule succeeds when the script block returns a true value; returning false or throwing causes parameter validation to fail.
param(
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Leaf })]
[string]$ConfigPath
)This is powerful, but it couples invocation to external state. A file may exist during validation and disappear before use; a network check may be slow; a validation rule with side effects is especially confusing. Prefer ValidateSet, ValidateRange, or ValidatePattern when they express the rule directly.
6. A rejection is part of the user interface, so make the reason actionable
PowerShell reports the parameter name and validation failure automatically. For complex ValidateScript rules, modern PowerShell can provide a custom ErrorMessage. The message should tell the caller what contract was violated, not merely repeat “invalid argument.”
param(
[ValidateScript(
{ $_ -ge 1 -and $_ -le 10 },
ErrorMessage = 'RetryCount {0} must be between 1 and 10.'
)]
[int]$RetryCount = 3
)Do not expose secrets in validation messages. If a token is malformed, report the required shape without echoing the token value.
7. Validation and sanitization are not interchangeable
Validation decides whether input is acceptable. Normalization may convert equivalent forms into one canonical representation, such as trimming a service name when whitespace is explicitly not meaningful. Escaping/encoding makes data safe for a specific downstream language or protocol. Treating these as one operation creates security mistakes.
| Operation | Question | Example |
|---|---|---|
| Validation | Is this allowed? | Environment must be dev/test/prod |
| Normalization | Which canonical form represents it? | Lowercase a case-insensitive identifier |
| Escaping/encoding | How is it represented safely here? | JSON serialization or native argument handling |
Never “sanitize” arbitrary text into executable PowerShell. Chapter 06 already established that untrusted text should not be passed to Invoke-Expression.
8. Cross-parameter rules usually belong in explicit script logic
Validation attributes are strongest when validating one parameter value. Some rules involve relationships: -Mode Repair may require -BackupPath; -Start must occur before -End. Keep those checks together near the start of the script with clear failure messages rather than hiding them in a complicated ValidateScript that reaches across state.
param(
[ValidateSet('Inspect','Repair')]
[string]$Mode = 'Inspect',
[string]$BackupPath
)
if ($Mode -eq 'Repair' -and [string]::IsNullOrWhiteSpace($BackupPath)) {
throw '-BackupPath is required when -Mode Repair is selected.'
}Chapter 09 later introduces parameter sets for mutually exclusive command shapes. At this stage, explicit relational checks are easier to understand and test.
9. Lab: exercise accepted and rejected CLI inputs
Create a disposable script whose boundary validates environment, service name, retry count, and an existing config file. Run both valid and intentionally invalid calls and read the resulting binding errors.
$root = Join-Path ([System.IO.Path]::GetTempPath()) 'ps-validation-lab'
New-Item -ItemType Directory -Path $root -Force | Out-Null
$config = Join-Path $root 'app.json'
Set-Content -LiteralPath $config -Value '{"enabled":true}'
$script = Join-Path $root 'Test-Input.ps1'
@'
param(
[ValidateSet('dev','test','prod')] [string]$Environment,
[ValidatePattern('^[a-z][a-z0-9-]{2,31}$')] [string]$ServiceName,
[ValidateRange(1,10)] [int]$RetryCount = 3,
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Leaf })] [string]$ConfigPath
)
[pscustomobject]@{ Environment=$Environment; Service=$ServiceName; Retry=$RetryCount; Config=$ConfigPath }
'@ | Set-Content -LiteralPath $script -Encoding utf8
& $script -Environment test -ServiceName api-gateway -RetryCount 4 -ConfigPath $config
# Try these separately and inspect each error:
# & $script -Environment qa -ServiceName api-gateway -RetryCount 4 -ConfigPath $config
# & $script -Environment test -ServiceName 'API!' -RetryCount 4 -ConfigPath $config
# & $script -Environment test -ServiceName api -RetryCount 99 -ConfigPath $config
# & $script -Environment test -ServiceName api -RetryCount 4 -ConfigPath (Join-Path $root 'missing.json')
Remove-Item -LiteralPath $root -Recurse -Force- A valid invocation emits one object.
- Each invalid example names the parameter whose rule failed.
- No validation rule mutates external state.
- The script body stays free of repeated basic validation checks.
10. Validation mistakes and the reasoning behind them
| Mistake | Why it is weak | Better design |
|---|---|---|
| Use ValidateScript for a fixed three-value choice | The rule is harder to read than necessary | Use ValidateSet |
| Use one giant regex for every policy | Maintenance and error diagnosis suffer | Use simple validation plus explicit logic |
| Perform side effects in validation | Invocation can mutate state before the operation starts | Keep validators observational |
| Call validation “sanitization” | Acceptability and context-safe encoding are different | Name each boundary operation precisely |
| Hide cross-parameter requirements inside unrelated validators | The interface becomes mysterious | Check relational rules explicitly or later use parameter sets |
11. Knowledge check
Question 1. Which attribute is usually clearest for a small fixed set such as dev/test/prod?
ValidateSet.Question 2. What does ValidatePattern use to test text?
Question 3. Inside ValidateScript, what does $_ represent?
Question 4. Why should validation normally avoid side effects?
Question 5. Is validation the same as escaping data for JSON, SQL, a native process, or PowerShell code?
12. Summary
Validation belongs close to the automation boundary. Use the simplest attribute that expresses the rule: ValidateSet for closed vocabularies, ValidatePattern for textual shape, ValidateRange for numeric bounds, null/empty validators for presence semantics, and ValidateScript only when a genuine custom predicate is required. Keep validators side-effect free, make errors actionable, and handle cross-parameter relationships explicitly.
13. Further reading
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.