Powershell - Multidimensional array issues - arrays

I've got some fun issues with a multidimensional array that just aren't making sense to me. I'm sure I'm just misusing something or expecting functionality that isn't quite there with PS. Here's what I'm trying to do:
$Packages = #()
$Packages += #("program 1","c:\path\to\program.exe","/switch")
$Packages += #("program 2","c:\path\to\program2.exe","/switch")
foreach ($Package in $Packages) {
$Name, $Path, $Args = $Package
Install-Program $Name $Path $Args
}
Install-Program is a function I've written that checks the package filetype (currently works with .msp, .msu, .msi, and .exe) and just has it install as silently as I can manage it (obviously not all .exe installers have a silent switch, grumble grumble).
The issue seems to be that when I iterate through the $Packages array, the $Package entry it pulls is from every element in each row of the array, i.e., it returns
$Package=$Packages[0][0] then $Package=$Packages[0][1] etc.,
instead of
$Package=$Packages[0] then $Package=$Packages[1] etc.
What's the least complicated way to make it do the latter?

Related

Using and filling an ArrayList with .Add() questions using Powershell

I'm attempting to add performance to a script that has an array of data of over 10,000 entries, then use it in a foreach-object statement to fill a blank ArrayList with new data by calling another function. I've been reading how I shouldn't use +=, which is how I learned, because the performance is dreadful as it tears down the array and rebuilds it for each item.
The issue I have is I need to call a function to fill an empty ArrayList, but I don't seem to be able to do this inside the .Add() method.
Old code:
Function get_gfe
Function get_os
$gfe = [System.Collections.ArrayList]#()
$gfe = get_gfe
$getos = [System.Collections.ArrayList]#()
$gfe | foreach { $getos += get_os $_}
This takes over an hour to fill $getos with the data.
I was hoping to use something like this instead, but it doesn't work, any help would be appreciated
$gfe | foreach { [void]$getos.Add(get_os $_)}
I know that you can use .Add($_), but that doesn't meet my needs and I couldn't find any references to using other code or calling functions inside the .Add()method.
Thanks in advance!
Why not expand the foreach-loop to something like this:
foreach ($entry in $gfe){
$os = get_os $entry
[void]$getos.add($os)
}
A foreach-loop also saves time compared to | piping into foreach-object.
Although of course since I don't know what your functions are actually doing, this could not be the most effective way to save time. You can determine that with measure-command.
Is it absolutely vital that $getos is of type System.Collections.ArrayList instead of a 'normal' array (System.Object[]) ?
If not, I think the next code could perform faster:
$getos = foreach ($entry in $gfe) {
get_os $entry # output the result of function get_os and collect in variable $getos
}
Thanks to all for the recommendations, they've helped me to gain a better understanding of foreach, arrays, and arrayLists. We've suspect the slowness is related to the foreach loop accessing a function, which uses an API for each serial number. We had recently upgrade our MDM console and swapped out the underlying hardware.

Why can't I lookup an array index inside a foreach loop in Powershell?

first question on here so forgive me if I make any mistakes, I will try to stick to the guidelines.
I am trying to write a PowerShell script that populates two arrays from data I read in via CSV file. I'm using the arrays to cross-reference directory names in order to rename each directory. One array contains the current name of the directory and the other array contains the new name.
This all seems to be working so far. I successfully create and populate the arrays, and using a short input and index lookup to check my work I can search one array for a current name and successfully retrieve the correct new name from the second array. However when I try to implement this code in a foreach loop that runs through every directory name, I can't lookup the array index (it keeps coming back as -1).
I used the code in the first answer found here as my template.
Read a Csv file with powershell and capture corresponding data . Here's my modification to the input lookup, which works just fine:
$input = Read-Host -Prompt "Merchant"
if($Merchant -contains $input)
{
Write-Host "It's there!"
$find = [array]::IndexOf($Merchant, $input)
Write-Host Index is $find
}
Here is my foreach loop that attempts to use the Index lookup, but returns -1 every time. However I know it's finding the file because it enters the if statement and prints "It's there!"
foreach($file in Get-ChildItem $targetDirectory)
{
if($Merchant -contains $file)
{
Write-Host "It's there!"
$find = [array]::IndexOf($Merchant, $file)
Write-Host Index is $find
}
}
I can't figure it out. I'm a PowerShell newb so maybe it's a simple syntax problem, but it seems like it should work and I can't find where I'm going wrong.
Your problem seems to be that $Merchant is a collection of file names (of type string), whereas $file is a FileInfo object.
The -contains operator expects $file to be a string, since $Merchant is a string array, and works as you expect (since FileInfo.ToString() just returns the file name).
IndexOf() isn't so forgiving. It recognizes that none of the items in $Merchant are of the type FileInfo, so it never finds $file.
You can either refer directly to the file name:
[array]::IndexOf($Merchant,$file.Name)
or, as #PetSerAl showed, convert $file to a string instead:
[array]::IndexOf($Merchant,[string]$file)
# or
[array]::IndexOf($Merchant,"$file")
# or
[array]::IndexOf($Merchant,$file.ToString())
Finally, you can call IndexOf() directly on the array, no need to use the static method:
$Merchant.IndexOf($file.Name)

Why is $args array empty when this function is called?

I found an error in how the following code works, excerpted from MSFT_Archive.psm1, the Archive DSC resource.
Function Get-CacheEntry
{
$key = [string]::Join($args).GetHashCode().ToString()
Trace-Message "Using ($key) to retrieve hash value"
$path = Join-Path $CacheLocation $key
if(-not (Test-Path $path))
{
Trace-Message "No cache value found"
return #{}
}
else
{
$tmp = Import-CliXml $path
Trace-Message "Cache value found, returning $tmp"
return $tmp
}
}
The line "$key = [string]::Join($args).GetHashCode().ToString()" does the wrong thing because $args always comes out as being an empty array. A typical call to this method is:
$cacheObj = Get-CacheEntry $Path $Destination
I added print statements and $Path and $Destination have values; they are not empty or nil. Because the $args array is empty, the value of $key is always the same, and consequently all cache files get the same name, regardless of the Zip archive being unpacked. Different inputs then lead to the same cache file being consulted, leading to the same file being repeatedly unpacked even when nothing changes.
The method has no named parameters, so $args should always have the list of unbound parameters. What is wrong?
I am using Powershell 4.0 on a Windows Server 2008R2 system with DSC Resource Kit Wave 10.
UPDATE: This problem is present in both Archive and xArchive resources.
A function with only [string]::Join($args) will produce empty results as you have seen. As EBGreen explains you need to have at least 2 arguments in the method (depending which overload you use). As an alternative you can try to use the -join operator instead.
$key = (-join $args).GetHashCode().ToString()
[string]::Join() does not have a single argument overload. Try this:
$key = [string]::Join('', $args).GetHashCode().ToString()

How to dim an empty array of a specific length

I need to copy a collection of 10 items into an array so I can index into it. So I need to use the CopyTo method and specify a destination array large enough to accommodate the collection.
So before calling CopyTo I need to dim an empty array of a specific size. I can't seem to find the correct PowerShell syntax for doing this.
This didn't work for me:
$snapshot = New-Object System.Array 10;
New-Object : Constructor not found. Cannot find an appropriate constructor for type System.Array.
Edit
Have worked around it with:
$snapshot = New-Object System.Collections.ArrayList;
$snapshot.AddRange($slider);
I think this should do the trick:
$snapshot = #()
Edit:
Ok, sorry about that.
Tested this one:
> $snapshot = New-Object object[] 10
> $snapshot.Length
10
I think just creating a new array will accomplish that in Powershell.
# A "collection of ten items"
$collection = #(1,2,3,4,5,6,7,8,9,0)
#This command creates a new array, sizes it, and copies data into the new array
$newCollection = $collection
Powershell creates a new array in this case. No need to specify the size of the array; Powershell sizes it automatically. And if you add an element to the array, Powershell will take care of the tedious initializeNew-copy-deleteOld routine you have to do in lower-level languages.
Note that $newCollection isn't a reference to the same array that $collection references. It's a reference to a whole new array. Modifying $collection won't affect $newCollection, and vice-versa.

Array.Add vs +=

I've found some interesting behaviour in PowerShell Arrays, namely, if I declare an array as:
$array = #()
And then try to add items to it using the $array.Add("item") method, I receive the following error:
Exception calling "Add" with "1" argument(s): "Collection was of a fixed size."
However, if I append items using $array += "item", the item is accepted without a problem and the "fixed size" restriction doesn't seem to apply.
Why is this?
When using the $array.Add()-method, you're trying to add the element into the existing array. An array is a collection of fixed size, so you will receive an error because it can't be extended.
$array += $element creates a new array with the same elements as old one + the new item, and this new larger array replaces the old one in the $array-variable
You can use the += operator to add an element to an array. When you
use
it, Windows PowerShell actually creates a new array with the values of the
original array and the added value. For example, to add an element with a
value of 200 to the array in the $a variable, type:
$a += 200
Source: about_Arrays
+= is an expensive operation, so when you need to add many items you should try to add them in as few operations as possible, ex:
$arr = 1..3 #Array
$arr += (4..5) #Combine with another array in a single write-operation
$arr.Count
5
If that's not possible, consider using a more efficient collection like List or ArrayList (see the other answer).
If you want a dynamically sized array, then you should make a list. Not only will you get the .Add() functionality, but as #frode-f explains, dynamic arrays are more memory efficient and a better practice anyway.
And it's so easy to use.
Instead of your array declaration, try this:
$outItems = New-Object System.Collections.Generic.List[System.Object]
Adding items is simple.
$outItems.Add(1)
$outItems.Add("hi")
And if you really want an array when you're done, there's a function for that too.
$outItems.ToArray()
The most common idiom for creating an array without using the inefficient += is something like this, from the output of a loop:
$array = foreach($i in 1..10) {
$i
}
$array
Adding to a preexisting array:
[collections.arraylist]$array = 1..10
$array.add(11) > $null

Resources