Select-Object of multiple properties - powershell

I am trying to find an elegant way to put the metadata of a table of type System.Data.DataTable into a multi-dimensional array for easy reference in my program. My approach to the issue so far seems tedious.
Assuming $DataTable being the DataTable in question
What I tried to do so far was:
$Types = $DataTable.Columns | Select-Object -Property DataType
$Columns= $DataTable.Columns | Select-Object -Property ColumnName
$Index = $DataTable.Columns | Select-Object -Property ordinal
$AllowNull = $DataTable.Columns | Select-Object -Property AllowDbNull
Then painfully going through each array, pick up individual items and put them in my multi-dimensional array $TableMetaData.
I read in the documentation of Select-Object and it seems to me that only 1 property can be selected at 1 time? I think I should be able to do all the above more elegantly and store the information in $TableMetaData.
Is there a way to easily pick up multiple properties and put them in a multi-dimensional array in 1 swoop?

I read the documentation of Select-Object and it seems to me that only 1 property can be selected at 1 time?
This is not true, Select-Object can take any number of arguments to the -Property parameter
$ColumnInfo = $DataTable.Columns | Select-Object -Property DataType,ColumnName,ordinal,AllowDbNull
Now $ColumnInfo will contain one object for each column, having all 4 properties.
Rather than using a multi-dimensional array, you should consider using a hashtable (#{}, an unordered dictionary):
$ColumnInfo = $DataTable.Columns | ForEach-Object -Begin { $ht = #{} } -Process {
$ht[$_.ColumnName] = $_
} -End { return $ht }
Here, we create an empty hashtable $ht (the -Begin block runs just once), then store each column object in $ht using the ColumnName as the key, and finally return $ht, storing it in $ColumnInfo.
Now you can reference metadata about each column by Name:
$ColumnInfo.Column2
# or
$ColumnInfo["Column2"]

One easy way to do this is to create an "empty" variable with Select-Object. Here is a sample command:
$DataTableReport = "" | Select-Object -Property DataType, ColumnName, ordinal, AllowDbNull
Then, link the $DataTableReport to the $Types, $Columns, $Index, and the $AllowNull properties as shown below:
$DataTableReport.Types = $DataTable.DataType
$DataTableReport.Columns = $DataTable.ColumnName
$DataTableReport.Index = $DataTable.ordinal
$DataTableReport.AllowNull = $DataTable.AllowDbNull
Finally, call the DataTableReport variable.
$DataTableReport # will display all the results in a tabular form.

Related

Powershell - Group-Object PSObject with multiple properties

I'm trying to take an array of PSObjects similar to
#{BakId=27; Name=DB_A; Lsn=123; File=A_01; Size=987}
#{BakId=28; Name=DB_B; Lsn=456; File=B_01; Size=876}
#{BakId=28; Name=DB_B; Lsn=456; File=B_02; Size=765}
#{BakId=28; Name=DB_B; Lsn=456; File=B_03; Size=654}
And create a new grouped object that removes redundant header info.
BakId Lsn Name Files
27 123 DB_A {#{File=A_01.bak;Size=777}}
28 456 DB_B {#{File=B_01.bak;Size=888}, #{File=B_02.bak;Size=999}, ...}
I tried using group-object but can only get it to work for one property. (all grouped properties go into Group.Name as a a string of comma separated values.)
This is the best I've come up with, but feels hacky.
$list | Group-Object -Property BakId | % {
$BakId = $_.Name
$Lsn = $_.Group[0].Lsn # <--- is there a better way than this?
$Name = $_.Group[0].Name # <--- Ditto
$Files = $_.Group | Select-Object -Property SequenceNumber, Size
Write-Output (New-Object -TypeName PSObject -Property #{ BakId=$BakId;Files = $Files })
}
Is there a better way?
Thanks
You can simplify the approach to constructing the output objects by using a single Select-Object call with calculated properties, and, relying on the order of the grouping properties, access their group-specific values via the .Values collection:
$list | Group-Object -Property BakId, Lsn, Name |
Select-Object #{n='BakId'; e={ $_.Values[0] }},
#{n='Lsn'; e={ $_.Values[1] }},
#{n='Name'; e={ $_.Values[2] }},
#{n='Files'; e={ $_.Group | Select-Object File, Size }}
Note:
$_.Values[<ndx>] takes the place of $_.Group[0].<name> in your approach; note that the latter only makes sense for the actual grouping properties, because any others will not be uniform throughout the group.
The .Values collection ([System.Collections.ArrayList]) on each group object output from Group-Object ([Microsoft.PowerShell.Commands.GroupInfo] instance) contains that group's shared grouping-property values as-is (original type), in the order specified.
By contrast, property .Name contains the stringified combination of all grouping-property values: a string containing a comma-separated list.
Unfortunately, such details are currently missing from Get-Help Group-Object.
If you wanted to avoid having to repeat the grouping properties (e.g., in preparation for writing a function wrapper; PSv3+):
$props = 'BakId', 'Lsn', 'Name'
$list | Group-Object -Property $props | ForEach-Object {
$propDefs = [ordered] #{}
foreach ($i in 0..($props.Length-1)) { $propDefs.[$props[$i]] = $_.Values[$i] }
$propDefs.Files = $_.Group | Select-Object File, Size
New-Object PSCustomObject -Property $propDefs
}

Issue with Powershell custom table

I'm trying to create a custom table based on two other tables (csv-imported) - some kind of a VLOOKUP, but I can't seem to find a solution. I've come up with the following (failing) code:
$DrawPlaces | select Module, Workplace, #{ Name = "IPaddress"; Expression = {$Workstations.workstation.where($_.WorkPlace -eq $Workstations.Workplace)}} -First 15
Both Drawplaces and $Workplaces are PSCustomObject. The result of this would then go to another variable.
I'm not even sure the logic or syntax is correct, but the result table has the IPaddress column empty. I've also tried with -match instead of -eq.
This doesn't make sense: $Workstations.workstation.where($_.WorkPlace -eq $Workstations.Workplace)
.where() requires a scriptblock parameter like .where({}).
Keeping in mind that inside the where-statement $_ is refering to the current object in the $workstations.workstation-loop, your where-statement is testing ex. $workstations.workstation[0].workplace -eq $workstations.workplace. Is that really what you want?
Are you trying to achieve this?
$DrawPlaces |
Select-Object -First 15 -Property #(
"Module",
"Workplace",
#{ Name = "IPaddress"; Expression = {
#Save the Workspace-value for the current object from $DrawPlaces
$wp = $_.WorkPlace;
#Find the workstation with the same workplace as $wp
$Workstations | Where-Object { $_.WorkPlace -eq $wp} | ForEach-Object { $_.Workstation }
}
}
)

PowerShell: How to change table headers in loop?

I want to collect data remotely and adapt the table headers later with the help of an xml-file. This should happen in a loop, looking like that:
foreach($tableheader in $table) {
$table.$tableheader = $xmlFile.$tableheader
}
Amongst others I tried the following:
$x = 0
$sitesonfig = Get-ConfigSite -AdminAddress localhost
foreach($Prop in ($siteconfig |get-member -MemberType Property | select -Property name))
{
$x += 1;
$siteconfig = $siteconfig | Select-Object * | format-table #{l="Smile$x";e={$_.$Prop}}
}
Yes, I know this looks silly, but I've got really no idea, how to change the headers one by one without listing each time all the other headers, too.
One possibility is to use a loop to create the header map that you pass into Format-Table.
Here is your second example modified to demonstrate this concept. You should be able to adapt this to grab the header info from your XML file.
$x = 0
$siteconfig = Get-ConfigSite -AdminAddress localhost
$headerMap = #()
foreach($Prop in ($siteconfig |get-member -MemberType Property | select -ExpandProperty name))
{
$x += 1;
$headerMap += #{
l="Smile$x";
e={ $_.$Prop }.GetNewClosure()
}
}
$siteconfig | Format-Table $headerMap
Important Points
Select -Property name needed to be changed to Select -ExpandProperty name. The reason for this is that Select-Object in PowerShell will return an object filtered down to the selected member but you need a string for grabbing the property value by name. The -ExpandProperty parameter will expand this to be the string value instead.
The expression block needs GetNewClosure() called on it to capture the value of $Prop at the time of script block creation versus at the time of calling. This will probably be a little confusing if you are new to the concept of closures and PowerShell's scoping rules. Without this, due to PowerShell's scoping rules, $Prop will evaluate to the value of $Prop at the time it is used by Format-Table. By calling GetNewClosure(), the value of $Prop is captured when GetNewClosure() is called which is what we want in this case.

Return object from array with highest value

I want to return an object from an array who's property has the highest value. Currently I am doing the following
Get-VM | Sort-Object -Property ProvisionedSpaceGB | Select-Object -Last 1
This works but is inefficient. I don't need the entire array sorted, I just need the object with largest value. Ideally I would use something like
Get-VM | Measure-Object -Property ProvisionedSpaceGB -Maximum
but this only returns the value of the object property, not the entire object. Is there a way to have measure-object return the base object?
Not directly. Measure-Object is intended to be an easy way to grab such values, not their input objects. You could get the maximum from Measure-Object and then compare against the array, but it takes a few steps:
$array = Get-VM
$max = ($array | measure-object -Property ProvisionedSpaceGB -maximum).maximum
$array | ? { $_.ProvisionedSpaceGB -eq $max}
You could also forgo Measure-Object entirely and iterate through the set, replacing the maximum and output as you go.
$max = 0
$array | Foreach-Object
{
if($max -le $_.ProvisionedSpaceGB)
{
$output = $_
$max = $_.ProvisionedSpaceGB
}
}
$output
This is a little dirtier so as to always return a single value. It would need a minor adjustment if you were to reuse it in a case where there may be multiple values that have the same maximum (filesize lengths when using Get-ChildItem, for example). It will replace $output with the latter iterate in a case where two or more objects have the same value for ProvisionedSpaceGB. You could turn $output into a collection easily enough to fix that.
I prefer the former solution myself, but I wanted to offer a different way to think about the problem.
You can use this:
$array = Get-VM | Sort-Object -Property ProvisionedSpaceGB -Descending
$array[0]

Powershell Select-Object from array not working

I am trying to seperate values in an array so i can pass them to another function.
Am using the select-Object function within a for loop to go through each line and separate the timestamp and value fields.
However, it doesn't matter what i do the below code only displays the first select-object variable for each line. The second select-object command doesn't seem to work as my output is a blank line for each of the 6 rows.
Any ideas on how to get both values
$ReportData = $SystemStats.get_performance_graph_csv_statistics( (,$Query) )
### Allocate a new encoder and turn the byte array into a string
$ASCII = New-Object -TypeName System.Text.ASCIIEncoding
$csvdata = $ASCII.GetString($ReportData[0].statistic_data)
$csv2 = convertFrom-CSV $csvdata
$newarray = $csv2 | Where-Object {$_.utilization -ne "0.0000000000e+00" -and $_.utilization -ne "nan" }
for ( $n = 0; $n -lt $newarray.Length; $n++)
{
$nTime = $newarray[$n]
$nUtil = $newarray[$n]
$util = $nUtil | select-object Utilization
$util
$tstamp = $nTime | select-object timestamp
$tstamp
}
Let me slightly modify the processing code, if it will help.
$csv2 |
Where-Object {$_.utilization -ne "0.0000000000e+00" -and $_.utilization -ne "nan" } |
Select-Object Utilization,TimeStamp
It will produce somewhat different output, but that should be better for working with.
The result are objects with properties Utilization and TimeStamp. You can pass them to the another function as you mention.
Generally it is better to use pipes instead of for loops. You don't need to care about indexes and it works with arrays as well as with scalar values.
If my updated code won't work: is the TimeStamp property really filled with any value?