Search code examples
powershellparametersparameter-passing

Proper style for passing complex objects to a powershell script


I am writing a script that operates on multiple pairs of clustered servers and am seeking help on how to author the parameters to the script. Each cluster has 2 nodes, and each node has a name, IP, and role(e.g., FileServer, Webserver, etc.). What's the best way to structure the command line parameters so that someone can pass in this information in a sensible way?

I could do something like this:

Param(
    $PrimaryNodeNames,
    $PrimaryNodeIPs,
    $PrimaryNodeRoles,
    $SecondaryNodeNames,
    $SecondaryNodeIPs
)

And then I could call

Myscript.ps1 -PrimaryNodeNames ("WebserverPrimary", "FileServerPrimary") -PrimaryNodeIPs ("192.168.1.1", "192.168.1.2") -PrimaryNodeRoles("Webserver", "FileServer") -SecondaryNodeNames ("WebserverBackup", "FileServerBackup") -SecondaryNodeIPs ("192.168.1.3", "192.168.1.4")
    

But this seems odd and confusing to split the information about each clustered node across multiple parameters like this. This will be especially hard as the set of clusters gets longer. Is there a better way to group together all information for each clustered node in a way that makes more sense?


Solution

  • If you want to pass objects each representing all relevant properties of each given server to your script, you have three basic options:

    • (a) Pass instances of (possibly [ordered]) hashtables whose entries model the properties of interest, e.g. using a literal:

      @{ PrimaryNodeName = 'Foo'; PrimaryNodeIP = 'Bar'; ... }
      
      • Then define your script's parameters as follows - note that for increased flexibility it is better to define the parameter as type System.Collections.IDictionary:

        param([System.Collections.IDictionary[]] $Server)
        
      • Sample invocation (separate multiple arguments with ,):

        ./Myscript.ps1 -Server @{ PrimaryNodeName = 'Foo'; PrimaryNodeIP = 'Bar'; ... }
        
    • (b) Pass [pscustomobject] instances, which are "property bags"; PowerShell has syntactic sugar that allows you to build on the hashtable-literal syntax to directly construct such objects:

      [pscustomobject] @{ PrimaryNodeName = 'Foo'; PrimaryNodeIP = 'Bar'; ... }
      
      • Then define your script's parameters as follows:[1]

        param([pscustomobject[]] $Server)
        
      • Sample invocation ((...) enclosure needed only for each literal argument; separate multiple arguments with ,):

        ./Myscript.ps1 -Server ([pscustomobject] @{ PrimaryNodeName = 'Foo'; PrimaryNodeIP = 'Bar'; ... })
        
    • (c) Define a custom PowerShell class; e.g.:

      class Server {
        [string] $PrimaryNodeName
        [string] $PrimaryNodeIP
        # ... 
      }
      
      • By default, you can pass a hashtable with the property values to initialize an instance with, and you can even use cast syntax for this, e.g.:

        [Server] @{ PrimaryNodeName = 'Foo'; PrimaryNodeIP = 'Bar' }
        
      • Alternatively, you can define a constructor to make construction more concise:

        class Server {
          [string] $PrimaryNodeName
          [string] $PrimaryNodeIP
          # ... 
          # Constructor
          Server($primaryNodeName, $primaryNodeIP) {
            $this.PrimaryNodeName = $primaryNodeName
            $this.PrimaryNodeIP = $primaryNodeIP
          }
        }
        
        • You must then use the intrinsic new method to construct an instance; e.g.:

           [Server]::new('Foo', 'Bar')        
          
      • Then define your script's parameters as follows - but note the caveat below:

        param([Server[]] $Server)
        
      • Sample invocation ((...) enclosure needed only for each literal argument; separate multiple arguments with ,):

        ./Myscript.ps1 -Server ([Server]::new('Foo', 'Bar'))
        

    Pros and cons, limitations:

    (a) and (b) give you no type safety: You'd have to examine each hashtable / custom object in your script's body to ensure that all required entries / properties are present.

    (c) gives you type safety, but comes with a caveat:

    • If you're calling a script that references your custom class, that class must have been defined before invocation of the script, otherwise its use in a param(...) declaration will fail.

    • You can work around this limitation by exposing your functionality via a function exported from a module instead.


    [1] Unfortunately, typing a parameter [pscustomobject] doesn't actually constrain it to "property bags", because - for historical reasons - [pscustomobject] is actually the same as [psobject], the general-purpose type for wrapping any .NET object. See GitHub issue #4344 for background information.