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.
Related
I have this code
function get-data()
{
$rec=[PSCustomObject]#()
$DLGP = "" | Select "Name","Grade","Score"
foreach($record in $data)
{
$DLGP.Name=$record.name
$DLGP.Grade=$record.grade
$DLGP.Score=$record.score
$rec += $DLGP
}
return $rec
}
$mydata=get-data
$mydata | Export-Csv -Path $outputPath -NoTypeInformation
The problem is, the data is not exported in the order that I have added it to $rec
How can I get it exported in the order it was added?
Without even using your function, a simple
$mydata = $data | select Name,Grade,Score
would yield the desired result.
Your function is IMO overcomplicated:
$Data = #"
Name,Grade,Score,Dummy
Anthoni,A,10,v
Brandon,B,20,x
Christian,C,30,y
David,D,40,z
"# | ConvertFrom-Csv
function get-data() {
ForEach($record in $data) {
[PSCustomObject]#{
Name =$record.name
Grade=$record.grade
Score=$record.score
}
}
}
$mydata=get-data
$mydata # | Export-Csv -Path $outputPath -NoTypeInformation
Returns here the pretty same order:
Name Grade Score
---- ----- -----
Anthoni A 10
Brandon B 20
Christian C 30
David D 40
LotPings' helpful answer provides effective solutions.
As for what you tried:
By constructing only a single [pscustomobject] instance outside the loop:
$DLGP = "" | Select "Name","Grade","Score"
and then only updating that one instance's properties in each loop iteration:
$DLGP.Name=$record.name
# ....
you effectively added the very same [pscustomobject] instance multiple times to the result array instead of creating a distinct object in each iteration.
Since the same object was being updated repeatedly, that object ended up having the properties of the last object in the input collection, $data.
As an aside:
[PSCustomObject] #() is effectively the same as #(): the [PSCustomObject] is ignored and you get an [object[]] array.
To type the array as one containing [PSCustomObject] instances, you'd have to use an array-typed cast: [PSCustomObject[]] #().
However, given that instances of any type can be cast to [PSCustomObject] - which is really the same as [psobject] - this offers no type safety and no performance benefit.
Also, since your $rec variable isn't type-constrained (it is defined as $rec = [<type>] ... rather than [<type>] $rec = ...) and you're using += to "add to" the array (which invariably requires creation of a new instance behind the scenes), $rec would revert to an [object[]] array after the first += operation.
I'm confused at how PowerShell treats Hashtable keys versus object properties.
Consider:
$list = #{
'one' = #{
name = 'one'
order = 80
};
'two' = #{
name = 'two'
order = 40
};
'twotwo' = #{
name = 'twotwo'
order = 40
};
'three' = #{
name = 'three'
order = 20
}
}
$list.Values|group-object { $_.order }
$list.Values|group-object -property order
The first Group-Object gives me what I expect (three groups), the second one does not (one big group). Clearly Hashtable keys are not object properties, but syntactically they are referenced in the same manner (var.name).
What is that second group-object actually doing?
What does it think the 'order' property is?
This is understandable confusion, but as you said, hashtable keys are not object properties.
It can be tempting to treat them the same (and sometimes that works), but this is a situation where it definitely won't.
And part of the reason is that you're using a different cmdlet, not the direct language semantics.
Hashtables can use dot notation for their keys but the keys are not properties, and when you use the -Property parameter of Group-Object, you are looking for properties specifically, not just "anything you can access with a dot".
The alternative form of that parameter that takes a scriptblock, as you saw, is code that will be executed and so it's whatever value that block returns that will be grouped on.
To more directly answer your questions:
What is that second group-object actually doing?
It's looking for a property (specifically) on the current object (which is a hashtable).
If you want to see what the properties on one of those objects looks like, try this:
$list.Values[0].PSObject.Properties | ft
What does it think the 'order' property is?
It doesn't think it's anything; it looks for a property with that name and if it finds one it uses that value; otherwise it uses $null.
You'll get the same result with:
$list.Values | group -Property FakeProp
or
$list.Values | group -Property { $null }
Addressing your question in the comment:
Is there any way to know "when you should" and "when you shouldn't",
Should I just defer to using script blocks, When is -Property usage
preferred over { ... }?
-Property (without a scriptblock) is preferred whenever the value you want to group is available as a direct property of the object being inspected. If it's a property of a property, or some calculated value, or a hashtable key/value, or anything else, use a scriptblock. I'll call those "complex values".
If the complex value is useful, you may want to add it as an actual property of the object itself to encapsulate it; then you can reference it directly. This example isn't really appropriate for your hashtable situation but consider objects that represent people. They have 2 properties: Name and DateOfBirth.
$people is an array of these objects, and you want to group people by age (please ignore my inaccurate age-determining code).
$people | Group-Object -Property { ([DateTime]::Now - $_.DateOfBirth).Days / 365 -as [int] }
That's ok if you never need to know the age again; of course that's unlikely and also this looks a bit messy. It should be more immediately clear that you want to "group by age".
Instead, you can add your own (calculated) Age property to the existing objects with Add-Member:
$people |
Add-Member -MemberType ScriptProperty -Name Age -Value {
([DateTime]::Now - $this.DateOfBirth).Days / 365 -as [int]
} -Force
From here on out, each object in the $people array has an Age property that is calculated based on the value of the DateOfBirth property.
Now you can make your code clearer:
$people | Group-Object -Property Age
Again this doesn't really address your hashtable issue; the truth is they don't work that well for grouping. If you're going to do a lot of grouping with them, and you don't really need hashtables, make them into objects:
$objs = $list.Values |
ForEach-Object -Process {
New-Object -TypeName PSObject -Property $_ # takes a hashtable
}
or
$objs = $list.Values |
ForEach-Object -Process {
[PSCustomObject]$_ # converts a hashtable to PSObject
}
Then:
$objs | Group-Object -Property Order
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.
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 }
}
}
)
Using POSH 3.0, as a quick example, if I populate an array and try to display the results with FT -Autosize, I can't access the variable after that anymore. There are quite a few properties to display so -autosize is important to utilize the whole width.
$x | Select #{E={$_.AID};L="Action"},#{E={$_.ID};L="SSN"}... | `
FT -AutoSize
}
#Now the object is no longer accessible
$x | ForEach {
$_
}
So my goal is to view the results in a nicely formatted way, while also keeping the values accessible in the variable. I had tried to make a copy of the variable, one to view the results and the other to proceed with the values in the variable, but same thing.
Anyone have any good thoughts about this? Thanks!
Better example:
[array]$recs= Invoke-Sqlcmd -ServerInstance $server `
-Database $db `
-InputFile 'c:\sqlquery.sql'
$arrRecs = #()
ForEach ($record in $recs) {
$newObjectRecs = New-Object PSCustomObject -Property #{
"todaysDate" = $_.TodaysDate
"cats" = $record.cats
"dogs" = $record.dogs
"surname" = $record.surname
"givenName" = $record.givenName }
$arrRecs += $newObjectRecs
}
$arrRecs | `
Select #{E={$_.cats};L="cats"},#{E={$_.dogs};L="dogs"},#{E={$_.TodaysDate};L="Date"},surname,givenName | `
Format-Table -AutoSize
$arrRecs | ForEach {
$_
Write-Host "---------"
}
$arrRecs
As an update to a comment earlier, if I iterate through the ForEach statement below when trying to display to the screen, it won't display the current item in the pipe, but when it gets to the end of the array, it displays all the items that were in the array.
If I comment out Format-Table -Autosize, it displays the current item in the pipe as expected.
As a kind of hack-around (yes, I made up a hyphenated word, but I liked it so you have to live with it) you could try pumping $x through a ForEach and creating a custom object for each record in $x. Something like:
$x | %{[PSCustomObject][Ordered]#{
"Action"=$_.AID
"SSN"=$_.ID
"Taco"=$_.FishBeefOrChicken
"More"=$_.OtherProperties
}}| FT -Auto
While the script you show should not alter $x at all, and $x should be both intact and accessible afterwards, sometimes all it takes to work through a problem is a new perspective and a different method of doing things to find the error that's eluding us.