Add new set of values to ArrayList - arrays

So I have the following ArrayList stored in $var:
ip_prefix region string
0.0.0.0/24 GLOBAL Something
0.0.0.0/24 GLOBAL Something
0.0.0.0/24 GLOBAL Something
0.0.0.0/24 GLOBAL Something
I need to add a row to this however the following code returns an error:
$var.add("127.0.0.1/32", "GLOBAL", "something")
Error:
Cannot find an overload for "Add" and the argument count: "3".
At line:1 char:1
+ $awsips.add("127.0.0.1/32", "GLOBAL", "SOMETHING")
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodException
+ FullyQualifiedErrorId : MethodCountCouldNotFindBest
I'm sure it's something simple I have to adjust, however Google searches had me going around in circles.

$var = New-Object System.Collections.ArrayList
$var.Add(#{"ip_prefix" = "0.0.0.0/24"; "region" = "GLOBAL"; string = "Something"})
$var.Add(#{"ip_prefix" = "127.0.0.1/32"; "region" = "GLOBAL"; string = "SOMETHING"})
$var
$var | %{ Write-Output "$($_.ip_prefix), $($_.region), $($_.string)" }
Or:
$var = #()
$var += #{"ip_prefix" = "0.0.0.0/24"; "region" = "GLOBAL"; string = "Something"}
$var += #{"ip_prefix" = "127.0.0.1/32"; "region" = "GLOBAL"; string = "SOMETHING"}

Should do the job
$obj = New-Object PSObject -Property #{
ip_prefix = "0.0.0.0/24"
region = "GLOBAL"
string = "Something"
}
$var+= $obj

Your output suggests that your array list contains custom objects with properties ip_prefix, region, and string.
You therefore need to add a single object with the desired property values to your array list.
By contrast, you attempted to add 3 indvividual elements to the array list, which is not only conceptually wrong, but also fails syntactically, given that the .Add() method only accepts a single argument (technically, there is a method for adding multiple items, .AddRange()).
In PSv3+, syntax [pscustomobject]#{...} constructs a custom object from a hashtable literal with the definition order of the entries preserved.
$null = $var.Add(
[pscustomobject] #{ ip_prefix="127.0.0.1/32"; region="GLOBAL"; string="something" }
)
Note how $null = ... is used to suppress the .Add() method's output (the index at which the item was inserted).
SQLAndOtherStuffGuy's answer is on the right track, but beware that $var += ... silently replaces the array list stored in $var with a regular PowerShell array ([System.Object[]]).

Related

Powershell: Piping output of pracl command to array

pracl is a sysinternal command that can be used to list the ACLs of a directory. I have a list of shares and I want to create a csv file such that for each ACL entry, I want the share path in one column and share permission in the next. I was trying to do that by using the following code
$inputfile = "share.txt"
$outputFile = "out.csv"
foreach( $path in Get-Content $inputfile)
{
$results=.\pracl.exe $path
{
foreach ($result in $results) {write-host $path,$line}
}
$objResult = [pscustomobject]#{
Path = $Path
Permission = $line
}
$outputArray += $objResult
$objresult
}
$outputArray | Export-Csv -Path $outputfile -NoTypeInformation
It failed with the following error :-
Method invocation failed because [System.Management.Automation.PSObject] does not contain a method named 'op_Addition'.
At C:\Users\re07393\1\sample.ps1:14 char:1
+ $outputArray += $objResult
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (op_Addition:String) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
Any suggestions ?
You're trying to create an array of [pscustomobject]s in your $outputArray variable iteratively, using +=, but you're not initializing $outputArray as an array - see the bottom section for an explanation of the resulting behavior.
Thus, the immediate solution to your problem is to do just that:
# Do this before your `foreach` loop, then `+=` will work for appending elements.
$outputArray = #()
However, using += to add to arrays is inefficient, because in reality a new array instance must be created every time, because arrays are immutable data structures. That is, every time += is used, PowerShell creates a new array instance behind the scenes to which the existing elements as well as the new element are copied.
A simpler and much more efficient approach is to let PowerShell create an array for you, by using the foreach loop as an expression and assigning it to a variable as a whole:
That is, whatever is output in every iteration of the loop is automatically collected by PowerShell:
A simplified example:
# Create an array of 10 custom objects
[array] $outputArray = foreach ($i in 1..10) {
# Create and implicitly output a custom object in each iteration.
[pscustomobject] #{
Number = $i
}
}
Note the use of type constraint [array] to the left of $outputArray, which ensures that the variable value is always an array, even if the loop happens to produce just one output object (in which case PowerShell would otherwise just store that object itself, and not wrap it in an array).
Note that you can similarly use for, if, do / while / switch statements as expressions.
In all cases, however, these statements can only serve as expressions by themselves; regrettably, using them as the first segment of a pipeline or embedding them in larger expressions does not work - see GitHub issue #6817.
As for what you tried:
$outputArray += $objResult
Since you didn't initialize $outputArray before the loop, the variable is implicitly created in the loop's first iteration:
If the LHS variable doesn't exist yet, += is effectively the same as =: that is, the RHS is stored as-is in the LHS variable, so that $outputArray now contains a [pscustomobject] instance.
In the second iteration, because $outputArray now has a value, += now tries to perform a type-appropriate + operation (such as numeric addition for numbers, and concatenation for strings), but no + (op_Addition()) operation is defined for type [pscustomobject], so the operation fails with the error message you saw.

Adding element to array in powershell scriptblock converts array to string

I noticed odd behaviour using arrays in scriptblocks. The following code shows the problem:
$array = #("x", "y")
Write-Host "$($array.GetType().Name)"
Write-Host "$($array.GetType().BaseType)"
$bad = {
$array += "z"
Write-Host "$($array.GetType().Name)"
Write-Host "$($array.GetType().BaseType)"
$array
}
$good = {
$array = $array.Clone()
$array += "z"
Write-Host "$($array.GetType().Name)"
Write-Host "$($array.GetType().BaseType)"
$array
}
& $good
& $bad
Executing the script will produce the following output:
Object[]
array
Object[]
array
x
y
z
String
System.Object
z
The scriptblock $bad does not work as I would expect. It converts the array to string, but it should simply add the element z to the array. If there is no element added, the array can be used as expected.
I noticed this behaviour in powershell 5.0 and 5.1 but not in the ISE. Is it a bug or can anyone explain this?
It's a scope issue. The variable on the left side of the assignment operation in the scriptblocks is defined in the local scope.
This statement
$array = $array.Clone()
clones the value of the global variable $array and assigns it to the local variable $array (same name, but different variable due to different scope). The local variable $array then contains a copy of the original array, so the next statement
$array += "z"
appends a new element to that array.
In your other scriptblock you immediately append a string to the (local) variable $array. In that context the local variable is empty, so $array += "z" has the same effect as $array = "z", leaving you with a variable containing just the string "z".
Specify the correct scope and you'll get the behavior you expect:
$array = #("x", "y")
$not_bad = {
$script:array += "z"
Write-Host "$($script:array.GetType().Name)"
Write-Host "$($script:array.GetType().BaseType)"
$script:array
}
& $not_bad
Beware, however, that this will actually modify the original array in the global/script scope (your $good example leaves the original array unchanged).
I'm not sure if I would consider this behavior a bug, but it's definitely a gotcha.
I would like to post my preferred solution which bases on Ansgars explanation:
$array = #("x", "y")
$not_bad = {
$array = $array + "z"
Write-Host "$($array.GetType().Name)"
Write-Host "$($array.GetType().BaseType)"
$array
}
& $not_bad
Important is the assignment to the local variable (or better to create a local variable) before adding further elements. A simple
$array = $array
would do, but this line may be confusing.

OverloadDefinitions on CSV field?

I created a question earlier where I asked how to save a specific CSV column into a PowerShell array. I got my question answered and my passwords and my usernames are now saved in two different arrays. However, the Server IP-Adresses doesn't want to be saved into the array I made for them to be saved in. When I try to log in to a server (with PowerShell inputting credentials automatically) I get this error:
Invoke-Command : One or more computer names are not valid. If you are trying to
pass a URI, use the -ConnectionUri parameter, or pass URI objects instead of
strings.
At C:xxx\xxx\xxx\xxx\pornfolder:37 char:15
+ ... $output = Invoke-Command -computername $AddressArray[$row] -credent ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (System.String[]:String[]) [Invoke-Command], ArgumentException
+ FullyQualifiedErrorId : PSSessionInvalidComputerName,Microsoft.PowerShell.Commands.InvokeCommandCommand
So I printed the array where the adresses are meant to be saved in, and I get this:
OverloadDefinitions
-------------------
System.Object&, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Address(int )
This is, honest to god, all that is saved in the array. No wonder the computername(s) are invalid.
Here is the code
$PasswordsArray = New-Object System.Collections.ArrayList
$UsernamesArray = New-Object System.Collections.ArrayList
$Importedcsv = Import-csv "C:\my\csv\file\is\located.here" -Delimiter ";"
$PasswordsArray += #($Importedcsv.password)
$AA += [string] #($Importedcsv.address)
$UsernamesArray += #($Importedcsv.username)
Basically your issue is caused by a convenience feature that was introduced in PowerShell v3 combined with awkward field naming. Since PowerShell v3 arrays are automatically unrolled when you use dot-notation on them ($arr.something). That way properties or methods can be called on the array elements via the array object without having to loop over the elements ($arr | ForEach-Object {$_.something}).
Your column title (address) conflicts with the name of a method of the array object (Address()).
PS C:\> $Importedcsv | Get-Member
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
address NoteProperty System.String address=192.168.0.10
password NoteProperty System.String password=#adfgad
username NoteProperty System.String username=advokathuset\user
PS C:\> Get-Member -InputObject $Importedcsv
TypeName: System.Object[]
Name MemberType Definition
---- ---------- ----------
Count AliasProperty Count = Length
Add Method int IList.Add(System.Object value)
Address Method System.Object&, mscorlib, Versio...
Clear Method void IList.Clear()
...
$Importedcsv | Get-Member unrolls the array (via the pipeline), so Get-Member is called on the array elements, whereas Get-Member -InputObject $Importedcsv operates on the array object itself (PowerShell imports CSVs as an array of objects).
Rename the column to something else, either at the source (where you create the file), or on import:
$filename = "C:\my\csv\file\is\located.here"
$headers = 'IPAddress', 'Username', 'Password'
$Importedcsv = Import-Csv $filename -Delimiter ";" -Header $headers
$AA = #($Importedcsv.IPAddress)
or expand the address property on the individual array items in a loop:
$AA = #($Importedcsv | ForEach-Object { $_.address } )
Note that a construct [string]#(...) will cast your array to a single string, so don't use it unless you actually want all addresses in a single string. To enforce a string array you'd use [string[]]#(...), but usually that's not necessary with PowerShell, so I'd recommend against using it unless you know what you need it for.
Also note that it's unnecessary to create an ArrayList object and append to it. The #() operator will already produce an array, so you should simplify things like this:
$PasswordsArray = New-Object System.Collections.ArrayList
$PasswordsArray += #($Importedcsv.password)
to this:
$PasswordsArray = #($Importedcsv.password)
unless you require specific features of an ArrayList.

Strange behaviour with strings and arrays

Here I assign the value of an array to a variable, I then alter the variable, the array changes also.
$TestArray = #{ "ValueA" = "A" ; "ValueB" = "B" ; "Number" = "" }
$TestNumbers = 1..10
foreach ($number in $testnumbers) {
$results = $TestArray
$results.Number = $number
Write-Host $TestArray.Number
}
I thought that $results = $TestArray would take a copy of $TestArray but this test shows that modifying $results also changes the corresponding value in $TestArray
Can anyone help me understand this behavior?
Doing:
$results = $TestArray
will make $results a reference to the same object referred to by TestArray. So, if you change one, the other will also be affected because they are the same object.
To instead make $results a copy of $TestArray, you can use its Clone method:
$results = $TestArray.Clone()
Also, just for the record, $TestArray is not actually an array. It is a hashtable (also called a hashmap) where keys are paired with values. An array would be something like:
$TestArray = (1, 2, 3)

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

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'

Resources