Adding members to PSCustomObject variable - understanding what's going on - powershell

I am trying to get my head around what is going on here, and I was wondering if anyone knew of a resource that might point me in the right direction, or could explain it a bit for me.
I am trying to create a PSCustomObject variable, and then members to it, like this:
$myObject += [PSCustomObject]#{
FirstName = 'Bill'
LastName = 'Bobbins'
Age = '30'
}
$myObject += [PSCustomObject]#{
FirstName = 'Ben'
LastName = 'Bobbins'
Age = '40'
}
So the first bit of code executes fine, but the second bit results in an error "Method invocation failed because [System.Management.Automation.PSObject] does not contain a method named 'op_Addition'." Also, if I pipe $myObject to get-member, I can see that $myObject is TypeName: System.Management.Automation.PSCustomObject.
Now, if I set $myObject to be an empty array first, and then try to add members, I am successful. This code works without error:
$myObject=#()
$myObject += [PSCustomObject]#{
FirstName = 'Bill'
LastName = 'Bobbins'
Age = '30'
}
$myObject += [PSCustomObject]#{
FirstName = 'Ben'
LastName = 'Bobbins'
Age = '40'
}
If I now pipe $myObject to get-member, I still get TypeName: System.Management.Automation.PSCustomObject. So my question is, why am I allowed to add multiple members to $myObject in the second example, but not in the first, when the data type is the same for both examples?
Any help is muchly appreciated!
Thanks

The issue here is with how Get-Member/the pipeline works - it's tripped me up before!
This will unroll the array, and give you the type of each element as it passes it across the pipeline:
$myObject | Get-Member
This will pass the whole object, and correctly give you the type as System.Object[]
Get-Member -InputObject $myObject
You can test this out by for example adding $myObject += "test string" to the end of your code and trying to get members both ways. The first will return both PSObject and String types.
Sidepoint: The $myObject = #() line can be avoided by specifying you are creating an array the first time you declare $myObject. Example:
[array]$myObject = [PSCustomObject]#{
[PSCustomObject[]]$myObject = [PSCustomObject]#{

Related

Strongly type PS custom object properties

I have been using hash tables to return complex data from functions, and it has worked well, but I would like to have the keys strongly typed, since I have booleans, strings, arrays of strings, ordered dictionaries and such in the returned hash tables. So, given something like this
[hashtable]$hashtable = #{
one = 1
two = "two"
}
I have the issue that the type of each key is weakly typed. I want to basically do this
[hashtable]$hashtable = #{
[int]one = 1
[string]two = "two"
}
But that's not valid code. So I thought I could do this
[psCustomObject]$object = [psCustomObject]#{
[int]one = 1
[string]two = "two"
}
But that's invalid too. I find this a bit ugly, and it also doesn't work
$object = New-Object -typeName:PSObject
$object | Add-Member -memberType:int -name:'one' -value:1
$object | Add-Member -memberType:string -name:'two' -value:'two'
So, am I SOL and there is no way, or no elegant way, to create a custom object with strongly typed properties?
Inside the hashtable literal you'll want to type-cast the value expression instead:
PS C:\> $object = [PSCustomObject]#{
one = [int]1
two = [string]"two"
}
PS C:\> $object|gm -MemberType NoteProperty
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
one NoteProperty int one=1
two NoteProperty string two=two
This will, however, not prevent anyone from storing a non-integer or non-string in any of the properties - psobject property are simply not strongly typed.
If you want type safety for properties you'll need to create a new type with the class keyword:
class MyOneTwo
{
[int]$One
[string]$Two
MyOneTwo(){}
MyOneTwo([int]$one, [string]$two){
$this.One = $one
$this.Two = $two
}
}
# Create instances with ::new(), New-Object or a cast:
$object = [MyOneTwo]::new(1,"2")
$object = New-Object MyOneTwo -Property #{ One = 1; Two = "2" }
$object = [MyOneTwo]#{ One = 1; Two = "2" }

How to to have an array as a return type on a custom PSObject

I've got the following:
# Compare the 2 lists and return the ones that exist in the top and children (meaning they're redundant).
$redundantUsers = $usersAssignedToThisGroup | where { $usersAssignedToGroupsInThisGroup -contains $_ }
# Build the results to output
$results += New-Object PSObject -property #{
group = $group.name #group assigned out of scope
users = #($redundantUsers)
}
I'm expecting to be able to call my script like:
$users = ./MyScript -myParam "something" | Select users
Then I'm expecting to be able to type $users[1] in the console and get the first element of that array.
But instead, I'm having to do $users[0].users[1].
How do I change my data and or call to do what I want?
I'm essentially just trying to let the script return the data in a useful way.

powershell hashtable key versus object property

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

Split hastable at key/value pair

How can I split a hashtable starting from a specific key/value pair?
I have hashtable like the following, just longer:
Name Value
---- -----
Name Alpha
Age 2
Position Trick
Start date 01-01-31
End date Unknown
Name Corax
Age 21
Position Sneak
Earnings 40'000
End Date Unknown
Name Horus
Age 22
Position Dead
Why Heresy
End date 03-30-30
I tried Group-Object but it failed.
I particularly wanted to separate it by Name and everything aside from Name, Age and Position are not consistent.
My actual issue is that I want to parse the hashtable for the Name and Age when Why = Heresy, and unfortunately, the original source of the data is a list of strings, which is the reason why I convert it to a hashtable.
Hashtables are not ordered, so you can't rely on a concept of "before" and "after". If you know the specific names of one complete set of keys, then you can loop through the hashtable and build two new ones, so if you wanted one hashtable to contain the Name, Age, and Position, and the other to contain everything else, you can do something like this:
$New1 = #{}
$New2 = #{}
$KeysGroup1 = #('Name','Age','Position') # This could just be one value
$MyHashTable.GetEnumerator().ForEach({
if ($_.Key -in $KeysGroup1) {
$New1[$_.Key] = $_.Value
} else {
$New2[$_.Key] = $_.Value
}
})
You can use an ordered dictionary if order is important for you. You can use a shortcut to create a literal ordered dictionary by preceding a literal hashtable with the [ordered] type accelerator:
$myOrdered = [ordered]#{ a = 1; z = 5; g = 2 }
From there you could do a similar approach to above that relies on order.
To create an ordered dictionary that isn't based on a literal:
$myOrdered = New-Object System.Collections.Specialized.OrderedDictionary
# or in PowerShell v5
$myOrdered = [System.Collections.Specialized.OrderedDictionary]::new()
$myOrdered.Add('key','value')
Edit based on comments
It sounds more like what you have is an array of hashtables and you now want to go about filtering these.
A [hashtable] can (and is often) used as a sort of proto-object, and it can be very useful for that because it often supports the same syntax, and it has built-in literal support.
But you're starting to run into their limits, and at this point I think you want to be dealing with an array of objects and not an array of hashtables.
Luckily, there are really easy ways to create objects in PowerShell right from a hashtable:
$obj = New-Object -TypeName PSObject -Property $myHash
$obj = [PSCustomObject]$myHash
$objArray = $myHashArray.ForEach({[PSCustomObject]$myHash})
Once you've got your array of objects, the real fun begins:
$heretics = $objArray.Where({$_.Why -eq 'Heresy'})
You'll notice I didn't even bother filtering out the other properties here. You shouldn't, until you really need to. Then you can use Select-Object or just access the properties you need. So for display purposes you might just do:
$heretics | Format-Table Name,Age
There's more stuff you can do with an object that you can't with hashtables, like add special types of properties:
$objArray | Add-Member -MemberType ScriptProperty -Name IsHeretic -Value { $this.Why -eq 'Heresy' } -Force
$heretics = $objArray.Where({$_.IsHeretic})
So, in the end; I figured something out, Thanks a lot to #briantist for guidance to the right direction.
My solution code is:
$startobj = (0..($hastable.count -1)) | where {$hashtable[$_].keys -like "*Name*"}
$endobj = (0..($hashtable.count -1)) | where {$hashtable[$_].keys -like "*End date*"}
$objindex = (0..($hashtable.count -1))
$numberofobjindex = 0..($hashtable.Where({$_.keys -like "*Name*"}).count - 1)
$hashtableparsed = foreach ($numba in $numberofobjindex) {
$Conv2data = $hashtable[($startobj[$numba]..$endobj[$numba])]
[PSCustomObject] #{
Name = $Conv2data.Name
Age = $Conv2data.Age
Why = $Conv2data.Why
}
}
$hashtableparsed
I realized that the hashtable data I was presented with had a repeating pattern with Name at the start of each cycle, and End date at the end.
So I basically did an indexing of the hashtable, and marked and indexed all instances where the cycle would start and end.
I then counted the cycles, and FOR EACH cycle captured the data of hashtable data in the lines particularly of that cycle and turned it into a PSCustomObject

Powershell: Child Property Deflation

Not sure if "deflating" is the right term, but I was wondering if there's a nicer way to specify that you only want to return a selection of an object's children's properties whilst using Select-Object? I'm using Powershell v5.
Here's a fully working example - the $test variable is hard coded here for demonstration purposes; in my use-case, the $test array is being returned from an API. I want to remove the name property from the testChild object during the projection.
The only way I could figure out how to do this is by using a foreach loop whilst piping into $result, then re-creating each child object and setting only the required properties, but it feels like powershell should have nicer way to do this.
$test = #(
[PSCustomObject]#{
testProp = "test1"
testChild = [PSCustomObject]#{
id = 1
name = "test name 1"
}
},
[PSCustomObject]#{
testProp = "test2"
testChild = [PSCustomObject]#{
id = 2
name = "test name 2"
}
}
)
$result = $test | select testProp, testChild | % {
$_.testChild = [PSCustomObject]#{
id = $_.testChild.id
}
$_
}
$result
This outputs:
testProp testChild
-------- ---------
test1 #{id=1}
test2 #{id=2}
Which is exactly what I want. I'd be nice if I could do something like this:
$result = $test | select testProp, testChild.id
Bearing in mind that I do not want flat properties returned - I want the original object with some properties removed / only the properties I specify included.
You can do this with Select-Object:
$result = $test | select testProp, #{
Name = 'id'
Expression = { $_.testChild.id }
}
Basically, Select-Object allows you to define your own properties, where the value is determined by an expression (a [ScriptBlock]), so you can run any code you want to determine the value. Within the block, $_ refers to the "current object" the same way it would in a ForEach-Object call.