Let's say you have two objects that are identical (meaning they have the same properties and the same values respectively).
How do you test for equality?
Example
$obj1 & $obj2 are identical
Here's what I've tried:
if($obj1 -eq $obj2)
{
echo 'true'
} else {
echo 'false'
}
# RETURNS "false"
if(Compare-Object -ReferenceObject $obj1 -DifferenceObject $obj2)
{
echo 'true'
} else {
echo 'false'
}
# RETURNS "false"
Edit
This is not identical
You can compare two PSObject objects for equality of properties and values by using Compare-Object to compare the Properties properties of both PSObjectobjects. Example:
if ( -not (Compare-Object $obj1.PSObject.Properties $obj2.PSObject.Properties) ) {
"object properties and values match"
}
else {
"object properties and values do not match"
}
If you want it in a function:
function Test-PSCustomObjectEquality {
param(
[Parameter(Mandatory = $true)]
[PSCustomObject] $firstObject,
[Parameter(Mandatory = $true)]
[PSCustomObject] $secondObject
)
-not (Compare-Object $firstObject.PSObject.Properties $secondObject.PSObject.Properties)
}
I'd suggest using Compare-Object for this task:
Function Test-Objects
{
Param(
[Parameter(Mandatory,Position=0)]
[PSCustomObject]$Obj1,
[Parameter(Mandatory,Position=1)]
[PSCustomObject]$Obj2
)
[Void](Compare-Object -ReferenceObject $Obj1.PSObject.Properties -DifferenceObject.PSObject.Properties $Obj2 -OutVariable 'Test')
## Tests whether they are equal, no return = success
If (-not $Test)
{
$True
}
Else
{
$False
}
}
PS C:\> $Obj1 = [PSCustomObject]#{
Property1 = 'Value1'
Property2 = 'Value2'
Property3 = 'Value3'
Property4 = 'Value4'
Property5 = 'Value5'
}
PS C:\> $Obj2 = [PSCustomObject]#{
Property1 = 'Value1'
Property2 = 'Value2'
Property3 = 'Value3'
Property4 = 'Value4'
Property5 = 'Value5'
}
PS C:\> Test-Objects $Obj1 $Obj2
True
PS C:\> $Obj2 | Add-Member -MemberType 'NoteProperty' -Name 'Prop6' -Value 'Value6'
PS C:\> Test-Objects $Obj1 $Obj2
False
If you'd like to test for equality for every object property, one at a time, in order to compare and contrast two objects and see which individual pieces are different, you can use the following function, adapted from this article on how to compare all properties of two objects in Windows PowerShell
Function Compare-ObjectProperties {
Param(
[PSObject]$leftObj,
[PSObject]$rightObj
)
$leftProps = $leftObj.PSObject.Properties.Name
$rightProps = $rightObj.PSObject.Properties.Name
$allProps = $leftProps + $rightProps | Sort | Select -Unique
$props = #()
foreach ($propName in $allProps) {
# test if has prop
$leftHasProp = $propName -in $leftProps
$rightHasProp = $propName -in $rightProps
# get value from object
$leftVal = $leftObj.$propName
$rightVal = $rightObj.$propName
# create custom output -
$prop = [pscustomobject] #{
Match = $(If ($propName -eq "SamAccountName" ) {"1st"} Else {
$(If ($leftHasProp -and !$rightHasProp ) {"Left"} Else {
$(If ($rightHasProp -and !$leftHasProp ) {"Right"} Else {
$(If ($leftVal -eq $rightVal ) {"Same"} Else {"Diff"})
})
})
})
PropName = $propName
LeftVal = $leftVal
RightVal = $rightVal
}
$props += $prop
}
# sort & format table widths
$props | Sort-Object Match, PropName | Format-Table -Property `
#{ Expression={$_.Match}; Label="Match"; Width=6},
#{ Expression={$_.PropName}; Label="Property Name"; Width=25},
#{ Expression={$_.LeftVal }; Label="Left Value"; Width=40},
#{ Expression={$_.RightVal}; Label="Right Value"; Width=40}
}
And then use like this:
$adUser1 = Get-ADUser 'Grace.Hopper' -Properties *
$adUser2 = Get-ADUser 'Katherine.Johnson' -Properties *
Compare-ObjectProperties $adUser1 $adUser2
Couple Interesting Notes:
How to Test if Element Has Property
How to Get Property Value by Name
How to Create a Custom PS Object
How to Create a Nested Conditional / Ternary Operator
How to Format Table with fixed Widths
Attempted to Colorize output with VT Escape Sequences or Write-PSObject, but couldn't get it to work with fixed column widths which took priority
Here's the function I used:
function Test-ObjectEquality {
param(
[Parameter(Mandatory = $true)]
$Object1,
[Parameter(Mandatory = $true)]
$Object2
)
return !(Compare-Object $Object1.PSObject.Properties $Object2.PSObject.Properties)
}
Examples:
PS C:\> $obj1 = [pscustomobject] #{ 'a' = '5'; 'b' = 7; };
PS C:\> $obj2 = [pscustomobject] #{ 'a' = '5'; 'b' = 7; };
PS C:\> Test-ObjectEquality $obj1 $obj2
True
PS C:\> $obj2 = [psobject] #{ 'a' = '5'; 'b' = 7; };
PS C:\> Test-ObjectEquality $obj1 $obj2
False
PS C:\> $obj2 = New-Object -TypeName PSObject -Property #{ 'a' = '5'; 'b' = 7; };
PS C:\> Test-ObjectEquality $obj1 $obj2
True
PS C:\> $obj2 = [pscustomobject] #{ 'c' = '6'; 'b' = 7; };
PS C:\> Test-ObjectEquality $obj1 $obj2
False
PS C:\> $obj2 = [pscustomobject] #{ 'a' = '5'; 'b' = 8; };
PS C:\> Test-ObjectEquality $obj1 $obj2
False
PS C:\> $obj2 = [pscustomobject] #{ 'a' = '5'; 'b' = 7; c = 8 };
PS C:\> Test-ObjectEquality $obj1 $obj2
False
PS C:\> $obj2 = [pscustomobject] #{ 'a' = '5'; 'b' = '7'; };
PS C:\> Test-ObjectEquality $obj1 $obj2
False
I certainly believe it's possible for this to miss things; however, if you look at what's in Properties you can see what's being compared for every property on an object:
PS C:\> $obj1.PSObject.Properties | Select-Object -First 1
MemberType : NoteProperty
IsSettable : True
IsGettable : True
Value : 5
TypeNameOfValue : System.String
Name : a
IsInstance : True
It's not often that I've cared about more than the MemberType, Name, TypeNameOfValue, or Value of an object's properties.
Also, note that if you really need to, you can compare .PSObject.Members instead of .PSObject.Properties. That will compare properties and methods, although you're only comparing the method calls and not the method definitions.
I wrote a function that checks for exact equality:
function Global:Test-IdenticalObjects
{
param(
[Parameter(Mandatory=$true)]$Object1,
[Parameter(Mandatory=$true)]$Object2,
$SecondRun=$false
)
if(-not ($Object1 -is [PsCustomObject] -and $Object2 -is [PsCustomObject))
{
Write-Error "Objects must be PsCustomObjects"
return
}
foreach($property1 in $Object1.PsObject.Properties)
{
$prop1_name = $property1.Name
$prop1_value = $Object1.$prop1_name
$found_property = $false
foreach($property2 in $Object2.PsObject.Properties)
{
$prop2_name = $property2.Name
$prop2_value = $Object2.$prop2_name
if($prop1_name -eq $prop2_name)
{
$found_property = $true
if($prop1_value -ne $prop2_value)
{
return $false
}
}
} # j loop
if(-not $found_property) { return $false }
} # i loop
if($SecondRun)
{
return $true
} else {
Test-IdenticalObjects -Object1 $Object2 -Object2 $Object1 -SecondRun $true
}
} # function
Related
Suppose you have the following function:
Function Test-Function {
Param (
[String[]]$ComputerNames = #($env:COMPUTERNAME, 'PC2'),
[String]$PaperSize = 'A4'
)
}
Get-DefaultParameterValuesHC -Path 'Test-Function'
Now to get the default values in the function arguments one can use AST:
Function Get-DefaultParameterValuesHC {
[OutputType([hashtable])]
Param (
[Parameter(Mandatory)]$Path
)
$ast = (Get-Command $Path).ScriptBlock.Ast
$selectParams = #{
Property = #{
Name = 'Name';
Expression = { $_.Name.VariablePath.UserPath }
},
#{
Name = 'Value';
Expression = { $_.DefaultValue.Extent.Text -replace "`"|'" }
}
}
$result = #{ }
$defaultValueParameters = #($ast.FindAll( {
$args[0] -is [System.Management.Automation.Language.ParameterAst] }
, $true) |
Where-Object { $_.DefaultValue } |
Select-Object #selectParams)
foreach ($d in $defaultValueParameters) {
$result[$d.Name] = foreach ($value in $d.Value) {
$ExecutionContext.InvokeCommand.ExpandString($value)
}
}
$result
}
The issue here is that the argument for $ComputerNames is read as a string while it is actually an array of string.
Is there a way that PowerShell can covnert a string to an array? Or even better, read the value correctly in the first place?
You need to look deeper into the AST structure...
I recommend you to play around with this PowerShell: AST Explorer GUI:
For your specific example:
Function Test-Function {
Param (
[String[]]$ComputerNames = #($env:COMPUTERNAME, 'PC2'),
[String]$PaperSize = 'A4'
)
}
$FunctionDefinitionAst = (Get-Command 'Test-Function').ScriptBlock.Ast
$Body = $FunctionDefinitionAst.Body
$ParamBlock = $Body.ParamBlock
$CNParameter = $ParamBlock.Parameters | Where-Object { $_.Name.VariablePath.UserPath -eq 'ComputerNames' }
$DefaultValue = $CNParameter.DefaultValue
$DefaultValue.SubExpression.Statements.PipelineElements.Expression.Elements
VariablePath : env:COMPUTERNAME
Splatted : False
StaticType : System.Object
Extent : $env:COMPUTERNAME
Parent : $env:COMPUTERNAME, 'PC2'
StringConstantType : SingleQuoted
Value : PC2
StaticType : System.String
Extent : 'PC2'
Parent : $env:COMPUTERNAME, 'PC2'
It's a bit of a hackish solution but this is what I came up with to solve the issue of not returning an array of string:
Function Get-DefaultParameterValuesHC {
Param (
[Parameter(Mandatory)]$Path
)
$ast = (Get-Command $Path).ScriptBlock.Ast
$selectParams = #{
Property = #{
Name = 'Name';
Expression = { $_.Name.VariablePath.UserPath }
},
#{
Name = 'Value';
Expression = {
if ($_.DefaultValue.StaticType.BaseType.Name -eq 'Array') {
$_.DefaultValue.SubExpression.Extent.Text -split ',' |
ForEach-Object { $_.trim() -replace "`"|'" }
}
else {
$_.DefaultValue.Extent.Text -replace "`"|'"
}
}
}
}
$result = #{ }
$defaultValueParameters = #($ast.FindAll( {
$args[0] -is [System.Management.Automation.Language.ParameterAst] }
, $true) |
Where-Object { $_.DefaultValue } |
Select-Object #selectParams)
foreach ($d in $defaultValueParameters) {
$result[$d.Name] = foreach ($value in $d.Value) {
$ExecutionContext.InvokeCommand.ExpandString($value)
}
}
$result
}
ExpandPath will only expand variables inside strings. To get the actual values (and not just the definition) you could use Invoke-Expression:
function Get-DefaultParameterValuesHC {
[OutputType([hashtable])]
Param (
[Parameter(Mandatory)]$Path
)
$result = #{ }
(Get-Command $Path).ScriptBlock.Ast.Body.ParamBlock.Parameters | where {$_.DefaultValue} | foreach {
$result[$_.Name.VariablePath.UserPath] = Invoke-Expression $_.DefaultValue.Extent.Text
}
$result
}
NOTE: This will actually invoke the default declaration, so any logic inside that expression will be run, just as when running the function. For example, a default value of $Parameter = (Get-Date) will always invoke Get-Date.
It would be preferable to create a function, that only returns the default declarations, and let the user decide to invoke the expression or not:
function Get-DefaultParameterDeclarations {
Param (
[Parameter(Mandatory, Position = 0)]
[string]$CommandName
)
(Get-Command $CommandName).ScriptBlock.Ast.Body.ParamBlock.Parameters | where {$_.DefaultValue} |
foreach {
[PSCustomObject]#{
Name = $_.Name.VariablePath.UserPath
Expression = $_.DefaultValue.Extent.Text
}
}
}
# get the declarations and (optionally) invoke the expressions:
Get-DefaultParameterDeclarations 'Test-Function' |
select Name, #{n="DefaultValue"; e={Invoke-Expression $_.Expression}}
I'm unable to display the calculated property column called Logon Type which is translated from a hash table.
The script below is working fine, but I just need to translate the raw value number into a more meaningful description.
function Get-LogonEvents {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[Alias('ServerName', 'Server', 'Name')]
[string[]]$ComputerName,
[Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
[PSCredential]$Credential,
[Parameter()]
[ValidateSet("Service", "Interactive", "RemoteInteractive", "NetworkCleartext", "CachedInteractive", "Unlock", "NewCredentials", "Network", "*")]
[string[]]$LogonType = #("Interactive", "RemoteInteractive", "CachedInteractive"),
[string]$UserName,
[Parameter()]
[switch]$Oldest,
[Parameter()]
[int64]$MaxEvents,
[Parameter()]
[datetime]$StartTime = (Get-Date 1/1/1900),
[Parameter()]
[datetime]$StopTime = (Get-Date 1/1/2100)
)
Begin {
Function ParseEventMessage {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline = $true)]
$obj
)
Begin {
$defaultDisplaySet = 'TimeCreated', 'MachineName', 'TargetDomainName', 'TargetUserName'
$defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet(‘DefaultDisplayPropertySet’, [string[]]$defaultDisplaySet)
$PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]#($defaultDisplayPropertySet)
$myHash = #{ }
}
Process {
([xml]($obj.ToXml())).event.eventdata.data | ForEach-Object { $myHash[$PSItem.name] = $PSItem.'#text' }
New-Object -TypeName PSObject -Property $myHash | ForEach-Object {
$PSItem.PSObject.TypeNames.Insert(0, "EventLogRecord.XMLParse")
$PSItem | Add-Member MemberSet PSStandardMembers $PSStandardMembers -PassThru |
Add-Member -MemberType NoteProperty -Name TimeCreated -Value $obj.timecreated -PassThru |
Add-Member -MemberType NoteProperty -Name MachineName -Value $obj.MachineName -PassThru
}
}
}
$hashLogonType = #{
"Interactive" = "2"
"Network" = "3"
"Service" = "5"
"Unlock" = "7"
"NetworkCleartext" = "8"
"NewCredentials" = "9"
"RemoteInteractive" = "10"
"CachedInteractive" = "11"
}
$filter = #"
<QueryList>
<Query Id="0" Path="Security">
<Select Path="Security">
*[System[
(EventID='4624')
and TimeCreated[#SystemTime>='{0}' and #SystemTime<='{1}']
]
and EventData[
Data[#Name='LogonType'] and ({2})
{3}
]
]
</Select>
</Query>
</QueryList>
"#
}
Process {
foreach ($obj in $ComputerName) {
if ($UserName) {
$joinUserName = "and Data[#Name='TargetuserName'] and (Data='{0}')" -f $UserName
}
$joinLogonType = ($LogonType | ForEach-Object { $hashLogonType[$PSItem] }) -replace '^', "Data='" -replace '$', "'" -join " or "
$objFilter = $filter -f (Get-Date $StartTime -Format s), (Get-Date $StopTime -Format s), $joinLogonType, $joinUserName
$hashEventParm = #{
ComputerName = $obj
FilterXml = $objFilter
}
if ($Credential) { $hashEventParm['Credential'] = $Credential }
if ($MaxEvents) { $hashEventParm['MaxEvents'] = $MaxEvents }
$objFilter | Write-Verbose
Get-WinEvent #hashEventParm | ParseEventMessage
}
}
End { }
}
$TargetDomainNameException = #('Window Manager','Font Driver Host')
$exceptionRegex = $TargetDomainNameException -join "|"
Get-LogonEvents -ComputerName 'Localhost' -MaxEvents 10 |
Where-Object { ($_.TargetDomainName -notmatch $exceptionRegex) } |
Select-Object WorkstationName,
TargetUserName,
TargetDomainName,
Type,
LogonType,
#{n ='LogonType'; e={$hashLogonType[[string]$_.LogonType]}},
#{n = 'Logon Type'; e = {$hashLogonType["$($_.LogonType)"]}},
ProcessName,
IPAddress,
#{n="Host Name"; e={([System.Net.Dns]::GetHostByAddress($_.IPaddress).Hostname)}},
TimeCreated |
Out-GridView
Error:
I have modifiedthe Calculated property like:
#{n = 'Logon Type'; e = {$hashLogonType["$($_.LogonType)"]}},
Somehow it is still not displaying the column "Logon Type", however, the raw value on LogonType column still showing as 10, 3 ,etc...?
I see two problems.
$hashLogonType is defined inside the function and won't be available in the global scope.
The keys for $hashLogonType are by [string] not by [int].
If you're able to modify the original function, you might consider adding a property where the string value of LogonType is saved.
Otherwise, keep a copy of $hashLogonType in your variable scope with integers as keys, and base your calculated property on that.
The easiest way to get what you want is to create your own hash table and use it in your pipeline.
# Create a hash table for your own use in your variable scope.
$myHashTable = #{
2 = "Interactive"
3 = "Network"
5 = "Service"
7 = "Unlock"
8 = "NetworkCleartext"
9 = "NewCredentials"
10 = "RemoteInteractive"
11 = "CachedInteractive"
}
# Shim object.
$exampleObject = [PSCustomObject]#{
LogonType = 2
WorkstationName = "myHost.example.com"
}
# Modify your pipeline to use your hash table.
$exampleObject |
Select-Object -Property WorkstationName, LogonType, #{label="Logon Title";expression={$myHashTable[$_.LogonType]}}
PS> ./Answer 02.ps1
WorkstationName LogonType Logon Title
--------------- --------- -----------
myHost.example.com 2 Interactive
In principle, it is possible to modify the original function. But, I don't have any data to test with. Maybe Doug can help. He seems to have access to an event log.
You would have to do two things.
Add a hash table with integer keys in scope for ParseEventMessage(). For example, add the hash table to ParseEventMessage()'s Begin block.
Where it says
Add-Member -MemberType NoteProperty -Name MachineName -Value $obj.MachineName -PassThru
Add another property by extending that pipeline:
Add-Member -MemberType NoteProperty -Name LogonTitle -Value {$myHashTable[$_.LogonType]} -PassThru
Edit: Yes Mike is absolutely correct, the hashtable was defined inside the get-logonevents function and not used. I've moved it out and now it should work.
I think you should reverse the assignment of the hashtable. Either as an int or a string should work then. I did it like this and it worked fine.
function Get-LogonEvents {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[Alias('ServerName', 'Server', 'Name')]
[string[]]$ComputerName,
[Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
[PSCredential]$Credential,
[Parameter()]
[ValidateSet("Service", "Interactive", "RemoteInteractive", "NetworkCleartext", "CachedInteractive", "Unlock", "NewCredentials", "Network", "*")]
[string[]]$LogonType = #("Interactive", "RemoteInteractive", "CachedInteractive"),
[string]$UserName,
[Parameter()]
[switch]$Oldest,
[Parameter()]
[int64]$MaxEvents,
[Parameter()]
[datetime]$StartTime = (Get-Date 1/1/1900),
[Parameter()]
[datetime]$StopTime = (Get-Date 1/1/2100)
)
Begin {
Function ParseEventMessage {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline = $true)]
$obj
)
Begin {
$defaultDisplaySet = 'TimeCreated', 'MachineName', 'TargetDomainName', 'TargetUserName'
$defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet(‘DefaultDisplayPropertySet’, [string[]]$defaultDisplaySet)
$PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]#($defaultDisplayPropertySet)
$myHash = #{ }
}
Process {
([xml]($obj.ToXml())).event.eventdata.data | ForEach-Object { $myHash[$PSItem.name] = $PSItem.'#text' }
New-Object -TypeName PSObject -Property $myHash | ForEach-Object {
$PSItem.PSObject.TypeNames.Insert(0, "EventLogRecord.XMLParse")
$PSItem | Add-Member MemberSet PSStandardMembers $PSStandardMembers -PassThru |
Add-Member -MemberType NoteProperty -Name TimeCreated -Value $obj.timecreated -PassThru |
Add-Member -MemberType NoteProperty -Name MachineName -Value $obj.MachineName -PassThru
}
}
}
$filter = #"
<QueryList>
<Query Id="0" Path="Security">
<Select Path="Security">
*[System[
(EventID='4624')
and TimeCreated[#SystemTime>='{0}' and #SystemTime<='{1}']
]
and EventData[
Data[#Name='LogonType'] and ({2})
{3}
]
]
</Select>
</Query>
</QueryList>
"#
}
Process {
foreach ($obj in $ComputerName) {
if ($UserName) {
$joinUserName = "and Data[#Name='TargetuserName'] and (Data='{0}')" -f $UserName
}
$joinLogonType = ($LogonType | ForEach-Object { $hashLogonType[$PSItem] }) -replace '^', "Data='" -replace '$', "'" -join " or "
$objFilter = $filter -f (Get-Date $StartTime -Format s), (Get-Date $StopTime -Format s), $joinLogonType, $joinUserName
$hashEventParm = #{
ComputerName = $obj
FilterXml = $objFilter
}
if ($Credential) { $hashEventParm['Credential'] = $Credential }
if ($MaxEvents) { $hashEventParm['MaxEvents'] = $MaxEvents }
$objFilter | Write-Verbose
Get-WinEvent #hashEventParm | ParseEventMessage
}
}
End { }
}
$hashLogonType = #{
2 = "Interactive"
3 = "Network"
5 = "Service"
7 = "Unlock"
8 = "NetworkCleartext"
9 = "NewCredentials"
10 = "RemoteInteractive"
11 = "CachedInteractive"
}
$TargetDomainNameException = #('Window Manager','Font Driver Host')
$exceptionRegex = $TargetDomainNameException -join "|"
Get-LogonEvents -ComputerName 'Localhost' -MaxEvents 10 -OutVariable LogonEvents |
Where-Object { ($_.TargetDomainName -notmatch $exceptionRegex) } |
Select-Object WorkstationName,
TargetUserName,
TargetDomainName,
Type,
#{n="LogonType";e={$hashLogonType.[int]$_.logontype}},
ProcessName,
IPAddress,
#{n="Host Name"; e={([System.Net.Dns]::GetHostByAddress($_.IPaddress).Hostname)}},
TimeCreated |
Out-GridView
If you have a hashtable containing nested hashtables, how to convert that to a PsObject recursively?
#{
Foo = #{
Bar = #{
Key = 'Value'
Test = 1
}
}
}
The result should be
$_.Foo.Bar.Key = 'Value'
$_.Foo.Bar.Test = 1
One approach is to create a recursive function:
function ConvertTo-PsObject {
param (
[hashtable] $Value
)
foreach ( $key in $Value.Keys | Where-Object { $Value[$_].GetType() -eq #{}.GetType() } ) {
$Value[$key] = ConvertTo-PsObject $Value[$key]
}
New-Object PSObject -Property $Value | Write-Output
}
Another way of doing it which hasn't been mentioned so far, and is more appropriate in many cases is to use New-Object PSObject.
$a = #{
Foo = #{
Bar = #{
Key = 'Value'
Test = 1
}
}
}
$b = New-Object -Type PSObject -Property $a
Doing it this way makes it work correctly with Format-Table for instance, and probably other places too.
Cast it to a [PsCustomObject]:
$a = [PsCustomObject]#{
Foo = #{
Bar = #{
Key = 'Value'
Test = 1
}
}
}
$a.Foo.Bar.Key # --> "Value"
$a.Foo.Bar.Test # --> 1
I am trying to write the values of the PowerShell cmdlet Get-NetTCPConnection to an array but nothing is being written to the list.
$list= #()
$outputs = Get-NetTCPConnection
foreach ($output in $outputs) {
$obj = New-Object PSObject -Property #{
TheLocalAddress = "EMPTY"
TheLocalPort = "EMPTY"
TheRemoteAddress = "EMPTY"
TheRemotePort = "EMPTY"
}
$obj.TheLocalAddress = $output.LocalAddress
$obj.TheLocalPort = $output.LocalPort
$obj.TheRemoteAddress = $output.RemoteAddress
$obj.TheRemotePort = $output.RemotePort
$list += $obj
}
$list
If the prefix The isn't required for the properties, why not use
$list = Get-NetTCPConnection | Select-Object LocalAddress,LocalPort,RemoteAddress,RemotePort
Or a more efficient [PSCustomObject] ?
$list = foreach ($Conn in Get-NetTCPConnection) {
[PSCustomObject]#{
TheLocalAddress = $Conn.LocalAddress
TheLocalPort = $Conn.LocalPort
TheRemoteAddress = $Conn.RemoteAddress
TheRemotePort = $Conn.RemotePort
}
}
$list
I want to create new instance of my custom PSObject. I have a Button object created as PSObject and I want to create new object Button2 which has the same members as Button does, but I can't find a way how to clone the original object without making it referenced in original object (if I change a property in Button2 it changes in Button as well). Is there a way how to do it similarly as with hashtables and arrays via some Clone() method?
Easiest way is to use the Copy Method of a PsObject ==> $o2 = $o1.PsObject.Copy()
$o1 = New-Object -TypeName PsObject -Property #{
Fld1 = 'Fld1';
Fld2 = 'Fld2';
Fld3 = 'Fld3'}
$o2 = $o1.PsObject.Copy()
$o2 | Add-Member -MemberType NoteProperty -Name Fld4 -Value 'Fld4'
$o2.Fld1 = 'Changed_Fld'
$o1 | Format-List
$o2 | Format-List
Output:
Fld3 : Fld3
Fld2 : Fld2
Fld1 : Fld1
Fld3 : Fld3
Fld2 : Fld2
Fld1 : Changed_Fld
Fld4 : Fld4
For some reason PSObject.Copy() doesn't work for all object types. Another solution to create a copy of an object is to convert it to/from Json then save it in a new variable:
$CustomObject1 = [pscustomobject]#{a=1; b=2; c=3; d=4}
$CustomObject2 = $CustomObject1 | ConvertTo-Json -depth 100 | ConvertFrom-Json
$CustomObject2 | add-Member -Name "e" -Value "5" -MemberType noteproperty
$CustomObject1 | Format-List
$CustomObject2 | Format-List
Indeed there is no clone method! However where there is a will...
$o = New-Object PsObject -Property #{ prop1='a' ; prop2='b' }
$o2 = New-Object PsObject
$o.psobject.properties | % {
$o2 | Add-Member -MemberType $_.MemberType -Name $_.Name -Value $_.Value
}
$o.prop1 = 'newvalue'
$o
$o2
Output:
prop2 prop1
----- -----
b newvalue
b a
Another possibility:
$o1 = New-Object PsObject -Property #{ prop1='a' ; prop2='b' }
$o2 = $o1 | select *
$o2.prop1 = 'newvalue'
$o1.prop1
$o2.prop1
a
newvalue
Here's a [pscustomobject] example with the hidden .psobject.copy():
$a = [pscustomobject]#{message='hi'}
$a.message
hi
$b = $a.psobject.copy()
$b.message
hi
$a.message = 'there'
$a.message
there
$b.message
hi
The Better way i found out was to use ConvertTo-Json and ConvertFrom-Json.
Ee -
Suppose you want to clone a object $toBeClonedObject, just run below code to clone.
$clonedObject = $toBeClonedObject | ConvertTo-Json | ConvertFrom-Json
Starting from PowerShell v5, you can use Class.
The problem with psobject.Copy() is, if you update the cloned object, then your template object's referenced properties will be also updated.
example:
function testTemplates
{
$PSCustomObjectTemplate = New-Object PSCustomObject -Property #{
List1 = [System.Collections.Generic.List[string]]#() # will be updated in template
String1 = "value1" # will not be updated in template
Bool1 = $false # will not be updated in template
}
$objectFromPSTemplate1 = $PSCustomObjectTemplate.psobject.Copy()
$objectFromPSTemplate1.List1.Add("Value")
$objectFromPSTemplate1.String1 = "value2"
$objectFromPSTemplate.Bool1 = $true
# $PSCustomObjectTemplate IS updated, so CANNOT be used as a clean template!
$PSCustomObjectTemplate
Class ClassTemplate {
[System.Collections.Generic.List[string]]$List1 = #() # will not be updated in template
[string]$String1 = "value1" # will not be updated in template
[bool]$Bool1 = $false # will not be updated in template
}
$objectFromClassTemplate = [ClassTemplate]::new()
$objectFromClassTemplate.List1.Add("Value")
$objectFromClassTemplate.String1 = "value2"
$objectFromClassTemplate.Bool1 = $true
# $ClassTemplate IS NOT updated, so can be used as a clean template!
[ClassTemplate]::new()
}
testTemplates
PS C:\Windows\system32> testTemplates
List1 String1 Bool1
----- ------- -----
{Value} value1 False
-> Template from PSCustomObject is updated (referenced property -List1)
List1 String1 Bool1
----- ------- -----
{} value1 False
-> Template from Class is safe
This usually works for me:
$Source = [PSCustomObject]#{ Value = 'Test' };
$Copy = ($Source | ConvertTo-Json) | ConvertFrom-Json;
Put this in a Utility class or define it in your current section
function clone($obj)
{
$newobj = New-Object PsObject
$obj.psobject.Properties | % {Add-Member -MemberType NoteProperty -InputObject $newobj -Name $_.Name -Value $_.Value}
return $newobj
}
Usage:
$clonedobj = clone $obj
Based on the answer by #TeraFlux, here's a function that will do a deep copy on multiple objects and accepts pipeline input.
Note, it leverages json conversion with a default depth of 100, which lends it to a few weaknesses
It's going to be slow on deep or complex objects, or objects with expensive (slow) pseudoproperties (methods pretending to be properties that are calculated on the fly when asked for)
Though it should still be faster than the Add-Member approach because the heavy lifting is going through a compiled function
Anything that can't be stored in JSON may get corrupted or left behind (methods will be a prime candidate for this type of error)
Though any object that can safely go through this process should be savable, able to be safely stored (for recovery) or exported for transportation
I would be interested in any caveats or improvements to deal with these
function Clone-Object {
[CmdletBinding()]
Param (
[Parameter(ValueFromPipeline)] [object[]]$objects,
[Parameter()] [int] $depth = 100
)
$clones = foreach( $object in $objects ){
$object `
| ConvertTo-Json `
-Compress `
-depth $depth `
| ConvertFrom-Json
}
return $clones
}
Here are some very basic unit tests
$testClone = {
$test1 = $null
$test2 = $null
$test3 = $null
$Test1 = [psCustomObject]#{a=1; b=2; c=3; d=4}
$Test2 = $Test1 | ConvertTo-Json -depth 100 | ConvertFrom-Json
$Test2 | add-Member -Name "e" -Value "5" -MemberType noteproperty
$Test3 = $test2 | Clone-Object
$Test3 | add-Member -Name "f" -Value "6" -MemberType noteproperty
$Test1.a = 7
$Test2.a = 8
#$Expected0 = [psCustomObject]#{a=1; b=2; c=3; d=4}
$Expected1 = [pscustomobject]#{a=7; b=2; c=3; d=4}
$Expected2 = [pscustomobject]#{a=8; b=2; c=3; d=4; e=5}
$Expected3 = [pscustomobject]#{a=1; b=2; c=3; d=4; e=5; f=6}
$results1 = #(); $results1+=$test1; $results1+=$expected1
$results2 = #(); $results2+=$test2; $results2+=$expected2
$results3 = #(); $results3+=$test3; $results3+=$expected3
$results1 | Format-Table # if these don't match then its probably passing references (copy not clone)
$results2 | Format-Table # if these don't match the core approach is incorrect
$results3 | Format-Table # if these don't match the function didn't work
}
&$testClone
Another option:
function Copy-Object($Object) {
$copy = #()
$Object.ForEach({
$currentObject = $_
$currentObjectCopy = New-Object $currentObject.GetType().Name
$currentObjectCopy.psobject.Properties.ForEach({
$_.Value = $currentObject.psobject.Properties[($_.Name)].Value
})
$copy += $currentObjectCopy
})
return $copy
}
Test objects:
class TestObjectA {
[string]$g
[int[]]$h
[string]getJ(){
return 'j'
}
}
class TestObjectB {
[string]$a
[int]$b
[hashtable]$c
[TestObjectA[]]$d
[string]getI(){
return 'i'
}
}
Tests:
$b = New-Object -TypeName TestObjectB -Property #{
a = 'value a'
b = 2
c = #{ e = 'value e'; f = 3 }
d = New-Object -TypeName TestObjectA -Property #{
g = 'value g'
h = #(4,5,6)
}
}
$bCopy = Copy-Object $b
# test with simple comparison
-not $(Compare-Object $b $bCopy)
True
# test json deep conversion output
$bJson = $b | ConvertTo-Json -Depth 10
$bCopyJson = $bCopy | ConvertTo-Json -Depth 10
-not $(Compare-Object $bJson $bCopyJson)
True
# test methods are intact
$bCopy.getI()
i
$bCopy.d.GetJ()
j
# test objects are seperate instances
$bCopy.b = 3
$b.b
2
$bCopy.b
3
Here's my version using Clixml
function Get-PSObjectClone {
param ( [psobject] $InputObject )
$_temp = New-TemporaryFile
$InputObject | Export-Clixml -Path $_temp -Depth 100
$_object = Import-Clixml -Path $_temp
Remove-Item $_temp -Force
Write-Output $_object
}
Works with everything I've thrown at it
Since Select-Object -Property expands wildcards in property names, a simple way to shallow-clone is this:
# Set up object
$o1 = [PSCustomObject]#{
Fld1 = 'Fld1';
Fld2 = 'Fld2';
Fld3 = 'Fld3'}
# Clone
$o2 = $o1 | Select-Object -Property *;
# Tests
$o1 -eq $o2;
$o1 | Format-List;
$o2 | Format-List;