Outputting arrays with PSObject; Limitted to 4 columns - powershell

A snippet of my code:
$ipaddress = '127.0.0.1'
$port = 135,137,138,139,443,445
for($i=0; $i -lt $port.length; $i++)
{
$out = new-object psobject
$out | add-member noteproperty Host $ipaddress
$out | add-member noteproperty Port $port[$i]
$out | add-member noteproperty Isopen $isopen[$i]
$out | add-member noteproperty Desc "Desc"
$out | add-member noteproperty Notes $Notes[$i]
$out | add-member noteproperty Issue $issue[$i]
Write-Output $out
}
What I'm trying to do is print out the results of my port scanner into a nice table.
This works fine when there's 4 or less columns:
But Whenever I add more columns, even though there's space on the screen, it converts it into a list:
When I try to append "Format-Table" to it, it writes out the headers each time:
Write-Output $out | Format-Table
If I copy the line "Write-Output $out" outside the loop, it only prints out the last member. Any ideas on how to tackle this?
Thanks

As you've found, PowerShell formats your output in a table by default, but opts for list view when the objects being formatted have more than 4 visible members.
You can override this by explicitly invoking your preferred Format-* command. Simply "collect" all the output objects in an variable then explicitly pipe them to Format-Table:
$ipaddress = '127.0.0.1'
$port = 135,137,138,139,443,445
$objects = for($i=0; $i -lt $port.length; $i++)
{
$out = new-object psobject
$out | add-member noteproperty Host $ipaddress
$out | add-member noteproperty Port $port[$i]
$out | add-member noteproperty Isopen $isopen[$i]
$out | add-member noteproperty Desc "Desc"
$out | add-member noteproperty Notes $Notes[$i]
$out | add-member noteproperty Issue $issue[$i]
Write-Output $out
}
$objects |Format-Table
Unless you're running your code on PowerShell 2.0, I'd suggest using the 3.0 [pscustomobject] syntax for creating your object (and perhaps turn the whole thing into a function):
function Get-PortStatus
{
param(
[string]$IPAddress = '127.0.0.1',
[intp[]]$Port = 135,137,138,139,443,445
)
# populate $isopen, $notes, $issue etc. here ...
for($i=0; $i -lt $port.length; $i++)
{
# Write-Output is implied when the new object isn't assigned to anything
[pscustomobject]#{
Host = $ipaddress
Port = $port[$i]
IsOpen = $isopen[$i]
Desc = "Desc"
Notes = $Notes[$i]
Issue = $issue[$i]
}
}
}
Now you can do:
PS C:\Users\Gabrielius> Get-PortStatus -IPAddress '10.0.0.10' -Port 80,443 |Format-Table

You could make your own type and table view in a .format.ps1xml file, if it's worth it to you. Here's a simple example. The format is documented at About Format.ps1xml There's actually format files for all the custom objects in powershell. This is rather boilerplate. I wish there were a $numPropsToTable preference variable.
myobject.format.ps1xml:
<?xml version="1.0" encoding="utf-8" ?>
<Configuration>
<ViewDefinitions>
<View>
<Name>myobject</Name>
<ViewSelectedBy>
<TypeName>myobject</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Width>16</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>16</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>16</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>16</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>16</Width>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Name</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Address</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>City</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>State</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Zip</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>
update-formatdata myobject.format.ps1xml
[pscustomobject]#{name='me';address='here';city='la';state='ca';zip=11111
PSTypeName = 'MyObject'}
Name Address City State Zip
---- ------- ---- ----- ---
me here la ca 11111

Related

How do I add a System.Collections.ArrayList to a PowerShell custom object?

My goal is to create a custom data object that has two discrete variables (fooName and fooUrl) and a list of fooChildren, each list item having two discrete variables variables childAge and childName.
Currently, I have this:
$fooCollection = [PSCustomObject] #{fooName=""; fooUrl=""; fooChildrenList=#()}
$fooCollection.fooName = "foo-a-rama"
$fooCollection.fooUrl = "https://1.2.3.4"
$fooChild = New-Object -TypeName PSobject
$fooChild | Add-Member -Name childAge -MemberType NoteProperty -Value 6
$fooChild | Add-Member -Name childName -MemberType NoteProperty -Value "Betsy"
$fooCollection.fooChildrenList += $fooChild
$fooChild = New-Object -TypeName PSobject
$fooChild | Add-Member -Name childAge -MemberType NoteProperty -Value 10
$fooChild | Add-Member -Name childName -MemberType NoteProperty -Value "Rolf"
$fooCollection.fooChildrenList += $fooChild
cls
$fooCollection.fooName
$fooCollection.fooUrl
foreach ($fooChild in $fooCollection.fooChildrenList)
{
(" " + $fooChild.childName + " " + $fooChild.childAge)
}
Which produces the following. So far so good
foo-a-rama
https://1.2.3.4
Betsy 6
Rolf 10
Problem: I don't like using += because as I understand it, using += results in a copy of $fooCollection.fooChildrenList being created (in whatever state it's in) each time += is executed.
So, instead of implementing fooChildrenList as #(), I want to implement fooChildrenList as New-Object System.Collections.ArrayList so I can add each row as needed. I've tried various ways of doing this in code but fooChildrenList winds up being unpopulated. For example:
$fooCollection = [PSCustomObject] #{fooName=""; fooUrl=""; fooChildrenList = New-Object System.Collections.ArrayList}
$fooCollection.fooName = "foo-a-rama"
$fooCollection.fooUrl = "https://1.2.3.4"
$fooChild.childName = "Betsy"
$fooChild.childAge = 6
$fooCollection.fooChildrenList.Add((New-Object PSObject -Property $fooChild))
$fooChild.childName = "Rolf"
$fooChild.childAge = 10
$fooCollection.fooChildrenList.Add((New-Object PSObject -Property $fooChild))
$fooCollection | get-member shows
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
fooChildrenList NoteProperty System.Collections.ArrayList fooChildrenList=
fooName NoteProperty string fooName=foo-a-rama
fooUrl NoteProperty string fooUrl=https://1.2.3.4
$fooCollection shows
fooName : foo-a-rama
fooUrl : https://1.2.3.4
fooChildrenList : {}
How do I add a System.Collections.ArrayList to a PowerShell custom object?
Well im not sure what issue you are getting it works fine for me
function New-Child([string]$Name, [int]$Age){
$Child = New-Object -TypeName PSobject
$Child | Add-Member -Name childAge -MemberType NoteProperty -Value $age -PassThru |
Add-Member -Name childName -MemberType NoteProperty -Value $name
return $child
}
$fooCollection = [PSCustomObject] #{fooName=""; fooUrl=""; fooChildrenList = New-Object System.Collections.ArrayList}
$fooCollection.fooName = "foo-a-rama"
$fooCollection.fooUrl = "https://1.2.3.4"
$fooCollection.fooChildrenList.Add((New-Child -Name "Betty" -Age 9)) | Out-Null
$fooCollection.fooChildrenList.Add((New-Child -Name "Ralf" -Age 15)) | Out-Null
$fooCollection.fooName
$fooCollection.fooUrl
foreach ($fooChild in $fooCollection.fooChildrenList)
{
" " + $fooChild.childName + " " + $fooChild.childAge
}
output
foo-a-rama
https://1.2.3.4
Betty 9
Ralf 15
The challenge is to add a copy of the $fooChild [pscustomobject] instance you're re-using every time you add to the list with .Add() (if you don't use a copy, you'll end up with all list elements pointing to the same object).
However, you cannot clone an existing [pscustomobject] (a.k.a [psobject]) instance with New-Object PSObject -Property.
One option (PSv3+) is to define the reusable $fooChild as an ordered hashtable instead, and then use a [pscustomobject] cast, which implicitly creates a new object every time:
$fooCollection = [PSCustomObject] #{ fooChildrenList = New-Object Collections.ArrayList }
# Create the reusable $fooChild as an *ordered hashtable* (PSv3+)
$fooChild = [ordered] #{ childName = ''; childAge = -1 }
# Create 1st child and add to list with [pscustomobject] cast
$fooChild.childName = 'Betsy'; $fooChild.childAge = 6
$null = $fooCollection.fooChildrenList.Add([pscustomobject] $fooChild)
# Create and add another child.
$fooChild.childName = 'Rolf'; $fooChild.childAge = 10
$null = $fooCollection.fooChildrenList.Add([pscustomobject] $fooChild)
# Output the children
$fooCollection.fooChildrenList
Note the $null = ..., which suppresses the typically unwanted output from the .Add() method call.
The above yields:
childName childAge
--------- --------
Betsy 6
Rolf 10
A slightly more obscure alternative is to stick with $fooChild as a [pscustomobject] instance and call .psobject.Copy() on it to create a clone.
ArcSet's helpful answer provides a more modular solution that creates new custom-object instances on demand via a helper function.
Finally, in PSv5+ you could define a helper class:
$fooCollection = [PSCustomObject] #{ fooChildrenList = New-Object Collections.ArrayList }
# Define helper class
class FooChild {
[string] $childName
[int] $childAge
}
# Create 1st child and add to list with [pscustomobject] cast
$null = $fooCollection.fooChildrenList.Add([FooChild] #{ childName = 'Betsy'; childAge = 6 })
# Create and add another child.
$null = $fooCollection.fooChildrenList.Add([FooChild] #{ childName = 'Rolf'; childAge = 10 })
# Output the children
$fooCollection.fooChildrenList
Note how instances of [FooChild] can be created by simply casting a hashtable that has entries matching the class property names.
Quick copy paste from something I have that I use to make some of my Arrays. I have to create the custom objects and then add them to the Array. It will need modified for your scenario but I think it will get you what you need.
[System.Collections.ArrayList]$SQL_Query_Results = #()
ForEach ($SQL_Index in $SQL_Table) {
$SQL_Array_Object = [PSCustomObject]#{
'Computer_Name' = $SQL_Table[$SQL_Index_Counter].ComputerID -replace ",", ""
'Project' = $SQL_Table[$SQL_Index_Counter].Project
'Site' = $SQL_Table[$SQL_Index_Counter].Site
'Description' = $SQL_Table[$SQL_Index_Counter].Description -replace ",", ""
'Physical_Machine' = $SQL_Table[$SQL_Index_Counter].IsPhysicalMachine
'Active' = $SQL_Table[$SQL_Index_Counter].IsActive
}
$SQL_Query_Results.Add($SQL_Array_Object) | Out-Null
}
Edited to show how Array was initially created.

How to search Windows Search Indexed in file

I have files indexed by the Windows Search service. I need function, which can find some string in text files. I have script in PowerShell, but it didn't work fine.
function search {
param($path, $word)
$c = $path + "\%"
$query = "SELECT
System.ItemName, System.ItemPathDisplay
FROM SystemIndex
WHERE System.ItemPathDisplay LIKE '$c' AND CONTAINS('$word')"
$ADOCommand = New-Object -ComObject ADODB.Command
$ADOConnection = New-Object -ComObject ADODB.Connection
$RecordSet = New-Object -ComObject ADODB.RecordSet
$ADOConnection.Open("Provider=Search.CollatorDSO;Extended Properties='Application=Windows';")
$RecordSet.Open($query, $ADOConnection)
try { $RecordSet.MoveFirst() }
catch [System.Exception] { "no records returned" }
while (-not($RecordSet.EOF)) {
if ($locatedFile) { Remove-Variable locatedFile }
$locatedFile = New-Object -TypeName PSObject
Add-Member -InputObject $locatedFile -MemberType NoteProperty -Name 'Name' -Value ($RecordSet.Fields.Item("System.ItemName")).Value
Add-Member -InputObject $locatedFile -MemberType NoteProperty -Name 'Path' -Value ($RecordSet.Fields.Item("System.ItemPathDisplay")).Value
$locatedFile
$RecordSet.MoveNext()
}
$RecordSet.Close()
$ADOConnection.Close()
$RecordSet = $null
$ADOConnection = $null
[gc]::Collect()
}
If $word = "Hello" it works fine for files where we have
*some text * Hello * some text*
in the file, but not when we have Hello without spaces like:
HelloWorld
We can't also search when $word is a phrase,for example "Hello World".
Anyone know how to fix it?
I believe the issue is with the CONTAINS in your query. You should add asterisk (*) wildcard character to the word that you search.
So instead of:
WHERE System.ItemPathDisplay LIKE '$c' AND CONTAINS('$word')
please try:
WHERE System.ItemPathDisplay LIKE '$c' AND CONTAINS('*$word*')

How do I create an anonymous object in PowerShell?

I want to create an object of arbitrary values, sort of like how I can do this in C#
var anon = new { Name = "Ted", Age = 10 };
You can do any of the following, in order of easiest usage:
Use Vanilla Hashtable with PowerShell 5+
In PS5, a vanilla hash table will work for most use cases
$o = #{ Name = "Ted"; Age = 10 }
Convert Hashtable to PSCustomObject
If you don't have a strong preference, just use this where vanilla hash tables won't work:
$o = [pscustomobject]#{
Name = "Ted";
Age = 10
}
Using Select-Object cmdlet
$o = Select-Object #{n='Name';e={'Ted'}},
#{n='Age';e={10}} `
-InputObject ''
Using New-Object and Add-Member
$o = New-Object -TypeName psobject
$o | Add-Member -MemberType NoteProperty -Name Name -Value 'Ted'
$o | Add-Member -MemberType NoteProperty -Name Age -Value 10
Using New-Object and hashtables
$properties = #{
Name = "Ted";
Age = 10
}
$o = New-Object psobject -Property $properties;
Note: Objects vs. HashTables
Hashtables are just dictionaries containing keys and values, meaning you might not get the expected results from other PS functions that look for objects and properties:
$o = #{ Name="Ted"; Age= 10; }
$o | Select -Property *
Further Reading
4 Ways to Create PowerShell Objects
Everything you wanted to know about hashtables
Everything you wanted to know about PSCustomObject
Try this:
PS Z:\> $o = #{}
PS Z:\> $o.Name = "Ted"
PS Z:\> $o.Age = 10
Note: You can also include this object as the -Body of an Invoke-RestMethod and it'll serialize it with no extra work.
Update
Note the comments below. This creates a hashtable.
With PowerShell 5+
Just declare as:
$anon = #{ Name="Ted"; Age= 10; }

Is there a PowerShell equivalent tracert that works in version 2?

I'm using PSVersion 2.0 and I was wondering is there a equivalent to the traceroute for it?
I'm aware that on PowerShell v4 there is Test-NetConnection cmdlet to do tracert but v2?! It can be done like:
Test-NetConnection "IPaddress/HOSTaname" -TraceRoute
Thanks
As mentioned in the comment, you can make your own "poor-mans-PowerShell-tracert" by parsing the output from tracert.exe:
function Invoke-Tracert {
param([string]$RemoteHost)
tracert $RemoteHost |ForEach-Object{
if($_.Trim() -match "Tracing route to .*") {
Write-Host $_ -ForegroundColor Green
} elseif ($_.Trim() -match "^\d{1,2}\s+") {
$n,$a1,$a2,$a3,$target,$null = $_.Trim()-split"\s{2,}"
$Properties = #{
Hop = $n;
First = $a1;
Second = $a2;
Third = $a3;
Node = $target
}
New-Object psobject -Property $Properties
}
}
}
By default, powershell formats objects with 5 or more properties in a list, but you can get a tracert-like output with Format-Table:
Fixed a few bugs in " Mid-Waged-Mans-Tracert" Version, modularized it, and added some customization pieces. #MrPaulch had a great PoC.
function Invoke-Traceroute{
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true,Position=1)]
[string]$Destination,
[Parameter(Mandatory=$false)]
[int]$MaxTTL=16,
[Parameter(Mandatory=$false)]
[bool]$Fragmentation=$false,
[Parameter(Mandatory=$false)]
[bool]$VerboseOutput=$true,
[Parameter(Mandatory=$false)]
[int]$Timeout=5000
)
$ping = new-object System.Net.NetworkInformation.Ping
$success = [System.Net.NetworkInformation.IPStatus]::Success
$results = #()
if($VerboseOutput){Write-Host "Tracing to $Destination"}
for ($i=1; $i -le $MaxTTL; $i++) {
$popt = new-object System.Net.NetworkInformation.PingOptions($i, $Fragmentation)
$reply = $ping.Send($Destination, $Timeout, [System.Text.Encoding]::Default.GetBytes("MESSAGE"), $popt)
$addr = $reply.Address
try{$dns = [System.Net.Dns]::GetHostByAddress($addr)}
catch{$dns = "-"}
$name = $dns.HostName
$obj = New-Object -TypeName PSObject
$obj | Add-Member -MemberType NoteProperty -Name hop -Value $i
$obj | Add-Member -MemberType NoteProperty -Name address -Value $addr
$obj | Add-Member -MemberType NoteProperty -Name dns_name -Value $name
$obj | Add-Member -MemberType NoteProperty -Name latency -Value $reply.RoundTripTime
if($VerboseOutput){Write-Host "Hop: $i`t= $addr`t($name)"}
$results += $obj
if($reply.Status -eq $success){break}
}
Return $results
}
I must admit I wanted to see whether someone already did this.
You can use the .Net Framework to implement a not-so-poor-mans-traceroute as a Powershell Script
Here a primer, that works fast, but dangerous.
Also, no statistics.
#
# Mid-Waged-Mans-Tracert
#
$ping = new-object System.Net.NetworkInformation.Ping
$timeout = 5000
$maxttl = 64
$address = [string]$args
$message = [System.Text.Encoding]::Default.GetBytes("MESSAGE")
$dontfragment = false
$success = [System.Net.NetworkInformation.IPStatus]::Success
echo "Tracing $address"
for ($ttl=1;$i -le $maxttl; $ttl++) {
$popt = new-object System.Net.NetworkInformation.PingOptions($ttl, $dontfragment)
$reply = $ping.Send($address, $timeout, $message, $popt)
$addr = $reply.Address
$rtt = $reply.RoundtripTime
try {
$dns = [System.Net.Dns]::GetHostByAddress($addr)
} catch {
$dns = "-"
}
$name = $dns.HostName
echo "Hop: $ttl`t= $addr`t($name)"
if($reply.Status -eq $success) {break}
}
Edit:
Removed some of the danger by adding a catch statement.
The only danger that is still present is the fact that we only send a single request per hop, which could mean that we don't reach a hop due to a innocent package drop.
Resolving that issue remains a readers exercise.
Hint: (Think of loops within loops)
Bonus: We now attempt to get the dns entry of each hop!
With at least PS 5 you can
Test-Netconnection stackoverflow.com -TraceRoute

How to create new clone instance of PSObject object

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;