Can't get DynamicParam to work when set values include commas

This topic contains 18 replies, has 5 voices, and was last updated by Profile photo of Rohn Edwards Rohn Edwards 1 year, 10 months ago.

  • Author
    Posts
  • #21897
    Profile photo of Chris Koch
    Chris Koch
    Participant

    Hi all! Having a weird problem and have spent a lot of time troubleshooting with no luck, so I'm hoping someone can assist. Here's the deal:

    Trying to include a dynamic parameter in my function to allow my users to select from a validated list of OU distinguished names given a previously-input $Domain parameter.

    The problem is that distinguished names have commas in them, and that's causing errors after I tab-complete the OU DN and hit Enter. (The actual dynamic variable list works fine... lists the distinguished names correctly etc.)

    If I tab-complete the OU DN, then go back and manually add single or double quotes around that value, it works as intended.

    If I add

     | foreach {"'$_'"} 

    to the end of the line defining the $arrSet variable, the validated list shows up with single quotes around each DN, but when I hit Enter it fails, telling me that the value (with single quotes) does not belong to the set (shows me a set with the exact value with quotes). So that seems weird, because it's saying 'A' isn't part of set "'A','B','C'"

    So I'm out of ideas. Anybody have anything else?

    Code below is a functional snippet of the much larger script, and throws the same errors I'm seeing. To make it work in your domain, you'll need to modify

     [ValidateSet('mydomain.local')] 

    and

     -SearchBase 'DC=mydomain,DC=local' 

    Thanks!

    function Thing {
        [CmdletBinding()]
        Param(
            [Parameter(Mandatory=$false,Position=1,ValuefromPipeline=$true,ValueFromPipelineByPropertyName=$true,HelpMessage='Please enter a valid domain name.')]
            [ValidateSet('mydomain.local')]
            [string]
            $Domain
        )
     
        DynamicParam {
                # Set the dynamic parameters' name
                $ParameterName = 'OrganizationalUnit'
                
                # Create the dictionary 
                $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
    
                # Create the collection of attributes
                $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
                
                # Create and set the parameters' attributes
                $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
                $ParameterAttribute.Mandatory = $true
                $ParameterAttribute.Position = 2
    
                # Add the attributes to the attributes collection
                $AttributeCollection.Add($ParameterAttribute)
    
                # Generate and set the ValidateSet 
                
       
                
                $arrSet = (Get-ADOrganizationalUnit -Filter * -SearchBase 'DC=mydomain,DC=local' -SearchScope OneLevel -Server $Domain).DistinguishedName
                $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet)
    
                # Add the ValidateSet to the attributes collection
                $AttributeCollection.Add($ValidateSetAttribute)
    
                # Create and return the dynamic parameter
                $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
                $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
                return $RuntimeParameterDictionary
        }
    
        begin {
            # Bind the parameter to a friendly variable
            $OrganizationalUnit = $PsBoundParameters[$ParameterName]
        }
    
        process {
            # Your code goes here
            Write-Output "Domain: $Domain"
            Write-Output ""
            Write-Output "Selected OU: $OrganizationalUnit"
            Write-Output ""
            Write-Output "OU List: $arrset"
        }
    
    }
    
  • #21898
    Profile photo of Will Anderson
    Will Anderson
    Keymaster

    Hey there Chris,

    You need to specify with binkies to allow your parameter to accept arrays:

    example [string[]]$ComputerName

    EDIT: Binkies is a technical term. Ask Jason Helmick. 🙂 Essentially, you need to encase square brackets withing the square brackets encasing your parameter.

  • #21899
    Profile photo of Chris Koch
    Chris Koch
    Participant

    Thanks for the reply! Did you try this successfully? I tried this against $arrset and $OrganizationalUnit to no avail.

  • #21900
    Profile photo of Will Anderson
    Will Anderson
    Keymaster

    I'm getting a different error with OrganizationalUnit, most likely because if you don't define what the parameter is supposed to be, it's behaviour is to default to string, and it's not liking it. I'm going to do some research on this.

  • #21902
    Profile photo of Will Anderson
    Will Anderson
    Keymaster

    You want to do the same thing with the string on line 39.

    $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string[]], $AttributeCollection)

    Also, your script doesn't seem to like it when an OU with spaces if the name isn't encapsulated in quotes.

  • #21903
    Profile photo of Chris Koch
    Chris Koch
    Participant

    Even using [string[]] on that variable, I'm still getting errors... Everything works fine if I tab complete any of the OU DNs, then go back and put quotes around it before hitting Enter on the command line. Fundamentally, it seems like the problem is that without quotes it's treating the value as an array because of the commas, right?

    Yeah, hadn't considered spaces, mostly because the SearchBase I'll be using will actually start from an OU under which we don't permit OU names with spaces. But same problem as with the commas... the interpreter sees the space (or a comma) in the larger string as something it should interpret.

    Need to find a functional way to make the list of values itself have quotes around each one... without errors after hitting Enter. Appreciate any help you can be!

  • #21904
    Profile photo of Will Anderson
    Will Anderson
    Keymaster

    Can you copy paste the error? I'd like to see what it is you're getting.

  • #21905
    Profile photo of Matt McNabb
    Matt McNabb
    Participant

    I expect you are probably getting the error:

    Thing : Cannot validate argument on parameter 'OrganizationalUnit'. The argument "OU=Servers" does not belong to the set "OU=Servers,dc=domain,dc=local..."

    The problem is Powershell will interpret the commas on the command line as creating a an array of strings so your OU name will get split up into parts and passed in separately. You can hack the validated set like this:

    $arrSet = (Get-ADOrganizationalUnit -Filter * -SearchBase 'dc=domain,dc=local' -SearchScope OneLevel -Server $Domain).DistinguishedName
    $arrSet = $arrSet | foreach {"'$_'"}
    $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute -ArgumentList $arrSet

    and tab-completion will appear to be correctly adding the quotes for you. However, this still won't match the validateset because you're input will be parsed and the quotes removed. So now you will be comparing OU=Servers,dc=domain,dc=local from the parameter argument to 'OU=Servers,dc=domain,dc=local' in the validated set. I just don't think it will work this way.

  • #21906
    Profile photo of Chris Koch
    Chris Koch
    Participant

    Sure thing!

    Below is if I just tab-complete the OrganizationalUnit parameter and hit Enter:

    PS C:\Windows\system32> Thing -Domain mydomain.local -OrganizationalUnit OU=Accounts,DC=mydomain,DC=local
    Thing : Cannot convert 'System.Object[]' to the type 'System.String' required by parameter 'OrganizationalUnit'. Specified method is not supported.
    At line:1 char:53
    + Thing -Domain mydomain.local -OrganizationalUnit OU=Accounts,DC=mydomain,DC=loc ..
    +                                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : InvalidArgument: (:) [Thing], ParameterBindingException
        + FullyQualifiedErrorId : CannotConvertArgument,Thing

    Below is if I do the same but go back and add quotes before hitting enter (Works!):

    PS C:\Windows\system32> Thing -Domain mydomain.local -OrganizationalUnit 'OU=Accounts,DC=mydomain,DC=local'
    Domain: mydomain.local
    
    Selected OU: OU=Accounts,DC=mydomain,DC=local
    
    OU List: OU=Accounts,DC=mydomain,DC=local OU=Domain Controllers,DC=mydomain,DC=local OU=Groups,DC=mydomain,DC=local OU=Workstations,DC=mydomain,DC=local 

    Below is if I add[b] | foreach {"'$_'"} [/b]to the end of the line defining $arrest, then tab-complete the OrganizationalUnit parameter like normally. ISE shows me a good list of OU DNs with quotes around them, so when I tab-complete the command line now already includes the single quotes around the DN.

    PS C:\Windows\system32> Thing -Domain mydomain.local -OrganizationalUnit 'OU=Accounts,DC=mydomain,DC=local'
    Thing : Cannot validate argument on parameter 'OrganizationalUnit'. The argument "OU=Accounts,DC=mydomain,DC=local" does not belong to the set 
    "'OU=Accounts,DC=mydomain,DC=local','OU=Domain Controllers,DC=mydomain,DC=local','OU=Groups,DC=mydomain,DC=local','OU=Workstations,DC=mydomain,DC=local'" specified by the 
    ValidateSet attribute. Supply an argument that is in the set and then try the command again.
    At line:1 char:53
    + Thing -Domain mydomain.local -OrganizationalUnit 'OU=Accounts,DC=mydomain,DC=loc ..
    +                                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : InvalidData: (:) [Thing], ParameterBindingValidationException
        + FullyQualifiedErrorId : ParameterArgumentValidationError,Thing 

    Similar result if I use [b] | foreach {"""$_"""}[/b] only it puts double quotes around the DN, not single quotes. This one is particularly weird, because it says effectively "xyz" does not belong to the set ""xyz","abc","123"". Which is kind of a lie. 🙂

     PS C:\Windows\system32> Thing -Domain mydomain.local -OrganizationalUnit "OU=Accounts,DC=mydomain,DC=local"
    Thing : Cannot validate argument on parameter "OrganizationalUnit". The argument "OU=Accounts,DC=mydomain,DC=local" does not belong to the set 
    ""OU=Accounts,DC=mydomain,DC=local","OU=Domain Controllers,DC=mydomain,DC=local","OU=Groups,DC=mydomain,DC=local","OU=Workstations,DC=mydomain,DC=local"" specified by the 
    ValidateSet attribute. Supply an argument that is in the set and then try the command again.
    At line:1 char:53
    + Thing -Domain mydomain.local -OrganizationalUnit "OU=Accounts,DC=mydomain,DC=loc ..
    +                                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : InvalidData: (:) [Thing], ParameterBindingValidationException
        + FullyQualifiedErrorId : ParameterArgumentValidationError,Thing

    Note: domain names and OUs changed to protect the innocent. 🙂 All I want to do here is pass the completed $OrganizationalUnit parameter (a full DN string with commas and/or spaces etc.) to a variable that I will be using later in the larger script. You'd think this would be easier lol, but I can't find anywhere where someone else is doing this.

  • #21907
    Profile photo of Chris Koch
    Chris Koch
    Participant

    Hi Matt! Just saw your post, and yes, that's what I was trying in code block #3 of my last post. But check out #4... it seems like that should work, since it is actually part of the set, but it doesn't... so odd.

    If you or anyone else has any other approach to listing domain OU DNs as a DynamicParam set, I'd love to hear alternatives! Thanks!

  • #21908
    Profile photo of Chris Koch
    Chris Koch
    Participant

    Okay, I just figured out a MUCH simpler example of this problem. I feel like, if we can get this to work, I can make it work in the more complicated version.

    All I need to do here is successfully select one of the $OrganizationalUnit values on the command line, then have it display properly with the write-output after hitting Enter.

    function Thing2 {
        [CmdletBinding()]
        Param(
            [Parameter(Mandatory=$true)]
            [ValidateSet('mydomain.local')]
            [string]
            $Domain,
    
            [Parameter(Mandatory=$true)]
            [ValidateSet('OU=Accounts,DC=mydomain,DC=local','OU=Domain Controllers,DC=mydomain,DC=local','OU=Groups,DC=mydomain,DC=local') ]
            [string]
            $OrganizationalUnit
    
        )
     
        Write-Output "Domain: $Domain"
        Write-Output ""
        Write-Output "Selected OU: $OrganizationalUnit"
        Write-Output ""
           
    
        }

    Suggestions on how to change this code to make that work?

  • #21909
    Profile photo of Rohn Edwards
    Rohn Edwards
    Participant

    What about something like this?

    Add-Type @"
        public class QuotedString {
    		
            public QuotedString(string quotedString) {
                _realString = quotedString.ToString();
            }
    
            string _realString;
    
            public override string ToString() {
                return string.Format("'{0}'", _realString);
            }
        }
    "@
    
    function Thing2 {
        [CmdletBinding()]
        Param(
            [Parameter(Mandatory=$true)]
            [ValidateSet("'mydomain.local'")]
            [QuotedString]
            $Domain,
     
            [Parameter(Mandatory=$true)]
            [ValidateSet("'OU=Accounts,DC=mydomain,DC=local'","'OU=Domain Controllers,DC=mydomain,DC=local'","'OU=Groups,DC=mydomain,DC=local'") ]
            [QuotedString]
            $OrganizationalUnit
     
        )
     
        $DomainString = $Domain.ToString().Trim("'")
        $OrganizationalUnitString = $OrganizationalUnit.ToString().Trim("'")
        Write-Output "Domain: $DomainString"
        Write-Output ""
        Write-Output "Selected OU: $OrganizationalUnitString"
        Write-Output ""
    }
    

    This should work if you can ensure that each of your ValidateSet() strings has single quotes around it (which is easier to do with dynamic parameters). What it's doing is leveraging a C# class along with PowerShell's coercion system to convert a regular string into a string with quotes around it. As Matt pointed out, quotes are removed when the parameter is bound, which means the strings don't actually match. Run these commands for an example of what's happening behind the scenes:

    $aString = "'{0}'" -f "A string"
    $aString # Notice the quotes
    
    $aString -eq 'A string'  # This will be false
    $aString -eq [QuotedString] "A string" # This will be true
    

    Once you've got your parameter bound, you need to convert it to a string without the quotes. Trim() will work just fine unless the real string starts or ends with a quote. If that's a possibility, you'll need to use something else, like SubString(), a regular expression, etc.

    There's probably a better way, but maybe this will work for you?

  • #21910
    Profile photo of Rohn Edwards
    Rohn Edwards
    Participant

    Something else that I haven't worked with much, but am planning to start using more, is custom argument completers. Check out this article: http://www.powershellmagazine.com/2012/11/29/using-custom-argument-completers-in-powershell-3-0/

    Then see if this works:

    function Thing2 {
        [CmdletBinding()]
        Param(
            [Parameter(Mandatory=$true)]
            [string]
            $Domain,
     
            [Parameter(Mandatory=$true)]
            [string]
            $OrganizationalUnit
     
        )
     
        Write-Output "Domain: $Domain"
        Write-Output ""
        Write-Output "Selected OU: $OrganizationalUnit"
        Write-Output ""
     
     
    }
    $NewThing2Completer = {
     
        param(
            [string[]] $InputObject
        )
    
        $SbStrings = @()
        $SbStrings += 'param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)'
        $SbStrings += 'if ($commandName -ne "Thing2") { return }'
        $SbStrings += $InputObject | ForEach-Object {
    @"
            New-Object System.Management.Automation.CompletionResult @(
                "'$_'", 
                "$_", 
                'ParameterValue', 
                "$_ (Tooltip text here)"
            )
    "@
        }
    
        [scriptblock]::Create($SbStrings -join "`n")
    }
    
    if (-not $global:options) { $global:options = @{CustomArgumentCompleters = @{};NativeArgumentCompleters = @{}}}
    $global:options['CustomArgumentCompleters']['Domain'] = & $NewThing2Completer @('mydomain.local')
    $global:options['CustomArgumentCompleters']['OrganizationalUnit'] = & $NewThing2Completer @(
        'OU=Accounts,DC=mydomain,DC=local',
        'OU=Domain Controllers,DC=mydomain,DC=local',
        'OU=Groups,DC=mydomain,DC=local'
    )
    $function:tabexpansion2 = $function:tabexpansion2 -replace 'End\r\n{','End { if ($null -ne $options) { $options += $global:options} else {$options = $global:options}'
    

    I'm not sure if I'm doing anything wrong with the check for the $commandName to be 'Thing2', but I added it and it seems to work. Without that check, as I understand it, any function with the -Domain or -OrganizationalUnit parameter would be affected (which might not be a bad thing). One major difference with this is that the input isn't being validated. The user is given suggestions instead of being forced. I don't know if that's acceptable in your scenario.

  • #21914
    Profile photo of Chris Koch
    Chris Koch
    Participant

    @Rohn — Your first post is perfect! That got me working exactly as I need!

    All I had to do was add the [b]Add-Type[/b] code at the beginning of the original script, then change

    $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)

    to

    $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [Quotedstring], $AttributeCollection)

    This was a brilliant approach — thank you so much!!

  • #21915
    Profile photo of Rohn Edwards
    Rohn Edwards
    Participant

    You're welcome!

  • #21924
    Profile photo of Dave Wyatt
    Dave Wyatt
    Moderator

    I'd consider this behavior to be a bug in PowerShell. If you're tab-completing a value that requires quotation marks, PowerShell should add them for you instead of leaving itself in argument parsing mode (which is guaranteed to fail due to the special characters.) It already does this for filenames and such. Love the workaround, though!

  • #21927
    Profile photo of Rohn Edwards
    Rohn Edwards
    Participant

    I agree that it should be considered a bug. There is a connect issue on it here: https://connect.microsoft.com/PowerShell/feedbackdetail/view/812233/auto-completed-parameter-values-with-spaces-do-not-have-quotes-around-them.

    The workaround is pretty "hacky", so hopefully it won't be needed in the future.

  • #21928
    Profile photo of Chris Koch
    Chris Koch
    Participant

    Added to my Connect watchlist! Thanks for the link Rohn!

    Any magic workarounds to the other possible Dynamic Parameter positioning bug I posted earlier? 🙂

  • #21929
    Profile photo of Rohn Edwards
    Rohn Edwards
    Participant

    Unfortunately, no. I'll go post what I know about it in that thread...

You must be logged in to reply to this topic.