82

I have a script that I can run remotely via Invoke-Command

Invoke-Command -ComputerName (Get-Content C:\Scripts\Servers.txt) `
               -FilePath C:\Scripts\ArchiveEventLogs\ver5\ArchiveEventLogs.ps1

As long as I use default parameters, it works fine. However, the script has 2 named [switch] parameters (-Debug and -Clear)

How can I pass the switched parameters via the Invoke-Command? I've tried the -ArgumentList but I'm getting errors so I must have the syntax wrong or something. Any help is greatly appreciated.

SteveC
  • 15,808
  • 23
  • 102
  • 173
Sean
  • 1,065
  • 2
  • 10
  • 10

5 Answers5

102

-ArgumentList is based on use with scriptblock commands, like:

Invoke-Command -Cn (gc Servers.txt) {param($Debug=$False, $Clear=$False) C:\Scripts\ArchiveEventLogs\ver5\ArchiveEventLogs.ps1 } -ArgumentList $False,$True

When you call it with a -File it still passes the parameters like a dumb splatted array. I've submitted a feature request to have that added to the command (please vote that up).

So, you have two options:

If you have a script that looked like this, in a network location accessible from the remote machine (note that -Debug is implied because when I use the Parameter attribute, the script gets CmdletBinding implicitly, and thus, all of the common parameters):

param(
   [Parameter(Position=0)]
   $one
,
   [Parameter(Position=1)]
   $two
,
   [Parameter()]
   [Switch]$Clear
)

"The test is for '$one' and '$two' ... and we $(if($DebugPreference -ne 'SilentlyContinue'){"will"}else{"won't"}) run in debug mode, and we $(if($Clear){"will"}else{"won't"}) clear the logs after."

Without getting hung up on the meaning of $Clear ... if you wanted to invoke that you could use either of the following Invoke-Command syntaxes:

icm -cn (gc Servers.txt) { 
    param($one,$two,$Debug=$False,$Clear=$False)
    C:\Scripts\ArchiveEventLogs\ver5\ArchiveEventLogs.ps1 @PSBoundParameters
} -ArgumentList "uno", "dos", $false, $true

In that one, I'm duplicating ALL the parameters I care about in the scriptblock so I can pass values. If I can hard-code them (which is what I actually did), there's no need to do that and use PSBoundParameters, I can just pass the ones I need to. In the second example below I'm going to pass the $Clear one, just to demonstrate how to pass switch parameters:

icm -cn $Env:ComputerName { 
    param([bool]$Clear)
    C:\Scripts\ArchiveEventLogs\ver5\ArchiveEventLogs.ps1 "uno" "dos" -Debug -Clear:$Clear
} -ArgumentList $(Test-Path $Profile)

The other option

If the script is on your local machine, and you don't want to change the parameters to be positional, or you want to specify parameters that are common parameters (so you can't control them) you will want to get the content of that script and embed it in your scriptblock:

$script = [scriptblock]::create( @"
param(`$one,`$two,`$Debug=`$False,`$Clear=`$False)
&{ $(Get-Content C:\Scripts\ArchiveEventLogs\ver5\ArchiveEventLogs.ps1 -delimiter ([char]0)) } @PSBoundParameters
"@ )

Invoke-Command -Script $script -Args "uno", "dos", $false, $true

PostScript:

If you really need to pass in a variable for the script name, what you'd do will depend on whether the variable is defined locally or remotely. In general, if you have a variable $Script or an environment variable $Env:Script with the name of a script, you can execute it with the call operator (&): &$Script or &$Env:Script

If it's an environment variable that's already defined on the remote computer, that's all there is to it. If it's a local variable, then you'll have to pass it to the remote script block:

Invoke-Command -cn $Env:ComputerName { 
    param([String]$Script, [bool]$Clear)
    & $ScriptPath "uno" "dos" -Debug -Clear:$Clear
} -ArgumentList $ScriptPath, (Test-Path $Profile)
mklement0
  • 382,024
  • 64
  • 607
  • 775
Jaykul
  • 15,370
  • 8
  • 61
  • 70
  • ArgumentList is available with -FilePath also. Invoke-Command [-FilePath] [[-Session] ] [-AsJob] [-HideComputerName] [-JobName ] [-T hrottleLimit ] [-ArgumentList ] [-InputObject ] [] – ravikanth Nov 19 '10 at 14:40
  • 1
    Yes ravikanth, but it seems the ArgumentList is by splatting to the script, so you can't specify *named* parameters? – Jaykul Nov 19 '10 at 14:58
  • 9
    It's a shame that StackOverflow doesn't understand PowerShell script ... the syntax highlighting of the script names leaves much to be desired. – Jaykul Nov 19 '10 at 14:59
  • OK, that looks good...have to try that out. However, I have a followup question: If I use icm -FilePath it will copy the script to the remote server and then execute. If I use icm -Scriptblock it does not appear to copy the script first -- it seems to assume the script exists already on the remote server in the path specified in the scriptblock. Is this this your experience as well? – Sean Nov 19 '10 at 15:16
  • 2
    Nothing really gets copied to the remote machine. The script you specify gets converted in to ScriptBlock and then ScriptBlock gets passed on to the remote machine and your understanding of -ScriptBlock is correct – ravikanth Nov 19 '10 at 15:30
  • Yeah, that's exactly right. If the script isn't on the remote machine, and that's your primary use case, I guess the best answer would be to change the parameter to be a boolean not a switch, and to specify the position :-( I don't like that ... let me add an option in my answer... – Jaykul Nov 19 '10 at 20:29
  • Before you posted the "other option" I played around with just a simple copy-item archiveeventlogs.ps1 to the remote servers before calling the Invoke-Command. That, of course, required a foreach statement. – Sean Nov 22 '10 at 19:07
  • any sample using variable not literal absolute path C:\Scripts\ArchiveEventLogs\ver5\ArchiveEventLogs.ps1 ??? I would prefer $myScriptPs1 – Kiquenet Jun 05 '12 at 08:15
  • Any example using a variable would get really complicated @Kiquenet, because inside Invoke-Command, variables aren't defined, which means that you have to pass in the variable (or generate a scriptblock from a string). Something like this: icm -cn $Env:ComputerName { param([String]$Script, [bool]$Clear) &$Script "uno" "dos" -Debug -Clear:$Clear } -ArgumentList $ScriptPath $(Test-Path $Profile) – Jaykul Jun 13 '12 at 04:32
  • The feature request link to connect has been retired. It would be a useful feature though. – js2010 Sep 22 '21 at 22:35
7

My solution to this was to write the script block dynamically with [scriptblock]:Create:

# Or build a complex local script with MARKERS here, and do substitutions
# I was sending install scripts to the remote along with MSI packages
# ...for things like Backup and AV protection etc.

$p1 = "good stuff"; $p2 = "better stuff"; $p3 = "best stuff"; $etc = "!"
$script = [scriptblock]::Create("MyScriptOnRemoteServer.ps1 $p1 $p2 $etc")
#strings get interpolated/expanded while a direct scriptblock does not

# the $parms are now expanded in the script block itself
# ...so just call it:
$result = invoke-command $computer -script $script

Passing arguments was very frustrating, trying various methods, e.g.,
-arguments, $using:p1, etc. and this just worked as desired with no problems.

Since I control the contents and variable expansion of the string which creates the [scriptblock] (or script file) this way, there is no real issue with the "invoke-command" incantation.

(It shouldn't be that hard. :) )

tambre
  • 4,625
  • 4
  • 42
  • 55
HerbM
  • 521
  • 6
  • 14
  • I agree. This is the only solution I could get working with parameters simply because there is no need to pass parameters. And not able to have the $error directly propagated. Other example: using [scriptblock]::Create("New-Item -Path '$BackupPath' -ItemType Directory -Force"), I have to force If ($result.Exists) in the caller to check if something went wrong. PSVersion=5.1 – reverpie May 14 '20 at 12:47
7

I suspect its a new feature since this post was created - pass parameters to the script block using $Using:var. Then its a simple mater to pass parameters provided the script is already on the machine or in a known network location relative to the machine

Taking the main example it would be:

icm -cn $Env:ComputerName { 
    C:\Scripts\ArchiveEventLogs\ver5\ArchiveEventLogs.ps1 -one "uno" -two "dos" -Debug -Clear $Using:Clear
}
RobG
  • 744
  • 6
  • 16
  • Don't know if it is new or not, but $Using was exactly what I was looking for. Thanks for that! – JesusIsMyDriver.dll Dec 12 '18 at 21:52
  • The `$using:` scope was introduced in v3 of PowerShell - see [the docs](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Remote_Variables?view=powershell-7.3#using-local-variables) – mklement0 Feb 11 '22 at 02:37
3

I needed something to call scripts with named parameters. We have a policy of not using ordinal positioning of parameters and requiring the parameter name.

My approach is similar to the ones above but gets the content of the script file that you want to call and sends a parameter block containing the parameters and values.

One of the advantages of this is that you can optionally choose which parameters to send to the script file allowing for non-mandatory parameters with defaults.

Assuming there is a script called "MyScript.ps1" in the temporary path that has the following parameter block:

[CmdletBinding(PositionalBinding = $False)]
param
(
    [Parameter(Mandatory = $True)] [String] $MyNamedParameter1,
    [Parameter(Mandatory = $True)] [String] $MyNamedParameter2,
    [Parameter(Mandatory = $False)] [String] $MyNamedParameter3 = "some default value"
)

This is how I would call this script from another script:

$params = @{
    MyNamedParameter1 = $SomeValue
    MyNamedParameter2 = $SomeOtherValue
}

If ($SomeCondition)
{
    $params['MyNamedParameter3'] = $YetAnotherValue
}

$pathToScript = Join-Path -Path $env:Temp -ChildPath MyScript.ps1

$sb = [scriptblock]::create(".{$(Get-Content -Path $pathToScript -Raw)} $(&{
        $args
} @params)")
Invoke-Command -ScriptBlock $sb

I have used this in lots of scenarios and it works really well. One thing that you occasionally need to do is put quotes around the parameter value assignment block. This is always the case when there are spaces in the value.

e.g. This param block is used to call a script that copies various modules into the standard location used by PowerShell C:\Program Files\WindowsPowerShell\Modules which contains a space character.

$params = @{
        SourcePath      = "$WorkingDirectory\Modules"
        DestinationPath = "'$(Join-Path -Path $([System.Environment]::GetFolderPath('ProgramFiles')) -ChildPath 'WindowsPowershell\Modules')'"
    }

Hope this helps!

CarlR
  • 1,718
  • 1
  • 17
  • 21
0

This is an unfortunate situation. Positional parameters work.

# test.ps1
param($myarg1, $myarg2, $myarg3)

"myarg1 $myarg1"
"myarg2 $myarg2"
"myarg3 $myarg3"
# elevated prompt
invoke-command localhost test.ps1 -args 1,$null,3

myarg1 1
myarg2
myarg3 3

Or you can hardcode a default.

# test2.ps1
param($myarg='foo2')

dir $myarg
invoke-command localhost test2.ps1

Cannot find path 'C:\Users\js\Documents\foo2' because it does not exist.

Or copy the script there:

$s = New-PSSession localhost
copy-item test2.ps1 $home\documents -ToSession $s
icm $s { .\test2.ps1 -myarg foo3 }

Cannot find path 'C:\Users\js\Documents\foo3' because it does not exist.
js2010
  • 23,033
  • 6
  • 64
  • 66