Can someone tell me why this invoke-command is not exporting all the variables?
icm -computername Server1 -scriptblock {
$A=$env:username;$B=hostname;$C=$env:userdomain;echo $A,$B,$C
} |
% {$_ | out-file "c:\temp\$($_.pscomputername).txt"}
The text file only contains "$C=$env:userdomain" variable. How can i export $A,$B,$C variables to the text file while keeping the $_.pscomputername as the name of the exported file?
thank you
The problem with your approach is that each individual object output by icm (Invoke-Command) causes the output file passed to Out-File in the % (ForEach-Object) script block to be rewritten, so that you end up with only the last one in the file.
Toni makes good points about the values you're returning from your remote script block, and about collecting the information in a single object, which solves your problem.
Applying the same idea to the multiple values you're trying to return, you can return them as a single array, as a whole, so that you only have one object to write, which contains all the information.
To that end, you not only must construct an array, but also prevent its enumeration on output, which is most easily done by using the unary form of , to create an aux. wrapper array (alternatively, use Write-Output -NoEnumerate; see this answer for more information).
icm -computername Server1 -scriptblock {
# Return the values as a single array, wrapped in
# in a aux. array to prevent enumeration.
, ($env:username, (hostname), $env:userdomain)
} |
% {
# Now each targeted computer returns only *one* object.
$_ | out-file "c:\temp\$($_.pscomputername).txt"
}
This is more typical usage. Instead of format-table, you can pipe to export-csv. It runs in parallel on both computers.
icm -computername com001,com002 -scriptblock {
[pscustomobject]#{Username=$env:username;
Hostname=hostname;
Userdomain=$env:userdomain}
} | format-table
Username Hostname Userdomain PSComputerName RunspaceId
-------- -------- ---------- -------------- ----------
js-adm COM001 AD com001 2d660bb5-761c-4e79-9e4f-c3b98f5f8c61
js-adm COM002 AD com002 7544a8ff-e419-49c2-9fc3-2a92f50c1424
Related
within powershell I'd like to learn the best way to call a variable to a start job so I don't have to edit the script for each server as it will be specific based on the client I've placed my script on.
$Servername = 'Server1'
$pingblock = {
pathping $servername | Out-File C:\client\PS\ServerPing.TXT
}
start-job $pingblock
when I run my code above I just get a file with the help as if I forgot the specify the $servername.
Use the -ArgumentList parameter on Start-Job e.g.:
Start-Job -Scriptblock {param($p) "`$p is $p"} -Arg 'Server1'
In your case:
$pingblock = {param($servername) pathping $servername | Out-File C:\...\ServerPing.txt}
Start-Job $pingblock -Arg Server1
To complement Keith Hill's helpful answer with a PSv3+ alternative:
The $using: scope modifier can be used to reference the values of variables in the caller's scope inside the script block passed to Start-Job, as an alternative to passing arguments (by default, a script block executed as a background job does not see any of the caller's variables or other definitions):
$Servername = 'Server1'
Start-Job { "Target: " + $using:ServerName } | Receive-Job -Wait -AutoRemoveJob
The above yields:
Target: Server1
Note:
The same technique can be used with:
Invoke-Command for remote execution - see this question.
Start-ThreadJob, available by default in PowerShell (Core) v6+, installable on demand in Windows PowerShell.
ForEach-Object -Parallel, available in PowerShell (Core) v7+ only.
Note that, as with -ArgumentList (-Args), it is only variable values that are being passed, not the variables themselves; that is, you cannot modify the caller's variables.[1]
[1] However, the thread-based concurrency features - Start-ThreadJob and ForEach-Object Parallel - permit indirect modification, namely if the variable value at hand happens to be an instance of a (mutable) .NET reference type, such as a hashtable, in which case the object that that the variable "points to" can be modified (if it is mutable). Note that taking advantage of that requires additional, nontrivial effort to make the concurrent modifications thread-safe, such as by use of concurrent (synchronized) collections - see this answer - and/or explicit locking of individual objects - see this answer.
Some other ways, $args and $input. This goes for invoke-command too, which I think uses the same mechanism. The $input method works in an unexpected way with arrays.
start-job { $args[0] } -args hi | receive-job -wait -auto
hi
echo hi | start-job { $input } | receive-job -wait -auto
hi
echo hi there | start-job { $input.gettype() } | receive-job -wait -auto
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
False False <GetReadEnumerator>d__20 System.Object
For arrays, it's probably better to use a foreach-object (%) loop instead, so it runs on each array item in parallel. See also start-threadjob or foreach-object -parallel in powershell 7. There's actually no -throttlelimit option to start-job, so use with care.
echo yahoo.com facebook.com |
% { $_ | start-job { test-netconnection $input } } |
receive-job -wait -auto | select * -exclude runspaceid,pssourcejob* | ft
ComputerName RemoteAddress ResolvedAddresses PingSucce
eded
------------ ------------- ----------------- ---------
yahoo.com 74.6.143.25 {74.6.143.25,...} True
facebook.com 31.13.71.36 {31.13.71.36} True
Command that output the result in string instead of objects:
ls | Out-String -Stream
Output:
Directory: C:\MyPath\dir1
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2022-01-22 5:34 PM 0 1.txt
-a--- 2022-01-22 5:34 PM 0 2.txt
-a--- 2022-01-22 5:34 PM 0 3.txt
I tried to get the same result using a function:
function f {
[CmdletBinding()]
param (
[Parameter(ValueFromPipeline,
ValueFromPipelineByPropertyName)]
$Content
)
process {
$Content | Out-String -Stream
}
}
ls | f
However, the output is separated for each item:
Directory: C:\MyPath\dir1
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2022-01-22 5:34 PM 0 1.txt
Directory: C:\MyPath\dir1
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2022-01-22 5:34 PM 0 2.txt
Directory: C:\MyPath\dir1
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2022-01-22 5:34 PM 0 3.txt
How can I use the function to get the same result as the first command?
As Abraham pointed out in his comment, you could capture all objects coming from the pipeline first and then output the objects as a stream so that it is displayed properly in the console.
It's important to note that both examples displayed below, are not truly "streaming functions", as mklement0 has pointed out in his helpful answer, both functions are first collecting all input coming from pipeline and then outputting the objects as a stream of strings at once, rather than object by object.
function f {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
[object] $Content
)
begin { $output = [System.Collections.Generic.List[object]]::new() }
process { $output.Add($Content) }
end { $output | Out-String -Stream }
}
As an alternative to the advanced function posted above, the example below would also work because of how the automatic variable $input works in the end block by enumerating the collection of all input to the function:
function f { $input | Out-String -Stream }
I think this question requires a counter question:
Why do you want to get objects from a pipeline as strings?
I suspect that you either have a rare requirement or do not fully understand the PowerShell Pipeline
In general, I would avoid using Out-String in the middle of the pipeline¹ as although it can be called from the middle of a pipeline, it already formats the output similar to Format-Table which is usually done at the end of the stream. The point is also that it is hard to determine the width of the columns upfront without knowing what comes next.
With the exception mentioned by #mklement0:
There's one good reason to use Out-String -Stream: to make up for Select-String's useless behavior with non-string input objects. In fact, PowerShell ships with proxy function oss, which wraps Out-String -Stream - see also: GitHub issue #10726 and his helpful answer.
The Out-String -Stream parameter also doesn't help for this as all it does is breaking the multiline string into separate strings:
By default, Out-String outputs a single string formatted as you would see it in the console including any blank headers or trailing newlines. The Stream parameter enables Out-String to output each line one by one. The only exception to this are multiline strings. In that case, Out-String will still output the string as a single, multiline string.
(ls | Out-String -Stream).count
8
(ls | Out-String -Stream)[3]
Mode LastWriteTime Length Name
But if you really have a need to devaluate the PowerShell Objects (which are optimized for streaming) to strings (without choking the pipeline), you might actually do this:
function f {
[CmdletBinding()] param ([Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]$Content)
begin { $First = $True }
process {
if ($First) { $Content |Out-String -Stream |Select-Object -SkipLast 1 }
else { $Content |Out-String -Stream |Select-Object -Skip 5 |Select-Object -SkipLast 1 }
$First = $False
}
}
It shows what you are are trying to do, but as said, I would really recommend against this if you do not have a very good reason. The usual way to do this and respecting the pipeline is placing the Out-String outside your cmdlet at the end of the stream:
ls |f |Out-String -Stream
As you've experienced, calling Out-String -Stream for each input object doesn't work as intended, because - aside from being inefficient - formatting each object in isolation invariably repeats the header in the (table-)formatted output.
The solutions in Santiago's helpful answer are effective, but have the disadvantage of collecting all pipeline input first, before processing it, as the following example demonstrates:
function f { $input | Out-String -Stream }
# !! Output doesn't appear until after the sleep period.
& { Get-Item $PROFILE; Start-Sleep 2; Get-Item $PROFILE } | f
Note: Output timing is one aspect, another is memory use; neither aspect may or may not matter in a given use case.
To wrap a cmdlet call in streaming fashion, where objects are processed as they become available, you need a so-called proxy (wrapper) function that utilizes a steppable pipeline.
In fact, PowerShell ships with an oss function that is a proxy function precisely around Out-String -Stream, as a convenient shortcut to the latter:
# Streaming behavior via the built-in proxy function oss:
# First output object appears *right away*.
& { Get-Item $PROFILE; Start-Sleep 2; Get-Item $PROFILE } | oss
Definition of proxy function oss (wraps Out-String -Stream); function body obtained with $function:oss:
function oss {
[CmdletBinding()]
param(
[ValidateRange(2, 2147483647)]
[int]
${Width},
[Parameter(ValueFromPipeline = $true)]
[psobject]
${InputObject})
begin {
$PSBoundParameters['Stream'] = $true
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Out-String', [System.Management.Automation.CommandTypes]::Cmdlet)
$scriptCmd = { & $wrappedCmd #PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
}
process {
$steppablePipeline.Process($_)
}
end {
$steppablePipeline.End()
}
<#
.ForwardHelpTargetName Out-String
.ForwardHelpCategory Cmdlet
#>
}
Note:
Most of the body is generated code - only the name of the wrapped cmdlet - Out-String - and its argument - -Stream - are specific to the function.
See this answer for more information.
Having a simple issue that's only affecting export-csv output, out-gridview and results to the console are fine. Looking to capture the top 5 processes by "handles" on a set of servers.
Code is as follows:
$Servers = "Server1", "Server2", "Server3"
$OutArray = #()
ForEach ($Item in $Servers)
$Top5 = Get-Process -Computer $Item | Sort Handles -descending |Select -First 5
$OutArray += New-Object PSObject -property # {
Server = $Item
Top5 = $Top5
} #OutArray
} #ForEach
$OutArray | Export-csv Test.csv
The results of which come out looking fine via console as follows
Server Top5
------ ----
SERVER1 {#{ProcessName=svchost.exe; PercentCpuLoad=13.79}, #{ProcessName=services.exe; PercentCpuLoad=11.4}, #{ProcessName=WmiPrvSE.exe; PercentCpuLoad=10.03}, #{ProcessName=irfilcol.exe; PercentCpuLoad=9.79}...}
...However, in the csv they show as follows:
Server Top5
Server1 System.Object[]
Server2 System.Object[]
Server3 System.Object[]
I'm thinking it's because the $Top5 variable is an variable with multiple properties (5 each) for one server. How would do I correct the code so that export-csv shows the actual values?
any help appreciated!
I would like the csv results to look like the following that's shown in GRIDVIEW
Using the suggestion from BenH to review the post from Powershell legend Boe Prox, I now have the following working:
$Top5 = Get-Process -Computer $Item | Sort Handles -descending |Select -expand Handles | |Select -First 5
$new = [pscustomobject]#{ Top5 = (#($Top5) -join ',')
}
Just about got this working now:
i'd like to add more piece of formatting, where the Top5Processes have the actual CPU % used in (brackets) right now, I've got the following for output
Top2Proc Top2CPU
services.exe,BESClient.exe 32.76,16.6
However, it would be nicer output-wise, if i could combine the above two values into one, so it looks like this:
Top2Proc
Services(32.76), BesClient.exe(16.6)
Any idea how that would be done?
Use Select-Object to turn your process objects into strings before piping them to Export-Csv:
$OutArray |Select-Object Server,#{Expression={$_.Top5.Name -join ';'}} |Export-Csv test.csv
If you want that table to appear in your csv file then you would need to format the string Top5 property as such. Using Out-String will do just that
Sends objects to the host as a series of strings.
So a simple change should get you what you want.
$Top5 = Get-Process -Computer $Item |
Sort Handles -descending |
Select -First 5 |
Out-String
It will look a little ugly when not displayed with a mono-space font much like you see in Out-GridView. Also consider using .Trim() to remove the leading and trailing whitespace on your $top5.
There are other ways to tackle this. You could use the above in conjunction with Format-Table / Format-List depending what you want. In general if you want the output to be saved as it is displayed in host Out-String is something to test with.
I would have tried to add one row for each process with a the first column being the computer name. That way you would have better structured output that can be sorted or queried as needed.
ComputerName ProcessName Handles
------------ ----------- -------
Computer1 avp 54639
Computer1 OUTLOOK 7708
Computer1 RDTabs 6108
Computer1 svchost 3160
Computer1 chrome 2530
Keep in mind that you can use other methods to export this data while keeping the objects entact. Really depends the data recipeint but remeber there are other cmdlets like Export-CLIMXL and ConvertTo-JSON | Set-Content.
Hoping someone can help me to figure out if this is possible. I have a 20 ish lines that scan a log file in a while loop. I need this to happen in parallel with the rest of the script. The log scanning loop is passing the entries into SQLite and other scripts need to act on this information - hence wanting to run them in parallel.
If i use the Job-Start command then it seems the -SciptBlock function will only accept one piped line of commands. I have too many commands to want to pipe so i need to run multiple lines in the scriptblock.
I tried several ways of doing it but the following examples give the least errors. I also tried it in an Invoke-Command -ScriptBlock -as job like in the second example - both ways will not accept a multiline scriptblock.
what am i doing wrong, please?
Start-Job -Name LogScan -ScriptBlock
{
$EventOld = ConvertFrom-Json (Get-content ( Get-ChildItem | Sort-Object -Property LastWriteTime | Select-Object -last 1 ) | Select-Object -last 1)
$RunLoop = 1
while (RunLoop -ge 1)
{
Start-Sleep -Milliseconds 333
$RunLoop = $RunLoop +1
$EventNew = ConvertFrom-Json (Get-content ( Get-ChildItem | Sort-Object -Property LastWriteTime | Select-Object -last 1 ) | Select-Object -last 1)
if ($EventOld.timestamp -ne $EventNew.timestamp)
{
# lots of commands and here passing the array to SQLite
}
$EventOld = $EventNew
}
}
Error is as follows:
Start-Job : Missing an argument for parameter 'ScriptBlock'.
Specify a parameter of type 'System.Management.Automation.ScriptBlock'
and try again. [..]
Kudos to briantist for helping me to get there :)
When using Start-Job and Invoke-Command, be careful to put the opening { for the -ScriptBlock parameter on the same line as the command. Do not put it on the line below as you would with an if or while command.
This leads to a the further issue of not spotting that you have not matched opening { and closing } brackets properly, as you are used to the convention of matching their indentation level to pair them up.
Please note that the references to $Event.timestamp in the code. These come from the fact that one of the JSON fields is called timestamp - it is not a method or property of a standard string or array.
I am trying to create a Hash Table that contains 3 columns.
SERVER_NAME PROCESS_NAME SERVER_STATUS PROCESS_AVAILABLE
SERVER1 app1.exe RUNNING YES
SERVER1 app2.exe RUNNING NO
SERVER2 app1.exe OFFLINE NO
SERVER2 app2.exe OFFLINE NO
SERVER3 app1.exe RUNNING YES
SERVER3 app2.exe RUNNING YES
So far, I've tried this
$SERVERLIST = Get-Content "$PSScriptRoot\servers\serverManager.bin"
$PROCESSMONITOR = Get-Content "$PSScriptRoot\process\application.bin"
$testList = #{Name=$SERVERLIST;Process=$PROCESSMONITOR}
The list of servers are in the "serverManager.bin" file. This is a CSV file that contains a list of the servers.
The list of processes that I am interested in monitoring are in the "application.bin" file. This is a CSV file that contains a list of the applications (as seen by PowerShell). [see code below]
Get-Process -ComputerName $server -name $process -ErrorAction SilentlyContinue
I want to build a report which tells an admin which server is running and which process is running from the list that we are interested in monitoring.
I can check if the process is running
I can check if a server is online
My question is what do I need to do to get output like what's posted above
While hashtables play a part in this answer you are not looking for hashtables at all really. Looking at about_hash_tables
A hash table, also known as a dictionary or associative array, is a
compact data structure that stores one or more key/value pairs.
While you can nest whatever you want into the value you really are not looking for a hashtable. What I think you want is a custom PowerShell object that contains the results of each of your queries.
Get-Process does take arrays for both -Computer and -Name but they would omit results where either the computer does not exist or the process does not. Since you want that information you need to run a single cmdlet for each computer/process pair.
I use a hashtable only to create each individual "row" which is converted to a PowerShell object and collected as an array. I don't want to confuse but I know this working with at least 2.0 which is why I do it this way.
$SERVERLIST | ForEach-Object{
$computer = $_
$PROCESSMONITOR | ForEach-Object{
$process = $_
$props = #{
Server_Name = $computer
Process_Name = $process
}
# Check if the computer is alive. Better this was if $processes is large
If(Test-Connection $computer -Quiet -Count 1){
$props.Server_Status = "Running"
$result = Get-Process -Name $process -ComputerName $computer -ErrorAction SilentlyContinue
If($result){
$props.Process_Available = "Yes"
} else {
$props.Process_Available = "No"
}
} else {
$props.Server_Status = "Offline"
$props.Process_Available = "No"
}
New-Object -TypeName psobject -Property $props
}
} | Select Server_Name,Process_Name,Server_Status,Process_Available
So now that we have a proper object you can now use other cmdlets like Where-Object, Sort-Object and etc.