Powershell - how to declare an array for use with += operator - arrays

I can intuitively figure out most languages, but apparently not Powershell.
I want to create an array of arrays (this will contain disk directories and a count so later I can verify we have at least that many files).
From that array of arrays, I want to pull out a single array of just the directory names so I can pass it to Get-ChildItem.
$DirInfo = #('d:\Work',2),
('d:\Temp',3)
$DirNameArray=#() #declare empty array
foreach ($item in $DirInfo)
{
$DirNameArray += , $item[0] #tried with and without the comma here
Write-Host 'Loop1 ' $item[0]
}
write-host $DirNameArray.count
#Let's Verify what we got so we know how many items we have in our array
Write-Host "Verify with a loop"
foreach ($dir in $DirNameArray)
{
Write-Host 'Loop2:' $dir
}
Write-Host "Verify the other way"
Write-Host $DirNameArray
Actual Results:
Loop1 d:\Work
Loop1 d:\Temp
1
Verify with a loop
Loop2: d:\Workd:\Temp
Verify the other way
d:\Workd:\Temp
What I don't understand is why Loop2 didn't execute twice.
It looks like the =+ is just stringing together the values instead of adding a new item to my array called $DirNameArray.
I'm still utterly baffled, one file I created does this and gives me the expected results:
$a = "one","two"
Write-Host $a.count
$a += "three"
Write-Host $a.count
Results:
2
3
So if the above worked, why didn't my code work?
A second file I created does this - and I don't understand the results. I even made the variable name different so I wouldn't be dealing with any prior definition or values of that variable:
$DirNameArray5="abc","def"
write-host $DirNameArray5.count
$DirNameArray5 += "xyz"
write-host $DirNameArray5.count
$DirNameArray5 += #("opq")
write-host $DirNameArray5.count
Results:
1
1
1
$DirNameArray7="abc","def"
write-host $DirNameArray7.count
$DirNameArray7 += "xyz"
write-host $DirNameArray7.count
$DirNameArray7 += #("opq")
write-host $DirNameArray7.count
Results:
2
3
4
So apparently, if you once define a variable as a string, it's hard to get Powershell to redefine it as an array.
But I still have my original question. How to define an empty array so I can add to it in a loop using the += operator.
$DirNameArray=#()
I finally used the .GetType() method to see what my variables actually were:
PS C:\Users\nwalters> $DirNameArray5.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String System.Object
PS C:\Users\nwalters> $DirNameArray7.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
Bottom line - this is what I want to do - in as few lines of code as possible with no loop:
[string]$x=#() # declare empty array
Write-Host $x.GetType()
$x += "one"
$x += "two"
Write-Host $x.count
Write-Host $x
Actual Results
System.String
1
onetwo
Desired Results:
object[] or string[]???
2
one two

Powershell will not let you create an empty array, or let you empty an array down to nothing (with one exception). There are two ways I have discovered to work around this issue:
Method 1: Use -OutVariable to a new variable name to create an array with your input
Example: gci C:\TestDir1 -OutVariable test
Using the .GetType() method returns:
$test.GetType()
Directory: C:\TestDir1
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 4/20/2020 11:36 AM 8015 Test1.xls
-a---- 6/26/2020 12:59 PM 0 test2.txt
Module : CommonLanguageRuntimeLibrary
Assembly : mscorlib, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
TypeHandle : System.RuntimeTypeHandle
DeclaringMethod :
BaseType : System.Object
UnderlyingSystemType : System.Collections.ArrayList
FullName : System.Collections.ArrayList
This will create a new variable named $test that is an array (since there are multiple items in it). $test.gettype() shows the object as an array.
Method 2: Explicitly declare an array with two dummy objects and then remove both dummy objects.
[System.Collections.ArrayList]$array = "value1", "value2"
$array.remove("value1")
$array.remove("value2")
Using the gettype method will still show $test is an array even though it is empty:
> $test.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True ArrayList System.Object
The array will now be completely empty and can be used to store any new input. This doesn't work unless you explicitly name the variable type like I did in my example (not sure why). Example shown below:
$test = "value1", "value2"
$test.Remove("value1")
Exception calling "Remove" with "1" argument(s): "Collection was of a fixed size."
At line:3 char:1
+ $test.Remove("value1")
+ ~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : NotSupportedException
P.S. I know this is an old thread, but it is unanswered and I just came upon this issue myself so I am answering it for anyone else that searches this issue.

The following code is tested on two different computers, both with PowerShell 2.0. Can you try this and post results.
# declare an empty array
$var = #()
write-host "var.count = '$($var.count)' var.type ='$($var.GetType())' var.type.BaseType = '$($var.GetType().BaseType)'"
# add a single item
$var += "single item"
write-host "var.count = '$($var.count)' var.type ='$($var.GetType())' var.type.BaseType = '$($var.GetType().BaseType)'"
# add an array
$var += , #("array 1 - item 1","array 1 - item 2")
write-host "var.count = '$($var.count)' var.type ='$($var.GetType())' var.type.BaseType = '$($var.GetType().BaseType)'"
# display the 'single item'
write-host "single item = '$($var[0])'"
# display first element of array item
write-host "first element of array item = '$($var[1][0])'"
gives me
var.count = '0' var.type ='System.Object[]' var.type.BaseType = 'array'
var.count = '1' var.type ='System.Object[]' var.type.BaseType = 'array'
var.count = '2' var.type ='System.Object[]' var.type.BaseType = 'array'
single item = 'single item'
first element of array item = 'array 1 - item 1'

Related

Find which array entry is contained in specific string in powershell

I have a string that I know will contain one entry from an array of strings. I'm looking for which array entry the string contains.
If I have the following variables:
$String = "This is my string"
$Array = "oh","my","goodness"
I can verify if any of the array entries exist in the string with:
$String -match $($Array -join "|")
But, this will only return true or false. I want to know which entry was matched.
-Contains seems to be going in the right direction, but that only verifies if an array contains a specific object, and I want to verify which of the array entries is contained within the string. I'm thinking something like a foreach loop would do the trick, but that seems like a fairly resource intensive workaround.
Any help is appreciated!
That's what the automatic variable $Matches is for.
See about_Regular_Expressions
$String = "This is my string"
$Array = "oh","my","goodness"
if($String -match $($Array -join "|"))
{
$Matches
}
Returns:
PS />
Name Value
---- -----
0 my
This is another way you can see the matches:
# Adding "This" for the example
$String = "This is my string"
[regex]$Array = "oh","my","goodness","This" -join '|'
$Array.Matches($String)
Returns:
PS />
Groups Success Name Captures Index Length Value
------ ------- ---- -------- ----- ------ -----
{0} True 0 {0} 0 4 This
{0} True 0 {0} 8 2 my

what bad consequence could using an array have 'in this situation'?

My question is two fold I'm playing around with PS and trying new stuff, I wrote a small script that generates full names by exporting first names from a txt.file and last names from another txt.file and finally randomly combines the 2 to create a full name, this is the script:
$Firstnames = Get-Content -Path 'C:\Users\user\Desktop\Firstname.txt'
$Lastnames = Get-Content -Path 'C:\Users\user\Desktop\Lastnames.txt'
$feminine = $firstnames | ?{$_ -like "*;frau"} | %{$_.Split(';')[0]}
Write-Host "Random full name generator"
$numberofNames = 1..(Read-Host 'how many combination do you want to generate?')
$numberofNames | foreach {
$f = $feminine[ (Get-Random $feminine.count)]
$l = $Lastnames[ (Get-Random $Lastnames.count)]
$full = $f+" "+$l
Write-output $full
}
1- now $numberofNames is an array 'a range operator', I would like to know what bad consequences could use this method have? is this the best approach for the users input?
2- what is the key difference between using for example $a = 100 and $a = 1..100?
in case you need to know:
$firstnames looks something like this:
Linda;frau
Aline;frau
Lisa;frau
Katja;frau
Karen;frau
and $lastnames:
smith
anderson
müller
klein
Rex
thank you
1- now $numberofNames is an array 'a range operator', i would like to know what bad consequences could using this method have? is this the best approach for the users input?
It's valid. Even if the user adds e.g. a string as input Powershell will throw an error that it could not cast the input to an int:
1 .. "test" | % {Write-Host $_ }
Cannot convert value "test" to type "System.Int32". Error: "Input string was not in a correct format."
At line:1 char:1
+ 1 .. "test" | % {Write-Host $_ }
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvalidCastFromStringToInteger
Even a neg. int shouldn't be a problem since you're not using the array content for any indexing operations.
2- what is the key difference between using for example $a = 100 and $a = 1..100?
$a = 100 points to integer with the value 100.
$a = 1..100 is an array with 100 entries.

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 insure json stays array

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)

Add One Object from an Array to another Array

I can't figure out what I'm doing wrong and hope someone can point me in the right direction.
I'm trying to iterate through an array of objects and testing on each object and when something is true, I want to take that object and add it to it's own array, as a single object (just like it was in the original array of objects). I seem to be adding the information to the new array, but when I reference the new array by doing newarray[0] it gives me the first item of the object, not the entire object itself.
The issue appears to be with this line:
$global:MachinesInAD += $global:MachineObject
The data in the csv file is a machine hostname, the machines IP address, an error code, and an agent ID.
e.g. MACHINENAME, 10.10.10.10, ERROR101, 123456FF
Function ReadExcelReport (){
$global:Report = "C:\TEMP\Tools\Scripts\agents.csv"
$Unresponsive = import-csv $global:Report | Where-Object {($_.State -eq "QUEUED" -or $_.State -eq "FAILED")} #Add items to array from spreadsheet where the state is equal to queued or failed
$global:UnresponsiveMachineInfo = #()
foreach ($item in $Unresponsive){
$global:UnresponsiveMachineInfo += ,#($item.'Hostname', $item.'IP Address',$item.'Error',$item.'Agent Cert ID') #Build the object - Add the following columns hostname, ip address, error, agent cert id
}
ADCheck
}
Function ADCheck (){
$Machine = $null
$global:MachinesInAD = #()
$global:MachinesNotInAD = #()
$global:MachineObject = New-Object system.object
$global:MachineObject = $Null
$global:MachinesInAD = $null
$global:UnresponsiveMachineInfo | foreach-object { #Iterate through each object in the array
$global:MachineObject = $_
$Machine = $_[0] #Set Machine to the hostname AKA the first element in the array for the current ($_) object (objects defined above)
try{
write-host "Checking A.D. for: $Machine"
if (Get-ADComputer $Machine){ #Check to see if the machine is in A.D.
write-host "Found $Machine in A.D." -ForegroundColor Green
$global:MachinesInAD += $global:MachineObject
}
}
catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] { #If the machine was NOT in A.D. catch the error it creates and...
write-warning -message "Machine $Machine not found in A.D."
$global:MachinesNotInAd += $MachineObject
}
}
}
This is happening because what you're calling an object, is just an array (which.. is an object, but your properties are elements, not properties).
Anyway, when you do this:
$global:MachinesInAD += $global:MachineObject
You end up concatenating the arrays.
#(1,2,3) + #(4,5,6)
That results in an array of 6 elements, not 3 numbers and an array.
You should use either a [hashtable] or a [PSObject] instead of an array; or as you did when you built the original one, you'll need to force it into a one elements array, something like:
$global:MachinesInAD += ,#($global:MachineObject)

Resources