Invoke Command to fetch data from remote server using powershell [duplicate] - powershell

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.

Related

How do I change which Powershell version is used in a remote session?

I have a Powershell script which creates a session to a virtual machine. In this remote session I want the machine to read data from a json file and return a custom object from that data, hence I am using the -AsHashtable flag for the ConvertFrom-Json command to do this. However this flag was introduced in Powershell 6 and when I write the powershell version from the remote session to the output the session displays "5.1" as powershell version...
The virtual machine has Powershell 7.2.5 installed and when manually logging onto the vm and launching Powershell 7 and executing the commands it operates as expected. How do I tell the session in my script to use Powershell 7?
This code assumes that the PowerShell ZIP package has been downloaded and unzipped to the remote computer's C:\Users\Public\PSv7\ path. If PowerShell 7 is installed with an installer on the remote computer, then C:\Users\Public\PSv7\pwsh.exe can probably be reduced to pwsh. The -ExecutionPolicy RemoteSigned is probably not needed, but I would recommend keeping -NoProfile for performance.
$ComputerName = 'RemotePCName'
$Temp = Invoke-Command -ComputerName $ComputerName -ScriptBlock {
if($PSVersionTable.PSVersion.Major -lt 7) {
return (C:\Users\Public\PSv7\pwsh.exe -NoProfile -ExecutionPolicy RemoteSigned $MyInvocation.MyCommand.ScriptBlock)
}
else {
return "[$($Env:ComputerName)] {$($PSVersionTable.PSVersion)}"
}
}
Write-Host "$Temp"
The code:
Runs a ScriptBlock on the remote system, saving result to $Temp.
In ScriptBlock: If the current PowerShell version is less than 7, then call pwsh passing self's ScriptBlock.
In ScriptBlock: In Else block, return the current machine's name and PowerShell version as a string.
After exit of ScriptBlock, Write $Temp to Host, showing the machine name and PowerShell version where the else part of the script was ran.
Commands, such as ConvertFrom-Json -AsHashtable, should run fine inside the else statement.
This is the first time I tried passing a ScriptBlock to pwsh.exe from within PowerShell, so, it appears to work, but I'm not sure how pwsh.exe is receiving the ScriptBlock or returning info on exit. I wouldn't be surprised if only [string] values can be returned from pwsh.exe.
Basic Parameter Version:
This version flips the if statement so the else part is where pwsh is called, and accepts any arguments.
$ComputerName = 'RemotePCName'
$MyData1 = 'ArbitraryData1'
$MyData2 = 'ArbitraryData2'
$Temp = Invoke-Command -ComputerName $ComputerName -ArgumentList $MyData1, $MyData2 -ScriptBlock {
if($PSVersionTable.PSVersion.Major -ge 7) {
return "[$($Env:ComputerName)] {$($PSVersionTable.PSVersion)} ($($args[0]), $($args[1]))"
}
else {
return (C:\Users\Public\PSv7\pwsh.exe -NoProfile -ExecutionPolicy RemoteSigned $MyInvocation.MyCommand.ScriptBlock -args $args)
}
}
Write-Host "$Temp"
Defined Parameter Version:
This version is the similar to the above basic parameter version, except the parameters are explicitly defined.
$ComputerName = 'RemotePCName'
$MyData1 = 'ArbitraryData1'
$MyData2 = 'ArbitraryData2'
$Temp = Invoke-Command -ComputerName $ComputerName -ArgumentList $MyData1, $MyData2 -ScriptBlock {
param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$Param1,
[Parameter(Mandatory = $true, Position = 1)]
[string]$Param2
)
if($PSVersionTable.PSVersion.Major -ge 7) {
return "[$($Env:ComputerName)] {$($PSVersionTable.PSVersion)} ($Param1, $Param2)"
}
else {
return (C:\Users\Public\PSv7\pwsh.exe -NoProfile -ExecutionPolicy RemoteSigned $MyInvocation.MyCommand.ScriptBlock -args $Param1, $Param2)
}
}
Write-Host "$Temp"
Update:
Interesting! At the time of creating the above examples I was largely throwing things at the wall and seeing what sticks. But, when looking into -Command, in it I found the following interesting statements:
The Command parameter only accepts a script block for execution when
it can recognize the value passed to Command as a ScriptBlock type.
This is only possible when running pwsh from another PowerShell host.
and:
When called from within an existing PowerShell session, the results
are returned to the parent shell as deserialized XML objects, not live
objects.
Haven't done any experiments yet, but this gives me hope that complex objects can be returned from a call to pwsh.exe.
Updated Alternative Version:
Having given some thought to how the above examples work, I realized that a script block can contain another script block. So, in this example the outer script block is ran on the remote system, and the inner script block, without any concern for what version it is currently running in, is passed to pwsh.exe. A hash table is created, returned, saved to $Temp, and then Format-Table is used to show its content. This means that complex objects can be returned to the calling script.
As with the very first example, this code assumes the PowerShell Zip package is copied to the PSv7 in the Public user profile, but if version 7 is installed, then can probably be reduced to pwsh.
$ComputerName = 'RemotePCName'
$MyData1 = 'ArbitraryData1'
$MyData2 = 'ArbitraryData2'
$Temp = Invoke-Command -ComputerName $ComputerName -ArgumentList $MyData1, $MyData2 -ScriptBlock {
return C:\Users\Public\PSv7\pwsh.exe -NoProfile -args $args {
param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$Param1,
[Parameter(Mandatory = $true, Position = 1)]
[string]$Param2
)
return #{
ComputerName = $Env:ComputerName
PSVersion = $PSVersionTable.PSVersion
Param1 = $Param1
Param2 = $Param2
}
}
}
$Temp | Format-Table

PowerShell 5.1 Invoke-Command executes without errors but not setting environment variable [duplicate]

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.

Calling function from remote script block

My powershell script is as below. I try to zip a folder at remote machine. I don't want to put Zip function inside ScriptBlock because it will be used in other parts of the script.
function Zip{
param([string]$sourceFolder, [string]$targetFile)
#zipping
}
$backupScript = {
param([string]$appPath,[string]$backupFile)
If (Test-Path $backupFile){ Remove-Item $backupFile }
#do other tasks
$function:Zip $appPath $backupFile
}
Invoke-Command -ComputerName $machineName -ScriptBlock $backupScript -Args $appPath,$backupFile
In $backupScript, it is giving error in $function:Zip line:
+ $function:Zip $appPath $backupFile
+ ~~~~~~~~
Unexpected token '$appPath' in expression or statement.
You have to refer to arguments in a scriptblock like:
$backupScript = {
param([string]$appPath,[string]$backupFile)
If (Test-Path $backupFile){ Remove-Item $backupFile }
#do other tasks
$function:Zip $args[0] $args[1]
}
Invoke-Command -ComputerName $machineName -ScriptBlock $backupScript -Args $appPath,$backupFile
Also, the function will not be known by the target machine, you'll have to define it within the script-block or pass it to the machine.
Here is an example:
How do I include a locally defined function when using PowerShell's Invoke-Command for remoting?
This example puts it in your perspective:
PowerShell ScriptBlock and multiple functions
I would find some way of getting your shared functions onto your server. We have a standard share on all our servers where we deploy common code. When we run code remotely, that code can then reference and use the shared code.

Background Job in Powershell

I'm trying to run a job in a background which is a .exe with parameters and the destination has spaces. For example:
$exec = "C:\Program Files\foo.exe"
and I want to run this with parameters:
foo.exe /param1 /param2, etc.
I know that Start-Job does this but I've tried tons of different combinations and it either gives me an error because of the white space or because of the parameters. Can someone help me out with the syntax here? I need to assume that $exec is the path of the executable because it is part of a configuration file and could change later on.
One way to do this is use a script block with a param block.
If there is a single argument with a space in it such as a file/folder path it should be quoted to treat it as a single item. The arguments are an array passed to the script block.
This example uses a script block but you can also use a PowerShell script using the -FilePath parameter of the Start-Job cmdlet instead of the -ScriptBlock parameter.
Here is another example that has arguments with spaces:
$scriptBlock = {
param (
[string] $Source,
[string] $Destination
)
$output = & xcopy $Source $Destination 2>&1
return $output
}
$job = Start-Job -scriptblock $scriptBlock -ArgumentList 'C:\My Folder', 'C:\My Folder 2'
Wait-Job $job
Receive-Job $job
Here is an example using the $args built-in variable instead of the param block.
$scriptBlock = {
$output = & xcopy $args 2>&1
return $output
}
$path1 = "C:\My Folder"
$path2 = "C:\My Folder 2"
"hello world" | Out-File -FilePath "$path1\file.txt"
$job = Start-Job -scriptblock $scriptBlock -ArgumentList $path1, $path2
Wait-Job $job
Receive-Job $job
Andy's trick generally works very well. If you have parameter sets, or otherwise want to move complex information into the job, you can also try this technique:
$jobArgs = #{Source="foo"; Destination="bar"}
$jobArgs |Export-CliXml -Path $env:\Temp\MyArgs.clixml
and in the job...
Start-Job {
.... $jobArgs = Import-CliXml -Path $env:\Temp\MyArgs.clixml
} | Wait-Job | Receive-Job
I use both approaches routinely.
I use -ArgumentList / ScriptBlock parameters when:
I am not dealing with parameter sets
I'm using an In-Memory Job (like the -AsJob capability in ShowUI or WPK) where the arguments are real objects, and they cannot die
I'm running in a user context where I can run a job, but can't store to disk (web servers, ISO compliant labs, etc)
If I need complex arguments, and they don't need to be passed in memory (or its otherwise convenient to have them on disk later), I'll use the hashtable approach.
Hope this Helps

How do I pass named parameters with Invoke-Command?

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.