locked
I can't get ParameterSets to work RRS feed

  • Question

  • I'm new to PowerShell, and I'm having trouble with parameter sets. I've created a function that will retrieve memory info from a computer. I have the function working properly, except for the parameters sets. Here's the syntax I want to support:

    #Run without parameters for memory usage on localhost:
      Get-MemoryUsage
    
    #Get memory usage on remote system:
      Get-MemoryUsage [-ComputerName] <String>
    #Examples:
    #  Get-MemoryUsage -ComputerName mydc01
    #  Get-MemoryUsage mydc01
    #  "mydc01", "mydc02" | Get-MemoryUsage
    
    #Get memory usage on remote system running on Hyper-V host, where "name" is the name of the virtual machine in Hyper-V manager:
      Get-MemoryUsage [-ComputerName] <String> [-HostName] <String> [-Name] <String>
    #Examples:
    #  Get-MemoryUsage -ComputerName mydc01 -HostName hvsvr -Name TheDC
    #  Get-MemoryUsage mydc01 hvsvr TheDC
    
    #Get memory usage on remote system running on Hyper-V host, where "ID" is the GUID of the virtual machine:
      Get-MemoryUsage [-ComputerName] <String> [-HostName] <String> [-ID] <GUID>
    #Examples:
    #  Get-MemoryUsage -ComputerName mydc01 -HostName hvsvr -ID 837b8cbc-b89c-4070-bf65-4b674bb380e4
    #  Get-MemoryUsage mydc01 hvsvr [GUID]837b8cbc-b89c-4070-bf65-4b674bb380e4
    
    #Lastly, I want to be able to pipeline in a [Microsoft.HyperV.PowerShell.VirtualMachine] object like so:
      Get-VM -ComputerName hvsvr -Name TheDC | Get-MemoryUsage -Computername mydc01
      $vm = Get-VM -Name TheDC ; Get-MemoryUsage mydc01 $vm

    To summarize syntax:

    • You must always specify the computer name, unless you want to target the local computer
    • If targeting a Hyper-V virtual machine, you must also specify one of the following:
    • 1. The Hyper-V server and virtual machine name
    • 2. The Hyper-V server and virtual machine GUID
    • 3. A [Microsoft.HyperV.PowerShell.VirtualMachine] object

    Here's the function declaration:

    function Get-MemoryUsage {
        [CmdletBinding()]
        param (
            [Parameter(Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, ParameterSetName="notVM", HelpMessage='Enter the computer name')]
            [Parameter(Position=0, ValueFromPipelineByPropertyName=$true, ParameterSetName="byName", HelpMessage='Enter the computer name of the guest virtual machine')]
            [Parameter(Position=0, ValueFromPipelineByPropertyName=$true, ParameterSetName="byID", HelpMessage='Enter the computer name of the guest virtual machine')]
            [Parameter(Position=0, ValueFromPipelineByPropertyName=$true, ParameterSetName="byObj", HelpMessage='Enter the computer name of the guest virtual machine')]
            [Alias('GuestName','GuestComputerName')] [String[]] $ComputerName=$env:COMPUTERNAME,
    
            [Parameter(Position=1, Mandatory=$true, ValueFromPipelineByPropertyName=$true, ParameterSetName="byName", HelpMessage='Enter the computer name of the Hyper-V host')]
            [Parameter(Position=1, Mandatory=$true, ValueFromPipelineByPropertyName=$true, ParameterSetName="byID", HelpMessage='Enter the computer name of the Hyper-V host')]
            [Alias('host','server')] [String[]] $HostName,
    
            [Parameter(Position=1, Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, ParameterSetName="byObj")]
            [Alias('VirtualMachine')] [Microsoft.HyperV.PowerShell.VirtualMachine[]] $VM,
    
            [Parameter(Position=2, Mandatory=$true, ValueFromPipelineByPropertyName=$true, ParameterSetName="byName", HelpMessage='Enter the name of the virtual machine in Hyper-V')]
            [Alias('VirtualMachineName','Guest')][String[]] $Name,
    
            [Parameter(Position=2, Mandatory=$true, ValueFromPipelineByPropertyName=$true, ParameterSetName="byID", HelpMessage='Enter the GUID of the virtual machine')]
            [Alias('VirtualMachineId','vmid')][GUID[]] $ID
        )
        process {
            Write-Host $PsCmdlet.ParameterSetName
        }
    }

    It seems to work as expected except for this example (sometimes the user will pipe in both Name and Id, and I cannot stop them):

    New-Object –Typename PSObject –Prop @{'HostName'="hvsvr";'GuestComputerName'="mydc01";'VirtualMachineId'=[GUID]"837b8cbc-b89c-4070-bf65-4b674bb380e4";'VirtualMachineName'="TheDC"} | Get-MemoryUsage
    
    #Error:
    Get-MemoryUsage : Parameter set cannot be resolved using the specified named parameters.
    At line:1 char:200
    + ... TheDC} | Get-MemoryUsage
    +                    ~~~~~~~~~~~~~~~
        + CategoryInfo          : InvalidArgument: (@{VirtualMachin...m3 server 2012}:PSObject) [Get-MemoryUsage], ParameterBindingException
        + FullyQualifiedErrorId : AmbiguousParameterSet,Get-MemoryUsage
    
    #If I remove either the 'VirtualMachineName' or 'VirtualMachineId' property, it works.  Or if I remove the [GUID] cast, it works.

    How can I fix this?

    This whole declaration seems sloppy to me.  Is there a better way?  Suggestions are welcome.


    -Tony

    • Edited by Tony MCP Thursday, July 24, 2014 11:56 PM
    Thursday, July 24, 2014 11:49 PM

Answers

  • Looks like you're thinking of it in terms of method overloading in something like C#.  Cmdlet parameter binding is a bit different. It's mostly based on the names of parameters rather than their types (since PowerShell will perform lots of type conversion magic for you anyway), and PowerShell also wants to prompt the user to enter any missing mandatory parameters. So, when you call Get-MemoryUsage -ComputerName <string>, PowerShell is left trying to figure out... should I just use the noVM set and call it a day? Should I prompt the user to enter one of the mandatory parameters in these other three sets which also use -ComputerName?

    That's where the DefaultParameterSetName feature comes into play.  You tell PowerShell which parameter set to assume is active (in this case, noVM. Be careful, because parameter set names are apparently case-sensitive.)

    [CmdletBinding(DefaultParameterSetName = 'noVM')]

    Now, as jrv pointed out, there's still a potentially ambigious situation here, and there's nothing you can do about it while trying to jam all of these particular parameter sets into a single cmdlet.  (You might decide to split the commands or change the parameter sets in some way, but that's a separate discussion.)  If you try to run Get-MemoryUsage -ComputerName <string> -HostName <string> , PowerShell is going to figure out that you're trying to use either the byName or byID parameter set, but it won't know which one.

    In this case, that may not be such a big deal, because you've stated that using -HostName without either a GUID or VM Name parameter is invalid anyway, but PowerShell isn't going to be able to do its usual behavior of prompting the user to enter the missing mandatory parameter.

    • Proposed as answer by jrv Friday, July 25, 2014 12:51 PM
    • Marked as answer by Tony MCP Saturday, July 26, 2014 1:21 AM
    Friday, July 25, 2014 12:42 PM

All replies

  • I can't get this to work either:

    Get-MemoryUsage mydc01 hvsvr [guid]837b8cbc-b89c-4070-bf65-4b674bb380e4
      or
    Get-MemoryUsage mydc01 hvsvr 837b8cbc-b89c-4070-bf65-4b674bb380e4

    • Edited by Tony MCP Thursday, July 24, 2014 11:53 PM
    Thursday, July 24, 2014 11:53 PM
  • Take a real close look at what you have posted:

    function Get-MemoryUsage {
        Param (
            [Parameter(
                Position=0, 
                ValueFromPipeline=$true,
                ValueFromPipelineByPropertyName=$true,
                ParameterSetName="notVM", 
                HelpMessage='Enter the computer name'
            )]
            [Parameter(Position=0, 
                ValueFromPipelineByPropertyName=$true,
                ParameterSetName="byName", 
                HelpMessage='Enter the computer name of the guest virtual machine'
            )]
            [Parameter(Position=0,
                ValueFromPipelineByPropertyName=$true, 
                ParameterSetName="byID", 
                HelpMessage='Enter the computer name of the guest virtual machine'
            )]
            [Parameter(
                Position=0, 
                ValueFromPipelineByPropertyName=$true, 
                ParameterSetName="byObj", 
                HelpMessage='Enter the computer name of the guest virtual machine'
            )]
            [Alias('GuestName','GuestComputerName')][String[]]$ComputerName=$env:COMPUTERNAME,
        
            [Parameter(Position=1, Mandatory=$true,
                ValueFromPipelineByPropertyName=$true, 
                ParameterSetName="byName", 
                HelpMessage='Enter the computer name of the Hyper-V host'
            )]
        
            [Parameter(Position=1, 
                Mandatory=$true, 
                ValueFromPipelineByPropertyName=$true, 
                ParameterSetName="byID", 
                HelpMessage='Enter the computer name of the Hyper-V host'
            )]
            [Alias('host','server')][String[]] $HostName,
            
            [Parameter(
                Position=1, 
                Mandatory=$true, 
                ValueFromPipeline=$true, 
                ValueFromPipelineByPropertyName=$true, 
                ParameterSetName="byObj"
            )]    
            [Alias('VirtualMachine')][Microsoft.HyperV.PowerShell.VirtualMachine[]]$VM,
        
            [Parameter(
                Position=2, 
                Mandatory=$true, 
                ValueFromPipelineByPropertyName=$true, 
                ParameterSetName="byName", 
                HelpMessage='Enter the name of the virtual machine in Hyper-V'
            )]
            [Alias('VirtualMachineName','Guest')][String[]] $Name,
    
            [Parameter(Position=2,
                Mandatory=$true, 
                ValueFromPipelineByPropertyName=$true, 
                ParameterSetName="byID", 
                HelpMessage='Enter the GUID of the virtual machine'
            )]
            [Alias('VirtualMachineId','vmid')][GUID[]] $ID
        )
        
        process {
            Write-Host $PsCmdlet.ParameterSetName
        }
    }

    It makes no sense.  YOu need to figure out why you have so many 'Parameter' statements.

    When laying out parameters use this method as it iseasier to spot errors:

    The order also helps to avoid errors and make incremental design easier:

            [Alias('VirtualMachine')]
            [Alias('VMach')]
            [ .... validation rules ...]
            [Parameter(
                Position=1, 
                Mandatory=$true, 
                ValueFromPipeline=$true, 
                ValueFromPipelineByPropertyName=$true, 
                ParameterSetName="byObj"
            )][Microsoft.HyperV.PowerShell.VirtualMachine[]]$VM,
        
        
    


    ¯\_(ツ)_/¯

    Friday, July 25, 2014 12:29 AM
  • You might also want to consider defining this:

     [CmdletBinding(DefaultParameterSetName='SomeSet')]


    ¯\_(ツ)_/¯


    • Edited by jrv Friday, July 25, 2014 12:37 AM
    Friday, July 25, 2014 12:34 AM
  • I restructured yours and looked at it.  There is a conflict somewhere but I cannot seem to se it.

    function Get-MemoryUsage {
        [CmdLetBinding(DefaultParameterSetName='notVM')]
        Param (
            [Alias('GuestName','GuestComputerName')]
            [Parameter(
                Position=0, 
                ValueFromPipeline=$true,
                ValueFromPipelineByPropertyName=$true,
                ParameterSetName="notVM", 
                HelpMessage='Enter the computer name'
            )]
            [Parameter(Position=0, 
                ValueFromPipelineByPropertyName=$true,
                ParameterSetName="byName", 
                HelpMessage='Enter the computer name of the guest virtual machine'
            )]
            [Parameter(Position=0,
                ValueFromPipelineByPropertyName=$true, 
                ParameterSetName="byID", 
                HelpMessage='Enter the computer name of the guest virtual machine'
            )]
            [Parameter(
                Position=0, 
                ValueFromPipelineByPropertyName=$true, 
                ParameterSetName="byObj", 
                HelpMessage='Enter the computer name of the guest virtual machine'
            )][String[]]$ComputerName=$env:COMPUTERNAME,
        
            [Alias('host','server')]
            [Parameter(
                Position=1, 
                Mandatory=$true,
                ValueFromPipelineByPropertyName=$true, 
                ParameterSetName="byName", 
                HelpMessage='Enter the computer name of the Hyper-V host'
            )]   
            [Parameter(
                Position=1, 
                Mandatory=$true, 
                ValueFromPipelineByPropertyName=$true, 
                ParameterSetName="byID", 
                HelpMessage='Enter the computer name of the Hyper-V host'
            )][String[]] $HostName,
            
            [Alias('VirtualMachine')]
            [Parameter(
                Position=1, 
                Mandatory=$true, 
                ValueFromPipeline=$true, 
                ValueFromPipelineByPropertyName=$true, 
                ParameterSetName="byObj"
            )]$VM,
        
            [Alias('VirtualMachineName','Guest')]
            [Parameter(
                Position=2, 
                Mandatory=$true, 
                ValueFromPipelineByPropertyName=$true, 
                ParameterSetName="byName", 
                HelpMessage='Enter the name of the virtual machine in Hyper-V'
            )][String[]]$Name,
    
            [Alias('VirtualMachineId','vmid')]
            [Parameter(Position=2,
                Mandatory=$true, 
                ValueFromPipelineByPropertyName=$true, 
                ParameterSetName="byID", 
                HelpMessage='Enter the GUID of the virtual machine'
            )][GUID[]] $ID
        )
        process {
            Write-Host $PsCmdlet.ParameterSetName
        }
    }


    ¯\_(ツ)_/¯

    Friday, July 25, 2014 1:20 AM
  • I see it.  Your parameter sets are ambiguous.  A selector for a parameter set must select a unique set and not resolve to two possible sets.

    Your resolve too many ways.


    ¯\_(ツ)_/¯

    Friday, July 25, 2014 1:29 AM
  • Look this over for awhile.  I think you will see wwhat is happening:

    function Get-MemoryUsage {
        [CmdLetBinding(DefaultParameterSetName='notVM')]
        Param (
            [Parameter(
                ParameterSetName="notVM"
            )]
            [Parameter(
                ParameterSetName="byName"
            )]
            [Parameter(
                ParameterSetName="byID"
            )]
            [Parameter(
                ParameterSetName="byObj"
            )][String[]]$ComputerName=$env:COMPUTERNAME,
        
            [Parameter(
                ParameterSetName="byName"
            )]   
            [Parameter(
                ParameterSetName="byID"
            )][String[]] $HostName,
            
            [Parameter(
                ParameterSetName="byObj"
            )]$VM,
        
            [Parameter(
                ParameterSetName="byName"
            )][String[]]$Name,
    
            [Parameter(
                ParameterSetName="byID" 
            )]$ID
        )
        process {
            Write-Host $PsCmdlet.ParameterSetName
        }
    }
    
    Get-MemoryUsage -HostName dd -ID ss
    Get-MemoryUsage -HostName dd -Name ss
    Get-MemoryUsage -ID ss
    Get-MemoryUsage -Name ss
    Get-MemoryUsage -HostName dd   # <<<---- error because it is ambiguous
    
    
    


    ¯\_(ツ)_/¯

    Friday, July 25, 2014 1:33 AM
  • Hi, thank you. I'm still learning all of this. I'll definitely adopt your suggestions on layout.

    I did try setting DefaultParameterSetName='notVM' just as you did, but it broke more things than it fixed.

    In your last post, you displayed an ambiguous example.  But, in my intended definition, you need to specify the id or name when specifying the hostname. So, I would expect some sort of error in that case.

    But, I'm tying to understand how your example works.  I think it's like this:

    Get-MemoryUsage [-ComputerName ] [[-VM] | [[-HostName] [-Name]] | [[-HostName] [-ID]]]


    Basically, it seems that you cannot specify these together: id/name, id/vm, name/vm, hostname/vm

    However, the syntax that I'm trying to permit is:

    Get-MemoryUsage [-computername] [ [-vm] | [-hostname -name] | [-hostname -id] ]

    To summarize syntax:

    • You must always specify the computer name, unless you want to target the local computer
    • If targeting a Hyper-V virtual machine, you must also specify one of the following:
    • 1. The Hyper-V server and virtual machine name
    • 2. The Hyper-V server and virtual machine GUID
    • 3. A [Microsoft.HyperV.PowerShell.VirtualMachine] object

    Do you have any suggestions on how to implement this?  I feel like it's really close.

    • Edited by Tony MCP Friday, July 25, 2014 6:09 AM
    Friday, July 25, 2014 6:05 AM
  • I was thinking about this.  I basically have these four parameter sets:

    • noVM: <string>
    • byName: <string> <string> <string>
    • byID: <string> <string> <GUID>
    • byObj: <string> <Microsoft.HyperV.PowerShell.VirtualMachine>

    Because all parameters are mandatory, each parameter set is unique.  So, I don't understand how PowerShell would detect any ambiguity.

    But, maybe it's not that simple because clearly something is not working the way I expect. I am still learning PowerShell, and it's possible I expect it wrong. :)


    -Tony

    Friday, July 25, 2014 7:12 AM
  • Looks like you're thinking of it in terms of method overloading in something like C#.  Cmdlet parameter binding is a bit different. It's mostly based on the names of parameters rather than their types (since PowerShell will perform lots of type conversion magic for you anyway), and PowerShell also wants to prompt the user to enter any missing mandatory parameters. So, when you call Get-MemoryUsage -ComputerName <string>, PowerShell is left trying to figure out... should I just use the noVM set and call it a day? Should I prompt the user to enter one of the mandatory parameters in these other three sets which also use -ComputerName?

    That's where the DefaultParameterSetName feature comes into play.  You tell PowerShell which parameter set to assume is active (in this case, noVM. Be careful, because parameter set names are apparently case-sensitive.)

    [CmdletBinding(DefaultParameterSetName = 'noVM')]

    Now, as jrv pointed out, there's still a potentially ambigious situation here, and there's nothing you can do about it while trying to jam all of these particular parameter sets into a single cmdlet.  (You might decide to split the commands or change the parameter sets in some way, but that's a separate discussion.)  If you try to run Get-MemoryUsage -ComputerName <string> -HostName <string> , PowerShell is going to figure out that you're trying to use either the byName or byID parameter set, but it won't know which one.

    In this case, that may not be such a big deal, because you've stated that using -HostName without either a GUID or VM Name parameter is invalid anyway, but PowerShell isn't going to be able to do its usual behavior of prompting the user to enter the missing mandatory parameter.

    • Proposed as answer by jrv Friday, July 25, 2014 12:51 PM
    • Marked as answer by Tony MCP Saturday, July 26, 2014 1:21 AM
    Friday, July 25, 2014 12:42 PM
  • That was an excellent and relatively short explanation for a fairly nuanced subject.


    ¯\_(ツ)_/¯

    Friday, July 25, 2014 12:52 PM
  • Thank you for explanation - it helps a lot.

    I think I understand the purpose of the DefaultParameterSetName, but it works funny.

    Without it, these work as expected:

    Get-MemoryUsage localhost
    # uses notVM
    
    New-Object –Typename PSObject –Prop @{'HostName'="localhost";'ComputerName'="testtpvm3";'Name'="vm3 server 2012"} | Get-MemoryUsage
    # uses byName
    

    But, when I set the default parameter set to "notVM", both commands use the "notVM" set.  If I specify the default as "byName", the second command works, but the first prompts for the missing info.

    So, the only way for me to get it to work, is not to specify a default.

    I am able to work around the ambiguous limitation because I can control who uses the function.  I am learning PowerShell, and I want the function to be as rock-solid as possible.

    Thanks for your help.


    -Tony

    Saturday, July 26, 2014 1:21 AM