PowerShell wait for output from function or invoke-command - powershell

I have a script block/function that returns PSCustomObject followed by Write-Host.
I want to get the output first then print the write-host but I can't seem to figure it out.
function ReturnArrayList {
param (
[int] $number
)
[System.Collections.ArrayList]$folderList = #()
$folderObject = [PSCustomObject]#{
Name = 'John'
number = $number
}
#Add the object to the array
$folderList.Add($folderObject) | Out-Null
return $folderList
}
$sb = {
param (
[int] $number
)
[System.Collections.ArrayList]$folderList = #()
$folderObject = [PSCustomObject]#{
Name = 'John'
number = $number
}
#Add the object to the array
$folderList.Add($folderObject) | Out-Null
return $folderList
}
ReturnArrayList -number 5
#Invoke-Command -ScriptBlock $sb -ArgumentList 5
Write-Host "This write host should come later"
Result:
This write host should come after
Name number
---- ------
John 5
Desired result:
Name number
---- ------
John 5
This write host should come after
How can I get the return result first and print the write-host message?
Thank you for your help in advance!

You can force PowerShell to write the output from ReturnArrayList to the screen before reaching Write-Host by piping it to either one of the Format-* cmdlets or Out-Default:
$object = ReturnArrayList -number 5
$object |Out-Default
Write-Host "This write host should come later"
Result:
Name number
---- ------
John 5
This write host should come later
Beware that your ReturnArrayList function does not actually return an ArrayList - PowerShell will automatically enumerate the item(s) in $folderlist, and since it only contains one item, the result is just a single PSCustomObject, "unwrapped" from the ArrayList so to speak:
PS ~> $object = ReturnArrayList -number 5
PS ~> $object.GetType().Name
PSCustomObject
To preserve enumerable types as output from functions, you'll need to either use Write-Output -NoEnumerate, or wrap the it in an array using the , operator:
function ReturnArrayList {
param (
[int] $number
)
[System.Collections.ArrayList]$folderList = #()
$folderObject = [PSCustomObject]#{
Name = 'John'
number = $number
}
#Add the object to the array
$folderList.Add($folderObject) | Out-Null
return ,$folderList
# or
Write-Output $folderList -NoEnumerate
}

Data is usually output to the pipeline, while Write-Host bypasses the pipeline and writes to the console directly.
Using Write-Output instead of Write-Host will fix this issue. You can easily find more in-depth information on this topic, and when not to Write-Host.

Related

Creating and passing an array from one function to another [duplicate]

This question already has answers here:
Boolean variable gets returned as an Object[]
(2 answers)
Closed 3 years ago.
What I'm trying to do is make it so I can create the array and check it in a single a single function as I call it in other functions so it'd be easier to just add $list = GetUserList instead of verifying the $list each time I plan on calling the GetUserList function.
https://pastebin.com/6h4MJH9n
What works:
function GetUserList {
$name = Read-Host "Please enter the users name (multiple users can be separated by commas)"
$names = $name.Split(",")
for ( $i = 0; $i -lt $names.Count; $i++ ) {
$firstn = $names[$i].Trim().Split(" ")[0]
$lastn = $names[$i].Trim().Split(" ")[-1]
$ulist += #([PSCustomObject]#{
First = "$firstn";
Last = "$lastn"
})
}
return $ulist
}
function UserList {
do {
$userlist = GetUserList
$userlist | Format-Table -AutoSize -Wrap
$again = Read-Host "Is this correct? (y/n)"
} until ( $again -eq "y" )
$userlist | ForEach-Object {
"First: $($_.First)"
"Last: $($_.Last)"
}
}
UserList
What doesn't work:
function GetUserList {
do {
$ulist = #()
$name = Read-Host "Please enter the users name (multiple users can be separated by commas)"
$names = $name.Split(",")
for ( $i = 0; $i -lt $names.Count; $i++ ) {
$firstn = $names[$i].Trim().Split(" ")[0]
$lastn = $names[$i].Trim().Split(" ")[-1]
$ulist += #([PSCustomObject]#{
First = "$firstn";
Last = "$lastn"
})
}
$ulist | Format-Table -AutoSize -Wrap
$again = Read-Host "Is this correct? (y/n)"
} until ( $again -eq "y" )
return $ulist
}
function UserList {
$userlist = GetUserList
$userlist | ForEach-Object {
"First: $($_.First)"
"Last: $($_.Last)"
}
}
UserList
I don't get an errors, it's just the code that doesn't work completely skips the for loop and I have no idea why.
the problem is situated in $ulist | Format-Table -AutoSize -wrap since you're not either
storing the formatted content in a variable,
nore sending the formatted content to the PowerShell host for printing (as stated in #Lee_Daily's comment in the OP)
PowerShell will return the formatted content to the output stream. Additionally to the formatted content you're also sending the content of $ulist to the output stream (via the Return $ulist statement). Based on that $userlist (via $userlist = GetUserList) contains the $ulist content PLUS the formatted $ulist content.
These can also be seen when debugging your code (see Set-PsBreakPoint):
[DBG]:> $userlist
First Last
----- ----
user 1
user 2
user 3
First Last
----- ----
user 1
user 2
user 3
As #Lee_Daily suggests change the line $ulist | Format-Table -AutoSize -wrap to $ulist | Format-Table -AutoSize -wrap | Out-Host. Piping to Out-Host will prevent that the output of Format-Table is written to the output-stream.
Further suggested reading:
about_Redirection
PowerShell streams devblog

Input and Output of foreach into columns in table

I currently want to check if a list of processes are running, then display the result within a table such as:
Process Status
======= ======
Process 1 Running
Process 2 Not Running
Process 3 Running
I have the below code which produces an output showing each input and output as a string, but it looks messy depending on the length of the Process name.
$Node = Read-Host -Prompt 'Input Node name'
$Process = #("Process1", "Process2", "Process3")
$Process | foreach-object {if(!(Get-Process -Name $_ -ComputerName $Node - ErrorAction SilentlyContinue)) {"$_ - Not Running"} else {"$_ - Running"}}
I am at a loss. All help appreciated.
Better (faster) to make a single remoting call to get all the processes, than one per process, so do that and store all the results - at least the names of the processes.
The next part is non-trivial. The way PowerShell and the neatly formatted tables work, is by making one object (bundle of things all together) for each table row, with each object having properties for each column name.
# Processes to look for
$Process = #("Process1", "Process2", "Process3")
$Node = Read-Host -Prompt 'Input Node name'
# Get running processes, and only keep their names
$runningProcesses = Get-Process -ComputerName $Node -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty Name
$Process | ForEach-Object {
# For each process name to look for, generate a hashtable of
# columns and their values,
# then cast it into a PS Object
[PSCustomObject]#{
'ProcessName' = $_
'Status' = if ($runningProcesses -contains $_) { "Running" } else { "Not Running" }
}
}
This gives a neat formatted table output, and is also structured data so you can feed the output of this into | ForEach-Object { $_.Status } and pick out the individual parts by name, something you can't do as neatly with your string formatted approach.
Try this:
$node = Read-Host -Prompt 'Input Node name'
$processList = "Process1", "Process2", "Process3"
$processList |
ForEach-Object {
[PsCustomObject]#{
NodeName = $node
ProcessName = $_
IsRunning = (Get-Process -Name $_ -ComputerName $node -ErrorAction SilentlyContinue | Select-Object -First 1) -ne $null
}
}
Output will be like this:
NodeName ProcessName IsRunning
-------- ----------- ---------
Node1 Process1 True
Node1 Process2 True
Node1 Process3 False

Calling the value of a variable from a variable value

I run a script in powershell
./name -i tom
then i want to call the value of a variable called $Tom from the input reference value of $i
$tom = 29
$andrew = 99
$bill = 5
Echo $i's age is $i
This would print:
toms age is 29
In powershell this would look like this:
Contents of name.ps1:
$person = $args
$ages = #{"Tom" = 23;
"Andrew" = 99;
"Bill" = 5}
$age = $ages[$person]
Write-Host "$person's age is $age"
And you would call it like this
.\name.ps1 "tom"
$argscontains all the arguments that you send to the script. So if you call the script like this: .\name.ps1 "tom" "bill", your result would be: tom bill's age is 23 5
I would use hashtable, but if you have global variables, you can use following:
#variables
$tom = 29
$andrew = 99
$bill = 5
#your parameter
$i = "tom"
#echo
Echo "$i's age is $((Get-Variable | ? {$_.Name -eq $i}).Value)"
An alternative approach to those provided. More code, arguably overkill, but I think it's good to get a handle on PowerShell's param feature.
# PowerShell's native argument/parameter support
param(
[string]$name
)
# Create an array with Name and Age properties as hashtable.
$people = #(
#{ Name = "Tom" ; Age = 29},
#{ Name = "Andrew" ; Age = 99},
#{ Name = "Bill" ; Age = 5}
)
# Find the person by comparing the argument to what is in your array
$person = $people | Where-Object {$_.Name -eq $name}
# Single name version: If the person is found, print what you would like. Otherwise let the user know name not found
if($person -ne $null){
Write-Host "$($person.Name) is $($person.Age) years old"
}else{
Write-Host "$name not found in list."
}
<# Multiple name version : get rid of the param section
foreach ($name in $args){
$person = $people | Where-Object {$_.Name -eq $name}
if($person -ne $null){
Write-Host "$($person.Name) is $($person.Age) years old"
}else{
Write-Host "$name not found in list."
}
}
#>

Change property on existing powershell object

I have a custom powershell object created with a function:
$obj = myfunction -name "hello" -value 5
After it's created I want to change the value property however doing it (demonstrated below) doesn't work
$obj.value = 1
I've searched and can't seem to find anything - can anybody explain how I can accomplish this?
Here is my function that creates an returns the object
function myfunction
{
[CmdletBinding()]
[OutputType([System.Collections.Hashtable])]
Param
(
[Parameter(Mandatory=$true,
Position=0)]
[String]
$name,
[Parameter(Mandatory=$true,
Position=1)]
[int]
$value,
)
Process
{
$myfunction = #{
name = $name
value = $value
}
write-output $myfunction
}
}
If you're returning a hashtable, you should be able to do what you wrote:
PS> $obj = myfunction -name "hello" -value 5
PS> $obj.value = 1
PS> $obj
Name Value
---- -----
name hello
value 1
However, the "safe" way is to use square braces.
You might want to try that, because my gut is that you tried to set .Values instead of .Value ... and .Values is a non-settable property of Hashtables.
PS> $obj = myfunction -name "hello" -value 5
PS> $obj["value"] = 1
PS> $obj
Name Value
---- -----
name hello
value 1
Either way, you could avoid all that by creating an actual PSCustomObject as #jaqueline-vanek did in her answer.
PS C:\Users\joshua> $obj = [PSCustomObject]#{ hello = 5 }
PS C:\Users\joshua> $obj.hello
5
PS C:\Users\joshua> $obj.hello = 1
PS C:\Users\joshua> $obj.hello
1
PowerShell: Creating Custom Objects
The OP is correct it does not work.
It should be,
$obj."Hello" to access the property value
$obj."Hello" = 1 to set the property.
You have to double quote the property name.

Powershell script: create loop for ResponseTime

I am having an issue with the way that my ping results "roll" out on the screen. I am using this code:
$servers = "192.168.2.10","192.168.2.80","192.168.2.254"
$collection = $()
foreach ($server in $servers)
{
$status = #{ "ServerName" = $server; "TimeStamp" = (Get-Date -f s) }
$testconnection = (Test-Connection $server -Count 1 -ea 0)
$response = ($testconnection | select ResponseTime)
if ($response)
{
$status["Results"] = "Up"
$status["Responsetime"] = $response
}
else
{
$status["Results"] = "Down"
}
New-Object -TypeName PSObject -Property $status -OutVariable serverStatus
$collection += $serverStatus
}
$collection | Export-Csv -Path ".\ServerStatus.csv" -NoTypeInformation
I would to like create a loop for the ResponseTime
The code that I am using now gives one response.
When I give a count of 2, it prints the ResponseTime next to eachother per IP-adres.
Output:
TimeStamp Responsetime Results ServerName
--------- ------------ ------- ----------
2014-10-22T23:30:17 {#{ResponseTime=6}, #{ResponseTime=4}} Up 192.168.2.10
2014-10-22T23:30:18 Down 192.168.2.80
2014-10-22T23:30:25 {#{ResponseTime=1}, #{ResponseTime=3}} Up 192.168.2.254
What I want is, that the script prints each ResponseTime under eachother like this:
TimeStamp Responsetime Results ServerName
--------- ------------ ------- ----------
2014-10-22T23:11:50 #{ResponseTime=419} Up 192.168.2.10
2014-10-22T23:11:51 #{ResponseTime=415} Up 192.168.2.10
2014-10-22T23:11:51 Down 192.168.2.80
2014-10-22T23:11:52 #{ResponseTime=470} Up 192.168.2.254
2014-10-22T23:11:52 #{ResponseTime=7} Up 192.168.2.254
Or like this:
TimeStamp Responsetime Results ServerName
--------- ------------ ------- ----------
2014-10-22T23:11:50 #{ResponseTime=419} Up 192.168.2.10
2014-10-22T23:11:51 Down 192.168.2.80
2014-10-22T23:11:51 #{ResponseTime=415} Up 192.168.2.254
2014-10-22T23:11:52 #{ResponseTime=470} Up 192.168.2.10
2014-10-22T23:11:51 Down 192.168.2.80
2014-10-22T23:11:52 #{ResponseTime=7} Up 192.168.2.254
It doesn't matter which one, my preference is the second one
Could you please help me with this matter. Even if it is not possible tell me aswell.
Thank you,
Chris
I'll chime in late, not because the other answer are wrong by any means, they are both functional, but more so because nobody has pointed out that you are recreating the wheel.
You test the connection, and specify an erroraction for it that silently continues leaving your variable null. Then you have to test to see if the variable has results, and treat it one way, or if it doesn't treat it another way. What you have just done is made your own Try/Catch scenario. If you actually use the error to stop you can use the built in Try/Catch. Consider this approach:
$servers = "www.google.com","localhost","www.amazon.com"
$collection = #()
foreach ($server in $servers)
{
Try{
$testconnection = Test-Connection $server -Count 2 -ErrorAction Stop
$testconnection | ForEach{$collection += New-Object PSObject -Property ([ordered]#{
'TimeStamp' = Get-Date -Format s
'Server' = $server
'ResponseTime' = $_.responsetime
'Results' = 'Up'})
}
}
Catch{
$collection += New-Object PSObject -Property ([ordered]#{
'TimeStamp' = Get-Date -Format s
'Server' = $server
'ResponseTime' = $null
'Results' = 'Unreachable'
})
}
}
$collection #| Export-Csv -Path ".\ServerStatus.csv" -NoTypeInformation
That tries to ping the server, and if it can it adds a custom object to the $collection array with the desired information. If the ping fails it also adds an object to the $collection showing that the server was unreachable.
Also, you had $collection = $(). I assume you were trying to create an empty array, which is correctly done $collection = #() (corrected in my suggested code). Now, I commented out the Export-CSV so I could see the results. This is what I saw:
TimeStamp Server ResponseTime Results
--------- ------ ------------ -------
2014-10-22T17:54:22 www.google.com 9 Up
2014-10-22T17:54:22 www.google.com 12 Up
2014-10-22T17:54:23 localhost 0 Up
2014-10-22T17:54:23 localhost 0 Up
2014-10-22T17:54:27 www.amazon.com Unreachable
Amazon didn't let me ping it, so it shows as unreachable.
Moving on to why your desired results are not practical... What you describe shows you pinging your servers and getting results from them at non-consecutive times. To do that you would have to do -count 1, and loop through the ForEach loop twice, so it would ping server 1 for 1 result, then server 2 for 1 result, then server 3 for 1 result. Then it would go back and ping server 1 for a second result, then server 2 for a second result, and then server 3 for a second result. If you wanted to do that you could I suppose, and it should give you your desired results, you would have to do something like this:
$servers = "www.google.com","localhost","www.amazon.com"
$collection = #()
$count = 2
for($i=1;$i -le $count;$i++){
ForEach($server in $servers){
do stuff to ping servers as described above, except change -count to 1
}
}
$collection | export-CSV '.\ServerStatus.csv' -notype
That will give you your desired results, but it is slower. If you have to run this against more than a few servers it will be noticeably slower. For just those three servers listed it made the entire process go from taking 3.7240945 seconds to taking 7.6104075 seconds (roughly double).
Instead of
$response = ($testconnection | select ResponseTime)
if ($response)
{
$status["Results"] = "Up"
$status["Responsetime"] = $response
}
do
if($testconnection)
{
$testconnection | % {
$status = #{"ServerName" = $server; "TimeStamp" = (Get-Date -f s); "Results" = "Up"; "Responsetime"= $_.responsetime};
New-Object -TypeName PSObject -Property $status -OutVariable serverStatus;
$collection += $serverStatus }
}
else
{
$status = #{"ServerName" = $server; "TimeStamp" = (Get-Date -f s); "Results" = "Down"};
New-Object -TypeName PSObject -Property $status -OutVariable serverStatus;
$collection += $serverStatus
}
The problem is that $testconnection or in your case $response is an array if the count of Test-Connection is greater then 1, so you have to loop through it and add the single entries to your collection.
Also to get the Value instead of the gibberish you get you have to call the .responsetime property.
In hopes I didn't make it too complicated I present this solution
$servers = "10.50.10.100","8.8.8.8","169.254.54.1"
$servers | ForEach-Object{
$server = $_
$timeStamp = (Get-Date -f s)
$testconnection = Test-Connection $server -Count 2 -ErrorAction 0
If(!$testconnection){
$props = #{
Server = $server
TimeStamp = $timeStamp
ResponseTime = ""
Results = "Down"
}
New-Object -TypeName PSObject -Property $props
} Else {
$testconnection | ForEach-Object{
$_ | Select-Object #{l='Server';e={$server}},#{l='TimeStamp';e={$timeStamp}},#{l='ResponseTime';e={$_.ResponseTime}},#{l='Results';e={"Up"}}
}
}
} | Export-Csv -Path ".\ServerStatus.csv" -NoTypeInformation
So your logic is still here but as you can see some things have been changed. Paul was right, in that you needed to loop for each ResponseTime element you had. I also have done that but with a different approach that, if nothing else, will show you some of the Power in PowerShell. A break down of the code
Pipe $servers into a ForEach-Object. ForEach in works fine however I wanted to skip the saving the variables and just output straight to Export-CSV which is why I changed it.
So if you use Test-Connection on a server that does not exist or errors for some reason then you need to create an object to represent that. Using the desired properties, build a object with required values. This is output to the pipe instead of using a temporary variable.
When a connection test is successful then we need to output a number or variables to match the number of returns.
Continuing from #3 we use Select-Object to output the desired values. l stand for label and e for expression. Yes you could easily just use another $props variable. Just illustrating another option.
Since we changed the ForEach in the first step we can just output straight to Export-CSV
Sample output
Server TimeStamp ResponseTime Results
------ --------- ------------ -------
10.50.10.100 2014-10-22T20:22:01 0 Up
10.50.10.100 2014-10-22T20:22:01 0 Up
8.8.8.8 2014-10-22T20:22:02 43 Up
8.8.8.8 2014-10-22T20:22:02 39 Up
169.254.54.1 2014-10-22T20:22:03 Down