Powershell: Transform TotalItemSize into INT - powershell

Hi i'm writing a PS Script to cofront mailboxes size to a limit and to send an email to users when this limit is exceeded.
I've prepared the Size variable like this:
$Size=Get-MailboxStatistics -Identity $_.samaccountname | Select-Object #{n="TotalItemSize";e={($_.totalitemsize -split " \(")[0]}}
and i get something like:
"samaccountname" #{TotalItemSize=1.991 GB}
I have 2 questions:
Is it possible to get rid of everything except 1.991 GB ?
Can i tranform this value into an INT?
Thanks in advance.

Have a look to $a
$a = (Get-MailboxStatistics -Identity jean-paul.blanc).TotalItemSize
$a | get-member
You can see that it contains a property value that is a Microsoft.Exchange.Data.ByteQuantifiedSize
Now have a look to Microsoft documentation, you can find the method you are looking for Tobytes() so you can write :
$a.value.ToBytes()
or in your case :
$size = (Get-MailboxStatistics -Identity "Your user identity").TotalItemSize.value.Tobytes()
Edited :
If you only have got the string let say "34.01 MB (35,666,338 bytes)"
You can rebuid localy the object using :
$a = [Microsoft.Exchange.Data.ByteQuantifiedSize]::parse("34.01 MB (35,666,338 bytes)")

This will get you the size as an int:
$Size=
Get-MailboxStatistics -Identity $_.samaccountname |
Select-Object -ExpandProperty totalitemsize
$Size = $Size -replace '^.+\((.+\))','$1' -replace '\D' -as [int]
I'd use that, and then divide by 1GB if you want an int GB value. Mailboxes with smaller sizes may be returned as MB or even KB. It's easier to start with the actual byte count and do the conversion yourself than parse all the possible string formats that may be returned.
But if you set the IssueWarningQuota on the mailbox, the system will automatically start sending them an email once a day when they exceed that quota.
Edit: there are also object methods available for getting the byte counts in various formats (like ToBytes()). These work fine as long as you're in an actual EMS shell. If you try to use the same script in an implicit remoting session it will fail because now you're working with deserialized objects, and you don't have those methods any more. The string parsing method is not as "pure" as using the object methods, but it's portable between those environments.

You can convert decimal to int (using rounding) by casting it as an [int64]:
[int64]$val = 1.991
Or if you want to round down you can use [math:
[math]::floor(1.991)

Related

Powershell: Extract Exchange property and Display in array

I need to get all Exchange users into an Array, with a column for their SIP address and another column for all SMTP addresses (as seen in the EmailAddresses field).
So for now I am trying this against a single user and should that work out I can use "-ResultSize Unlimited" and do all. Although for now:
$list_UsersExtCloud =
Get-Recipient e12367 |
Select-Object Alias, EmailAddresses, #{Name = 'SIP'; Expression = { $_.EmailAddresses | Where-Object { $_ -like "*sip*" } -join ',' } }
I could extract the SIP by iterating using a FOR LOOP through each user properties with EmailAddresses | where {$_ -like "*sip*"}, but I'm trying to extract it in the EXPRESSION statement itself.
How can I get this to work?
You are on the right track, however you are trying to join the objects without referencing the sub-properties you are actually interested in. Try something like:
$list_UsersExtCloud =
Get-Recipient e12367 |
Select-Object Alias, EmailAddresses, #{ Name = 'SIP'; Expression = { ($_.EmailAddresses | Where-Object { $_.Prefix -eq "SIP" }).ProxyAddressString -join ',' } }
I changed the Where clause to use the Prefix. I grouped the address objects then unrolled the ProxyAddressString from them. This give good input to the -join. You can also do this with the AddressString property, I wasn't sure which you were really interested in.
Note: If you let this output without assigning to a variable the EmailAddresses property will likely bump the SIP property off the screen. I found that a little confusing while working this out, thought I'd mention it.
Warning: Get-Recipient has a known issue. When making very large queries it won't page properly and will return an LDAP cookie error. So this may blow up if you have enough recipients. An alternative is to query for mail enabled objects using Get-ADObject then customize the object similarly using Select-Object.

Alternatives to (Measure-Object -sum).Sum

I'm stuck in the following situation:
I have to get information out of a CSV file. I imported the CSV using Import-Csv.
My raw data looks like this:
45227;01.10.2018 03:24:00;Xxxx Xxxx Xxxxx x XX xxxxxxxxxxxxxx Xxxxx xxx Xxxxxxxxxxxxxxxxxxx;;3;XXXX;XXXX;XXX#XX.com;;;3.7;;
where the column containing 3.7 is the value of interest ("Points").
Here comes my first problem --> Using Import-Csv, powershell will save this information in a [string] property. To avoid that i used the following line:
| Select #{Name="Points";Expression={[decimal]$_.Points}}
Now i'm getting a Selected.System.Management.Automation.PSCustomObject-typed object containing that property as a [decimal]. Now i wanted to sum up all the points, that were used by the same e-mail address:
$Data[$Index].Points += (
$Imported_CSV | where {$_.Sender -eq $Imported_CSV_Unique.Sender} |
measure Points -sum
).Sum
This seemed to work just fine, but if i open up $Data[$Index] | gm i'm getting this: Points NoteProperty double Points=71301.6000000006
The property changed to [double]. I dug a bit and i found out that Powershell's GenericMeasureInfo.Sum Property can only give back a Nullable<Double> instance as a property value.
It seems like i'm producing an overflow of [double], because the number being displayed is totally wrong. I want to stick to decimal or integer so i have an output like 71123.4 or something like that.
Is there any other approach for that, so i don't have to use (Measure-Object -sum).Sum ?
Thanks in advance!
tl;dr:
If you need to control the specific numeric data type used for summing up numbers:
Avoid Measure-Object, which invariably uses [double] calculations.
Instead, use the LINQ Sum method (accessible in PSv3+) with a cast to the desired numeric type:
[Linq.Enumerable]::Sum(
[decimal[]] #(
$Imported_CSV | where {$_.Sender -eq $Imported_CSV_Unique.Sender}
).Points
)
Mathias R. Jessen's helpful answer shows you an elegant way to sum your Points column grouped by rows that share the same email address and Theo's helpful answer improves on it by truly summing the points as [decimal] values.
Some general points about Measure-Object with -Sum and floating-point data types:
You correctly state:
The property [data type] changed to double [...] i found out that Powershell's GenericMeasureInfo.Sum property can only give back a Nullable<Double> as property value.
Indeed: Measure-Object -Sum:
invariably uses [double] values to sum up the inputs.
it coerces the inputs to [double]s, if possible - even if they're not numbers.
If an input cannot be coerced to a [double] (e.g., 'foo'), an non-terminating error is emitted, but summing continues with any remaining inputs.
The above implies that even strings are acceptable input to Measure-Object -Sum, because they'll be converted to [double] on demand during summation.
That means that you could use your Import-Csv command directly, as in the following example (which uses two [pscustomobject] instances to simulate Import-Csv's output):
PS> ([pscustomobject] #{ Points = '3.7' }, [pscustomobject] #{ Points = '1.2' } |
Measure-Object Points -Sum).Sum
4.9 # .Points property values were summed correctly.
71301.6000000006 [...] It seems like i'm producing an overflow of "double"
Overflow would imply exceeding the maximum value that can be stored in a [double], which is (a) unlikely ([double]::MaxValue is 1.79769313486232E+308, i.e., greater than 10 to the power of 308) and (b) would produce a different symptom; e.g.:
PS> ([double]::MaxValue, [double]::MaxValue | Measure-Object -Sum).Sum
∞ # represents positive infinity
What you do get, however, is rounding errors due to the [double] type's internal binary representation, which doesn't always have an exact decimal representation, which can lead to baffling calculation results; e.g.:
PS> 1.3 - 1.1 -eq 0.2
False # !! With [double]s, 1.3 - 1.1 is NOT exactly equal to 0.2
For more information, see https://floating-point-gui.de/
Using [decimal] values does solve this problem, but note that this comes at the expense of a smaller range (in effect, you get 28 decimal digits of precision - the absolute value of the max. number depends on where the decimal point is placed; as an integer, it is 79,228,162,514,264,337,593,543,950,335, i.e. close to 8 * 1028).
If you do need the precision of [decimal]s, you must avoid Measure-Object and do your own summing.
In the context of your original command, you could use the Sum LINQ method:
[Linq.Enumerable]::Sum(
[decimal[]] #(
$Imported_CSV | where {$_.Sender -eq $Imported_CSV_Unique.Sender}
).Points
)
The use of #(...) (the array subexpression operator) rather than just (...) around the pipeline command ensures that the overall command doesn't fail in case the pipeline happens to return no rows. #(...) turns the non-output into an empty array, for which .Sum() correctly returns 0.
Without it, the [decimal[]] cast would result in $null, and PowerShell wouldn't be able to find the [decimal[]]-typed overload of the .Sum() method and report an error, "Multiple ambiguous overloads found for "Sum" and the argument count: 1".
The above command invariably requires all matching CSV rows (represented as custom objects) into memory as a whole, whereas Measure-Object - as most cmdlets in the PowerShell pipeline - would process them one by one, which requires only a constant amount of memory (but is slower).
If loading all matching rows into memory at once is not an option, use the ForEach-Object (foreach) cmdlet, but note that this would only make sense if you substituted an actual Import-Csv call for the already-in-memory array $Imported_Csv:
# Replace $Imported_Csv with the original Import-Csv call to
# get memory-friendly one-by-one processing.
$Imported_CSV | where {$_.Sender -eq $Imported_CSV_Unique.Sender} |
foreach -Begin { [decimal] $sum = 0 } -Process { $sum += $_.Points } -End { $sum }
I'd start by grouping all the sender addresses together and then sum them individually:
Import-Csv .\data.csv |Group-Object Sender |ForEach-Object {
[pscustomobject]#{
Sender = $_.Name
SumOfPoints = ($_.Group |Measure-Object Points -Sum).Sum
}
}
Measure-Object will automatically cast the Points strings to [double] - if you require more precision you can manually cast to [decimal] like before:
Import-Csv .\data.csv |Select-Object Sender,#{Name="Points";Expression={[decimal]$_.Points}} |Group-Object Sender |ForEach-Object {
[pscustomobject]#{
Sender = $_.Name
SumOfPoints = ($_.Group |Measure-Object Points -Sum).Sum
}
}
Using grouping like Mathias already did, here is how you can get the sum without losing the decimal precision, as I have commented before:
# faking the Import-Csv here with a here-string.
# in real life, you would use: Import-Csv <yourdata.csv> -Delimiter ';'
$data = #"
Sender;Date;Description;Something;Number;Whatever;DontKnow;Email;Nothing;Zilch;Points;Empty;Nada
45227;01.10.2018 03:24:00;Xxxx Xxxx Xxxxx x XX xxxxxxxxxxxxxx Xxxxx xxx Xxxxxxxxxxxxxxxxxxx;;3;XXXV;XXXA;XXX#XX.com;;;3.7;;
45227;01.10.2018 03:24:00;Xxxx Xxxx Xxxxx x XX xxxxxxxxxxxxxx Xxxxx xxx Xxxxxxxxxxxxxxxxxxx;;3;XXXW;XXXB;XXX#XX.com;;;4.7;;
45226;01.10.2018 03:24:00;Xxxx Xxxx Xxxxx x XX xxxxxxxxxxxxxx Xxxxx xxx Xxxxxxxxxxxxxxxxxxx;;3;XXXX;XXXC;XXX#XX.com;;;4.777779;;
45225;01.10.2018 03:24:00;Xxxx Xxxx Xxxxx x XX xxxxxxxxxxxxxx Xxxxx xxx Xxxxxxxxxxxxxxxxxxx;;3;XXXY;XXXD;XXX#XX.com;;;4.8;;
45225;01.10.2018 03:24:00;Xxxx Xxxx Xxxxx x XX xxxxxxxxxxxxxx Xxxxx xxx Xxxxxxxxxxxxxxxxxxx;;3;XXXZ;XXXE;XXX#XX.com;;;4.9;;
"# | ConvertFrom-Csv -Delimiter ';'
#get the two columns you need from the Csv and group them by Sender
$data | Select-Object Sender, Points | Group-Object Sender | ForEach-Object {
# add the 'Points' values as decimal
[decimal]$sum = 0
foreach ($value in $_.Group.Points) { $sum += [decimal]$value }
[PSCustomObject]#{
Sender = $_.Name
Sum = $sum
}
}
Output from the above would be:
Sender Sum
------ ---
45227 8,4
45226 4,777779
45225 9,7

Comparing Two Arrays Without Using -Compare

I have two array's, one contains multiple columns from a CSV file read in, and the other just contains server names, both type string. For this comparison, I plan on only using the name column from the CSV file. I don't want to use -compare because I want to still be able to use all CSV columns with the results. Here is an example of data from each array.
csvFile.Name:
linu40944
windo2094
windo4556
compareFile:
linu40944
windo2094
linu24455
As you can see, they contain similar server names, except $csvFile.Name contains 25,000+ records, and $compareFile contains only 3,500.
I've tried:
foreach ($server in $compareFile) {
if ($csvFile.Name -like $server) {
$count++
}
}
Every time I run this, it takes forever to run, and results in $count having a value in the millions when it should be roughly 3,000. I've tried different variations of -match, -eq, etc. where -like is. Also note that my end goal is to do something else where $count is, but for now I'm just trying to make sure it is outputting as much as it should, which it is not.
Am I doing something wrong here? Am I using the wrong formatting?
One possible thought given the size of your data.
Create a hashtable (dictionary) for every name in the first/larger file. Name is the Key. Value is 0 for each.
For each name in your second/smaller/compare file, add 1 to the value in your hashtable IF it exists. If it does not exist, what is your plan???
Afterwards, you can dump all keys and values and see which ones are 0, 1, or >1 which may or may not be of value to you.
If you need help with this code, I may be able to edit my answer. Since you are new, to StackOverflow, perhaps you want to try this first yourself.
Build custom objects from $compareFile (so that you can compare the same property), then use Compare-Object with the parameter -PassThru for the comparison. Discriminate the results using the SideIndicator.
$ref = $compareFile | ForEach-Object {
New-Object -Type PSObject -Property #{
'Name' = $_
}
}
Compare-Object $csvFile $ref -Property Name -PassThru | Where-Object {
$_.SideIndicator -eq '<='
} | Select-Object -Property * -Exclude SideIndicator
The trailing Select-Object removes the additional property SideIndicator that Compare-Object adds to the result.

Powershell: Filtering server list by the 2nd octet of the IP address

I am trying to go through a list of servers for further querying via WMI. Unfortunately, if the scripts hits a server that cannot connect via WMI, it takes a long time before timing out.
These are generally servers in our DMZ, and allocated a specific address in the 2nd octet of the IP address, .92 for example. So I am looking to filter out these servers as the first step in my query, so it can be ignore in any further WMI queries.
I have found many examples on how to do this, and again, I cannot use WMI methods as this defeats the object (eg: Get-WmiObject Win32_NetworkAdapterConfiguration).
In the example below, the server "SERVER" has an IP of 192.9.4.1 and I want to filter out all servers with ".9" in the second octet. so I use a simple Test-Connection cmdlet, and aim to split the result. Before I can split it, the result of the Test-Connection is:
$IP4Address = Test-Connection _Computer SERVER -Count 1 | Select IPV4Address
#{IPV4Address=192.9.4.1}
Which means that I need to count 19 chars from the beginning to get my "9".
IPV4Address
$Octet = $IP4Address -split ("")
If ($Octet[19] -eq '9')
{write-host "In DMZ"}
Else
{write-host "Not in DMZ"}
Before you ask, I did try -split (".") but this doesn't seem to take any effect.
So why does the result come out like #{IPV4Address=192.9.4.1}? Is there a better solution?
The -split operator takes a regex pattern, not a character literal.
In regex, . is a wildcard meaning "any one character", resulting in a collection of empty strings (the positions between each character in the input string).
To match a literal dot, escape it with \
PS C:\> '10.0.122.12' -split '\.'
10
0
122
12
You don't need to split the IP Address, just use -match:
if ($IP4Address -match '^\d+\.9\.')
{
write-host "In DMZ"
}
else
{
write-host "Not in DMZ"
}
Regex:
^\d+\.9\.'
Your receive the output #{IPV4Address=192.9.4.1} because you are selecting the object. If you just want to get the string, use the -ExpandProperty parameter:
$IP4Address = Test-Connection _Computer SERVER -Count 1 | Select -ExpandProperty IPV4Address
Just wanted to show you another approach that is not split based as well as point out the issue you are having with the "ipaddress" object you have. What you are seeing in your variable is an object with an ipaddress property as supposed to a string which is what you are looking for. You need to expand that property or call the property from your object $IP4Address.Ipaddress. The former is the easier solution for single properties.
You can cast the result as the type accelerator [ipaddress]. This was you can call the method and return the octet you are look to filter on.
$IP4Address = Test-Connection -Computer SERVER -Count 1 | Select -ExpandProperty IPV4Address
([ipaddress]$IP4Address).GetAddressBytes()[1]
You would need to be careful with this approach as you should also be doing some checking to be sure that $IP4Address is not null.
if($IP4Address){([ipaddress]$IP4Address).GetAddressBytes()[1]}
To only get the value of the IPV4Address property you will have to expand it:
$IP4Address = Test-Connection -Computer SERVER -Count 1 |
Select-Object -ExpandProperty IPV4Address
Then you can use the .Split() method:
if($IPV4Address.Split(".")[1] -eq "9") {
"In DMZ"
} else {
"Not in DMZ"
}
PS : I usually use -Count 2 rather than -Count 1

Parse string in Powershell

Learning powershell, trying to find out how to parse the first value from this resultset (10.60.50.40):
IPAddresses
-----------
{10.60.50.40, fe80::5ddf:a8f4:e29c:b66}
Normally I would just look it up, however, I don't know if {x, x} is a standard datatype of sorts in Powershell land.
Do I have to do rough string parsing, or is there some standard command to extract the first one, such as:
... | Select-Object IPAddresses | Select-String [0]
(I just made the select string part up. I'm lost.)
This is most likely the result of of the IPAddresses property of your object containing an array. The output you're seeing is stylized for display purposes, so it's not a string you would have to parse. Assuming your object is $obj, you should be able to do either of these:
$obj.IPAddresses[0]
$obj.IPAddresses | Select-Object -First 1
One solution is to use split function to convert the string into array and work with that like in the next steps:
Split the string into an array using the split function (comma is the item delimiter).
Grab the first item of the array (or whatever needed) and then also sanitize it (remove unnecessary curly bracket).
Example below:
$str = "{10.60.50.40, fe80::5ddf:a8f4:e29c:b66}"
$strArr = $str.Split(",")
Write-Host $strArr[0].Replace("{", "")
This is what I ended up doing:
$str = ... | Select-Object IPAddresses | ForEach {$_.IpAddresses}
Write-Host $str[0]
Depending on the source of your IPAddresses, this might not be optimal. You might get multiple IPAddresses per devices.
You might want to combine both approaches:
$str = ... | Select-Object -ExpandProperty IPAddresses | Select-Object -First 1
This will return the First IP address in your list per device.