I have a PowerShell script that accepts 3 named parameters. Please let me know how to pass the same from command line. I tried below code but same is not working. It assigns the entire value to P3 only. My requirement is that P1 should contain 1, P2 should 2 and P3 should be assigned 3.
Invoke-Command -ComputerName server -FilePath "D:\test.ps1" -ArgumentList {-P1 1 -P2 2 -P3 3}
Ihe below is script file code.
Param (
[string]$P3,
[string]$P2,
[string]$P1
)
Write-Output "P1 Value :" $P1
Write-Output "P2 Value:" $P2
Write-Output "P3 Value :" $P3
One option:
$params = #{
P1 = 1
P2 = 2
P3 = 3
}
$ScriptPath = 'D:\Test.ps1'
$sb = [scriptblock]::create(".{$(get-content $ScriptPath -Raw)} $(&{$args} #params)")
Invoke-Command -ComputerName server -ScriptBlock $sb
The code by mjolinor works great, but it took me several minutes to understand it.
The code makes a simple thing - generates a content of script block with built-in parameters:
&{
Param (
[string]$P3,
[string]$P2,
[string]$P1
)
Write-Output "P1 Value:" $P1
Write-Output "P2 Value:" $P2
Write-Output "P3 Value:" $P3
} -P1 1 -P2 2 -P3 3
Then this script block is passed to Invoke-Command.
To simplify the code:
".{$(get-content $ScriptPath -Raw)} $(&{$args} #params)"
$scriptContent = Get-Content $ScriptPath -Raw
$formattedParams = &{ $args } #params
# The `.{}` statement could be replaced with `&{}` here, because we don't need to persist variables after script call.
$scriptBlockContent = ".{ $scriptContent } $formattedParams"
$sb = [scriptblock]::create($scriptBlockContent)
Let's make a basic C# implementation:
void Run()
{
var parameters = new Dictionary<string, string>
{
["P1"] = "1",
["P2"] = "2",
["P3"] = "3"
};
var scriptResult = InvokeScript("Test.ps1", "server", parameters)
Console.WriteLine(scriptResult);
}
string InvokeScript(string filePath, string computerName, Dictionary<string, string> parameters)
{
var innerScriptContent = File.ReadAllText(filePath);
var formattedParams = string.Join(" ", parameters.Select(p => $"-{p.Key} {p.Value}"));
var scriptContent = "$sb = { &{ " + innerScriptContent + " } " + formattedParams + " }\n" +
$"Invoke-Command -ComputerName {computerName} -ScriptBlock $sb";
var tempFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".ps1");
File.WriteAllText(tempFile, scriptContent);
var psi = new ProcessStartInfo
{
FileName = "powershell",
Arguments = $#"-ExecutionPolicy Bypass -File ""{tempFile}""",
RedirectStandardOutput = true,
UseShellExecute = false
};
var process = Process.Start(psi);
var responseText = process.StandardOutput.ReadToEnd();
File.Delete(tempFile);
return responseText;
}
The code generates a temporary script and executes it.
Example script:
$sb = {
&{
Param (
[string]$P3,
[string]$P2,
[string]$P1
)
Write-Output "P1 Value:" $P1
Write-Output "P2 Value:" $P2
Write-Output "P3 Value:" $P3
} -P1 1 -P2 2 -P3 3
}
Invoke-Command -ComputerName server -ScriptBlock $sb
Here's a simple solution:
[PowerShell]::Create().AddCommand('D:\test.ps1').AddParameters(#{ P1 = 1; P2 = 2; P3 = 3 }).Invoke()
Here's output:
PS C:\Windows\system32> [PowerShell]::Create().AddCommand('D:\test.ps1').AddParameters(#{ P1 = 1; P2 = 2; P3 = 3 }).Invoke()
P1 Value :
1
P2 Value:
2
P3 Value :
3
If you are trying to use the -FilePath with named parameters (-P1 1 -P2 2), then I found this will work. Use a script block to run the file, instead of the using -FilePath.
Invoke-Command -ComputerName server -ScriptBlock {& "D:\test.ps1" -P1 1 -P2 2 -P3 3}
Use a hashtable :
icm -ComputerName test -ScriptBlock{$args} -ArgumentList #{"p1"=1;"p2"=2;"p3"=3}
Use a hashtable, indeed!
#TestPs1.ps1
Param (
[string]$P3,
[string]$P2,
[string]$P1
)
Write-Output "P1 Value :" $P1
Write-Output "P2 Value:" $P2
Write-Output "P3 Value :" $P3
$params = #{
P3 = 3
P2 = 2
}
#(just to prove it doesn't matter which order you put them in)
$params["P1"] = 1;
#Trhough the use of the "Splat" operator, we can add parameters directly onto the module
& ".\TestPs1.ps1" #params
outputs:
P1 Value :
1
P2 Value:
2
P3 Value :
3
If you're willing to skip Invoke-Command altogether...
Your script could look like this:
([string]$args).split('-') | %{
if ($_.Split(' ')[0].ToUpper() -eq "P1") { $P1 = $_.Split(' ')[1] }
elseif ($_.Split(' ')[0].ToUpper() -eq "P2") { $P2 = $_.Split(' ')[1] }
elseif ($_.Split(' ')[0].ToUpper() -eq "P3") { $P3 = $_.Split(' ')[1] }
}
Write-Output "P1 Value :" $P1
Write-Output "P2 Value :" $P2
Write-Output "P3 Value :" $P3
And you would call it like this:
D:\test.ps1 -P1 1 -P2 2 -P3 3
Related
I have a script with a foreach cycle. It has about a dozen functions, each collecting information from remote machines' C$ share (cutting text files, checking file version, etc.)
This is however taking some time, since each machine's data collected after one by one. (sometimes it runs with 500+ input)
Wish to put this into runspaces with parallel execution, but so far no examples worked. I am quite new to the concept.
Current script's outline
$inputfile = c:\temp\computerlist.txt
function 1
function 2
function 3, etc
foreach cycle
function 1
function 2
function 3
All results written to screen with write-host for now.
This example pings a number of server in parallel, so you easily can modify it for your demands:
Add-Type -AssemblyName System.Collections
$GH = [hashtable]::Synchronized(#{})
[System.Collections.Generic.List[PSObject]]$GH.results = #()
[System.Collections.Generic.List[string]]$GH.servers = #('server1','server2','server3');
[System.Collections.Generic.List[string]]$GH.functions = #('Check-Server');
[System.Collections.Generic.List[PSObject]]$jobs = #()
#-----------------------------------------------------------------
function Check-Server {
#-----------------------------------------------------------------
# a function which runs parallel
param(
[string]$server
)
$result = Test-Connection $server -Count 1 -Quiet
$GH.results.Add( [PSObject]#{ 'Server' = $server; 'Result' = $result } )
}
#-----------------------------------------------------------------
function Create-InitialSessionState {
#-----------------------------------------------------------------
param(
[System.Collections.Generic.List[string]]$functionNameList
)
# Setting up an initial session state object
$initialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
foreach( $functionName in $functionNameList ) {
# Getting the function definition for the functions to add
$functionDefinition = Get-Content function:\$functionName
$functionEntry = New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList $functionName, $functionDefinition
# And add it to the iss object
[void]$initialSessionState.Commands.Add($functionEntry)
}
return $initialSessionState
}
#-----------------------------------------------------------------
function Create-RunspacePool {
#-----------------------------------------------------------------
param(
[InitialSessionState]$initialSessionState
)
$runspacePool = [RunspaceFactory]::CreateRunspacePool(1, ([int]$env:NUMBER_OF_PROCESSORS + 1), $initialSessionState, $Host)
$runspacePool.ApartmentState = 'MTA'
$runspacePool.ThreadOptions = "ReuseThread"
[void]$runspacePool.Open()
return $runspacePool
}
#-----------------------------------------------------------------
function Release-Runspaces {
#-----------------------------------------------------------------
$runspaces = Get-Runspace | Where { $_.Id -gt 1 }
foreach( $runspace in $runspaces ) {
try{
[void]$runspace.Close()
[void]$runspace.Dispose()
}
catch {
}
}
}
$initialSessionState = Create-InitialSessionState -functionNameList $GH.functions
$runspacePool = Create-RunspacePool -initialSessionState $initialSessionState
foreach ($server in $GH.servers)
{
Write-Host $server
$job = [System.Management.Automation.PowerShell]::Create($initialSessionState)
$job.RunspacePool = $runspacePool
$scriptBlock = { param ( [hashtable]$GH, [string]$server ); Check-Server -server $server }
[void]$job.AddScript( $scriptBlock ).AddArgument( $GH ).AddArgument( $server )
$jobs += New-Object PSObject -Property #{
RunNum = $jobCounter++
JobObj = $job
Result = $job.BeginInvoke() }
do {
Sleep -Seconds 1
} while( $runspacePool.GetAvailableRunspaces() -lt 1 )
}
Do {
Sleep -Seconds 1
} While( $jobs.Result.IsCompleted -contains $false)
$GH.results
Release-Runspaces | Out-Null
[void]$runspacePool.Close()
[void]$runspacePool.Dispose()
This would be concurrent and run in about 10 seconds total. The computer could be localhost three times if you got it working.
invoke-command comp1,comp2,comp3 { sleep 10; 'done' }
Simple attempt at api (threads):
$a = [PowerShell]::Create().AddScript{sleep 5;'a done'}
$b = [PowerShell]::Create().AddScript{sleep 5;'b done'}
$c = [PowerShell]::Create().AddScript{sleep 5;'c done'}
$r1 = $a.BeginInvoke(); $r2 = $b.BeginInvoke() $r3 = $c.BeginInvoke()
$a.EndInvoke($r1); $b.EndInvoke($r2); $c.EndInvoke($r3)
a done
b done
c done
I have Powershell job.
$cmd = {
param($a, $b)
$a++
$b++
}
$a = 1
$b = 2
Start-Job -ScriptBlock $cmd -ArgumentList $a, $b
How to pass $a and $b by a reference so when the job is done they will be updated? Alternatively how to pass variables by reference to runspaces?
Simple sample I just wrote (don't mind the messy code)
# Test scriptblock
$Scriptblock = {
param([ref]$a,[ref]$b)
$a.Value = $a.Value + 1
$b.Value = $b.Value + 1
}
$testValue1 = 20 # set initial value
$testValue2 = 30 # set initial value
# Create the runspace
$Runspace = [runspacefactory]::CreateRunspace()
$Runspace.ApartmentState = [System.Threading.ApartmentState]::STA
$Runspace.Open()
# create the PS session and assign the runspace
$PS = [powershell]::Create()
$PS.Runspace = $Runspace
# add the scriptblock and add the argument as reference variables
$PS.AddScript($Scriptblock)
$PS.AddArgument([ref]$testValue1)
$PS.AddArgument([ref]$testValue2)
# Invoke the scriptblock
$PS.BeginInvoke()
After running this the for the testvalues are updated since they are passed by ref.
Passing parameters by reference is always awkward in PowerShell, and probably won't work for PowerShell jobs anyway, as #bluuf pointed out.
I would probably do something like this instead:
$cmd = {
Param($x, $y)
$x+1
$y+1
}
$a = 1
$b = 2
$a, $b = Start-Job -ScriptBlock $cmd -ArgumentList $a, $b |
Wait-Job |
Receive-Job
The above code passes the variables $a and $b to the scriptblock and assigns the modified values back to the variables after receiving the job output.
a more comprehensive script with example .
it should also include ability to pass $host or something, to make write-host from the passed script, output to the console . but i don't have time to figure out how to do this .
$v = 1
function newThread ([scriptblock]$script, [Parameter(ValueFromPipeline)]$param, [Parameter(ValueFromRemainingArguments)]$args) {
process {
$Powershell = [powershell]::Create()
$Runspace = [runspacefactory]::CreateRunspace()
# allows to limit commands available there
# $InitialSessionState = InitialSessionState::Create()
# $Runspace.InitialSessionState = $InitialSessionState
$Powershell.Runspace = $Runspace
$null = $Powershell.AddScript($script)
$null = $Powershell.AddArgument($param)
foreach ($v_f in $args) {
$null = $Powershell.AddArgument($v_f)
}
$Runspace.Open()
$Job = $Powershell.BeginInvoke()
[PSCustomObject]#{
Job=$Job
Powershell=$Powershell
}
}
}
$script = {
param([ref]$v,$v2)
$v.Value++
$v2
}
$thread = newThread $script ([ref]$v) 3
do {} until ($thread.Job.IsCompleted)
$v1 = $thread.Powershell.EndInvoke($thread.Job)
$thread.Powershell.Dispose()
write-host "end $($v,$v1[0])"
Trying to create a table in word and would like to speed up the process a bit, without jobs this works fine but it's just slow. When I run this script everything works fine except when the script gets to
$Table.Cell($x,<column>).Range.Text = <string>
It gives the error
You cannot call a method on a null-valued expression.
+ CategoryInfo : InvalidOperation: (Cell:String) [],
RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
and when it gets to
$table = $doc.tables.item(1)
It gives the error
You cannot call a method on a null-valued expression.
+ CategoryInfo : InvalidOperation: (item:String) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
I'm guessing it has something to do with
[System.Runtime.InteropServices.Marshal]::GetActiveObject('Word.Application')
not taking all of the document's information...
Here's my code
#More code above
$owners = Import-CSV $ownerspath -header #("Owners")
foreach($name in $owners) {
#Document creation
[ref]$SaveFormat = "microsoft.office.interop.word.WdSaveFormat" -as [type]
$Word = New-Object -comobject word.application
$Word.Visible = $false
$Doc = $Word.Documents.Add()
$Range = $Doc.Range()
$owner = $name.Owners
$g = import-csv $exportpath$owner.csv -header #("Server name", "Description", "OS", "OS EOL", "SQL", "SQL EOL")
$path = "$exportpath$owner.docx"
if(($g.count + 1) -eq 1) {
$rows = 2
}
else {
$rows = $g.count + 1
}
#Table creation
$Selection = $word.selection
$Doc.Tables.Add($Range,$Rows,6) | Out-Null
$Table = $Doc.Tables.item(1)
$Table.Style = "Medium Shading 1 - Accent 1"
$Table.Cell(1,1).Range.Text = "Server name"
$Table.Cell(1,2).Range.Text = "Server description"
$Table.Cell(1,3).Range.Text = "OS Version"
$Table.Cell(1,4).Range.Text = "OS EOL Date"
$Table.Cell(1,5).Range.Text = "SQL Version"
$Table.Cell(1,6).Range.Text = "SQL EOL Date"
$x = 2
foreach($l in $g) {
$sn = $l.'Server name'
$d = $l.Description
$oper = $l.OS
$opereol = $l.'OS EOL'
$sequel = $l.SQL
$sequeleol = $l.'SQL EOL'
$scriptblock = {
param($sn, $d, $oper, $opereol, $sequel, $sequeleol, $x)
$word = [System.Runtime.InteropServices.Marshal]::GetActiveObject('Word.Application')
$doc = $word.documents
$table = $doc.tables.item(1)
#Server names, OS, OS EOL, SQL, SQL EOL, and coloring for the EOL dates if the need them are added in this foreach loop.
$Table.Cell($x,1).Range.Text = $sn
$Table.Cell($x,2).Range.Text = $d
$Table.Cell($x,3).Range.Text = $oper
if($l.'OS EOL' -eq 'Out of date') {
$Table.Cell($x,4).Range.shading.BackgroundPatternColor = 255
$Table.Cell($x,4).Range.Text = $opereol
$OoDTotal += 1
}
elseif($l.'OS EOL' -like "*!*") {
$Table.Cell($x,4).Range.shading.BackgroundPatternColor = 65535
$Table.Cell($x,4).Range.Text = $opereol
$CloseTotal += 1
}
else {
$Table.Cell($x,4).Range.Text = $opereol
$UtDTotal += 1
}
$Table.Cell($x,5).Range.Text = $sequel
if($l.'SQL EOL' -eq 'Out of date') {
$Table.Cell($x,6).Range.shading.BackgroundPatternColor = 255
$Table.Cell($x,6).Range.Text = $sequeleol
$OoDTotal += 1
}
elseif($l.'SQL EOL' -like "*!*") {
$Table.Cell($x,6).Range.shading.BackgroundPatternColor = 65535
$Table.Cell($x,6).Range.Text = $sequeleol
$CloseTotal += 1
}
else {
$Table.Cell($x,6).Range.Text = $sequeleol
$UtDTotal += 1
}
}
Start-Job $scriptblock -ArgumentList $sn, $d, $oper, $opereol, $sequel, $sequeleol, $x
$x++
}
while(get-job -state "Running") {
start-sleep -seconds 1
}
Get-Job | Receive-Job
Get-Job | Remove-Job
#More code below
EDIT: I am running this script on Posh V2 with Office Word 2007
This is a scoping issue. You aren't passing in all of the necessary objects (e.g. at least and most specifically, the Word COM object) into your scriptblock. PowerShell jobs actually create completely separate instances of the powershell process. Go ahead and check:
Start-Job -ScriptBlock { start-sleep -Seconds 20 }
Start-Job -ScriptBlock { start-sleep -Seconds 20 }
PS C:\Users\hiyo!> Get-Process powershell
Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id SI ProcessName
------- ------ ----- ----- ----- ------ -- -- -----------
388 22 40336 48820 ...54 0.45 2720 1 powershell
443 25 54360 66860 ...75 0.58 7240 1 powershell
And if you actually try to job this out the way you want and pass in all the right params, you might run into write-lock issues trying to open multiple instances of the file for R/W...
I've got a build script (PowerShell 4 on Windows 2012 R2) that runs NUnit in a background job and returns NUnit's output. This output is collected in a Collections.Generic.List[string].
$nunitJob = Start-Job -ScriptBlock {
param(
[string]
$BinRoot,
[string]
$NUnitConsolePath,
[string[]]
$NUnitParams,
[string]
$Verbose
)
Set-Location -Path $BinRoot
$VerbosePreference = $Verbose
Write-Verbose -Message ('{0} {1}' -f $NUnitConsolePath,($NUnitParams -join ' '))
& $NUnitConsolePath $NUnitParams 2>&1
$LASTEXITCODE
} -ArgumentList $binRoot,$nunitConsolePath,$nunitParams,$VerbosePreference
$nunitJob | Wait-Job -Timeout ($timeoutMinutes * 60) | Out-Null
$jobKilled = $false
if( $nunitJob.JobStateInfo.State -eq [Management.Automation.JobState]::Running )
{
$jobKilled = $true
$errMsg = 'Killing {0} tests: exceeded {1} minute timeout.' -f $assembly.Name,$timeoutMinutes
Write-Error -Message $errMsg
}
$output = New-Object 'Collections.Generic.List[string]'
$nunitJob |
Stop-Job -PassThru |
Receive-Job |
ForEach-Object {
if( -not $_ )
{
[void]$output.Add( '' )
return
}
switch -Regex ( $_ )
{
'^Tests run: (\d+), Errors: (\d+), Failures: (\d+), Inconclusive: (\d+), Time: ([\d\.]+) seconds$'
{
$testsRun = $Matches[1]
$errors = $Matches[2]
$failures = $Matches[3]
$inconclusive = $Matches[4]
$duration = New-Object 'TimeSpan' 0,0,$Matches[5]
break
}
'^ Not run: (\d+), Invalid: (\d+), Ignored: (\d+), Skipped: (\d+)$'
{
$notRun = $Matches[1]
$invalid = $Matches[2]
$ignored = $Matches[3]
$skipped = $Matches[4]
break
}
}
# Error happens here:
[void] $output.Add( $_ )
}
Intermittently, our build will fail with this error:
Cannot find an overload for "Add" and the argument count: "1".
At line:XXXXX char:XXXXX
+ [void] $output.Add( $_ )
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodException
+ FullyQualifiedErrorId : MethodCountCouldNotFindBest
Any idea why PowerShell would not be able to find List[string]'s Add method?
I've opened a console Window and played around with passing different typed objects to Add without getting an error.
> $o = New-Object 'Collections.Generic.List[string]'
> $o.Add( '' )
> $o.Add( $null )
> $o.Add( 1 )
> $o
1
When you redirect stderr to stdout, you can get System.Management.Automation.ErrorRecords interspersed with strings. Stderr is automatically converted to ErrorRecords while stdout is strings.
So you'll probably want to look to see if the output contains ErrorRecords of interest and if not then filter them out.
I guess you can't just do this:
$servicePath = $args[0]
if(Test-Path -path $servicePath) <-- does not throw in here
$block = {
write-host $servicePath -foreground "magenta"
if((Test-Path -path $servicePath)) { <-- throws here.
dowork
}
}
So how can I pass my variables to the scriptblock $block?
Keith's answer also works for Invoke-Command, with the limit that you can't use named parameters. The arguments should be set using the -ArgumentList parameter and should be comma separated.
$sb = {
param($p1,$p2)
$OFS=','
"p1 is $p1, p2 is $p2, rest of args: $args"
}
Invoke-Command $sb -ArgumentList 1,2,3,4
Also see here and here.
A scriptblock is just an anonymous function. You can use $args inside the
scriptblock as well as declare a param block, for example
$sb = {
param($p1, $p2)
$OFS = ','
"p1 is $p1, p2 is $p2, rest of args: $args"
}
& $sb 1 2 3 4
& $sb -p2 2 -p1 1 3 4
BTW, if using the script block to run in a separate thread (multi threaded):
$ScriptBlock = {
param($AAA,$BBB)
return "AAA is $($AAA) and BBB is $($BBB)"
}
$AAA = "AAA"
$BBB = "BBB1234"
$null = Start-Job $ScriptBlock -ArgumentList $AAA,$BBB
then yields:
$null = Start-Job $ScriptBlock -ArgumentList $AAA,$BBB
Get-Job | Receive-Job
AAA is AAA and BBB is BBB1234
For anyone reading in 2020 who wants to use local variables in a remote session script block, starting in Powershell 3.0 you can use local variables directly in the scriptblock with the "$Using" scope modifier. Example:
$MyLocalVariable = "C:\some_random_path\"
acl = Invoke-Command -ComputerName REMOTEPC -ScriptBlock {Get-Acl $Using:MyLocalVariable}
Found in example 9 of https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/invoke-command?view=powershell-7
By default PowerShell won't capture variables for a ScriptBlock. You can explicitly capture by calling GetNewClosure() on it, however:
$servicePath = $args[0]
if(Test-Path -path $servicePath) <-- does not throw in here
$block = {
write-host $servicePath -foreground "magenta"
if((Test-Path -path $servicePath)) { <-- no longer throws here.
dowork
}
}.GetNewClosure() <-- this makes it work
Three example syntax:
$a ={
param($p1, $p2)
"p1 is $p1"
"p2 is $p2"
"rest of args: $args"
}
//Syntax 1:
Invoke-Command $a -ArgumentList 1,2,3,4 //PS> "p1 is 1, p2 is 2, rest of args: 3 4"
//Syntax 2:
&$a -p2 2 -p1 1 3 //PS> "p1 is 1, p2 is 2, rest of args: 3"
//Syntax 3:
&$a 2 1 3 //PS> "p1 is 2, p2 is 1, rest of args: 3"
I know this article is a bit dated, but I wanted to throw this out as a possible alternative. Just a slight variation of the previous answers.
$foo = {
param($arg)
Write-Host "Hello $arg from Foo ScriptBlock" -ForegroundColor Yellow
}
$foo2 = {
param($arg)
Write-Host "Hello $arg from Foo2 ScriptBlock" -ForegroundColor Red
}
function Run-Foo([ScriptBlock] $cb, $fooArg){
#fake getting the args to pass into callback... or it could be passed in...
if(-not $fooArg) {
$fooArg = "World"
}
#invoke the callback function
$cb.Invoke($fooArg);
#rest of function code....
}
Clear-Host
Run-Foo -cb $foo
Run-Foo -cb $foo
Run-Foo -cb $foo2
Run-Foo -cb $foo2 -fooArg "Tim"
Other possibility:
$a ={
param($p1, $p2)
"p1 is $p1"
"p2 is $p2"
"rest of args: $args"
};
$a.invoke(1,2,3,4,5)