none
How to propagate CmdletBinding parameters through Invoke-Command? RRS feed

  • Question

  • I have a set of scripts with the CmdletBinding attribute applied that run scripts on other servers.

    For example, I have a Stop-MyServices that stops my services on all my servers:

    function Stop-MyServices {
      [CmdletBinding(SupportsShouldProcess = $true)]
      param (
        [switch]$Force = $true
      )
    
      Invoke-Command -ComputerName $MyComputers `
        -ArgumentList $Force `
        -ScriptBlock {
          param ($Force)
    	  
          & C:\Scripts\Stop-MyServices.ps1 -Force:$force
      }
    }

    What I would like to happen is that, when issue this command:

    Stop-MyServices -WhatIf

    the WhatIf switch propagates into the C:\Scripts\Stop-MyServices.ps1 script on each machine.

    How can I do it?


    Paulo Morgado


    Thursday, May 23, 2013 2:36 PM

All replies

  • I did it like this:

    function Stop-MyServices {
      [CmdletBinding(SupportsShouldProcess = $true)]
      param (
        [switch]$Force = $true
      )
    
      Invoke-Command -ComputerName $MyComputers `
        -ArgumentList $Force, $ConfirmPreference, $DebugPreference, $ErrorActionPreference, $ProgressPreference, $VerbosePreference, $WarningPreference, $WhatIfPreference `
        -ScriptBlock {
          param ($Force, $Confirm, $Debug, $ErrorAction, $Progress, $Verbose, $Warning, $WhatIf)
          $ConfirmPreference = $Confirm
          $DebugPreference = $Debug
          $ErrorActionPreference = $ErrorAction
          $ProgressPreference = $Progress
          $VerbosePreference = $Verbose
          $WarningPreference = $Warning
          $WhatIfPreference = $WhatIf
    
          & C:\Scripts\Stop-MyServices.ps1 -Force:$Force
         }
    }


    Is this the only way?


    Paulo Morgado



    Thursday, May 23, 2013 2:38 PM
  • not the only way. You can force a switch to pass whichever boolean value you want like this:

        $whatifvalue = $true
        remove-item ./z.txt -whatif:$whatifvalue

        $whatifvalue = $false
        remove-item ./z.txt -whatif:$whatifvalue


    Al Dunbar -- remember to 'mark or propose as answer' or 'vote as helpful' as appropriate.

    Thursday, May 23, 2013 7:57 PM
  • Hi Al,

    Thanks for pointing out parts missing on the scripts I posted.

    I know I can do that.

    I also know that I don't need a variable/parameter for -WhatIf, -Verbose and others and I don't need to explicit them if I'm running on my machine.

    What I would like to know is if I can make this implicitly flow to the other machines.


    Paulo Morgado

    Thursday, May 23, 2013 9:30 PM
  • a good question. Sorry, but I don't have the answer...

    Al Dunbar -- remember to 'mark or propose as answer' or 'vote as helpful' as appropriate.

    Friday, May 24, 2013 8:45 PM
  • Hopefully, some one will.


    Paulo Morgado

    Friday, May 24, 2013 10:08 PM
  • Hi,

    Wish these posts can help you:

    http://blogs.technet.com/b/heyscriptingguy/archive/2012/07/07/weekend-scripter-cmdletbinding-attribute-simplifies-powershell-functions.aspx

    http://poshcode.org/3390

    http://serverfault.com/questions/406030/how-can-i-run-a-powershell-function-remotely

    Regards,


    Please remember to click “Mark as Answer” on the post that helps you, and to click “Unmark as Answer” if a marked post does not actually answer your question. This can be beneficial to other community members reading the thread.

    Thursday, May 30, 2013 9:22 AM
  • I'm sorry, but I'm failing to see how those post would help me.

    Paulo Morgado

    Thursday, May 30, 2013 11:49 AM
  • Hi,

    Per the current situation, if it's convenient for you, I suggest you submit a case to Microsoft specific team?

    Thank you.

    Regards,


    Please remember to click “Mark as Answer” on the post that helps you, and to click “Unmark as Answer” if a marked post does not actually answer your question. This can be beneficial to other community members reading the thread.

    Wednesday, June 19, 2013 11:48 AM
  • How would I do that?

    Paulo Morgado

    Thursday, June 20, 2013 12:53 AM
  • You might try splatting with PSBoundParameters:

    function Stop-MyServices {
      [CmdletBinding(SupportsShouldProcess = $true)]
      param ()
    
      Invoke-Command -ComputerName $MyComputers `
        -ScriptBlock {  
          & C:\Scripts\Stop-MyServices.ps1 @PSBoundParameters
      }
    }

    As long as the script you are calling has this:

    [CmdletBinding(SupportsShouldProcess = $true)]
    param()


    It will support all of the command parameters.

    Mike

    Edit - just to add, splatting works for all parameters, not just common parameters. So, if you had an optional parameter named $Service in the outer function, and the inner script that is called supports the same parameter, then @PSBoundParameters will pass that as well. You have to be a bit careful - if you support a parameter on the outer function, it must also be supported on the inner script call, or an exception will be thrown (if the parameter is specified on the outer function but is not supported on the called script).



    • Edited by M.Walker Thursday, June 20, 2013 3:23 AM One more fix...
    Thursday, June 20, 2013 3:19 AM
  • FWIW, I forgot to mention that any named parameter can be passed using the ":" syntax, not just switch parameters:

        get-birthday -name:Johnny -age:7


    Al Dunbar -- remember to 'mark or propose as answer' or 'vote as helpful' as appropriate.

    Thursday, June 20, 2013 2:12 PM
  • You might try splatting with PSBoundParameters:

    function Stop-MyServices {
      [CmdletBinding(SupportsShouldProcess = $true)]
      param ()
    
      Invoke-Command -ComputerName $MyComputers `
        -ScriptBlock {  
          & C:\Scripts\Stop-MyServices.ps1 @PSBoundParameters
      }
    }

    As long as the script you are calling has this:

    [CmdletBinding(SupportsShouldProcess = $true)]
    param()


    It will support all of the command parameters.

    Mike

    Edit - just to add, splatting works for all parameters, not just common parameters. So, if you had an optional parameter named $Service in the outer function, and the inner script that is called supports the same parameter, then @PSBoundParameters will pass that as well. You have to be a bit careful - if you support a parameter on the outer function, it must also be supported on the inner script call, or an exception will be thrown (if the parameter is specified on the outer function but is not supported on the called script).



    You can remove parameters from the PSBoundParameters table before passing it on to your inner function, though. That's how most proxy functions are written. Something like this:

    if ($PSBoundParameters['ParamName']) {
        $PSBoundParameters.Remove('ParamName') | Out-Null
    }
    
    # call the next command with @PSBoundParameters , as in your previous example.

    • Proposed as answer by Peterson_Xiao Friday, June 28, 2013 11:17 AM
    Thursday, June 20, 2013 2:18 PM
  • For testing purposes I've created this script on the remote server (Test-Parameters.ps1):

    param (
        [switch]$PassThru = $true, 
        [switch]$Force = $true
    )
    
    $PSBoundParameters
    
    Write-Host "PassThru = $PassThru"
    Write-Host "Force = $Force"
    
    Write-Host "ConfirmPreference = $ConfirmPreference"
    Write-Host "DebugPreference = $DebugPreference"
    Write-Host "ErrorActionPreference = $ErrorActionPreference"
    Write-Host "ProgressPreference = $ProgressPreference"
    Write-Host "VerbosePreference = $VerbosePreference"
    Write-Host "WarningPreference = $WarningPreference"
    Write-Host "WhatIfPreference = $WhatIfPreference"
    

    And this local script:

    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [switch]$PassThru = $true, 
        [switch]$Force = $true
    )
    
    Invoke-Command -ComputerName swapmip02.marte.gbes -Authentication Negotiate -ScriptBlock {
        & C:\Scripts\Test-Parameters.ps1 @PSBoundParameters
    }
    

    When I call the local script like this:

    Test-RemoteParameters.ps1 -Verbose -Force -PassThru -WhatIf -ErrorAction Stop

    I get this:

    PassThru = True

    Force = True

    ConfirmPreference = High

    DebugPreference = SilentlyContinue

    ErrorActionPreference = Continue

    ProgressPreference = Continue

    VerbosePreference = SilentlyContinue

    WarningPreference = Continue

    WhatIfPreference = False

    Preference values are not propagated.


    Paulo Morgado

    • Proposed as answer by MBSJ Tuesday, August 2, 2016 10:22 AM
    • Unproposed as answer by MBSJ Tuesday, August 2, 2016 10:22 AM
    Friday, June 28, 2013 2:26 PM
  • Your Test-Parameters.ps1 script isn't using CmdletBinding, which would be required to get those parameters working from the command line.

    Change the first few lines to this:

    [CmdletBinding(SupportsShouldProcess=$true)]
    param (
        [switch]$PassThru = $true, 
        [switch]$Force = $true
    )


    • Edited by David Wyatt Friday, June 28, 2013 7:37 PM edit
    • Proposed as answer by Peterson_Xiao Tuesday, July 23, 2013 1:26 PM
    Friday, June 28, 2013 3:01 PM
  • Sorry for the late reply.

    I have a script module exporting the Get-ApmService function that goes like this:

    function Get-ApmService {
        [CmdletBinding(SupportsShouldProcess = $true)]
    	param (
    		[Alias('n', 'i', 'in')]
    		[string[]]$IncludeNames,
    		[Alias('e', 'en')]
    		[string[]]$ExcludeNames,
            [Alias('f', 'if')]
    		[string[]]$IncludeFQDNs,
    		[Alias('ef')]
    		[string[]]$ExcludeFQDNs,
            [ValidateSet('All', 'Server', 'Desktop', 'DCRUM', 'BSM', 'Portal', 'Management', 'Database', 'Synthetic', 'AgentManager', 'Agent', 'Security', 'License', IgnoreCase = $true)]
    		[Alias('r', 'cr', 'icr')]
    		[string[]]$IncludeComputerRoles = 'All',
            [ValidateSet('None', 'Server', 'Desktop', 'DCRUM', 'BSM', 'Portal', 'Management', 'Database', 'Synthetic', 'AgentManager', 'Agent', 'Security', 'License', IgnoreCase = $true)]
    		[Alias('ecr')]
    		[string[]]$ExcludeComputerRoles = 'None',
            [ValidateSet('All', 'APM', 'Database', 'License', IgnoreCase = $true)]
    		[Alias('s', 'sr', 'isr')]
    		[string[]]$IncludeServiceRoles = 'All',
            [ValidateSet('None', 'APM', 'Database', 'License', IgnoreCase = $true)]
    		[Alias('esr')]
    		[string[]]$ExcludeServicerRoles = 'None'
    	)
    
        $Computers = (Get-ApmComputer -IncludeNames $IncludeNames -ExcludeNames $ExcludeNames -IncludeFQDNs $IncludeFQDNs -ExcludeFQDNs $ExcludeFQDNs -ExcludeComputerRoles $ExcludeComputerRoles -IncludeComputerRoles $IncludeComputerRoles)
    	
    	Get-Services -Computers $Computers -IncludeServiceRoles $IncludeServiceRoles -ExcludeServicerRoles $ExcludeServicerRoles
    }

    The Get-ApmComputer gets a list of computer definitions where each definition has an FQDN property.

    The Get-Services goes like this:

    function Get-Services {
        [CmdletBinding(SupportsShouldProcess = $true)]
    	param (
    		[PSObject[]]$Computers,
    		[string[]]$IncludeServiceRoles,
    		[string[]]$ExcludeServicerRoles
    	)
    	
    	if ($Computers -eq $null) {
    		throw 'Missing computers.'
    	}
    
        $PSBoundParameters.Remove('Computers') | Out-Null
    $PSBoundParameters | fl *
    	
    	Invoke-Command -HideComputerName:$false -ComputerName $Computers.FQDN -Authentication Negotiate `
                    -ScriptBlock {
    		            $ScriptFile = 'C:\Scripts\ESI\Get-Services.ps1'
    		            if (Test-Path $ScriptFile) { & $ScriptFile @PSBoundParameters }
    	            }
    }

    And the remote script goes like this:

    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [ValidateSet('All', 'APM', 'Database', 'License', IgnoreCase = $true)]
        [Alias('s', 'sr', 'isr')]
        [string[]]$IncludeServiceRoles = 'All',
        [ValidateSet('None', 'APM', 'Database', 'License', IgnoreCase = $true)]
        [Alias('esr')]
        [string[]]$ExcludeServicerRoles = 'None'
    ) 
    
    $IncludeServiceRoles
    $ExcludeServicerRoles
    $VerbosePreference
    
    $ServiceRoles = $IncludeServiceRoles | select -Unique
    
    if ($IncludeServiceRoles -contains 'All') {
        $ServiceRoles = @('APM', 'Database', 'License')
    }
    
    if ($ExcludeServiceRoles -notcontains 'None') {
        $ServiceRoles = ($ServiceRoles | ? { $ExcludeServiceRoles -notcontains $_ })
    }
    
    if ($ServiceRoles -contains 'Database') {
        Get-Service -Name 'MSSQL$SQL01', 'SQLBrowser'
    }
    
    if ($ServiceRoles -contains 'License') {
        Get-Service -Name 'Compuware License Service'
    }
    
    if ($ServiceRoles -contains 'APM') {
        Get-Service -Name 'CompuwareCommonComponents_1'
    }

    When I invoke this:

    Get-ApmService -r License -s License -v

    I get this output:

    Key   : IncludeServiceRoles
    Value : {License}
    
    Key   : ExcludeServicerRoles
    Value : {None}
    
    
    
    All
    None
    
    PSComputerName                                    RunspaceId                                        Value
    --------------                                    ----------                                        -----
    swapmip01.marte.gbes                              e37aa9ab-b335-44a0-9e53-079f6334fa12              SilentlyContinue
    swapmip01.marte.gbes                              e37aa9ab-b335-44a0-9e53-079f6334fa12
    swapmip01.marte.gbes                              e37aa9ab-b335-44a0-9e53-079f6334fa12
    swapmip01.marte.gbes                              e37aa9ab-b335-44a0-9e53-079f6334fa12
    swapmip01.marte.gbes                              e37aa9ab-b335-44a0-9e53-079f6334fa12

    Nothing seems to be following through to the remote script.

    What does the @ in @PSBoundParameters mean?

    @PSBoundParameters

    @PSBoundParameters

    @PSBoundParameters


    Paulo Morgado

    Friday, August 2, 2013 11:18 AM
  • @Array or @Hashtable is the notation for splatting;  see Get-Help about_Splatting for all the details on this.

    Right now, your main problem is that you're not passing the arguments you need through Invoke-Command.  Your ScriptBlock needs to define parameters, and Invoke-Command must pass them with the -ArgumentList parameter.  As far as I know, this technique only supports positional parameters, which makes switch parameters (including the common ones like -Verbose, -WhatIf, etc) kind of a pain, and splatting @PSBoundParameters won't work here (because $PSBoundParameters is a hashtable, and in this case, we need an array).

    Since you specifically mentioned WhatIf in your original post, I'll focus on that one for now.  Here's what the code might look like:

    Invoke-Command -HideComputerName:$false -ComputerName $Computers.FQDN -Authentication Negotiate ` -ScriptBlock { param ( [string[]]$IncludeServiceRoles, [string[]]$ExcludeServicerRoles, [bool] $WhatIf # Switch parameters can't be bound positionally, so we have to fake it with a boolean instead. )

    # Here we build a new hashtable to splat to the script.
    $ht = @{ IncludeServiceRoles=$IncludeServiceRoles; ExcludeServiceRoles=$ExcludeServicerRoles; WhatIf=$WhatIf # You can specify switch parameters as $true or $false when splatting via a hashtable. } $ScriptFile = 'C:\Scripts\ESI\Get-Services.ps1' if (Test-Path $ScriptFile) { & $ScriptFile @ht } } -ArgumentList @( # Arguments passed in an array, in the same order as their position in the param block of the -ScriptBlock $IncludeServiceRoles, $ExcludeServicerRoles, [bool]$WhatIfPreference # When you specify -WhatIf, the local variable name is $WhatIfPreference, of type [switch]. )



    • Edited by David Wyatt Friday, August 2, 2013 12:29 PM edit
    Friday, August 2, 2013 12:27 PM
  • Strangly enough, help splatting gave me nothing.

    So, other than having a generic way to send or receive parameters, regarding the preference switches I'm pretty much where I strated. I need to explicitly script them all in the invocation.

    It's not all lost. I learned about splatting.


    Paulo Morgado

    Friday, August 2, 2013 12:47 PM
  • Yeah, it's kind of annoying that Invoke-Command, Start-Job, etc don't have the option to send in a Hashtable instead of an array (so they can support named arguments and switches).  Not sure why that is.
    Friday, August 2, 2013 1:44 PM