I want to remove an array element from json array (PSObject) if value matches as follows:
$code = 12345
$myObject = #{ ArrayPair= #(#{ code = 12345; perm = "RW" }, #{ code = 23456; perm = "RW" })}
if ($true) { # $revoke
$myObject.ArrayPair = $myObject.ArrayPair | Where-Object -FilterScript {$_.code -ne $code}
}
At the start ArrayPair has 2 array elements, after executing the filter, ArrayPair is no longer an array but rather an object with two elements. How can I keep it as an array so I can continue to add new pairs to the array?
json Values before and After removal:
Before value:
{"ArrayPair": [{"perm": "RW","code": 12345},{"perm": "RW","code": 23456}]}
After Value removal
{"ArrayPair": { "perm": "RW", "code": 23456 }}
You can force the object to stay an array like this:
$code = 12345
$myObject = #{ ArrayPair= #(#{ code = 12345; perm = "RW" }, #{ code = 23456; perm = "RW" })}
[array]$myObject.ArrayPair = $myObject.ArrayPair | Where-Object -FilterScript {$_.code -ne $code}
$myObject.ArrayPair.GetType()
#Returns
#IsPublic IsSerial Name BaseType
#-------- -------- ---- --------
#True True Object[] System.Array
To add additional entries to your array you have to try it like this:
$myObject.ArrayPair += #{code = 2134; perm= "RR"}
This way you can add entries to the array and the result looks like this:
PS C:\> $myObject.ArrayPair
Name Value
---- -----
code 23456
perm RW
code 2134
perm RR
Please be aware that += doens't really add objects to the array, but instead recreates the array with new values.
If you try to add the objects via $myObject.ArrayPair.Add(#{code = 2134; perm= "RR"}) you get an error.
Please take a look at this answer for further explanations:
PowerShell Array.Add vs +=
I while deleting elements if there was just one left, I found that I had to double force the object to make sure that it remained an array type:
[array]$temp = $result.data.app.roles.admin | Where-Object -FilterScript {$_.club -ne $ClubNo}
$result.data.app.roles.admin = [array]($temp)
Related
I'm trying to find the row with an attribute that is larger than the other row's attributes. Example:
$Array
Name Value
---- ----
test1 105
test2 101
test3 512 <--- Selects this row as it is the largest value
Here is my attempt to '1 line' this but It doesn't work.
$Array | % { If($_.value -gt $Array[0..($Array.Count)].value){write-host "$_.name is the largest row"}}
Currently it outputs nothing.
Desired Output:
"test1 is the largest row"
I'm having trouble visualizing how to do this efficiently with out some serious spaghetti code.
You could take advantage of Sort-Object to rank them by the property "Value" like this
$array = #(
[PSCustomObject]#{Name='test1';Value=105}
[PSCustomObject]#{Name='test2';Value=101}
[PSCustomObject]#{Name='test3';Value=512}
)
$array | Sort-Object -Property value -Descending | Select-Object -First 1
Output
Name Value
---- -----
test3 512
To incorporate your write host you can just run the one you select through a foreach.
$array | Sort-Object -Property value -Descending |
Select-Object -First 1 | Foreach-Object {Write-host $_.name,"has the highest value"}
test3 has the highest value
Or capture to a variable
$Largest = $array | Sort-Object -Property value -Descending | Select-Object -First 1
Write-host $Largest.name,"has the highest value"
test3 has the highest value
PowerShell has many built in features to make tasks like this easier.
If this is really an array of PSCustomObjects you can do something like:
$Array =
#(
[PSCustomObject]#{ Name = 'test1'; Value = 105 }
[PSCustomObject]#{ Name = 'test2'; Value = 101 }
[PSCustomObject]#{ Name = 'test3'; Value = 512 }
)
$Largest = ($Array | Sort-Object Value)[-1].Name
Write-host $Largest,"has the highest value"
This will sort your array according to the Value property. Then reference the last element using the [-1] syntax, then return the name property of that object.
Or if you're a purist you can assign the variable like:
$Largest = $Array | Sort-Object Value | Select-Object -Last 1 -ExpandProperty Name
If you want the whole object just remove .Name & -ExpandProperty Name respectively.
Update:
As noted PowerShell has some great tools to help with common tasks like sorting & selecting data. However, that doesn't mean there's never a need for looping constructs. So, I wanted to make a couple of points about the OP's own answer.
First, if you do need to reference array elements by index use a traditional For loop, which might look something like:
For( $i = 0; $i -lt $Array.Count; ++$i )
{
If( $array[$i].Value -gt $LargestValue )
{
$LargestName = $array[$i].Name
$LargestValue = $array[$i].Value
}
}
$i is commonly used as an iteration variable, and within the script block is used as the array index.
Second, even the traditional loop is unnecessary in this case. You can stick with the ForEach loop and track the largest value as and when it's encountered. That might look something like:
ForEach( $Row in $array )
{
If( $Row.Value -gt $LargestValue )
{
$LargestName = $Row.Name
$LargestValue = $Row.Value
}
}
Strictly speaking you don't need to assign the variables beforehand, though it may be a good practice to precede either of these with:
$LargestName = ""
$LargestValue = 0
In these examples you'd have to follow with a slightly modified Write-Host command
Write-host $LargestName,"has the highest value"
Note: Borrowed some of the test code from Doug Maurer's Fine Answer. Considering our answers were similar, this was just to make my examples more clear to the question and easier to test.
Figured it out, hopefully this isn't awful:
$Count = 1
$CurrentLargest = 0
Foreach($Row in $Array) {
# Compare This iteration vs the next to find the largest
If($Row.value -gt $Array.Value[$Count]){$CurrentLargest = $Row}
Else {$CurrentLargest = $Array[$Count]}
# Replace the existing largest value with the new one if it is larger than it.
If($CurrentLargest.Value -gt $Largest.Value){ $Largest = $CurrentLargest }
$Count += 1
}
Write-host $Largest.name,"has the highest value"
Edit: its awful, look at the other answers for a better way.
How do I check for empty values in an hashtable, and list the item name as well ?
I could do if ($Vars.ContainsValue($null)) but this does not get me what item that has a $null value
$Vars = #{
1 = "CustomerID";
2 = "DepartmentID";
3 = "Environment";
4 = "JoinDomain";
5 = ""
}
if I do a foreach ($var in $vars) I get the whole hashtable?
First of all this is not an array, because those are written as #('element1', 'element2'). This concerns a HashTable which is indicated as #{} and is enumerated by the GetEnumerator() method.
After that method it's simply a matter of filtering out what you need with the key and/or the value property.
$Vars = #{
1 = "CustomerID";
2 = "DepartmentID";
3 = "Environment";
4 = "JoinDomain";
5 = ""
}
$VerbosePreference = 'Continue'
$Vars.GetEnumerator() | Where-Object {
-not $_.Value
} | ForEach-Object {
Write-Verbose "The key '$($_.Key)' has no value"
# Other code for handling the key with no value
}
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
}
I've been struggling with this for a couple of days, and I'm not sure how to conquer it. I need to do the following:
Import a csv of users with the following values:
ID, Name, Region
Create an array based on the Region values that I can then use to populate with ID's and Names with that region, ie.
Array_SEA
AA_SCOM, Adam Andrews, SEA
Array_OAK
BB_SCOM, Bob Barker, OAK
Here's the code I've got right now:
$list2 = ipcsv .\TSE_Contact_List.csv | sort-object BU
$arraylist =#()
foreach ($vitem in $list2)
{
$arraylist += New-Object PsObject -Property #{'Array' = "Array_" + $vitem.bu}
}
foreach ($varray in $arraylist)
{
$arr = new-variable -Name $varray
$arr.value += $varray.array
$arr
}
This produces the following error for records with a duplicate regions:
New-Variable: A variable with name '#{Array=Array_SCA}' already exists.
I'm also getting the following when it tries to add values:
Property 'value' cannot be found on this object; make sure it exists and is settable.
I get that I'm not actually creating arrays in the second section, but I'm not sure how to pass the output of the variable to an array name without turning the variable declaration into the array name, if that makes sense.
I've tried the following with hash tables, and it gets closer:
$list2 = ipcsv .\TSE_Contact_List.csv | sort-object BU
$arraylist =#{}
foreach ($vitem in $list2){$arraylist[$vitem.bu] = #()}
foreach ($record in $list2)
{
$arraylist[$vitem.bu] += ($record.SCOMID,$record.Name,$record.BU)
Write-host "Array: "
$arraylist[$vitem.bu]
write-host ""
}
The output on this shows no errors, but it just keeps showing the added fields for all of the records for each iteration of the list, so I don't think that it's actually assigning each unique BU to the array name.
I like the hashtable-approach, but I would finetune it a little. Try:
$list2 = ipcsv .\TSE_Contact_List.csv | sort-object BU
$arraylist = #{}
foreach ($vitem in $list2){
if($arraylist.ContainsKey($vitem.BU)) {
#Array exists, add item
$arraylist[($vitem.BU)] += $vitem
} else {
#Array not found, creating it
$arraylist[($vitem.BU)] = #($vitem)
}
}
#TEST: List arrays and number of entries
$arraylist.GetEnumerator() | % {
"Array '$($_.Key)' has $($_.Value.Count) items"
}
You could also use Group-Object like:
$list2 = ipcsv .\TSE_Contact_List.csv | Group-Object BU
#TEST: List groups(regions) and number of entries
$list2 | % {
"Region '$($_.Name)' has $(#($_.Group).Count) items"
}
I'm still quite new to PowerShell and am trying to create a few functions that weaves together for creating and administrating an array. And I'm having some problems with getting one of these functions to work as intended.
I need the second function (AddToArray) to add an element to the specified index. None of the existing elements can be overwritten or removed.
For example, if I have an array with four indexes and all have the value 5 and I call the function AddToArray 2 4. I need the function to write for in the third index and move the existing ones one down step, so the array now looks like this:
5
5
4
5
5
This is my code so far that shows my CreateArray function and the little code piece for AddToArray function. I've been trying for a while now, but I just can't see the solution.
function CreateArray($Item1, $Item2)
{
$arr = New-Object Array[] $Item1;
# Kontrollerar om $Item2 har fått någon input och skriver in det i arrayen
if ($Item2)
{
for($i = 0; $i -lt $arr.length; $i++)
{
$arr[$i] = $Item2;
}
}
# Standard värde på arrayens index om inget värde anges vid funktionens anrop
else
{
$Item2 = "Hej $env:username och välkommen till vårat script!";
for($i = 0; $i -lt $arr.length; $i++)
{
$arr[$i] = $Item2;
}
}
$script:MainArray = $arr;
}
function AddToArray ($index, $add)
{
$MainArray[$index] = $add;
}
Arrays in .NET don't directly support insertion and they are normally fixed size. PowerShell does allow for easy array resizing but if the array gets large and you're appending (causing a resize) a lot, the performance can be bad.
One easy way to do what you want is to create a new array from the pieces e.g.:
if ($index -eq 0) {
$MainArray = $add,$MainArray
}
elseif ($index -eq $MainArray.Count - 1) {
$MainArray += $add
}
else {
$MainArray = $MainArray[0..($index-1)], $add, $MainArray[$index..($MainArray.Length-1)]
}
But that is kind of a spew. I would use a List for this, which supports insertion and is more efficient than an array.
$list = new-object 'System.Collections.Generic.List[object]'
$list.AddRange((1,2,3,4,5))
$list.Insert(2,10)
$list
And if you really need an array, call the $list.ToArray() method when you're done manipulating the list.
Arrays don't have an .insert() method, but collections do. An easy way to produce a collection from an array is to use the .invoke() method of scriptblock:
$array = 5,5,4,5,5
$collection = {$array}.invoke()
$collection
$collection.GetType()
5
5
4
5
5
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Collection`1 System.Object
Now you can use the .insert() method to insert an element at an arbitrary index:
$collection.Insert(2,3)
$collection
5
5
3
4
5
5
If you need it to be an array again, an easy way to convert it back to an array is to use the pipeline:
$collection | set-variable array
$array
$array.GetType()
5
5
3
4
5
5
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array