Posts Tagged ‘Error message’

postheadericon Better error messages for PowerShell ValidatePattern

If you’ve been writing advanced PowerShell 2.0 functions, you’ve probably used some of the Validate* attributes to enforce valid parameter values, and you may have noticed that their error messages leave a lot to be desired. For example, imagine that you have a parameter which takes a 10-digit phone number:


function Test-PhoneNumber {
param( [ValidatePattern('^\(?\d{3}\)?[-\s.]?\d{3}[-\s.]\d{4}$')]$number )
<# do stuff #>
}
 

The problem

Mark Schill (Meson) brought this up at our virtual PowerShell User Group on IRC tonight: the error messages are confusing and not helpful. For instance, when you try that function and pass an invalid phone number (let’s say you forget to include the area code), you get this error message:

Test-PhoneNumber : Cannot validate argument on parameter 'number'. The argument "555-1212" does not match the "^\(?\d{3}\)?[-\s.]?\d{3}[-\s.]\d{4}$" pattern. Supply an argument that matches "^\(?\d{3}\)?[-\s.]?\d{3}[-\s.]\d{4}$" and try the command again.

There aren’t very many people who can read regular expressions, but only someone who can would really find the entirety of that error message useful. Everyone else is going to get (at most) this out of it: Cannot validate argument on parameter ‘number’. The argument “555-1212” does not match ... yadda, yadda.

That being the case, we’d like to hide the rest of that message … particularly the part that says: “Supply an argument that matches …” which is, frankly, just annoying or insulting depending on who your users are. In fact, as Mark suggested, I’d like to replace it with a custom message … something like: Cannot validate argument on parameter ‘number’. The supplied value is not a valid phone number. Please supply a full 10-digit number like: (123) 555-1212

Custom Validation Properties

So, this evening I decided to do something about it. It’s really pretty simple to write your own Validate*Attribute … you just have to derive from ValidateEnumeratedArgumentsAttribute, and override ValidateElement. Here’s an example ValidatePattern that supports a custom error message. I’ll paste the C# code separately, but basically you can just use Add-Type -TypeDefinition and pass this code as a here-string …


using System;
using System.ComponentModel;
using System.Management.Automation;
using System.Text.RegularExpressions;
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class ValidatePatternExAttribute : ValidateEnumeratedArgumentsAttribute
{
   private RegexOptions _options = RegexOptions.IgnoreCase;
   private string _pattern;
   private string _message;

   protected override void ValidateElement(object element)
   {
      if (element == null)
      {
         throw new ValidationMetadataException(_message + "\nValidatePatternEx Failure: Argument Is Null");
      }
      string input = element.ToString();
      Regex regex = null;
      regex = new Regex(_pattern, _options);
      if (!regex.Match(input).Success)
      {
         throw new ValidationMetadataException(_message + "\nValidatePatternEx failure, the value didn't match the pattern: " + _pattern);
      }
   }

   public RegexOptions Options
   {
      get
      {
         return _options;
      }
      set
      {
         _options = value;
      }
   }

   public string Pattern
   {
      get
      {
         return _pattern;
      }
      set
      {
         if (string.IsNullOrEmpty(value))
         {
            throw new ArgumentException("RegularExpression Pattern is null or empty", "message");
         }
         _pattern = value;
      }
   }

   public string Message
   {
      get
      {
         return _message;
      }
      set
      {
         if (string.IsNullOrEmpty(value))
         {
            throw new ArgumentException("Error Message is null or empty", "message");
         }
         _message = value;
      }
   }
}

Using it in PowerShell

When you use that in PowerShell, assuming that you called Add-Type with that as-is, you only have to change your function slightly, using ValidatePatternEx instead of ValidatePattern, and passing named properties, including the message.


function Test-PhoneNumber {
param(
   [ValidatePatternEx(Pattern='^\(?\d{3}\)?[-\s.]?\d{3}[-\s.]\d{4}$', Message='Please enter a 10-digit phone number like: (123) 555-1212')]
   $number
)
wrote-host $number -fore magenta
}

But once you do, you will get much better error messages:

Test-PhoneNumber : Cannot validate argument on parameter 'number'. Please enter a 10-digit phone number like: (123) 555-1212 ValidatePatternEx failure, the value didn't match the pattern: ^\(?\d{3}\)?[-\s.]?\d{3}[-\s.]\d{4}$

Other Applications

Of course, you may have noticed that this is really a completely custom parameter validator. You aren’t limited to just adding nice error messages to the validation error — you can create whatever validator you can dream up. Let me know what you come up with!

postheadericon Another Module Manifest Gotcha

Shay Levy, ScriptFanatic, wrote about the PowerShellHostVersion on his blog this morning, explaining how it is not a field you should use in your module manifests.

Of course, there’s an exception: if your module is dependent on a specific host. For instance, if you’ve written a module for PoshConsole which exploits the WpfHost display features or the BgHost hotkeys feature … or if you’ve written ISEPack based on the menu and script-editor features of PowerShell ISE), then it makes sense to specify the PowerShellHostName and the PowerShellHostVersion together. However, if you aren’t taking a dependency on a specific host … you should definitely never specify PowerShellHostVersion by itself.

RequiredModules

I’ve got another one that I think you should not use. PowerShell has several ways of specifying prerequisites for your modules, such as assemblies that should be loaded, modules that must be nested, etc. In every case except RequiredModules, PowerShell will actually import those things for you.

That is, if you specify that you RequiredAssemblies = “System.Windows.Forms” it will automatically load it. If you specify NestedModules = “BitsTransfer” it will automatically load that. But if you specify RequiredModules = “BitsTransfer” you’re sore out of luck.

Not only does PowerShell not load them for you, it doesn’t give you all the information you need to load them when it fails. For example, consider that you have a module called “ReallyRequired” and one called “TestModule” which requires ReallyRequired. It’s metadata file is very simple, it looks like this:

@{
    Author="Joel Bennett"
    ModuleToProcess="TestModule.psm1"; ModuleVersion="1.0.0.0"
    RequiredModules=@{ModuleName="ReallyRequired";GUID="84b5ab08-3620-4f72-bffd-44d2a6bb506d"; ModuleVersion="2.5.0.0"
}

Before we try to import it, we might try to check which modules it requires, and since it doesn’t appear to require any, we’ll go ahead and import it:

[1]: get-module -list TestModule | Select RequiredModule

RequiredModules                                                                                          
---------------                                                                                          
{}

[2]: Import-Module TestModule

Import-Module : The required module 'ReallyRequired' is not loaded. Load the module or remove the module from 'RequiredModules' in the file 'C:\Users\Joel\Documents\WindowsPowerShell\Modules\TestModule\TestModule.psd1'.

Apparently, there’s just no way to tell which module(s) you need to preload (or even whether there are any) until you get an error message. You might think you could just write a script to test for that error and then run Import-Module ReallyRequired … but what you don’t know is that after you load your copy of the ReallyRequired module:

[3]: ImportModule TestModule

Import-Module : The required module 'ReallyRequired' with version '2.5.0.0' is not loaded. Load the module or remove the module from 'RequiredModules' in the file 'C:\Users\Joel\Documents\WindowsPowerShell\Modules\TestModule\TestModule.psd1'.

If you’re like me, right now you’re tearing out your hair wondering why the error message didn’t tell you that in the first place. Then you have to go off and try to find a newer version of your RequiredModules. And that’s not even the crazy thing! The crazy thing is, if you find the wrong one, you could still get a message about the GUID not matching next. 8-O

So yeah, RequiredModules is really unlikely to be helpful right now. You’re better off just putting an Import-Module ReallyRequired -version 2.5 -ErrorAction Stop at the top of you script module (although that won’t help you if you’re writing a binary cmdlet module).

Some good news

This is just one of the things that the new PoshCode is being designed to address, so I’ll be blogging more about this shortly, but for now, you can use this function to list RequiredModules:

function Get-RequiredModules {
Param($Path)
$dataSource = Get-Content (Resolve-Path $Path) -delim [char]0

## We are ONLY going to "Tokenize" this to make sure there isn't an extra "}" which could lead to an exploit:
    $null = [Management.Automation.PSParser]::Tokenize("", [ref]$null)  # work around a PowerShell BUG
    $ParseErrors = New-Object "System.Collections.ObjectModel.Collection[System.Management.Automation.PSParseError]"
    $global:tokens = [Management.Automation.PSParser]::Tokenize($dataSource, [ref]$ParseErrors)
    if($ParseErrors.Count -gt 0) {
        $ParseErrors | %{ throw "$($_.Message)`n$File at line:$($_.Token.StartLine) char:$($_.Token.Start)" }
    }
$dataSource = $dataSource -replace "`n# SIG #","`n # SIG #"
return (Invoke-Expression "DATA {`n $dataSource `n}").RequiredModules
}
Reblog this post [with Zemanta]
Archives