Okay, so I've got a bit of code which calls another PS script from within a PS script, and passes in a couple of parameters: Invoke-Command -ScriptBlock { param($script,$a1,$a2) &$script #($a1,$a2) } -ArgumentList #($scriptToRun,$p1,$p2) -ComputerName localhost -Credential $cred
The problem I'm experiencing however, is the receiving script is getting both $a1 & $a2 combined together in $args[0]. What I can't figure out as yet, is how do I split out the two array elements again?
Alternatively, how can I get them to pass correctly without the #()?
Splatting isn't required here, so simply remove the #():
Invoke-Command -ScriptBlock {
param($script,$a1,$a2)
& $script $a1 $a2
} -ArgumentList $scriptToRun,$p1,$p2 ...
The default behavior is to assign each argument from the argument list to the corresponding positional parameter in the parameter definition of the script block.
Or, if you want to use splatting, you need to do it like this:
Invoke-Command -ScriptBlock {
param($script,$params)
& $script #params
} -ArgumentList $scriptToRun,#($p1,$p2) ...
Related
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.
-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)
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. :) )
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
}
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!
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.
I want to have in a .ps1 file some code that creates a PSSession that can be used in other .ps1 scripts (in order to avoid code duplication).
At first I thought i need a function that creates a PSSession and returns it but I am confused about how they the function outputs work.
Here is my function:
function newRemoteSession
{
param([string]$ipAddress)
$accountName = 'admin'
$accountPassword = 'admin'
$accountPasswordSecure = ConvertTo-SecureString $accountPassword -AsPlainText -Force
$accountCredential = New-Object System.Management.Automation.PSCredential ($accountName, $accountPasswordSecure)
Try
{
$remoteSession = New-PSSession -ComputerName $ipAddress -UseSSL -Credential $accountCredential -SessionOption (New-PSSessionOption -SkipCACheck -SkipCNCheck) -ErrorAction Stop
}
Catch [System.Management.Automation.RuntimeException] #PSRemotingTransportException
{
Write-Host 'Could not connect with default credentials. Please enter credentials...'
$remoteSession = New-PSSession -ComputerName $ipAddress -UseSSL -Credential (Get-Credential) -SessionOption (New-PSSessionOption -SkipCACheck -SkipCNCheck) -ErrorAction Stop
Break
}
return $remoteSession
}
However when I call $s = newRemoteSession(192.168.1.10), $s is empty.
When I run a script with
Write-Host '00'
$s = newRemoteSession('192.168.1.10')
$s
Write-Host '02'
function newRemoteSession
{
........
Write-Host '01'
$remoteSession
}
I get only '00' in the console, but I know the function runs because I get the credential prompt.
EDIT:
Ok, now it works:
The Break in the Catch was stopping everything.
The function call must be without parenthesis.
The second code was wrong because the function must be defined before the call.
You've found the problems yourself, but since they're common pitfalls, let me elaborate a little:
Only ever use break / continue inside a loop (for, foreach, while, do) or in the branch handlers of a switch statement.
Otherwise, PowerShell looks up the call stack for an enclosing loop and, in the absence of one, exits the script.
This is what happened with the break in your catch block.
Do not use (...) to enclose the list of function (command) arguments and do not separate arguments with ,:
In PowerShell, functions - just like cmdlets - are called with shell-like syntax, without parentheses and with whitespace-separated arguments (, is only used to construct an array to be passed as a single argument)[1]
.
# WRONG: method-invocation syntax does NOT apply to *functions*
# (It *happens* to work *in this case*, however, because only a
# a *single* argument is passed and the (...) is a no-op in this case,
# but this syntax should never be used.)
newRemoteSession('192.168.1.10')
# OK: Shell-like syntax, which PowerShell calls *argument mode*:
# Note that the argument needn't be quoted, because it
# contains no shell metacharacters.
# If there were additional arguments, you'd separate them with *whitespace*
newRemoteSession 192.168.1.10
# BETTER: You can even use *parameter names* (again, as with cmdlets),
# which helps readability.
newRemoteSession -ipAddress 192.168.1.10
In PowerShell, functions must be defined before you can call them.
You initially didn't notice, because a previous definition of your newRemoteSession function happened to exist.
[1] For more information about PowerShell's two fundamental parsing modes -
argument mode and expression mode - see this answer of mine.
How is it possible to use the parameters collected in a hash table for use with ArgumentList on Invoke-Command?
$CopyParams = #{
Source = 'E:\DEPARTMENTS\CBR\SHARE\Target'
Destination = 'E:\DEPARTMENTS\CBR\SHARE\Target 2'
Structure = 'yyyy-MM-dd'
}
Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock ${Function:Copy-FilesHC} -ArgumentList #CopyParams
Whatever I try, it's always complaining about the 'Source':
Cannot validate argument on parameter 'Source'. The "Test-Path $_" validation script for the argument with
value "System.Collections.Hashtable" did not return true. Determine why the validation script failed
This blog talks about a similar problem, but I can't get it to work.
The same is true for a simple Copy-Item within Invoke-Command, example:
Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock {Copy-Item} -ArgumentList #CopyParams
Invoke-Command : Missing an argument for parameter 'ArgumentList'. Specify a parameter of type 'System.Obj
ect[]' and try again.
At line:11 char:89
+ ... ck {Copy-Item} -ArgumentList #CopyParams
Thank you for your help.
One-liner, to convert a remote script to accept named parameters from a hash.
Given a scriptblock which you wish to call like this:
$Options = #{
Parameter1 = "foo"
Parameter2 = "bar"
}
Invoke-Command -ComputerName REMOTESERVER -ArgumentList $Options -ScriptBlock {
param(
$Parameter1,
$Parameter2
)
#Script goes here, this is just a sample
"ComputerName: $ENV:COMPUTERNAME"
"Parameter1: $Parameter1"
"Parameter2: $Parameter2"
}
You can convert it like so
Invoke-Command -Computername REMOTESERVER -ArgumentList $Options -ScriptBlock {param($Options)&{
param(
$Parameter1,
$Parameter2
)
#Script goes here, this is just a sample
"ComputerName: $ENV:COMPUTERNAME"
"Parameter1: $Parameter1"
"Parameter2: $Parameter2"
} #Options}
What's going on? Essentially we've wrapped the original script block like so:
{param($Options)& <# Original script block (including {} braces)#> #options }
This makes the original script block an anonymous function, and creates the outer script block which has a parameter $Options, which does nothing but call the inner script block, passing #options to splat the hash.
Here's one way to approach passing named parameters:
function Copy-FilesHC
{
param ($Source,$Destination,$Structure)
"Source is $Source"
"Desintation is $Destination"
"Structure is $Structure"
}
$CopyParams = #{
Source = 'E:\DEPARTMENTS\CBR\SHARE\Target'
Destination = "'E:\DEPARTMENTS\CBR\SHARE\Target 2'" #Nested quotes required due to embedded space in value.
Structure = 'yyyy-MM-dd'
}
$SB = [scriptblock]::Create(".{${Function:Copy-FilesHC}} $(&{$args}#CopyParams)")
Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock $SB
Basically, you create a new script block from your invoked script, with the parameters splatted to that from the hash table. Everything is already in the script block with the values expanded, so there's no argument list to pass.
I found a workaround, but you have to make sure that your Advanced function which is located in your module file is loaded up front in the local session. So it can be used in the remote session. I wrote a small helper function for this.
Function Add-FunctionHC {
[CmdletBinding(SupportsShouldProcess=$True)]
Param(
[String]$Name
)
Process {
Try {
$Module = (Get-Command $Name -EA Stop).ModuleName
}
Catch {
Write-Error "Add-FunctionHC: Function '$Name' doesn't exist in any module"
$Global:Error.RemoveAt('1')
Break
}
if (-not (Get-Module -Name $Module)) {
Import-Module -Name $Module
}
}
}
# Load funtion for remoting
Add-FunctionHC -Name 'Copy-FilesHC'
$CopyParams = #{
Source = 'E:\DEPARTMENTS\CBR\SHARE\Target\De file.txt'
Destination = 'E:\DEPARTMENTS\CBR\SHARE\Target 2'
}
$RemoteFunctions = "function Copy-FilesHC {${function:Copy-FilesHC}}" #';' seperated to add more
Invoke-Command -ArgumentList $RemoteFunctions -ComputerName 'SERVER' -Credential $Cred -ScriptBlock {
Param (
$RemoteFunctions
)
. ([ScriptBlock]::Create($RemoteFunctions))
$CopyParams = $using:CopyParams
Copy-FilesHC #CopyParams
}
The big advantage is that you don't need to copy your complete function in the script and it can stay in the module. So when you change something in the module to the function it will also be available in the remote session, without the need to update your script.
I recently experienced a similar problem and solved it by building the hash (or rebuilding the hash) inside the invoke by leveraging the $using variable scope (more on that here)
it looks something like this:
$Source = 'E:\DEPARTMENTS\CBR\SHARE\Target'
$Destination = 'E:\DEPARTMENTS\CBR\SHARE\Target 2'
$Structure = 'yyyy-MM-dd'
Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock {
$CopyParms= #{
'Source'=$Using:Source
'Destination'=$Using:Destination
'Structure'=$Using:Structure
}
Function:Copy-FilesHC #CopyParms
}
This is what works for me:
$hash = #{
PARAM1="meaning of life"
PARAM2=42
PARAM3=$true
}
$params = foreach($x in $hash.GetEnumerator()) {"$($x.Name)=""$($x.Value)"""}
I know this is late, but I ran into the same problem and found a solution that worked for me. Assigning it to a variable within the scriptblock and then using that variable to splat didn't show any problems.
Here's an example:
$param=#{"parameter","value"}
invoke-command -asjob -session $session -ScriptBlock {$a=$args[0];cmdlet #a } -ArgumentList $param
I'm trying to add multiple csv-imported lists to a remote executed PS script. It works fine when I pass only one list using the following command:
Invoke-Command -filepath $createSAPSPath -ConnectionUri $uri -Credential $credential -ArgumentList (,$accountsList)
What would be the correct syntax for multiple list objects? I've tried:
Invoke-Command -filepath $createSAPSPath -ConnectionUri $uri -Credential $credential -ArgumentList (,$accountsList),(,$groupsList)
But it doesn't seem to work...
Thanks,
Glenn
You have to realize why you need unary comma in the first situation, to understand why its not needed in the second one.
Parameter -ArgumentList takes Object[] argument type. If you are passing single collection, you need to prevent PowerShell from treating single argument (that happens to be a collection) as collection of arguments passed to this parameter.
If you pass something that is collection already (e.g. $AnyObject, $EvenCollection), regardless of the type of individual objects, PowerShell will do what users usually expect: pass first collection to first parameter, second collection to the second parameter.
To sum it up: you should be able to run this like that:
Invoke-Command -filepath $createSAPSPath -ConnectionUri $uri -Credential $credential -ArgumentList $accountsList, $groupList
... and get expected results.
Try it this way:
$AccountList = 'Account1','Account2'
$GroupList = 'Group1','Group2'
invoke-command {$args[0];'*****';$args[1]} -ArgumentList (,$AccountList,$GroupList)
Account1
Account2
*****
Group1
Group2
Another one...
$myArrList = [System.Collections.ArrayList]#()
$myArrList2 = [System.Collections.ArrayList]#()
//add your elements here
$arguments = #()
$arguments += (,$myArrList)
$arguments += (,$myArrList2)
$j = Start-Job -Name $('job-' +$env:COMPUTERNAME) -ScriptBlock $scriptBlockJob -ArgumentList $arguments
I'm tearing my hair out trying to invoke-command but pass the path to the exe as a parameter
eg:
I want to take this command
powershell Invoke-Command -ComputerName localhost -ScriptBlock { param($command ) C:\windows\system32\getmac.exe /$command } -ArgumentList ?
and translate it into a form like this
powershell Invoke-Command -ComputerName localhost -ScriptBlock { param($path, $command ) $path\getmac.exe /$command } -ArgumentList C:\windows\system32,?
I've tried all manner of quoting, ampersands and other contortions but can't get it to work. The above attempt results in
Unexpected token '\getmac.exe' in expression or statement.
At line:1 char:97
(I don't really want to invoke getmac on localhost, this is the runnable, SO distilled version)
Try this option. It shows me help for cscript.exe.
C:\>powershell.exe Invoke-Command -ComputerName localhost -ScriptBlock { param($path, $command ) cmd /c $path $command } -args '"C:\windows\system32\cscript.exe"','"/?"'
I tried other options using & and then path and arguments and it was giving me missing } exception. Then using cmd /c instead of & inside scriptblock fixed the issue.
Powershell won't parse a string as a command that way. For e.g. if you do this:
$path="C:\Windows\System32"
$path\getmac.exe
You would get the same error. The trick to work around this is to use the invoke operator &:
&$path\getmac.exe
or in your example, like this (also note that for a command that you pass to the powershell executable, you must wrap it in scriptblock braces):
powershell -command {Invoke-Command -ComputerName localhost -ScriptBlock { param($path, $command ) &$path\getmac.exe /$command } -ArgumentList C:\windows\system32,?}