Cmdlet to round numbers in output - powershell

Is there a cmdlet that rounds numbers (all float values) in the output?
When I run the following command:
get-vm |select ProvisionedSpaceGB,UsedSpaceGB
I get this output as a table:
ProvisionedSpaceGB UsedSpaceGB
------------------ -----------
1224,0003194380551576614379883 349,88938544876873493194580078
1224,0003062393516302108764648 321,74483488313853740692138672
502,80292716529220342636108399 74,052481059916317462921142578
700,00035238638520240783691406 484,56624550372362136840820312
800,0003144945949316024780273 322,26342210918664932250976562
I am aware that I can define that in the select cmdlet like this:
get-vm | select #{ n="ProvisionedSpaceGB"; e={[math]::round( $_.ProvisionedSpaceGB, 2 )}},
#{ n="UsedSpaceGB"; e={[math]::round( $_.UsedSpaceGB, 2 )}}
And then get this output:
ProvisionedSpaceGB UsedSpaceGB
------------------ -----------
1224,00 349,58
1224,00 320,32
502,80 74,05
700,00 484,57
800,00 322,26
But there must be an easier way, like piping it into another cmdlet:
get-vm |select ProvisionedSpaceGB,UsedSpaceGB |RoundNumbers -Precision 2
To get the second output.

I doubt there is a built-in PowerShell cmdlet for that, however - you could write your own filter:
filter Round-FloatValues
{
Param(
[parameter(ValueFromPipeline=$true)]
$objects,
[int]$Precision = 2
)
$objects.PsObject.Properties | ForEach-Object {
if ($_.TypeNameOfValue -eq 'System.Double')
{
[math]::round( $_.Value, $Precision )
}
else
{
$_.Value
}
}
}
Usage:
get-vm |select ProvisionedSpaceGB,UsedSpaceGB |Round-FloatValues -Precision 2

Related

How to replace Filter Origin in this PowerShell command with Windows Firewall's display name?

Get-WinEvent -FilterHashtable #{ LogName="Security"; Id=5152; } | ? { $_.Message -like "*Outbound*" -and -not($_.message -like "*ICMP*")} | select Message | ft -wrap
Found that in here, after running it, the results look like this:
filter origin has this ID which is Firewall's unique name but I want to see a more user friendly name so I can understand immediately which Firewall rule, based on its display name that I set, blocked this connection.
Update:
I want to do something like this. but it doesn't work like this and I need help fixing it. basically, I want to keep the same output format that the original script shows and only replace things like this {a42a62ec-83d9-4ab5-9d54-4dbd20cfab17} with their display name.
$data = (Get-WinEvent -FilterHashtable #{ LogName="Security"; Id=5152; } |
? { $_.Message -like "*Outbound*" -and -not($_.message -like "*ICMP*")}).message
$data -replace "(?<=Filter Origin:[^{]+){.+?}",{(Get-NetFirewallRule -Name $Matches[0]).DisplayName}
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_comparison_operators?view=powershell-7.2#replacement-with-a-script-block
You can turn events into xml and access each field seperately. I don't have your exact event type.
$a = Get-WinEvent #{ LogName='Security' } -maxevents 1
$xml = [xml]$a.toxml()
$xml.event.eventdata.data
Name #text
---- -----
SubjectUserSid S-1-5-19
SubjectUserName LOCAL SERVICE
SubjectDomainName NT AUTHORITY
SubjectLogonId 0x3e5
PreviousTime 2023-01-03T14:40:58.3894712Z
NewTime 2023-01-03T14:40:58.3975397Z
ProcessId 0x59c
ProcessName C:\Windows\System32\svchost.exe
$xml.event.eventdata.data | ? name -eq processname | % '#text'
C:\Windows\System32\svchost.exe
Get-WinEvent #{ LogName='Security' } | % { $xml = [xml]$_.toxml()
$xml.event.eventdata.data | ? name -eq 'processname' | % '#text' }
Did a quick google search and saw this documentation on troubleshooting firewalls, and it points to Get-NetFireWallRule being able to get the display name from the ID. That said, you can use some handy RegEx of (?<=Filter Origin:[^{]+){.+?} to get the unique ID and query its friendly name:
Get-WinEvent -FilterHashtable #{ LogName="Security"; Id=5152; } |
? { $_.Message -like "*Outbound*" -and $_.Message -notlike "*ICMP*" } |
Select TimeCreated, #{
Name = 'Msg'
Expression = {
if ($_.Message -match ($pattern = '(?<=Filter Origin:[^{]+){.+?}'))
{
$_.Message -replace $pattern, (Get-NetFirewallRule -Name $Matches[0]).DisplayName
}
else
{
$_.Message
}
}
} | Ft -Wrap
Placing it inside an if statement allows it to leave the message alone if no match was found for patterns that may be the unique ID. See RegEx101 for more info on the pattern itself.

Parsing multiple valus in multiple variables

I am trying to find a way to execute a command in powershell and put each line of the result in a different variable to use them later in my script. For example if i execute the below command:
C:\> Get-VMHost -Name hq-esxi-prod-01a.nsx.gss | Get-VM | select Name
I will get the below:
Name
----
HQ-LinServ-01a
HQ-Win2012-01a
HQ-Web-02a
I want to have a script that will add each line in a different variable in a script (excluding the first which is name).
how can i do that.
Thank you for your help.
You can use Set-Variable in a loop to put each value in a separate variable:
$i = 0
... | Get-Vm | Select-Objet -Expand Name | ForEach-Object {
Set-Variable -Name "vm$i" -Value $_
$i++
}
However, that usually isn't good advice. It's more common to put all names in one (array) variable:
$vmList = ...| Get-Vm | Select-Object -Expand Name
so you can access individual names via $vmList[<index>], or (if you need access by some kind of name) in a hashtable:
$i = 0
$vmList = #{}
... | Get-Vm | Select-Objet -Expand Name | ForEach-Object {
$vmList["vm$i"] = $_
$i++
}
Best practice would depend on the particular scenario you need this for, though.
Thank you for your reply,
I have tried you answer but it seems that i am using PowerCLI for VMware it does not include Select-Object -Expand (not sure i had an exception), However your answer have mad me reach to a suitable answer for this.
I have used the below and it worked fine using foreach and adding the values in an array and then reading them as below:
$p1vmname = Get-VMHost -Name hq-esxi-prod-01a.nsx.gss | Get-VM | select Name
$p1vmlist = #()
foreach ($p1line in $p1vmname)
{
$p1vmlist += $p1line
}
$p1 = 0
do {
$x = $p1+1
Write-Host -BackgroundColor:Black -ForegroundColor:Yellow "vm number $x is "$p1vmlist[$p1]"."
$p1++
}
until ($p1 -eq $p1vmc)
}
However when using this the names was not totally correct as they had some additional characters as below:
vm number 1 is #{Name=HQ-Web-01a}
vm number 2 is #{Name=HQ-LinServ-01a}
vm number 3 is #{Name=HQ-Win2012-01a}
so i used split and trim to get rid of these as below and worked fine.
$p1vmname = Get-VMHost -Name hq-esxi-prod-01a.nsx.gss | Get-VM | select Name
$p1vmlist = #()
foreach ($p1line in $p1vmname)
{
$p1vmlist += $p1line
}
$p1 = 0
do {
$x = $p1+1
$p1vmlist[$p1] = ($p1vmlist[$p1]) -split("=") | Select-Object -Last 1
$p1vmlist[$p1] = $p1vmlist[$p1].trimend("}")
Write-Host -BackgroundColor:Black -ForegroundColor:Yellow "vm number $x is "$p1vmlist[$p1]"."
$p1++
}
until ($p1 -eq $p1vmc)
}
Output:
vm number 1 is HQ-Web-01a .
vm number 2 is HQ-LinServ-01a .
vm number 3 is HQ-Win2012-01a .
Thank you so much for your answer that helped me a lot.
I am really enjoying scripting now.

Seeking balanced combination of fast, terse, and legible code to add up values from an array of objects

Given the following array of objects:
Email Domain Tally
----- ----- -----
email1#domainA.com domainA.com 4
email1#domainB.com domainB.com 1
email2#domainC.com domainC.com 6
email4#domainA.com domainA.com 1
I'd like to "group by" Domain and add up Tally as I go. The end result would like this:
Domain Tally
------ -----
domainA.com 5
domainB.com 1
domainC.com 6
I have something that works but I feel like it's overly complicated.
$AllTheAddresses = Get-AllTheAddresses
$DomainTally = #()
foreach ($Addy in $AllTheAddresses)
{
if ($DomainTally | Where-Object {$_.RecipientDomain -eq $Addy.RecipientDomain})
{
$DomainTally |
Where-Object {$_.RecipientDomain -eq $Addy.RecipientDomain} |
ForEach-Object {$_.Tally += $Addy.Tally }
}
else
{
$props = #{
RecipientDomain = $Addy.RecipientDomain
Tally = $Addy.Tally
}
$DomainTally += New-Object -TypeName PSObject -Property $props
}
}
In my example, I'm creating the addresses as hashtables, but PowerShell will let you refer to the keys by .Property similar to an object.
If you're truly just summing by the Domain, then it seems like you don't need anything more complicated than a HashTable to create your running total.
The basic summation:
$Tally = #{}
$AllTheAddresses | ForEach-Object {
$Tally[$_.Domain] += $_.Tally
}
Using this sample data...
$AllTheAddresses = #(
#{ Email = "email1#domainA.com"; Domain = "domainA.com"; Tally = 4 };
#{ Email = "email1#domainB.com"; Domain = "domainB.com"; Tally = 1 };
#{ Email = "email1#domainC.com"; Domain = "domainC.com"; Tally = 6 };
#{ Email = "email1#domainA.com"; Domain = "domainA.com"; Tally = 1 }
)
And you get this output:
PS> $tally
Name Value
---- -----
domainC.com 6
domainB.com 1
domainA.com 5
Here is a "PowerShellic" version, notice the piping and flow of the data.
You could of course write this as a one liner (I did originally before I posted the answer here). The 'better' part of this is using the Group-Object and Measure-Object cmdlets. Notice there are no conditionals, again because the example uses the pipeline.
$AllTheAddresses |
Group-Object -Property Domain |
ForEach-Object {
$_ |
Tee-Object -Variable Domain |
Select-Object -Expand Group |
Measure-Object -Sum Tally |
Select-Object -Expand Sum |
ForEach-Object {
New-Object -TypeName PSObject -Property #{
'Domain' = $Domain.Name
'Tally' = $_
}
} |
Select-Object Domain, Tally
}
A more terse version
$AllTheAddresses |
Group Domain |
% {
$_ |
Tee-Object -Variable Domain |
Select -Expand Group |
Measure -Sum Tally |
Select -Expand Sum |
% {
New-Object PSObject -Property #{
'Domain' = $Domain.Name
'Tally' = $_
}
} |
Select Domain, Tally
}
Group-Object is definitely the way to go.
In the interest of terseness:
Get-AllTheAddresses |Group-Object Domain |Select-Object #{N='Domain';E={$_.Name}},#{N='Tally';E={($_.Group.Tally |Measure-Object).Sum}}

Powershell - Prefix each line of Format-Table with String

I would like to know if there is an easy way of prefixing each line of a powershell table with a String.
For example, if I create an Array using the following code:
$Array = #()
$Object = #{}
$Object.STR_PARAM = "A"
$Object.INT_PARAM = 1
$Array += [PSCustomObject] $Object
$Object = #{}
$Object.STR_PARAM = "B"
$Object.INT_PARAM = 2
$Array += [PSCustomObject] $Object
Calling Format-Table give the following output:
$Array | Format-Table -AutoSize
STR_PARAM INT_PARAM
--------- ---------
A 1
B 2
Instead, I would like to have the following:
$Array | Format-Table-Custom -AutoSize -PrefixString " "
STR_PARAM INT_PARAM
--------- ---------
A 1
B 2
And if possible, I would also like to be able to use the Property parameter like this:
$SimpleFormat = #{Expression={$_.STR_PARAM}; Label="String Param"},
#{Expression={$_.INT_PARAM}; Label="Integer Param"};
$Array | Format-Table-Custom -Property $SimpleFormat -AutoSize -PrefixString "++"
++String Param Integer Param
++------------ -------------
++A 1
++B 2
Any help would be appreciated. Thanks.
You could just use the format expressions directly:
$f = #{Expression={"++" + $_.STR_PARAM}; Label="++String Param"},
#{Expression={$_.INT_PARAM}; Label="Integer Param"};
$Array | Format-Table $f -AutoSize
Output
++String Param Integer Param
-------------- -------------
++A 1
++B 2
Update to use expression and filter
Filter Format-Table-Custom
{
Param
(
[string]
$PrefixString,
[object]
$Property
)
end {
$rows = $input | Format-Table $property -AutoSize | Out-String
$lines = $rows.Split("`n")
foreach ($line in $lines) {
if ($line.Trim().Length -gt 0) {
$PrefixString + $line
}
}
}
}
$f = #{Expression={"--" + $_.STR_PARAM}; Label="--String Param"},
#{Expression={$_.INT_PARAM}; Label="Integer Param"};
$Array | Format-Table-Custom -Property $f -PrefixString "++"
Output
++--String Param Integer Param
++-------------- -------------
++--A 1
++--B 2

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