PowerShell replace value in an array - arrays

I'm quite new in PowerShell and I would need a little bit of support how to replace values in an array. Please have a look at my example:
[array[]]$nodes = #()
[array[]]$nodes = get-NcNode | select-object -property Node, #{Label = "slot"; expression = {#("a")*4}}
$nodes
Node slot
---- ----
nn01 {a,a,a,a}
nn02 {a,a,a,a}
nn03 {a,a,a,a}
nn04 {a,a,a,a}
$nodes[0].slot[0]
a
$nodes[0].slot[0] = "b" #I try to replace a with b
$nodes[0].slot[0]
a #It didn’t work
$nodes[0].slot.SetValue("b",0) #I try to replace a with b
$nodes[0].slot[0]
a #It didn’t work
$nodes[0] | Add-Member -MemberType NoteProperty -Name slot[0] -Value "b" -Force
$nodes[0]
Node slot slot[0]
---- ---- -------
nn01 {a,a,a,a} b #That’s not what I wanted

If you really need an array of arrays (type [array[]]), your problem is solved as follows:
$nodes[0][0].slot[0] = "b"
That is, each of your $nodes elements is itself an array, and the way you filled $nodes, each [pscustomobject] instance output by your get-NcNode | select-object ... pipeline became its own element of $nodes, but each as a single-element sub-array - hence the need for the extra [0] index access.[1]
However, it sounds like a regular array ([array], effectively the same as [object[]]) is sufficient in your case, where each element holds a (single, scalar) [pscustomobject]:
# Type constraint [array] creates a regular [object[]] array.
[array] $nodes = get-NcNode | select-object -property Node, #{Label = "slot"; expression = {#("a")*4}}
With $nodes defined like this, your original code should work.
[1] On getting a value - but not on setting - you can get away without the extra index, thanks to PowerShell's member-access enumeration feature.

Related

Powershell: how to turn an array of objects into an array of integers?

I create an array of objects that hold the lengths of files with this command:
$lengths = dir -File | Sort-Object -Descending | Select-Object Length -First 5
However, I want this to be an array of integers, not objects, so I can edit the values easily. How can I convert this $lengths object array into an integer array?
Thank You! :)
The answer was provided in comments but to give it closure, you can expand the values of the objects using Select-Object -ExpandProperty. It's worth noting that your Sort-Object statement is not actually sorting by Length unless you specify the property to be sorted:
$length = dir -File | Sort-Object Length -Descending | Select-Object -ExpandProperty Length -First 5
Personal preference to expand the values, you can use ForEach-Object, the syntax is far easier in my opinion but there is no -First X here:
$length = dir -File | ForEach-Object Length | Sort-Object -Descending
$length type will be object[] (object array) even though it will contain an array of int64 values, this is default type for arrays in PowerShell, you can however provide a desired output type either by type casting or hard typing. Looking into the Cast Operator [..] is a good starting point.
# type casting
$length = [Int64[]] (dir -File | ....)
# hard typing
[Int64[]] $length = dir -File | ....
For the latter, $length is constrained to be assigned int64 values or values that can be type coerced to int64, i.e.:
[int64[]] $length = 0..10
# can be assigned an `int32` and the array element will be converted to `int64`
$length = 1
$length[0].GetType() # => `int64`
# can be assigned a `string` which contains for example a decimal value
$length = '1.1', 2
# gets converted to int64 (no longer a decimal value)
$length[0] # => 1
# cannot be assigned a `string` which does not contain a type of numeric value
$length = 1, 'hello' # => ...Cannot convert value "hello" to type "System.Int64"...
While the former only enforces the type of the array elements however a new array of different type can be assigned:
$length = [int64[]] (0..10)
$length[0] = 'hello' # => ...Cannot convert value "hello" to type "System.Int64"...
$length = 'new', 'object', 'array' # succeeds
Unfortunately, you can't use Memeber Access Enumeration for Length because it's also a property of the collection (Unlike, for example, (gci -af).LastwriteTime). But another way to obtain your desired array is:
$Length = (gci -af).Foreach({$_.Length}) | sort -Descending
Without casting, $Length is the generic object array:
PS > $Length.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[]
But all its elements are the same type:
PS > ($Length | gm).TypeName | select -Unique
System.Int64

Powershell Compare 2 Arrays of Hashtables based on a property value

I have one array of hashtables like the one below:
$hashtable1 = #{}
$hashtable1.name = "aaa"
$hashtable1.surname =#()
$hashtable1.surname += "bbb"
$hashtable2 = #{}
$hashtable2.name = "aaa"
$hashtable2.surname =#()
$hashtable2.surname += "ccc"
$hashtable3 = #{}
$hashtable3.name = "bbb"
$hashtable3.surname = #()
$hashtable3.surname += "xxx"
$A = #($hashtable1; $hashtable2; $hashtable3)
I need to iterate though the array and I need to find out duplicates based on hashtable[].name
Then I need to group those hashtable.surname to hashtable[].surname so that the result will be an array of hashtables that will group all for name all the surnames:
$hashtable1.name = "aaa"
$hashtable1.surname = ("bbb","ccc")
$hashtable3.name = "bbb"
$hashtable3.surname = ("xxx")
I was looking into iterating to empty array
+
I have found this link:
powershell compare 2 arrays output if match
but I am not sure on how to reach into the elements of the hashtable.
My options:
I was wondering if -contain can do it.
I have read about compare-object but I am not sure it can be done like that.
(It looks a bit scary in the moment)
I am on PS5.
Thanks for your help,
Aster
You can group your array items by the names using a scriptblock like so.
Once grouped, you can easily build your output to do what you seek.
#In PS 7.0+ you can use Name directly but earlier version requires the use of the scriptblock when dealing with arrays of hashtables.
$Output = $A | Group-Object -Property {$_.Name} | % {
[PSCustomObject]#{
Name = $_.Name
Surname = $_.Group.Surname | Sort-Object -Unique
}
}
Here is the output variable content.
Name Surname
---- -------
aaa {bbb, ccc}
bbb xxx
Note
Improvements have been made in PS 7.0 that allows you to use simply the property name (eg: Name) in Group-Object for arrays of hashtables, just like you would do for any other arrays type. For earlier version though, these particular arrays must be accessed by passing the property in a scriptblock, like so: {$_.Name}
References
MSDN - Group_Object
SS64 - Group Object
Dr Scripto - Use a Script block to create custom groupings in PowerShell

Query PSCustomObject Array for row with largest value

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.

Compare given size with required size and show mismatch

I'm currently having a bit of trouble with my current project. I have two arrays - the first array contains reference values for disk size:
$RequiredDisks0 = New-Object System.Object
$RequiredDisks0 | Add-Member -Type NoteProperty -Name "DeviceID" -Value "C:"
$RequiredDisks0 | Add-Member -Type NoteProperty -Name "SizeGB" -Value "80"
The second array contains the disk information of the underlying system:
$SystemDisks = Get-WmiObject Win32_LogicalDisk |
Where {$_.DriveType -eq 3} |
select DeviceID,
#{Name="Size(GB)";Expression={[decimal]("{0:N0}" -f ($_.Size/1gb))}}
What I would like to do, is check the given array against the reference array to see if any given disks are smaller than required. I've found out that I can compare the arrays by using
Compare-Object -ReferenceObject $RequiredDisks -DifferenceObject $SystemDisks -Property SizeGB,DeviceID
And I indeed receive the differences as follows:
SizeGB DeviceID SideIndicator
------ -------- -------------
99 C: =>
15 H: =>
100 I: =>
80 C: <=
25 H: <=
200 I: <=
Where I'm having trouble is working with the output. The result I'd like to achieve is an output stating "Disk n is smaller than required!". I know that everything with the side indicator "<=" is the required value and everything with the "=>" side indicator is the given value. I've tried a foreach statement but I am unable to process the data as needed - I need to check the given value against the required value and if it's smaller, tell me so. How can I again compare these values as required? Basically a "foreach object where SideIndicator is <= compare to object where SideIndicator is => and DeviceID equals DeviceID". How do I translate that into proper code?
It looks to me like the Compare-Object is doing a double comparison on both properties. The documentation or another StackOverflow soul may be able to help with that command.
My approach would be to translate your pseudo-code into code:
foreach ($disk in $SystemDisks){
$ref = $RequiredDisks | Where-object {$_.DeviceID -eq $disk.DeviceID}
if([int]($disk.SizeGB) -lt [int]($ref.SizeGB){
Write-Output "Disk $($disk.DeviceID) is smaller than required!"
}
}

Quick File to Hashtable in PowerShell

Given an array of key-value pairs (for example read in through ConvertFrom-StringData), is there a streamlined way of turning this into a Hashtable or similar to allow quick lookup? I.e. a way not requiring me to loop through the array and manually build up the hashtable myself.
Example data
10.0.0.1=alice.example.com
10.0.0.2=bob.example.com
Example usage
$names = gc .\data.txt | ConvertFrom-StringData
// $names is now Object[]
$map = ?
// $map should now be Hashtable or equivalent
echo $map['10.0.0.2']
// Output should be bob.example.com
Basically what I'm looking for is a, preferably, built-in file-to-hashtable function. Or an array-to-hashtable function.
Note: As #mjolnior explained, I actually got hash tables, but an array of single value ones. So this was fixed by reading the file -raw and hence didn't require any array to hashtable conversion. Updated the question title to match that.
Convertfrom-Stringdata does create a hash table.
You need to give it the key-value pairs as a single multi-line string (not a string array)
$map = Get-Content -raw .\data.txt | ConvertFrom-StringData
$map['10.0.0.2']
bob.example.com
When you use Get-Content without the -Raw switch, you're giving ConvertFrom-StringData an array of single-line strings, and it's giving you back an array of single-element hash tables:
$map = Get-Content .\data.txt | ConvertFrom-StringData
$map.gettype()
$Map[0].GetType()
$map[0]
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
True True Hashtable System.Object
Key : 10.0.0.1
Value : alice.example.com
Name : 10.0.0.1
I usually do the following to create a hashtable from a list of key/value pairs:
$hash = #{}
Get-Content 'C:\input.txt' | Where-Object {
$_ -like '*=*'
} | ForEach-Object {
$key, $value = $_ -split '\s*=\s*', 2
$hash[$key] = $value
}
This might not be what you're looking for, but it avoids converting the whole thing into a hash.
$content = #("10.0.0.1=alice.example.com","10.0.0.2=bob.example.com");
$content | ForEach-Object {
$keyval = $_.split("=");
if ($keyval[0] -eq "10.0.0.2") {
$keyval[1]
}
}
The output will be every value on the right side of the = where the left side matches that IP.

Resources