Chapter 08Lesson 03~140 minutes

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 production

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

Keep validation side-effect free. Do not create files, modify services, or call deployment APIs merely to decide whether a parameter is valid.

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.

OperationQuestionExample
ValidationIs this allowed?Environment must be dev/test/prod
NormalizationWhich canonical form represents it?Lowercase a case-insensitive identifier
Escaping/encodingHow 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

MistakeWhy it is weakBetter design
Use ValidateScript for a fixed three-value choiceThe rule is harder to read than necessaryUse ValidateSet
Use one giant regex for every policyMaintenance and error diagnosis sufferUse simple validation plus explicit logic
Perform side effects in validationInvocation can mutate state before the operation startsKeep validators observational
Call validation “sanitization”Acceptability and context-safe encoding are differentName each boundary operation precisely
Hide cross-parameter requirements inside unrelated validatorsThe interface becomes mysteriousCheck 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?

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

Next lesson

Understand where script and function state lives and why globals create hidden coupling

Continue to Lesson 4, where the chapter builds on this boundary with understand where script and function state lives and why globals create hidden coupling.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.