I have a very weird problem happening. I have an object populated with a bunch of rows. I can access them all well, but I need to append a "." (dot) to every value inside it, so I end up converting each record to a string using a for each loop and adding a "." after trimming the values. The issue now however, is that I would like to assign each of these rows (And the rows have only one column/Item) to another array/object, so I can access them later.
The issue is that even when I declare an object/array variable and assign the string converted data rows, the object variable keeps getting converted to a String, and I just cant seem to avoid it.
Please help. Here is a modified code sample:
[String] $DBNms = #();
ForEach($DBName in $objDBNms){
$tempText += $DBName.Item(0).ToString().Trim() + "."
$DBNms += $tempText
}
Write-Host($DBNms.GetType()) #And this is showing up a string, while I want it to be an array.
And if I print $DBNms, it indeed shows up a string concatenated together into a single string, while I actually want it to be like $DBNms[0] = first Item value, $DBNms[1] = second Item value and so on.
[String] $DBNms = #(); makes $DBNms a type-constrained variable, due to type literal [string] being placed to the left of variable $DBNms, whose type is then locked in as a string; that is, as a single string.
You were looking to create a string array, which in PowerShell is represented as [string[]]:
[string[]] $DBNames = #()
PowerShell's type conversions are automatic (and very flexible), so that [String] $DBNms = #() doesn't report an error, it quietly converts the empty array (#()) to the empty string (as required by the type constraint):
PS> [string] $DBNames = #(); '' -eq $DBNames
True
A much more efficient way to collect values from multiple iterations in an array is to use the foreach statement as an expression, which case PowerShell collects the outputs automatically for you:
[string[]] $DBNms = foreach ($DBName in $objDBNms){
# Output this expression's value as-is.
# PowerShell will collect the individual iterations' values for you.
$DBName.Item(0).ToString().Trim() + "."
}
The above is not only more concise than your original approach, it is more importantly more efficient:
In reality you cannot directly add (append to) an array, because it is an immutable data structure.
What PowerShell has to do whenever you use += with an array is to allocate a new array behind the scenes, with the original elements copied over and the new element(s) appended; the new array is then automatically assigned back to the variable.
Note: The alternative and next best solution to using the whole loop as an expression is to use an efficiently extensible list type, notably [System.Collections.Generic.List[object]](System.Collections.Generic.List`1):
# Create the list.
# The 'System.' namespace prefix is optional.
[Collections.Generic.List[string]] $DBNms = #()
foreach ($DBName in $objDBNms) {
# Add to the list, using its .Add() method.
# Note: Do NOT try to add with +=
$DBNms.Add($DBName.Item(0).ToString().Trim() + ".")
}
You'll need this approach if your loop does more than outputting a single value per iteration, such as needing to append to multiple collections.
Note: It used to be common to use System.Collections.ArrayList instances instead; however:
Use of this type is no longer recommended (see the warning in the linked help topic); use [System.Collections.Generic.List[object]] instead, which additionally allows you to strongly type the collection (replace [object] with the type of interest, such as [string] in the code above).
It has the drawback of its .Add() method having a return value, which you need to silence explicitly (e.g., $null = $lst.Add(...)), so that it doesn't accidentally pollute your code's output stream (produce unexpected extra output).
Related
So I have an object that contains an array:
package MyObject;
sub new {
my($type) = #_;
my $self->{Params}{Status}{Packages} = [];
}
I have a add new package sub which appends onto this "Package" array like:
sub add_package {
my($self, $package_obj) = #_;
push $self->{Params}{Status}{Packages}, $package;
}
Now when I go to find all the packages in my array I have issues. Whenever I try and pull out the packages like this:
foreach my $package($self->{Params}{Status}{Packages}) {
# do something with $package.
}
This only loops through one time. Now from what I understand the hash actually stores a pointer to the array so I tried to do:
foreach my $package(#$self->{Params}{Status}{Packages}) {
# do something with $package.
}
But then there is an error saying that $self is not an array. I did notice when I do:
scalar $self->{Params}{Status}{Packages};
It returns:
#ARRAY(0xSome Address);
What am I missing? And how can I use a foreach loop to go through my array?
$self->{Params}{Status}{Packages} is a reference to an array, in Perl terminology. When you have a reference to something, put the right character in front of it to dereference it. If the reference is more than just a name with possibly some sigils in front, you need to surround it with braces. It's a matter of precedence: #$self->{Params}{Status}{Packages} is parsed as (#$self)->{Params}{Status}{Packages}, but you need
#{$self->{Params}{Status}{Packages}}
i.e. the array referenced by the expression $self->{Params}{Status}{Packages}.
In this case, you need to wrap it all in the array dereference block #{} so perl knows which portion you're trying to dereference...
for my $package (#{ $self->{Params}{Status}{Packages} }){
print "$package\n";
}
Also, just to keep things consistent, I prefer to always deref the array with the block when extracting, or inserting:
push #{ $self->{Params}{Status}{Packages} }, $package;
UPDATE: As of 5.24.0+, autoderef (using keys(), values() or each() with a reference) will almost certainly be removed, and replaced with postfix references. However, using the #{} and %{} will continue to be supported, and is backwards compatible, so I'd recommend using them at all times.
In my view, the clearest way to do this is to extract the array reference to a temporary scalar variable, which makes accessing the array very straightforward
my $packages = $self->{Params}{Status}{Packages};
for my $package ( #$packages ) {
# do something with $package.
}
Also, if you have use strict and use warnings enabled as you should, your add_package subroutine will produce the message
push on reference is experimental
This isn't something you can safely ignore. Experimental features may change their behaviour or disappear completely in later versions of Perl, and it is unwise to make use of them in production code. You can fix your subroutine in a similar way, like this
sub add_package {
my ($self, $package_obj) = #_;
my $packages = $self->{Params}{Status}{Packages};
push #$packages, $package;
}
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.
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
I'm trying to get data out of an array by using following command:
$newarray = $current_ds2_data -match $codenumber
In this case the $current_ds2_data is an array as a result of the "Import-Csv" command and the $codenumber contains the value I want to search for in the array. This work OK.
Following is an example of the value of $newarray:
P_SYS_InternalName : #D_OCEV_ABC-
P_OCEV_Price : 0.15
P_NDS_ValidPN : 12345678
P_OCEV_PriceUnit :
P_NDS_VersionNumber : 1
Now I want to modify the value of the P_OCEV_Price field by doing
$newarray.P_OCEV_Price = 0.2
however this doesn't seem to work. It appears that $newarray.P_OCEV_Price contains no value. Somehow PS doesn't recognize P_OCEV_Price to be a cell of the array.
I also tried using
$newarray.["P_OCEV_Price"] = 0.2
to comply with hash-table formating
Next to this I tried defining the $newarray explicitly as an array or hash-table by using
$newarray = #()
or
$newarray = #{}
So far nothing seems to work. What am I doing wrong??
Since your $newarray variable is an array, you won't be able to use the simple $newarray.P_OCEV_Price syntax to change that value. Here are two alternate options that may help you, depending on your source data:
# Change the price of the first matching item
$newarray[0].P_OCEV_Price = 0.2
# Change the price for all matching items
$newarray | Foreach-Object { $_.P_OCEV_Price = 0.2 }
In cases like this, I usually like to point out that arrays of size 1 are easy to confuse with single objects in Powershell. If you try looking at $newarray and $newarray[0] with simple output statements the results will probably look identical. It's a good idea to keep GetType() handy in these cases.
# This will show a type of Object[]
$newarray.GetType()
# The type here will show PSCustomObject instead
($newarray[0]).GetType()
I have the following code :
set arr1(a1) t1
set arr2(a2) t2
set l1 {}
lappend l1 arr1
lappend l1 arr2
set arr3(a3) $l1
foreach names [array names arr3] {
set value $arr3($names)
puts "names = $names, value = $value"
foreach ar $value {
if {[array exists $ar]} {
puts "$ar is an array"
foreach {key val} [array get $ar] {
set d1 $ar($key)
puts "ar key = $key value = $val "
}
}
}
}
but when I run the tcl script it fails for the line "set d1 $ar($key)" . The error msg is 'can't read "ar(a1)": variable isn't array' . Can you please suggest what is causing the error and how do I resolve the same.
When you use the syntax $ar($key), you are looking up the key $key in the array ar and returning its value. This is how Tcl is defined to work, it's in the basic language syntax. However, you're using the ar variable to hold a scalar value, not an array (the two are completely separate; arrays aren't values, though lists and dictionaries are). That's why you're getting the error message.
To read from an array that is named in a variable, you either need to use a longer piece of syntax so that you substitute the variable name and then read from that variable (Tcl doesn't do this for you by default, since it's quite dangerous if you're not prepared for it) or you need to make an alias to the named array variable.
Double-Substitution via set
set d1 [set ${ar}($key)]
This works because $… is really (under the hood) an alias for set with a single argument. (Well, except that it doesn't actually call the command; they both call the same C API.) We use the ${...} form to limit what the initial $ uses as its variable name. Be aware that if you put an array element name in ar, you'll get odd results from this.
Aliasing an Array or an Element
upvar 0 $ar theAlias
set d1 $theAlias($key)
The upvar command links variables together, and in particular when used with 0 as its first argument, it aliases variables in the current scope. By establishing theAlias as a fixed alias to the actual array (the one named by $ar), we can then access it just like a normal array. You could also alias directly to an element:
upvar 0 ${ar}($key) theAlias
set d1 $theAlias
Note the same syntax as used with the set solution above; we want the name of the element, not to read it. (Warning: do not alias to elements of the global env array; the code that couples to the system environment variables does not work in a friendly way with aliases.)
The main issue with using upvar is that you can't turn theAlias back into a non-aliased variable (though you can retarget the alias by calling upvar again) other than by throwing away the current stack frame (trivial for a procedure body, not too hard for a namespace via namespace delete, but problematic with the global namespace as deleting that terminates the whole Tcl interpreter).