PowerShell copy an array completely - arrays

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 } }

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

Powershell match properties and then selectively combine objects to create a third

I have a solution for this but I believe it is not the best method as it takes forever so I am looking for a faster/better/smarter way.
I have multiple pscustomObject objects pulled from .csv files. Each object has at least one common property. One is relatively small (around 200-300 items/lines in the object) but the other is sizable (around 60,000-100,000 items). The contents of one may or may not match the contents of the other.
I need to find where the two objects match on a specific property and then combine the properties of each object into one object with all or most properties.
An example snippet of the code (not exact but for this it should work - see the image for the sample data):
DataTables
Write-Verbose "Pulling basic Fruit data together"
$Purchase = import-csv "C:\Purchase.csv"
$Selling = import-csv "C:\Selling.csv"
Write-Verbose "Combining Fruit names and removing duplicates"
$Fruits = $Purchase.Fruit
$Fruits += $Selling.Fruit
$Fruits = $Fruits | Sort-Object -Unique
$compareData = #()
Foreach ($Fruit in $Fruits) {
$IndResults = #()
$IndResults = [pscustomobject]#{
#Adding Purchase and Selling data
Farmer = $Purchase.Where({$PSItem.Fruit -eq $Fruit}).Farmer
Region = $Purchase.Where({$PSItem.Fruit -eq $Fruit}).Region
Water = $Purchase.Where({$PSItem.Fruit -eq $Fruit}).Water
Market = $Selling.Where({$PSItem.Fruit -eq $Fruit}).Market
Cost = $Selling.Where({$PSItem.Fruit -eq $Fruit}).Cost
Tax = $Selling.Where({$PSItem.Fruit -eq $Fruit}).Tax
}
Write-Verbose "Loading Individual results into response"
$CompareData += $IndResults
}
Write-Output $CompareData
I believe the issue is in lines like these:
Farmer = $Purchase.Where({$PSItem.Fruit -eq $Fruit}).Farmer
If I understand this it is looking through the $Purchase object each time it goes through this line. I am looking for a way to speed that whole process up instead of having it look through the entire object for each match attempt.
Using this Join-Object:
$Purchase | Join $Selling -On Fruit | Format-Table
Result (using Simon Catlin's data):
Fruit Farmer Region Water Market Cost Tax
----- ------ ------ ----- ------ ---- ---
Apple Adam Alabama 1 MarketA 10 0.1
Cherry Charlie Cincinnati 2 MarketC 20 0.2
Damson Daniel Derby 3 MarketD 30 0.3
Elderberry Emma Eastbourne 4 MarketE 40 0.4
Fig Freda Florida 5 MarketF 50 0.5
using Join-Object
http://ramblingcookiemonster.github.io/Join-Object/
Join-Object -Left $purchase -Right $selling -LeftJoinProperty fruit -RightJoinProperty fruit -Type OnlyIfInBoth | ft
I had this very problem when trying to consolidate employee data from our HR system against employee data in our AD forest. With many thousands of rows, the process was taking an age.
I eventually walked away from custom objects and reverted to old school hash tables.
The hash tables entries themselves then held a sub-hash table with the data. In your instance, the outer hash would be keyed on $fruit, with the sub-hash containing the various attributes, e.g.: farmer, region, Etc.
Hash tables are lightning quick in comparison. It's a shame that PowerShell is slow in this regard.
Shout if you need more info.
26/01 Example code... assuming I'm correctly understanding the requirement:
PURCHASE.CSV:
Fruit,Farmer,Region,Water
Apple,Adam,Alabama,1
Cherry,Charlie,Cincinnati,2
Damson,Daniel,Derby,3
Elderberry,Emma,Eastbourne,4
Fig,Freda,Florida,5
SELLING.CSV
Fruit,Market,Cost,Tax
Apple,MarketA,10,0.1
Cherry,MarketC,20,0.2
Damson,MarketD,30,0.3
Elderberry,MarketE,40,0.4
Fig,MarketF,50,0.5
CODE
[String] $Local:strPurchaseFile = 'c:\temp\purchase.csv';
[String] $Local:strSellingFile = 'c:\temp\selling.csv';
[HashTable] $Local:objFruitHash = #{};
[System.Array] $Local:objSelectStringHit = $null;
[String] $Local:strFruit = '';
if ( (Test-Path -LiteralPath $strPurchaseFile -PathType Leaf) -and (Test-Path -LiteralPath $strSellingFile -PathType Leaf) ) {
#
# Populate data from purchase file.
#
foreach ( $objSelectStringHit in (Select-String -LiteralPath $strPurchaseFile -Pattern '^([^,]+),([^,]+),([^,]+),([^,]+)$' | Select-Object -Skip 1) ) {
$objFruitHash[ $objSelectStringHit.Matches[0].Groups[1].Value ] = #{ 'Farmer' = $objSelectStringHit.Matches[0].Groups[2].Value;
'Region' = $objSelectStringHit.Matches[0].Groups[3].Value;
'Water' = $objSelectStringHit.Matches[0].Groups[4].Value;
};
} #foreach-purchase-row
#
# Populate data from selling file.
#
foreach ( $objSelectStringHit in (Select-String -LiteralPath $strSellingFile -Pattern '^([^,]+),([^,]+),([^,]+),([^,]+)$' | Select-Object -Skip 1) ) {
$objFruitHash[ $objSelectStringHit.Matches[0].Groups[1].Value ] += #{ 'Market' = $objSelectStringHit.Matches[0].Groups[2].Value;
'Cost' = [Convert]::ToDecimal( $objSelectStringHit.Matches[0].Groups[3].Value );
'Tax' = [Convert]::ToDecimal( $objSelectStringHit.Matches[0].Groups[4].Value );
};
} #foreach-selling-row
#
# Output data. At this point, you could now build a PSCustomObject.
#
foreach ( $strFruit in ($objFruitHash.Keys | Sort-Object) ) {
Write-Host -Object ( '{0,-15}{1,-15}{2,-15}{3,-10}{4,-10}{5,10:C}{6,10:P}' -f
$strFruit,
$objFruitHash[$strFruit]['Farmer'],
$objFruitHash[$strFruit]['Region'],
$objFruitHash[$strFruit]['Water'],
$objFruitHash[$strFruit]['Market'],
$objFruitHash[$strFruit]['Cost'],
$objFruitHash[$strFruit]['Tax']
);
} #foreach
} else {
Write-Error -Message 'File error.';
} #else-if
I needed to do this myself for something similar. I wanted to take two system array objects and compare them pulling out the matches without having to manipulate the input data each time. Here's the method I used, which although I appreciate this is inefficient, it was instantaneous for the 200 or so records I had to work with.
I tried to translate what I was doing (users and their old and new home directories) into farmers, fruit and markets etc so I hope it makes sense!
$Purchase = import-csv "C:\Purchase.csv"
$Selling = import-csv "C:\Selling.csv"
$compareData = #()
foreach ($iPurch in $Purchase) {
foreach ($iSell in $Selling) {
if ($iPurch.fruit -match $iSell.fruit) {
write-host "Match found between $($iPurch.Fruit) and $($iSell.Fruit)"
$hash = #{
Fruit = $iPurch.Fruit
Farmer = $iPurch.Farmer
Region = $iPurch.Region
Water = $iPurch.Water
Market = $iSell.Market
Cost = $iSell.Cost
Tax = $iSell.Tax
}
$Build = New-Object PSObject -Property $hash
$Total = $Total + 1
$compareData += $Build
}
}
}
Write-Host "Processed $Total records"

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: modify elements of array

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
}

Resources