Custom error from parameters in PowerShell - powershell

Is it possible to have ValidateScript generate a custom error message when a test fails, like say Test-Path?
Instead of this:
Test-Folder : Cannot validate argument on parameter 'Folder'. The "Test-Path $_ -Path Type Container" validation script for the argument with value "blabla" did not return a result of True. Determine why the validation script failed, and then try the comma and again.
It would be nice to have it report this instead in the $Error variable:
The 'Folder' is not found, maybe there are network issues?
Code:
Function Test-Folder {
Param (
[parameter(Mandatory=$true)]
[ValidateScript({Test-Path $_ -PathType Container})]
[String]$Folder
)
Write-Host "The folder is: $Folder"
}
Workaround 1:
I could remove the Mandatory=$true and change it as below. But this doesn't give me the correct Get-Help syntax and doesn't do the Test-Path validation, because it only checks if the parameter is present.
Function Test-Folder {
Param (
[parameter()]
[String]$Folder = $(throw "The $_ is not found, maybe there are network issues?")
)
Write-Host "The folder is: $Folder"
}
Workaround 2:
I found this workaround on a blog post, but the problem is that it generates two errors instead of one.
Function Test-Folder {
Param (
[parameter(Mandatory=$true)]
[ValidateScript({
if (Test-Path $_ -PathType Container) {$true}
else {Throw "The $_ is not found, maybe there are network issues?"}})]
[String]$Folder
)
Write-Host "The folder is: $Folder"
}
Workaround 3:
I could also try to make it more clear by adding a comment section. However, this is still not the desired result as the error needs to be readable to end users.
Function Test-Folder {
Param (
[parameter(Mandatory=$true)]
[ValidateScript({
# The folder is not found, maybe there are network issues?
Test-Path $_ -PathType Container})]
[String]$Folder
)
Write-Host "The folder is: $Folder"
}

Your ValidateScript should look something like this:
[ValidateScript({
try {
$Folder = Get-Item $_ -ErrorAction Stop
} catch [System.Management.Automation.ItemNotFoundException] {
Throw [System.Management.Automation.ItemNotFoundException] "${_} Maybe there are network issues?"
}
if ($Folder.PSIsContainer) {
$True
} else {
Throw [System.Management.Automation.ValidationMetadataException] "The path '${_}' is not a container."
}
})]
That will give you a message like this:
Test-Folder : Cannot validate argument on parameter 'Folder'. Cannot
find path '\\server\Temp\asdf' because it does not exist. Maybe there are
network issues?
Or:
Test-Folder : Cannot validate argument on parameter 'Folder'. The path
'\\server\Temp\asdf' is not a container.
If the older versions of PowerShell are throwing a double error, you may need to test inside the function:
Function Test-Folder {
Param (
[parameter(Mandatory=$true)]
[String]$Folder
)
try {
$Folder = Get-Item $_ -ErrorAction Stop
} catch [System.Management.Automation.ItemNotFoundException] {
Throw [System.Management.Automation.ItemNotFoundException] "The '${Folder}' is not found, maybe there are network issues?"
}
if (-not $Folder.PSIsContainer) {
Throw [System.Management.Automation.ApplicationFailedException] "The path '${_}' is not a container."
}
Write-Host "The folder is: ${Folder}"
}
The part that I always hated in PowerShell was trying to figure out what error to catch; without catching all. Since I finally figure it out, here's how:
PS > Resolve-Path 'asdf'
Resolve-Path : Cannot find path '.\asdf' because it does not exist.
At line:1 char:1
+ Resolve-Path 'asdf'
+ ~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (asdf:String) [Resolve-Path], ItemNotFoundE
xception
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.ResolvePathCommand
PS > $Error[0].Exception.GetType().FullName
System.Management.Automation.ItemNotFoundException

You can just do
ValidateScript({Test-Path $_ -PathType Container}, ErrorMessage="Must exist")]
That gives you error messages like:
Cannot validate argument on parameter 'Folder'. Must exist
At least in current powershell versions (7.x at the time of writing this).
Generally, for you powershell-only devs, when you use attributes (stuff like [ValidateScript), then you can sometimes also set additional properties with the syntax as above. To see which properties, you can just google for the name of the attribute postfixed with "Attribute", eg "ValidateScriptAttribute", and then look at the "Properties" section for all writeable properties.

I think you've found the straightforward workarounds.
The parameter validation logic is extensible, but requires some C#. If you implement the abstract class System.Management.Automation.ValidateArgumentsAttribute, your implementation can throw a System.Management.Automation.ValidationMetadtaException that PowerShell will use to report the error, and you can naturally use any message you like when creating that exception.

I am not sure.
A suggestion: maybe you want to just trap the error, and make your own message.
trap [Error.Type] {
#"
The message you want displayed.
With maybe some additional information.
I.e.: The full message itself: {0}' -f $_.Error.Message;
continue;
}

Related

Function to Get Value from Registry - 2 issues: DWORD, console error

This is ma very simple PowerShell function to get reg value from registry:
Function GetRegKey {
Param([String]$GetRegKeyPath, $GetRegKeyName)
If (Test-Path -Path $GetRegKeyPath) {
If (Get-ItemProperty -Path $GetRegKeyPath -Name $GetRegKeyName) {
$GetRegKeyVal = (Get-ItemPropertyValue $GetRegKeyPath $GetRegKeyName)
Return $GetRegKeyVal
} Else {
write-output ("Warning! Path: '$GetRegKeyPath' is exist, but RegKey: '$GetRegKeyName' is: not exist.") }
} Else {
write-output ("Warning! Path: '$GetRegKeyPath' is not exist!")
Return $null }
}
Example:
GetRegKey "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion" "ProgramFilesDir"
will give output like:
C:\Program Files
I have two things what is not working:
If $GetRegKeyName is not exist script besides go to command write-output ("Warning! Path: '$GetRegKeyPath' is exist, but RegKey: '$GetRegKeyName' is: not exist.") is also display in console error:
Is this can be somehow avoid? I did try with pipe | Out-Null but its bassically not a solution I guess.
Second thing what is not works here is reg with DWORD value.
Is command Get-ItemProperty -Path $GetRegKeyPath -Name $GetRegKeyName
can't be used with DWORD?
I suggest streamlining your function as follows, which only requires a single Get-ItemPropertyValue (note that I've renamed the function to GetRegValue, because it is a registry value's (data) that is returned):
Function GetRegValue {
Param(
[Parameter(Mandatory)]
[String] $KeyPath,
[Parameter(Mandatory)]
[String] $ValueName
)
try {
Get-ItemPropertyValue -ErrorAction Stop $KeyPath $ValueName
}
catch [System.Management.Automation.ItemNotFoundException] {
Write-Warning "Path: '$KeyPath' does not exist."
}
catch [System.Management.Automation.PSArgumentException] {
Write-Warning "Path: '$KeyPath' exists, but RegKey: '$ValueName' doesn't."
}
}
A try / catch statement is used to match errors by their exception type, from which the specific error condition can be inferred.
Note that any exception type not matched would result in a script-terminating error; that is, error conditions other than the matched ones cause a fatal error (by default), such as a lack of permissions.
The common -ErrorAction parameter is used with Stop in order to promote non-terminating errors to (script-)terminating ones, which allows them to be caught with try / catch.
As an aside: Due to a longstanding bug still present as of PowerShell 7.3.1, Get-ItemPropertyValue inappropriately reports a (statement-)terminating error when the key path exists, but not the value name - see GitHub issue #5906.
Write-Warning is used to emit the warnings, which uses the dedicated warning output stream, which is conceptually preferable and also gives you automatic coloring of the message.
As for DWORD values:
PowerShell is capable of returning types of all data stored in the registry (they get mapped onto equivalent .NET types), so there shouldn't be a problem with REG_DWORD registry values.
You can verify by calling the function above as follows:
GetRegValue HKCU:\Console FontSize
This should output an [int] (System.Int32) value such as 1048576.
Note that if the value is too large to fit into [int], you'll get a [uint32] (System.UInt32) value instead.

Is it possible to implement IValidateSetValuesGenerator for script parameter validation?

In the example in the powershell documentation you need to implement a class, like:
Class SoundNames : System.Management.Automation.IValidateSetValuesGenerator { ... }
But, how can you do this in a script based cmdlet? (See "Parameters in scripts" in about_Scripts).
In a script cmdlet the first line must be param( ... ).
Unless "SoundNames" is already defined, then it fails with Unable to find type [SoundNames]
If this type is already defined in the powershell session, this works ... but I'd like a stand-alone script that doesn't require the user do something first to define this type (e.g. dot-sourcing some other file).
I want to define a parameter that accepts only *.txt filenames (for existing files in a specific directory).
You can't do this in a (stand-alone) script, for the reason you state yourself:
The param block (possibly preceded by a [CmdletBinding()] attribute, must be at the very start of the file (leaving using statements aside).
This precludes defining a custom class that implements the IValidateSetValuesGenerator interface for use in your param block.
To work around this limitation, use a [ValidateScript()] attribute:
param(
[ValidateScript({
if (-not (Get-Item -ErrorAction Ignore "$_.txt")) {
throw "$_ is not the (base) name of a *.txt file in the current dir."
}
return $true
})]
[string] $FileName
)
Note that this doesn't give you tab-completion the way that IValidateSetValuesGenerator-based validation would automatically give you.
To also provide tab-completion, additionally use an [ArgumentCompleter()] attribute:
param(
[ValidateScript({
if (-not (Get-Item -ErrorAction Ignore "$_.txt")) {
throw "$_ is not the (base) name of a *.txt file in the current dir."
}
return $true
})]
[ArgumentCompleter({
param($cmd, $param, $wordToComplete)
(Get-Item "$wordToComplete*.txt").BaseName
})]
[string] $FileName
)

Unable to utilize ErrorMessage property with ValidateSet in Function

I am currently practicing the use of functions in PowerShell and am running into an error. I created the function below to accept DC Super Hero names and return the name of the hero being passed to the function.
function Get-DCHero {
[CmdletBinding()]
param (
[Parameter(Mandatory)][ValidateSet('Batman','Superman','Aquaman','Wonder Woman','Flash',ErrorMessage = "'{0}' is not a DC Super Hero. Please trying one of the following: '{1}'")]
[string]$Name
)
Write-OutPut "$Name is a DC Super hero."
}
As of now the function works properly without the ErrorMessage portion on the ValidateSet. When including the ErrorMessage portion I am receiving the following error:
Get-DCHero -Name
Property 'ErrorMessage' cannot be found for type 'System.Management.Automation.CmdletBindingAttribute'.
At C:\Users\AAP8801\DCSuperHero.ps1:5 char:98
+ ... n','Flash', ErrorMessage = "'{0}' is not a DC Super Hero. Please tryi ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (ErrorMessage = ...llowing: '{1}'":NamedAttributeArgume
ntAst) [], RuntimeException
+ FullyQualifiedErrorId : PropertyNotFoundForType
When a parameter is passed to the function that is not part of the validation set I would like to able to edit the error message being throw. Can anyone tell me why I am unable to do this successfully?
To complement Santiago Squarzon's helpful answer by spelling out the [ValidateScript()] workaround he mentions, which is slightly easier than defining a custom attribute class:
function Get-DCHero {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[ValidateScript({
$set = 'Batman','Superman','Aquaman','Wonder Woman','Flash'
if ($_ -in $set) { return $true } # OK
throw "'$_' is not a DC superhero. Please try one of the following: '$($set -join ',')'"
})]
[string]$Name
)
"$Name is a DC superhero."
}
As Mathias explained in comments, the ErrorMessage property is available on the PowerShell SDK v6.2.0 and above.
As a workaround in Windows PowerShell, you could use the ValidateScript Attribute Declaration or you could create your own attribute declaration by inheriting from ValidateEnumeratedArgumentsAttribute, Base Type of the ValidateSet Class.
The following example can help you get started, and if you're interested in learning more, I would recommend you these nice articles from Kevin Marquette:
Powershell: Creating and using custom attributes
Powershell: Creating parameter validators and transforms
Class Definition
using namespace System.Management.Automation
class MyDCHeroSet : ValidateEnumeratedArgumentsAttribute {
[scriptblock] $Set
[string] $ErrorMessage
[void] ValidateElement([object] $Object) {
$ValidValues = & $this.Set
if($Object -notin $ValidValues) {
throw [ValidationMetadataException]::new(
[string]::Format(
$this.ErrorMessage,
$Object, ($ValidValues -join ',')
)
)
}
}
}
Implementation and Testing
function Get-DCHero {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[MyDCHeroSet(
Set = { 'Batman', 'Superman', 'Aquaman', 'Wonder Woman', 'Flash' },
ErrorMessage = "'{0}' is not a DC Super Hero. Please trying one of the following: '{1}'"
)]
[string] $Name
)
Write-Output "$Name is a DC Super hero."
}
Get-DCHero -Name Spiderman

What is the best practice for returning error from a PowerShell cmdlet?

I am writing a PowerShell-based XML module for our application configuration needs. Following is the one of the functions.
<#
.Synopsis
To update an XML attribute value
.DESCRIPTION
In the XML file for a particular attribute, if it contains valueToFind then replace it with valueToReplace
.EXAMPLE
-------------------------------Example 1 -------------------------------------------------------------------
Update-XMLAttribute -Path "C:\web.Config" -xPath "/configuration/system.serviceModel/behaviors/serviceBehaviors/behavior/serviceMetadata" -attribute "externalMetadataLocation" -valueToFind "http:" -ValueToReplace "https:"
Look for the XPath expression with the attribute mentioned and search whether the value contains "http:". If so, change that to "https":
.EXAMPLE
-------------------------------Example 2 -------------------------------------------------------------------
Update-XMLAttribute -Path "C:\web.Config" -xPath "/configuration/system.serviceModel/behaviors/serviceBehaviors/behavior/serviceMetadata" -attribute "externalMetadataLocation" -valueToFind "http:" -ValueToReplace "https:"
Same as Example 1 except that the attribute name is passed as part of the XPath expression
#>
function Update-XMLAttribute
{
[CmdletBinding()]
[OutputType([int])]
Param
(
# Web configuration file full path
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=0)]
[string]$Path,
# XPath expression up to the parent node
[string] $xPath,
# This parameter is optional if you mentioned it in xPath itself
[string] $attribute,
[string] $valueToFind,
[string] $ValueToReplace
)
Try
{
If (Test-path -Path $Path)
{
$xml = New-Object XML
$xml.Load($Path)
# If the xPath expression itself contains an attribute name then the value of attribute will be processed and taken
If ($xPath.Contains("#")) {
$xPath, $attribute = $xPath -split '/#', 2
}
# Getting the node value using xPath
$Items = Select-Xml -XML $xml -XPath $xPath
ForEach ($Item in $Items)
{
$attributeValue = $Item.node.$attribute
Write-Verbose "Attribute value is $attributeValue "
if ($attributeValue.contains($valueToFind)) {
Write-Verbose "In the attribute $attributeValue - $valueToFind is to be repalced with $ValueToReplace"
$Item.node.$attribute = $attributeValue.replace($valueToFind, $ValueToReplace)
}
}
$xml.Save($Path)
Write-Verbose " Update-XMLAttribute is completed successfully"
}
Else {
Write-Error " The $path is not present"
}
}
Catch {
Write-Error "$_.Exception.Message"
Write-Error "$_.Exception.ItemName"
Write-Verbose " Update-XMLAttribute is failed"
}
} # End Function Update-XMLAttribute
As this cmdlet will be consumed by many I don't think simply writing into console will be the right approach.
As of now in my script if no errors, I can assume that mine is successfully completed.
What is the standard practice to get the results from a PowerShell cmdlet so that the consumer knows whether it is successfully completed or not?
The standard practice is to throw exceptions. Each different type of error has a separate exception type which can be used to diagnose further.
Say, file is not represented, you do this:
if (-not (Test-Path $file))
{
throw [System.IO.FileNotFoundException] "$file not found."
}
Your cmdlet should document all the possible exceptions it will throw, and when.
Your function should throw if it runs into an error. Leave it to the caller to decide how the error should be treated (ignore, log a message, terminate, whatever).
While you can throw an exception, which PowerShell will catch and wrap in an ErrorRecord, you have more flexibility using the ThrowTerminatingError method. This is the typical approach for a C# based cmdlet.
ThrowTerminatingError(new ErrorRecord(_exception, _exception.GetType().Name, ErrorCategory.NotSpecified, null));
This allows you to pick an error category and provide the target object. BTW what you have above isn't what I'd call a cmdlet. Cmdlets are compiled C# (typically). What you have is an advanced function. :-)
From an advanced function you can access this method like so:
$pscmdlet.ThrowTerminatingError(...)

Passing an argument to a powershell script to be used for the Test-Path -include option?

this is my first time asking a question so bear with me. I am teaching myself powershell by writing a few basic maintenance scripts. My question is in regard to a clean up script I am writing which accepts arguments to determine the target directory and files to delete.
The Problem:
The script accepts an optional argument for a list of file extensions to look for when processing the deletion of files. I am trying to test for the existence of the files prior to actually running the delete. I use test-path with the –include parameter to run the check within a ValidateScript block. It works if I pass in a single file extension or no file extensions, however when I try to pass in more than one file extension it fails.
I have tried using the following variations on the code inside the script:
[ValidateScript({ Test-Path $targetDirChk -include $_ })]
[ValidateScript({ Test-Path $targetDirChk -include "$_" })]
[ValidateScript({ Test-Path $targetDirChk -include ‘$_’ })]
For each of the above possibilities I have run the script from the command line using the following variations for the multi extension file list:
& G:\batch\DeleteFilesByDate.ps1 30 G:\log *.log,*.ext
& G:\batch\DeleteFilesByDate.ps1 30 G:\log “*.log, *.ext”
& G:\batch\DeleteFilesByDate.ps1 30 G:\log ‘*.log, *.ext’
Example of the error message:
chkParams : Cannot validate argument on parameter 'includeList'. The " Test-Path $targetDirChk -include "$_" " validation script for the argument with value "*.log, *.ext" did not return true. Determine why the validation script failed and then try the command again.
At G:\batch\DeleteFilesByDate.ps1:81 char:10
+ chkParams <<<< #args
+ CategoryInfo : InvalidData: (:) [chkParams], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,chkParams
The full script is below. I have not yet added the actual code to delete files, because I am still working on accepting and validating the arguments passed in.
I have searched google and stackoverflow but I have not found a solution to this particular problem. I assume I am either doing something wrong with the code, or there is a better way to accomplish what I want to do.
Note:
I should mention that I also tried running the test-path with multiple file extensions outside of the script with no problems:
PS G:\batch\powershell> test-path G:\log\* -include *.log
True
PS G:\batch\powershell> test-path G:\log\* -include *.log, *.ext
True
Script:
# Check that the proper number of arguments have been supplied and if not provide usage statement.
# The first two arguments are required and the third is optional.
if ($args.Length -lt 2 -or $args.Length -gt 3 ){
#Get the name of the script currently executing.
$ScriptName = $MyInvocation.MyCommand.Name
$ScriptInstruction = #"
usage: $ScriptName <Number of Days> <Directory> [File Extensions]
This script deletes files from a given directory based on the file date.
Required Paramaters:
<Number of Days>:
This is an integer representing the number of days worth of files
that should be kept. Anything older than <Number of Days> will be deleted.
<Directory>:
This is the full path to the target folder.
Optional Paramaters:
[File Extensions]
This is the set of file extensions that will be targeted for processing.
If nothing is passed all files will be processed.
"#
write-output $ScriptInstruction
break
}
#Function to validate arguments passed in.
function chkParams()
{
Param(
[Parameter(Mandatory=$true,
HelpMessage="Enter a valid number of days between 1 and 999")]
#Ensure the value passed is between 1 and 999.
#[ValidatePattern({^[1-9][0-9]{0,2}$})]
[ValidateRange(1,999)]
[Int]
$numberOfDays,
[Parameter(Mandatory=$true,
HelpMessage="Enter a valid target directory.")]
#Check that the target directory exists.
[ValidateScript({Test-Path $_ -PathType 'Container'})]
[String]
$targetDirectory,
[Parameter(Mandatory=$false,
HelpMessage="Enter the list of file extensions.")]
#If the parameter is passed, check that files with the passed extension(s) exist.
[ValidateScript({ Test-Path $targetDirChk -include "$_" })]
[String]
$includeList
)
#If no extensions are passed check to see if any files exist in the directory.
if (! $includeList ){
$testResult = Test-path $targetDirChk
if (! $testResult ){
write-output "No files found in $targetDirectory"
exit
}
}
}
#
if ($args[1].EndsWith('\')){
$targetDirChk = $args[1] + '*'
} else {
$targetDirChk = $args[1] + '\*'
}
chkParams #args
-Include on Test-Path is a string[]. You probably want to mirror that definition:
[ValidateScript({ Test-Path $targetDirChk -include $_ })]
[String[]]
$includeList
And drop the "" from there because they will force the argument to be a string and thus trying to match a file that looks like `foo.log blah.ext.
You also have to either put parentheses around that argument when calling the function or remove the space.