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
Related
I have a PowerShell 7.1 helper script that I use to copy projects from subversion to my local device. I'd like to make this script easier for me to use by enabling PowerShell to auto-complete parameters into this script. After some research, it looks like I can implement an interface to provide valid parameters via a ValidateSet.
Based on Microsoft's documentation, I attempted to do this like so:
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[ValidateSet([ProjectNames])]
[String]
$ProjectName,
#Other params
)
Class ProjectNames : System.Management.Automation.IValidateSetValuesGenerator {
[string[]] GetValidValues() {
# logic to return projects here.
}
}
When I run this, it does not auto-complete and I get the following error:
❯ Copy-ProjectFromSubversion.ps1 my-project
InvalidOperation: C:\OneDrive\Powershell-Scripts\Copy-ProjectFromSubversion.ps1:4
Line |
4 | [ValidateSet([ProjectNames])]
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Unable to find type [ProjectNames].
This makes sense since the class isn't defined until after the parameters. So I moved the class above the parameters. Obviously this is a syntax error. So how do I do this? Is it not possible in a simple PowerShell script?
Indeed, you've hit a catch-22: for the parameter declaration to work during the script-parsing phase, class [ProjectNames] must already be defined, yet you're not allowed to place the class definition before the parameter declaration.
The closest approximation of your intent using a stand-alone script file (.ps1) is to use the ValidateScript attribute instead:
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[ValidateScript(
{ $_ -in (Get-ChildItem -Directory).Name },
ErrorMessage = 'Please specify the name of a subdirectory in the current directory.'
)]
[String] $ProjectName # ...
)
Limitations:
[ValidateScript] does not and cannot provide tab-completion: the script block, { ... }, providing the validation is only expected to return a Boolean, and there's no guarantee that a discrete set of values is even involved.
Similarly, you can't reference the dynamically generated set of valid values (as generated inside the script block) in the ErrorMessage property value.
The only way around these limitations would be to duplicate that part of the script block that calculates the valid values, but that can become a maintenance headache.
To get tab-completion you'll have to duplicate the relevant part of the code in an [ArgumentCompleter] attribute:
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[ValidateScript(
{ $_ -in (Get-ChildItem -Directory).Name },
ErrorMessage = 'Please specify the name of a subdirectory in the current directory.'
)]
[ArgumentCompleter(
{
param($cmd, $param, $wordToComplete)
# This is the duplicated part of the code in the [ValidateScipt] attribute.
[array] $validValues = (Get-ChildItem -Directory).Name
$validValues -like "$wordToComplete*"
}
)]
[String] $ProjectName # ...
)
I'm looking for a way to make a cmdlet which receives parameter and while typing, it prompts suggestions for completion from a predefined array of options.
I was trying something like this:
$vf = #('Veg', 'Fruit')
function Test-ArgumentCompleter {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[ValidateSet($vf)]
$Arg
)
}
The expected result should be:
When writing 'Test-ArgumentCompleter F', after clicking the tub button, the F autocompleted to Fruit.
To complement the answers from #mklement0 and #Mathias, using dynamic parameters:
$vf = 'Veg', 'Fruit'
function Test-ArgumentCompleter {
[CmdletBinding()]
param ()
DynamicParam {
$RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
$AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
$ParameterAttribute.Mandatory = $true
$AttributeCollection.Add($ParameterAttribute)
$ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($vf)
$AttributeCollection.Add($ValidateSetAttribute)
$RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter('Arg', [string], $AttributeCollection)
$RuntimeParameterDictionary.Add('Arg', $RuntimeParameter)
return $RuntimeParameterDictionary
}
}
Depending on how you want to predefine you argument values, you might also use dynamic validateSet values:
Class vfValues : System.Management.Automation.IValidateSetValuesGenerator {
[String[]] GetValidValues() { return 'Veg', 'Fruit' }
}
function Test-ArgumentCompleter {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[ValidateSet([vfValues])]$Arg
)
}
note: The IValidateSetValuesGenerator class [read: interface] was introduced in PowerShell 6.0
In addition to mklement0's excellent answer, I feel obligated to point out that in version 5 and up you have a slightly simpler alternative available: enum's
An enum, or an "enumeration type", is a static list of labels (strings) associated with an underlying integral value (a number) - and by constraining a parameter to an enum type, PowerShell will automatically validate the input value against it AND provide argument completion:
enum MyParameterType
{
Veg
Fruit
}
function Test-ArgumentCompleter {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[MyParameterType]$Arg
)
}
Trying to tab complete the argument for -Arg will now cycle throw matching valid enum labels of MyParameterType:
PS ~> Test-ArgumentCompleter -Arg v[<TAB>]
# gives you
PS ~> Test-ArgumentCompleter -Arg Veg
PowerShell generally requires that attribute properties be literals (e.g., 'Veg') or constants (e.g., $true).
Dynamic functionality requires use of a script block (itself specified as a literal, { ... }) or, in specific cases, a type literal.
However, the [ValidateSet()] attribute only accepts an array of string(ified-on-demand) literals or - in PowerShell (Core) v6 and above - a type literal (see below).
Update:
If you're using PowerShell (Core) v6+, there's a simpler solution based on defining a custom class that implements the System.Management.Automation.IValidateSetValuesGenerator interface - see the 2nd solution in iRon's helpful answer.
Even in Windows PowerShell a simpler solution is possible if your validation values can be defined as an enum type - see Mathias R. Jessen's helpful answer.
To get the desired functionality based on a non-literal array of values, you need to combine two other attributes:
[ArgumentCompleter()] for dynamic tab-completion.
[ValidateScript()] for ensuring on command submission that the argument is indeed a value from the array, using a script block.
# The array to use for tab-completion and validation.
[string[]] $vf = 'Veg', 'Fruit'
function Test-ArgumentCompleter {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
# Tab-complete based on array $vf
[ArgumentCompleter({
param($cmd, $param, $wordToComplete) $vf -like "$wordToComplete*"
})]
# Validate based on array $vf.
# NOTE: If validation fails, the (default) error message is unhelpful.
# You can work around that in *Windows PowerShell* with `throw`, and in
# PowerShell (Core) 7+, you can add an `ErrorMessage` property:
# [ValidateScript({ $_ -in $vf }, ErrorMessage = 'Unknown value: {0}')]
[ValidateScript({
if ($_ -in $vf) { return $true }
throw "'$_' is not in the set of the supported values: $($vf -join ', ')"
})]
$Arg
)
"Arg passed: $Arg"
}
To add to the other helpful answers, I use something similiar for a script I made for work:
$vf = #('Veg', 'Fruit','Apple','orange')
$ScriptBlock = {
Foreach($v in $vf){
New-Object -Type System.Management.Automation.CompletionResult -ArgumentList $v,
$v,
"ParameterValue",
"This is the description for $v"
}
}
Register-ArgumentCompleter -CommandName Test-ArgumentCompleter -ParameterName Arg -ScriptBlock $ScriptBlock
function Test-ArgumentCompleter {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[String]$Arg )
}
Documentation for Register-ArgumentCompleter is well explained on Microsoft Docs. I personally don't like to use the enum statement as it didnt allow me to uses spaces in my Intellisense; same for the Validate parameter along with nice features to add a description.
Output:
EDIT:
#Mklement made a good point in validating the argument supplied to the parameter. This alone doesnt allow you to do so without using a little more powershell logic to do the validating for you (unfortunately, it would be done in the body of the function).
function Test-ArgumentCompleter {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
$Arg )
if($PSBoundParameters.ContainsKey('Arg')){
if($VF -contains $PSBoundParameters.Values){ "It work:)" }
else { "It no work:("}
}
}
I think it's worth sharing another alternative that complements the helpful answers from mklement0, Mathias, iRon and Abraham. This answer attempts to show the possibilities that PowerShell can offer when it comes to customization of a Class.
The Class used for this example offers:
Compatibility with Windows PowerShell 5.1 and PowerShell Core.
Validation, Completion and custom Error Message.
For the example below I'll be using completion and validation on values from a current directory, the values are fed dynamically at runtime with Get-ChildItem -Name.
Class
When referring to the custom validation set I've decided to use the variable $this, however that can be easily change for a variable name of one's choice:
[psvariable]::new('this', (& $this.CompletionSet))
The completion set could be also a hardcoded set, i.e.:
[string[]] $CompletionSet = 'foo', 'bar', 'baz'
However that would also require some modifications in the class logic itself.
using namespace System.Management.Automation
using namespace System.Management.Automation.Language
using namespace System.Collections
using namespace System.Collections.Generic
class CustomValidationCompletion : ValidateEnumeratedArgumentsAttribute, IArgumentCompleter {
[scriptblock] $CompletionSet = { Get-ChildItem -Name }
[scriptblock] $Validation
[scriptblock] $ErrorMessage
CustomValidationCompletion() { }
CustomValidationCompletion([scriptblock] $Validation, [scriptblock] $ErrorMessage) {
$this.Validation = $Validation
$this.ErrorMessage = $ErrorMessage
}
[void] ValidateElement([object] $Element) {
$context = #(
[psvariable]::new('_', $Element)
[psvariable]::new('this', (& $this.CompletionSet))
)
if(-not $this.Validation.InvokeWithContext($null, $context)) {
throw [MetadataException]::new(
[string] $this.ErrorMessage.InvokeWithContext($null, $context)
)
}
}
[IEnumerable[CompletionResult]] CompleteArgument(
[string] $CommandName,
[string] $ParameterName,
[string] $WordToComplete,
[CommandAst] $CommandAst,
[IDictionary] $FakeBoundParameters
) {
[List[CompletionResult]] $result = foreach($item in & $this.CompletionSet) {
if(-not $item.StartsWith($wordToComplete)) {
continue
}
[CompletionResult]::new("'$item'", $item, [CompletionResultType]::ParameterValue, $item)
}
return $result
}
}
Implementation
function Test-CompletionValidation {
[alias('tcv')]
[CmdletBinding()]
param(
[CustomValidationCompletion(
Validation = { $_ -in $this },
ErrorMessage = { "Not in set! Must be one of these: $($this -join ', ')" }
)]
[ArgumentCompleter([CustomValidationCompletion])]
[string] $Argument
)
$Argument
}
Demo
Given a function that has validation for a parameter:
function Test-Validation {
[CmdletBinding()]
param (
[Parameter()]
[ValidateScript({
# Add some validation that can throw.
if (-not (Test-Path -Path $_ -PathType Container)) {
throw "OutDir must be a folder path, not a file."
}
return $true
})]
[System.String]
$Folder
)
Process {
$Folder + " is a folder!"
}
}
We should be able to check the error type and set that as the ExpectedType in a Pester Test.
Test-Validation -Folder C:\Temp\file.txt
Test-Validation : Cannot validate argument on parameter 'Folder'. OutDir must be a folder path, not a file.
At line:1 char:17
+ Test-Validation C:\Temp\file.txt
+ ~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidData: (:) [Test-Validation], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Test-Validation
$Error[0].Exception.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
False True ParameterBindingValidationException System.Management.Automation.ParameterBindingException
However, when testing in Pester, the test fails because it cannot find the type.
$ShouldParams = #{
Throw = $true
ExpectedMessage = "Cannot validate argument on parameter 'OutDir'. OutDir must be a folder path, not a file."
ExceptionType = ([System.Management.Automation.ParameterBindingValidationException])
}
{ Test-Validation -Folder C:\Temp\file.txt } | Should #ShouldParams
# Result
RuntimeException: Unable to find type [System.Management.Automation.ParameterBindingValidationException].
How can I fix this test so that I know I am not just catching any exception type?
To generalize your own answer a bit:
Since working with the types of objects starting from a given instance isn't that common in PowerShell, the fact that an instance's type may be non-public isn't usually obvious, as long as the type derives from (is a subclass of) the expected public type.
While you can obtain an object's non-public type via .GetType(), you cannot refer to it via a type literal (e.g. [System.Management.Automation.ParameterBindingException]), such as for use in Pester tests or parameter declarations.
You can call .GetType().IsPublic on any given instance to check whether its type is public, and .GetType().BaseType to get that type's base type - though you may have to call the latter multiple types until you reach a type for which .IsPublic is $true - see the convenience function at the bottom.
In the case at hand .GetType().BaseType.FullName is sufficient to reach the public base type:
# Provoke a non-public [System.Management.Automation.ParameterBindingValidationException]
# exception.
try { & { param([ValidateScript({ $false })] $foo)} bar } catch { $err = $_ }
# Output the full name of the exception type underlying the
# statement-terminating error that the failed validation reported:
$err.Exception.GetType().FullName
# It is only its *base* type that is public and therefore usable as a type
# literal ([...]), such as in a Pester test.
$err.Exception.GetType().BaseType.FullName
The above yields:
System.Management.Automation.ParameterBindingValidationException # non-public
System.Management.Automation.ParameterBindingException # public base type
Below is convenience function Get-PublicType, which, given any instance, reports the most derived type in the inheritance chain of the instance's type that is public (which may be the instance's type itself:
Sample call:
PS> Get-PublicType $err.Exception
PublicType NonPublicDerivedType Instance
---------- -------------------- --------
System.Management.Automation.ParameterBindingException {System.Management.Automation.ParameterBindingValidationException} System.Management.Automation.ParameterBindingValidationException: Cannot validate argument on par…
Get-PublicType source code:
function Get-PublicType {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
$Instance
)
process {
$type = $Instance.GetType()
$nonPublicTypes = #()
while (-not $type.IsPublic) {
$nonPublicTypes += $type
$type = $type.BaseType
}
# $type.FullName
[pscustomobject] #{
PublicType = $type
NonPublicDerivedType = $nonPublicTypes
Instance = $Instance
}
}
}
The reason why you cannot capture this type is because it is not a public class within [System.Management.Automation]. Instead you can set the -ExceptionType to the class it derives from [System.Management.Automation.ParameterBindingException] and your test will now pass with validation for the exception type thrown.
$ShouldParams = #{
Throw = $true
ExpectedMessage = "Cannot validate argument on parameter 'OutDir'. OutDir must be a folder path, not a file."
ExceptionType = ([System.Management.Automation.ParameterBindingException])
}
{ Test-Validation -Folder C:\Temp\file.txt } | Should #ShouldParams
I'd like to check if a variable is null:
function send_null_param ([ref]$mycredentials){
if (! $mycredentials) {
Write-Host 'Got no credentials'
$mycredentials = Get-Credential 'mydomain.com\myuserid'
} else {
Write-Host 'Got credentials'
}
}
$myidentifier = $null
send_null_param ([ref]$myidentifier)
This code is based on:
https://www.thomasmaurer.ch/2010/07/powershell-check-variable-for-null/,
but this does not work.
How can I fix this?
ps. There is something in Stack Overflow for a string being null but not something more generic:
Check if a string is not NULL or EMPTY
Since you're trying to assign $myCredential with Get-Credential in the case of it being absent, then I assume you want your parameter to be a [PSCredential].
In that case, strongly type your parameter, and mark it as mandatory (by the way [ref] is not needed at all :
function Get-MyCredential {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[PSCredential]
$Credential
)
Write-Host "Got credential with username '$($Credential.Username)'"
}
This way, you really don't have to do any checking at all. Making it Mandatory lets PowerShell enforce that for you, and making it a [PSCredential] ensures that the object is a valid [PSCredential] from the start.
The only other case you might want to check for, depending on what you're doing with the credential, is an empty credential.
To do that you can compare it to [PSCredential]::Empty, and you can do it in a validation attribute so it gets done on parameter binding:
function Get-MyCredential {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[PSCredential]
[ValidateScript( {
$_ -ne [PSCredential]::Empty
} )
$Credential
)
Write-Host "Got credential with username '$($Credential.Username)'"
}
You can do other validation in there if you want (checking for a certain username format, like if it needs to be an email address or something). If it's complex it may be better done within the function body, depends on the scenario.
But for the most part you probably don't need additional validation at all.
This works as intended. You are using a [ref] in your parameter. You can think of it as a pointer. And if you pass a variable to a pointer, the pointer will contain the address of the variable. The value doesn't matter.
A [ref] isn't a pointer, but the concept is It is an object of the type 'System.Management.Automation.PSReference'.
An object of the type PSReference saves the actual Value of the object you're referencing under the property 'Value' and when the function is complete it will save the value back to the original variable.
Your code would work if you use the 'Value'-Property of the 'mycredentials' variable in your if-statement:
function send_null_param ([ref]$mycredentials){
if (! $mycredentials.Value) {
Write-host 'Got no credentials'
$mycredentials = Get-Credential 'mydomain.com\myuserid'
}
else {Write-host 'Got credentials'}
}
$myidentifier=$null
send_null_param ([ref]$myidentifier)
I agree with briantist if there is no special reason you shouldn't be using a [ref].
Add the param block to your function and make it mandatory.
Function New-Creds
{
[CmdletBinding()]
[Alias('nc')]
Param
(
[Parameter(Mandatory=$true,
HelpMessage = 'This is a required field. It cannot be blank')]$MyCredentials
)
# Code begins here
$MyCredentials
}
Results
New-Creds -MyCredentials
New-Creds : Missing an argument for parameter 'MyCredentials'. Specify a parameter of type 'System.Object' and try again.
At line:1 char:11
+ New-Creds -MyCredentials
+ ~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [New-Creds], ParameterBindingException
+ FullyQualifiedErrorId : MissingArgument,New-Creds
New-Creds
cmdlet New-Creds at command pipeline position 1
Supply values for the following parameters:
(Type !? for Help.)
MyCredentials: !?
This is a required field. It cannot be blank
MyCredentials: SomeCreds
SomeCreds
New-Creds -MyCredentials AnotherCred
AnotherCred
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;
}