supporting parameter wildcards

Welcome Forums General PowerShell Q&A supporting parameter wildcards

This topic contains 7 replies, has 3 voices, and was last updated by

js
 
Participant
8 months, 4 weeks ago.

  • Author
    Posts
  • #94278
    js

    Participant
    Points: 224
    Helping Hand
    Rank: Participant

    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 $_ }
        }
    }
    
  • #94288

    Participant
    Points: 229
    Helping Hand
    Rank: Participant

    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.

  • #94291
    js

    Participant
    Points: 224
    Helping Hand
    Rank: Participant

    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.

    • #94314

      Participant
      Points: 229
      Helping Hand
      Rank: Participant

      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
      
  • #94350
    js

    Participant
    Points: 224
    Helping Hand
    Rank: Participant

    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
    
    • #94365

      Participant
      Points: 229
      Helping Hand
      Rank: Participant

      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.

  • #94360

    Participant
    Points: 0
    Rank: Member

    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.

  • #94576
    js

    Participant
    Points: 224
    Helping Hand
    Rank: Participant

    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                         
    

The topic ‘supporting parameter wildcards’ is closed to new replies.