PowerShell: modify elements of array - arrays

My cmdlet get-objects returns an array of MyObject with public properties:
public class MyObject{
public string testString = "test";
}
I want users without programming skills to be able to modify public properties (like testString in this example) from all objects of the array.
Then feed the modified array to my second cmdlet which saves the object to the database.
That means the syntax of the "editing code" must be as simple as possible.
It should look somewhat like this:
> get-objects | foreach{$_.testString = "newValue"} | set-objects
I know that this is not possible, because $_ just returns a copy of the element from the array.
So you'd need to acces the elements by index in a loop and then modify the property.This gets really quickly really complicated for people that are not familiar with programming.
Is there any "user-friendly" built-in way of doing this? It shouldn't be more "complex" than a simple foreach {property = value}

I know that this is not possible, because $_ just returns a copy of the element from the array (https://social.technet.microsoft.com/forums/scriptcenter/en-US/a0a92149-d257-4751-8c2c-4c1622e78aa2/powershell-modifying-array-elements)
I think you're mis-intepreting the answer in that thread.
$_ is indeed a local copy of the value returned by whatever enumerator you're currently iterating over - but you can still return your modified copy of that value (as pointed out in the comments):
Get-Objects | ForEach-Object {
# modify the current item
$_.propertyname = "value"
# drop the modified object back into the pipeline
$_
} | Set-Objects
In (allegedly impossible) situations where you need to modify a stored array of objects, you can use the same technique to overwrite the array with the new values:
PS C:\> $myArray = 1,2,3,4,5
PS C:\> $myArray = $myArray |ForEach-Object {
>>> $_ *= 10
>>> $_
>>>}
>>>
PS C:\> $myArray
10
20
30
40
50
That means the syntax of the "editing code" must be as simple as possible.
Thankfully, PowerShell is very powerful in terms of introspection. You could implement a wrapper function that adds the $_; statement to the end of the loop body, in case the user forgets:
function Add-PsItem
{
[CmdletBinding()]
param(
[Parameter(Mandatory,ValueFromPipeline,ValueFromRemainingArguments)]
[psobject[]]$InputObject,
[Parameter(Mandatory)]
[scriptblock]$Process
)
begin {
$InputArray = #()
# fetch the last statement in the scriptblock
$EndBlock = $Process.Ast.EndBlock
$LastStatement = $EndBlock.Statements[-1].Extent.Text.Trim()
# check if the last statement is `$_`
if($LastStatement -ne '$_'){
# if not, add it
$Process = [scriptblock]::Create('{0};$_' -f $Process.ToString())
}
}
process {
# collect all the input
$InputArray += $InputObject
}
end {
# pipe input to foreach-object with the new scriptblock
$InputArray | ForEach-Object -Process $Process
}
}
Now the users can do:
Get-Objects | Add-PsItem {$_.testString = "newValue"} | Set-Objects
The ValueFromRemainingArguments attribute also lets users supply input as unbounded parameter values:
PS C:\> Add-PsItem { $_ *= 10 } 1 2 3
10
20
30
This might be helpful if the user is not used to working with the pipeline

Here's a more general approach, arguably easier to understand, and less fragile:
# $dataSource would be get-object in the OP
# $dataUpdater is the script the user supplies to modify properties
# $dataSink would be set-object in the OP
function Update-Data {
param(
[scriptblock] $dataSource,
[scriptblock] $dataUpdater,
[scriptblock] $dataSink
)
& $dataSource |
% {
$updaterOutput = & $dataUpdater
# This "if" allows $dataUpdater to create an entirely new object, or
# modify the properties of an existing object
if ($updaterOutput -eq $null) {
$_
} else {
$updaterOutput
}
} |
% $dataSink
}
Here are a couple of examples of use. The first example isn't applicable to the OP, but it's being used to create a data set that is applicable (a set of objects with properties).
# Use updata-data to create a set of data with properties
#
$theDataSource = #() # will be filled in by first update-data
update-data {
# data source
0..4
} {
# data updater: creates a new object with properties
New-Object psobject |
# add-member uses hash table created on the fly to add properties
# to a psobject
add-member -passthru -NotePropertyMembers #{
room = #('living','dining','kitchen','bed')[$_];
size = #(320, 200, 250, 424 )[$_]}
} {
# data sink
$global:theDataSource += $_
}
$theDataSource | ft -AutoSize
# Now use updata-data to modify properties in data set
# this $dataUpdater updates the 'size' property
#
$theDataSink = #()
update-data { $theDataSource } { $_.size *= 2} { $global:theDataSink += $_}
$theDataSink | ft -AutoSize
And then the output:
room size
---- ----
living 320
dining 200
kitchen 250
bed 424
room size
---- ----
living 640
dining 400
kitchen 500
bed 848
As described above update-data relies on a "streaming" data source and sink. There is no notion of whether the first or fifteenth element is being modified. Or if the data source uses a key (rather than an index) to access each element, the data sink wouldn't have access to the key. To handle this case a "context" (for example an index or a key) could be passed through the pipeline along with the data item. The $dataUpdater wouldn't (necessarily) need to see the context. Here's a revised version with this concept added:
# $dataSource and $dataSink scripts need to be changed to output/input an
# object that contains both the object to modify, as well as the context.
# To keep it simple, $dataSource will output an array with two elements:
# the value and the context. And $dataSink will accept an array (via $_)
# containing the value and the context.
function Update-Data {
param(
[scriptblock] $dataSource,
[scriptblock] $dataUpdater,
[scriptblock] $dataSink
)
% $dataSource |
% {
$saved_ = $_
# Set $_ to the data object
$_ = $_[0]
$updaterOutput = & $dataUpdater
if ($updaterOutput -eq $null) { $updaterOutput = $_}
$_ = $updaterOutput, $saved_[1]
} |
% $dataSink
}

Related

Modifying ArrayList in Powershell also modifies the original array [duplicate]

I'm trying to create a complete copy of an existing array. Every time I try this it doesn't seem to work. The thing is that I'm modifying the Object names inside the new copied array, but they're also changed in the original array..
The code below is highly simplified as there is a lot more happening then only renaming object names but it proves the point I think.
Some example code:
Function Get-Fruits {
Param (
$Fruits = #('Banana', 'Apple', 'Pear')
)
foreach ($F in $Fruits) {
[PSCustomObject]#{
Type = $F
}
}
}
$FruitsOriginal = Get-Fruits
Function Rename-ObjectName {
# Copy the array here
$FruitsNew = $FruitsOriginal # Not a true copy
$FruitsNew = $FruitsOriginal | % {$_} # Not a true copy
$FruitsNew = $FruitsOriginal.Clone() # Not a true copy
$FruitsNew | Get-Member | ? MemberType -EQ NoteProperty | % {
$Name = $_.Name
$FruitsNew | % {
$_ | Add-Member 'Tasty fruits' -NotePropertyValue $_.$Name
$_.PSObject.Properties.Remove($Name)
}
}
}
Rename-ObjectName
The desired result is 2 completely separate arrays.
$FruitsOriginal
Type
----
Banana
Apple
Pear
$FruitsNew
Tasty fruits
------------
Banana
Apple
Pear
Thank you for your help.
# Copy the array here
$FruitsCopy = #()
$FruitsCopy = $FruitsCopy + $FruitsOriginal
You can use serialisation to deep clone your array:
#Original data
$FruitsOriginal = Get-Fruits
# Serialize and Deserialize data using BinaryFormatter
$ms = New-Object System.IO.MemoryStream
$bf = New-Object System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
$bf.Serialize($ms, $FruitsOriginal)
$ms.Position = 0
#Deep copied data
$FruitsNew = $bf.Deserialize($ms)
$ms.Close()
Since Powershell 3.0, same approach as Jaco's answer but using PSSerializer.
It uses a CliXML format compatible with Export-Clixml & Import-Clixml and personally I find it easier to read.
In theory, supports a nested hierarchy up to [int32]::MaxValue levels-deep
# Original data
$FruitsOriginal = Get-Fruits
# Serialize and Deserialize data using PSSerializer:
$_TempCliXMLString = [System.Management.Automation.PSSerializer]::Serialize($FruitsOriginal, [int32]::MaxValue)
$FruitsNew = [System.Management.Automation.PSSerializer]::Deserialize($_TempCliXMLString)
# Deep copy done.
If you're copying an array of objects/value that contains all "truthy" values, or you want to quickly filter out null and "falsey" values, then this works great:
$FruitsNew = $FruitsOriginal|?{$_}
Depending on what you need to do with the objects, and if they're simple enough (as in your example), you could just replace them with a new object.
$NewFruits = $FruitsOriginal | %{ [PSCustomObject]#{ "Tasty Fruits" = $_.Type } }

Tuples/ArrayList of pairs

I'm essentially trying to create a list of pairs which is proving frustratingly difficult
Note before anyone mentions Hashtables that there will be duplicates which I don't care about.
For example, if I do
$b = #{"dog" = "cat"}
I get
Name Value
---- -----
dog cat
which is good. However, I'm then unable to add the likes of
$b += #{"dog" = "horse"}
Item has already been added. Key in dictionary: 'dog' Key being added: 'dog'
I'm just trying to create a table of data which I can add to using something like .Add() or +=.
I believe a list of hashtables should accomplish what you are after.
$ht = #{foo='bar'}
$list = [System.Collections.Generic.List[hashtable]]::new()
$list.Add($ht)
$list.Add($ht)
$list.Add($ht)
$list
If you want a list of tuples I'd recommend actually building a list of tuples:
$list = #()
$tuple1 = New-Object 'Tuple[String,String]' 'dog', 'cat'
$tuple2 = New-Object 'Tuple[String,String]' 'dog', 'horse'
$list += $tuple1
$list
# Output:
#
# Item1 Item2 Length
# ----- ----- ------
# dog cat 2
$list += $tuple2
$list
# Output:
#
# Item1 Item2 Length
# ----- ----- ------
# dog cat 2
# dog horse 2
Note that in this example $list is a regular array, meaning that, as #mklement0 pointed out in his answer, appending to it with the += assignment operator will re-create the array with size increased by 1, put the new item in the new empty slot, then replace the original array. For a small number of append operations this usually isn't a big issue, but with increasing number of append operations the performance impact becomes significant.
Using an ArrayList instead of a plain array avoids this issue:
$list = New-Object Collections.ArrayList
$tuple1 = New-Object 'Tuple[String,String]' 'dog', 'cat'
$tuple2 = New-Object 'Tuple[String,String]' 'dog', 'horse'
$list.Add($tuple1) | Out-Null
$list
# Output:
#
# Item1 Item2 Length
# ----- ----- ------
# dog cat 2
$list.Add($tuple2) | Out-Null
$list
# Output:
#
# Item1 Item2 Length
# ----- ----- ------
# dog cat 2
# dog horse 2
The Add() method of ArrayList objects outputs the index where the item was appended. Out-Null suppresses that output that is in most cases undesired. If you want to work with these index numbers you can collect them in a variable instead of discarding them ($i = $list.Add($t1)).
If you want to avoid having to specify the type of a new tuple all the time you can wrap it into a reusable function like this:
function New-Tuple {
Param(
[Parameter(Mandatory=$true)]
[ValidateCount(2,2)]
[string[]]$Values
)
New-Object 'Tuple[String,String]' $Values
}
$tuple1 = New-Tuple 'dog', 'cat'
$tuple2 = New-Tuple 'dog', 'horse'
or, in a more generic way, like this:
function New-Tuple {
Param(
[Parameter(
Mandatory=$true,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true
)]
[ValidateCount(2,20)]
[array]$Values
)
Process {
$types = ($Values | ForEach-Object { $_.GetType().Name }) -join ','
New-Object "Tuple[$types]" $Values
}
}
$tuples = ('dog', 'cat'), ('dog', 'horse') | New-Tuple
Persistent13's helpful answer offers an effective and efficient solution - albeit a slightly obscure one.
Creating an array of hashtables is the simplest solution, though it's important to note that "extending" an array means implicitly recreating it, given that arrays are fixed-size data structures, which can become a performance problem with many iterations:
# Using array-construction operator ",", create a single-element array
# containing a hashtable
$b = , #{ dog = "cat"}
# "Extend" array $b by appending another hashtable.
# Technically, a new array must be allocated that contains the original array's
# elements plus the newly added one.
$b += #{ dog = "horse"}
This GitHub issue discusses a potential future enhancement to make PowerShell natively support an efficiently extensible list-like data type or for it to even default to such a data type. (As of Windows PowerShell v5.1 / PowerShell Core 6.2.0, PowerShell defaults to fixed-size arrays, of type [object[]]).
Thanks to mklement0 as below is probably the simplest solution
$b = , #{ dog = "cat"}
$b += #{ dog = "horse"}
And Persistent13's method also yields an easy route
$ht = #{foo='bar'}
$list = [System.Collections.Generic.List[hashtable]]::new()
$list.Add($ht)
$list.Add($ht)
$list.Add($ht)
$list
I suppose i was just surprised that there wasn't a more ingrained way to get what i feel is a pretty basic/standard object type

How to search an array of PSobjects for a specific objects key using PSv2

I have an array of Objects. I want to search the array to determine if the a specific value exists in $obj.NewName. If the value is unique, then put the value in $obj.NewName. If it does exist, increment value and search again until the value is unique.
Function CreateObject ($origionalName)
{
$newName = $null
$obj = $null
$obj = New-Object PSObject
$obj | Add-Member -type NoteProperty -Name OrigionalName -Value $origionalName
$obj | Add-Member -type NoteProperty -Name NewName -Value $newName
Return ($obj)
}
Function Get-NewPatchName ($patch)
{
$patch.OrigionalPatchName.split("-") | Foreach {
## Pipelined string is $_. Check if it starts with "KB".
If ($_.substring(0,2) -like "kb" )
{
## This create the new name based on the substring containing "KB...", and adds the existing file extension.
$tempName = $_
## If there is a version number in the original file name, include that in the new name
If ($patch.OrigionalPatchName -like "*-v*")
{
$intPosition = ($patch.OrigionalPatchName.ToUpper()).IndexOf("-V")
$tempName += ($patch.OrigionalPatchName.substring($intPosition+1,2))
}
## Determine if name is unique
{
## STUCK HERE!!!!!!!!!!
}
}
}
}
$patchList = #()
$nameList = "AMD64-all-windows6.0-kb2900986-x64_63", `
"AMD64-all-windows6.0-kb3124275-x64_57", `
"AMD64-all-windows6.0-kb2900986-x64_63"
Foreach ($name in $nameList){
$patchList += CreateObject $name }
Foreach ($patch in $patchList) {
Get-NewPatchName $patch }
Foreach ($patch in $patchList) {
Write-Host "Origional Patch Name: " $patch.OrigionalName
Write-Host "New Patch Name : " $patch.NewName
}
Expected results:
Origional Patch Name: AMD64-all-windows6.0-kb2900986-x64_63
New Patch Name : kb2900986
Origional Patch Name: AMD64-all-windows6.0-kb3124275-x64_57
New Patch Name : kb3124275
Origional Patch Name: AMD64-all-windows6.0-kb2900986-x64_63
New Patch Name : kb2900986a
I would process $origionalName down to it's kb#, Example:
$tempName = "kb2900986"
Then I want to see if $tempName already exists in patchList under any $patch.newName. If it does, then I would increment the $tempName to
$tempName = "kb2900986a"
and run the search again. Once $tempName is unique
$patch.newName = $tempName
I already figured out how to process and increment the $tempName. I'm just stuck on how to search the $patchList to determine if the $tempName currently exists for any $patch.newName.
I'm working under the assumption you are using a newer version of PS, this WILL NOT work in PS2 as it makes use of a somewhat sloppy feature of newer versions of the shell that makes life much easier for the task you are tackling. code below would go in the area you've identified with the comments.
if($patchlist.newname -contains $tempname){
*do whatever you need to do if the list already contains the name*
} else {
*do whatever you need to do if the list doesn't contain the name*
}
This makes use of a powershell feature that will automatically access the .newname property of each item in the $patchlist array and present it as it's own array of values when calling $patchlist.newname its an interesting array feature that was introduced in PS3 I believe. I'd also make sure your function stays atomic by passing in patchlist(preferably with a different name) but that's not really necessary.
The PS2 Version
$templist = foreach($name in $patchlist){
$name.NewName
}
if($templist -contains $tempname){
*do whatever you need to do if the list already contains the name*
} else {
*do whatever you need to do if the list doesn't contain the name*
}
That will work for any version of powershell, it's not exactly optimized(creates the $templist array for each iteration) but it works and you can make changes as needed if you need additional speed
Mike Garuccio's helpful answer contains important pointers to the solution.
Here's an idiomatic PowerShell v2+ solution that also streamlines the code in the question:
# Convert an array of patch names to custom objects containing
# an .OriginalName property with the input patch name, and a yet-to-be-assigned
# .NewName property.
Function CreateObjects ([string[]] $originalNames)
{
foreach ($name in $originalNames) {
[pscustomobject] #{ OriginalName = $name; NewName = $null }
}
}
# Assign a unique name to the .NewName property of all input objects.
Function Set-NewPatchNames ([pscustomobject[]] $patches)
{
# Initialize an aux. hashtable in which we'll keep track of the unique
# new names assigned so far.
$htNewNames = #{}
foreach ($patch in $patches) {
# Extract the KB number from the original name.
# Note: For brevity, the "-v" version-number extraction mentioned in the
# question is skipped.
$newNameCandidate = $patch.OriginalName -replace '.*-kb(\d+).*', '$1'
# Is the candidate for the new name unique?
if ($htNewNames.Contains($newNameCandidate)) { # Name already used.
# Find a unique variation of the name.
$rootName = $newNameCandidate
# Loop and append 'a', 'b', ... until a unique name is found.
# Note: With more than 26 duplicates, you'll run out of letters,
# at which point seemingly random Unicode chars. will be used.
$suffixCharCode = ([int] [char] 'a') - 1
do {
++$suffixCharCode
$newNameCandidate = $rootName + [char] $suffixCharCode
} while ($htNewNames.Contains($newNameCandidate))
}
# Assign the new name and record it in the hashtable of names used so far.
$patch.NewName = $newNameCandidate
$htNewNames.Add($newNameCandidate, $null)
}
}
# The input list of patch names.
$nameList = "AMD64-all-windows6.0-kb2900986-x64_63",
"AMD64-all-windows6.0-kb3124275-x64_57",
"AMD64-all-windows6.0-kb2900986-x64_63"
# Create a collection of custom objects with the original patch name
# stored in .OriginalName, and a yet-to-be-filled .NewName property.
$patchList = CreateObjects $nameList
# Fill the .NewName properties with unique names.
Set-NewPatchNames $patchList
# Output the resulting objects.
$patchList
In PSv5.1, this yields (the code does work correctly in PSv2, but yields slightly less readable output):
OriginalName NewName
------------ -------
AMD64-all-windows6.0-kb2900986-x64_63 2900986
AMD64-all-windows6.0-kb3124275-x64_57 3124275
AMD64-all-windows6.0-kb2900986-x64_63 2900986a

Using intermediate variable to work with array (reference type)

I am trying to use $a variable in this script for working with intermediate steps so that I don't have to use $array[$array.Count-1] repeatedly. Similarly for $prop as well . However, values are being overwritten by last value in loop.
$guests = Import-Csv -Path C:\Users\shant_000\Desktop\UploadGuest_test.csv
$output = gc '.\Sample Json.json' | ConvertFrom-Json
$array = New-Object System.Collections.ArrayList;
foreach ($g in $guests) {
$array.Add($output);
$a = $array[$array.Count-1];
$a.Username = $g.'EmailAddress';
$a.DisplayName = $g.'FirstName' + ' ' + $g.'LastName';
$a.Password = $g.'LastName' + '123';
$a.Email = $g.'EmailAddress';
foreach ($i in $a.ProfileProperties.Count) {
$j = $i - 1;
$prop = $a.ProfileProperties[$j];
if ($prop.PropertyName -eq "FirstName") {
$prop.PropertyValue = $g.'FirstName';
} elseif ($prop.PropertyName -eq "LastName") {
$prop.PropertyValue = $g.'LastName';
}
$a.ProfileProperties[$j] = $prop;
}
$array[$array.Count-1] = $a;
}
$array;
All array elements are referencing one actual variable: $output.
Create an entirely new object each time by repeating JSON-parsing:
$jsontext = gc '.\Sample Json.json'
..........
foreach ($g in $guests) {
$a = $jsontext | ConvertFrom-Json
# process $a
# ............
$array.Add($a) >$null
}
In case the JSON file is very big and you change only a few parts of it you can use a faster cloning technique on the changed parts (and their entire parent chain) via .PSObject.Copy():
foreach ($g in $guests) {
$a = $output.PSObject.Copy()
# ............
$a.ProfileProperties = $a.ProfileProperties.PSObject.Copy()
# ............
foreach ($i in $a.ProfileProperties.Count) {
# ............
$prop = $a.ProfileProperties[$j].PSObject.Copy();
# ............
}
$array.Add($a) >$null
}
As others have pointed out, appending $object appends a references to the same single object, so you keep changing the values for all elements in the list. Unfortunately the approach #wOxxOm suggested (which I thought would work at first too) doesn't work if your JSON datastructure has nested objects, because Copy() only clones the topmost object while the nested objects remain references to their original.
Demonstration:
PS C:\> $o = '{"foo":{"bar":42},"baz":23}' | ConvertFrom-Json
PS C:\> $o | Format-Custom *
class PSCustomObject
{
foo =
class PSCustomObject
{
bar = 42
}
baz = 23
}
PS C:\> $o1 = $o
PS C:\> $o2 = $o.PSObject.Copy()
If you change the nested property bar on both $o1 and $o2 it has on both objects the value that was last set to any of them:
PS C:\> $o1.foo.bar = 23
PS C:\> $o2.foo.bar = 24
PS C:\> $o1.foo.bar
24
PS C:\> $o2.foo.bar
24
Only if you change a property of the topmost object you'll get a difference between $o1 and $o2:
PS C:\> $o1.baz = 5
PS C:\> $o.baz
5
PS C:\> $o1.baz
5
PS C:\> $o2.baz
23
While you could do a deep copy it's not as simple and straightforward as one would like to think. Usually it takes less effort (and simpler code) to just create the object multiple times as #PetSerAl suggested in the comments to your question.
I'd also recommend to avoid appending to an array (or arraylist) in a loop. You can simply echo your objects inside the loop and collect the entire output as a list/array by assigning the loop to a variable:
$json = Get-Content '.\Sample Json.json' -Raw
$array = foreach ($g in $guests) {
$a = $json | ConvertFrom-Json # create new object
$a.Username = $g.'EmailAddress'
...
$a # echo object, so it can be collected in $array
}
Use Get-Content -Raw on PowerShell v3 and newer (or Get-Content | Out-String on earlier versions) to avoid issues with multiline JSON data in the JSON file.

PowerShell copy an array completely

I'm trying to create a complete copy of an existing array. Every time I try this it doesn't seem to work. The thing is that I'm modifying the Object names inside the new copied array, but they're also changed in the original array..
The code below is highly simplified as there is a lot more happening then only renaming object names but it proves the point I think.
Some example code:
Function Get-Fruits {
Param (
$Fruits = #('Banana', 'Apple', 'Pear')
)
foreach ($F in $Fruits) {
[PSCustomObject]#{
Type = $F
}
}
}
$FruitsOriginal = Get-Fruits
Function Rename-ObjectName {
# Copy the array here
$FruitsNew = $FruitsOriginal # Not a true copy
$FruitsNew = $FruitsOriginal | % {$_} # Not a true copy
$FruitsNew = $FruitsOriginal.Clone() # Not a true copy
$FruitsNew | Get-Member | ? MemberType -EQ NoteProperty | % {
$Name = $_.Name
$FruitsNew | % {
$_ | Add-Member 'Tasty fruits' -NotePropertyValue $_.$Name
$_.PSObject.Properties.Remove($Name)
}
}
}
Rename-ObjectName
The desired result is 2 completely separate arrays.
$FruitsOriginal
Type
----
Banana
Apple
Pear
$FruitsNew
Tasty fruits
------------
Banana
Apple
Pear
Thank you for your help.
# Copy the array here
$FruitsCopy = #()
$FruitsCopy = $FruitsCopy + $FruitsOriginal
You can use serialisation to deep clone your array:
#Original data
$FruitsOriginal = Get-Fruits
# Serialize and Deserialize data using BinaryFormatter
$ms = New-Object System.IO.MemoryStream
$bf = New-Object System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
$bf.Serialize($ms, $FruitsOriginal)
$ms.Position = 0
#Deep copied data
$FruitsNew = $bf.Deserialize($ms)
$ms.Close()
Since Powershell 3.0, same approach as Jaco's answer but using PSSerializer.
It uses a CliXML format compatible with Export-Clixml & Import-Clixml and personally I find it easier to read.
In theory, supports a nested hierarchy up to [int32]::MaxValue levels-deep
# Original data
$FruitsOriginal = Get-Fruits
# Serialize and Deserialize data using PSSerializer:
$_TempCliXMLString = [System.Management.Automation.PSSerializer]::Serialize($FruitsOriginal, [int32]::MaxValue)
$FruitsNew = [System.Management.Automation.PSSerializer]::Deserialize($_TempCliXMLString)
# Deep copy done.
If you're copying an array of objects/value that contains all "truthy" values, or you want to quickly filter out null and "falsey" values, then this works great:
$FruitsNew = $FruitsOriginal|?{$_}
Depending on what you need to do with the objects, and if they're simple enough (as in your example), you could just replace them with a new object.
$NewFruits = $FruitsOriginal | %{ [PSCustomObject]#{ "Tasty Fruits" = $_.Type } }

Resources