Author Posts

February 20, 2018 at 9:31 pm

How do I support wildcards, so I can run myfunc j* or myfunc d*? I'm using validateset to support parameter command line completion.

function myfunc {
    param (
        [ValidateSet('joe','john','dave','dan')]
        [string[]]$name
    )

    process {
        $name | foreach { echo $_ }
    }
}

February 20, 2018 at 10:16 pm

There is the 'Supposed to do' – meaning what the product / feature is designed to do;
then there is the 'should do' – meaning what one feels/ believes a product or feature should do, just because they want it to or some other product / feature allowed such things.

The latter is not something one can expect to work, because it was never a design factor or use case, or the vendor would have documented that way. If the product / feature vendor does to state it specifically in their product / tech guides, then it is not an option.

Now all that being said, we all know that vendors never completely document all that is ever in their product/ features. Hence the discovery, of UDF's (undefined features), workaround and vulnerabilities.

So, no where in the PowerShell or MSDN dev documentation says you can do what you ask they way you are trying to do this. So, back to the 'Supposed to do' part of this.

Validating Parameter Input

Windows PowerShell can validate the arguments passed to cmdlet parameters in several ways. Windows PowerShell can validate the length, the range, and the pattern of the characters of the argument. It can validate the number of arguments available (the count). These validation rules are defined by validation attributes that are declared with the Parameter attribute on public properties of the cmdlet class.

To validate a parameter argument, the Windows PowerShell runtime uses the information provided by the validation attributes to confirm the value of the parameter before the cmdlet is run. If the parameter input is not valid, the user receives an error message. Each validation parameter defines a validation rule that is enforced by Windows PowerShell.

Windows PowerShell enforces the validation rules based on the following attributes.

ValidateSet Attribute Declaration

The ValidateSetAttribute attribute specifies a set of possible values for a cmdlet parameter argument. This attribute can also be used by Windows PowerShell functions.


When this attribute is specified, the Windows PowerShell runtime determines whether the supplied argument for the cmdlet parameter matches an element in the supplied element set.

The cmdlet is run only if the parameter argument matches an element in the set.

If no match is found, an error is thrown by the Windows PowerShell runtime.

Remarks


• This attribute can be used only once per parameter.
• If the parameter value is an array, every element of the array must match an element of the attribute set.

• The ValidateSetAttribute attribute is defined by the ValidateSetAttribute class.

'msdn.microsoft.com/en-us/library/ms714432(v=vs.85).aspx'

So, since you are saying, in your code, that you only want the set listed to be accepted – specifically a 1:1 matching, then a wildcard is not really a thing, since you are not designing for it as a use case.

February 20, 2018 at 10:55 pm

This works perfectly fine with that ValidateSet:

myfunc joe,john

But I am only using it for command line completion. If there's another way to do that, I'd be happy to do so.

February 21, 2018 at 6:11 am

Though that is what you can use this for. However, your end goal (allowing wildcard to select any string match) is not an option since that is not unique element in a set.

If you'd try, it will just error out as expected.

 
myfunc -name 'j*'

myfunc : Cannot validate argument on parameter 'name'. The argument "j*" does not belong to the set "joe,john,dave,dan" specified by the ValidateSet attribute.

Because though you have an [string[]] 'array' defined, you will only get the intelli-sense popup for one element, then you'd have to use the tab key to cycle to get any one of the others.

So, you have to put these names in a collection. Show those names to the user tell then enter a specific name / name set or of part of one, capture that the For Loop to return a result that you can loop to process.

I am going to assume, that list is not a static one, and what you have here is just a sample. So, if you were going to do this using say some sort of list from a file, or a direct call from ADDS, then using a DynamcisParameterSet would be the option.

Dynamic ValidateSet in a Dynamic Parameter
'blogs.technet.microsoft.com/pstips/2014/06/09/dynamic-validateset-in-a-dynamic-parameter'

... though this still is a 1:1 thing.

SO, my suggestion to you is to rethink your use case here. If all you are trying to do is present a list of names for a user to select from. Why do this at the command line at all. Use Out-GridView (which filiterable by the user live), that spits back those selections that your code can act on. The other option is to create your own Datagrid form for the same purpose.

For example:

    function Get-TargetUsers
    {
        [cmdletbinding()]

        [Alias('gtu')]

        param
        (

        )

        [string[]]$Usernames =  ('joe','john','dave','dan' `
        | Out-GridView -OutputMode Multiple -Title 'Select a user. Use CTRL+Click to slect multiple users')

        foreach ($TargetUser in $Usernames)
        {"You selected $TargetUser"}
    }


Results

    gtu
    You selected joe

    gtu
    You selected dan
    You selected joe

    gtu
    You selected john
    You selected dave

February 21, 2018 at 3:45 pm

I was looking for something like the way get-process works. I found this example, but it's in C#: https://msdn.microsoft.com/en-us/library/ff602033(v=vs.85).aspx

This is a start:

function myfunc ($name) {

    $list = 'joe','john','dave','dan'
    $options = [System.Management.Automation.WildcardOptions]::IgnoreCase
    $wildcard = [WildcardPattern]::new($name, $options)
    
    foreach ($i in $list) {
        if ( $wildcard.IsMatch($i) ) {
            $i
        }
    }
}

myfunc j*
joe
john

myfunc d*
dave
dan

Hmmm...

$list -like 'j*'
joe
john

February 21, 2018 at 5:29 pm

The easiest way to handle this is by using argument completers, but those are only available for PowerShell v5+, or when you use the TabExpansionPlusPlus module.

Here's a video covering the basics (the very beginning gets deep in the weeds of dynamic parameters, but after that there's an introduction to argument completers)

Here's where you can get the demo files.

While [ValidateSet()] requires what the user provides to be whitelisted, argument completers are just used to provide suggestions, and they will allow any value to make it through the parameter binding process. That means you have to restructure your command so that it doesn't necessarily trust the user's input, and it takes care of translating wildcards (your last example started doing that). A user would see the same behavior either way, i.e., they type the parameter name and then they can use TAB to cycle through the valid options.

It is possible to do this without argument completers, but other solutions are going to get complicated real fast. I can think of some other options using dynamic parameters (I think the video and demo files above actually have an example with this exact scenario) and something called an ArgumentTransformationAttribute (these are cool, but not well suited for this task).

One more thing about argument completers: I generally make modules that test to see if completers are available, and if they are, I call Register-ArgumentCompleter, and if they're not, I write a warning to the user letting them know that they're not getting the full experience and how they can fix that. That way your commands still work exactly the same (in this case allowing wildcards), but you wouldn't be able to do tab expansion until they opt-in to getting it.

If you know the minimum version of PowerShell you're going to support, we can help you with some concrete examples.

February 21, 2018 at 7:57 pm

Yeppers, I get that, but as you have done, you had to take a different approach than the native ValidateSet.

So, there are always ways, of getting to X or Y, but many times that means completely rethinking how you go about it.

Rohn's point at every valid and the work he did in the video and the samples are really decent (I've used it myself and pointed others to it for the dynamic params stuff specifically), yet, your approach at this sample has merit as well, and worth further investigation.

February 24, 2018 at 4:53 pm

EDIT:

function myfunc {
  param (
      [Parameter(ValueFromPipeline=$True)]
      [string[]]$names = ('joe','john','dave','dan')
  )

  begin {
    $list = 'joe','john','dave','dan'
    $options = [Management.Automation.WildcardOptions]'IgnoreCase,Compiled'
  }

  process {
    foreach ($name in $names) {
      if ([WildcardPattern]::ContainsWildcardCharacters($name)) {
        $wildcard = [WildcardPattern]::new($name, $options)
        foreach($item in $list) {
          if (-not $wildcard.IsMatch($item)) { continue }
          $item
        }
      } else {
        $name
      } 
    } 
  } 
} 

PS /Users/js> myfunc j*,d*                                                                       
joe
john
dave
dan

PS /Users/js> echo j*,d* | myfunc                                                                
joe
john
dave
dan   

PS /Users/js> myfunc fred,barney                                                                 
fred
barney

PS /Users/js> $options                                                                           
Compiled, IgnoreCase