References to arrays/objects in PowerShell - powershell

I use the reference Variable to pass parameters to functions and to manipulate values across functions using the same base variable.
For my other script this worked fine, and maybe this is just a thought problem here, but why this isn't working?:
$Script:NestedLists = #("test", #("test_level_2"))
function AddToReference
{
param([ref]$RefVar)
$RefVar.Value += #("hi")
}
AddToReference -RefVar ([ref]($Script:NestedLists[1]))
$Script:NestedLists[1]
I thought the output of $Script:NestedLists[1] would be "test_level_2" and "hi" but it is just "test_level_2"

This little change made it work
$Script:NestedLists = #("test",#("test_level_2"))
function AddToReference
{
param ([ref]$RefVar) ($RefVar.value)[1] += , "hi"
}
addtoreference ([ref]$Script:NestedLists)
$Script:NestedLists[1]
Why moving the [1] to $refvar made it work, I have no idea, I wish I had a better understanding. Also, this get's really tricky because if you add a value to the first array in the $script it moves the [1] to [2] etc...
I would personally do something to keep each array separate, and at the end just combine them as needed...
$a = "first","array"
$b = "second","array"
$script:nested = $null
$script:nested += , $a
$script:nested += , $b
The "+= ," combines each array in a nested array. so [0] would equal $a and [1] would equal $b

Related

Returning a ArrayList from a function in powershell contains indexes [duplicate]

This question already has answers here:
Powershell Join-Path showing 2 dirs in result instead of 1 - accidental script/function output
(1 answer)
Why does Range.BorderAround emit "True" to the console?
(1 answer)
Create a Single-Element Json Array Object Using PowerShell
(2 answers)
Closed 1 year ago.
I am new to PowerShell and there is a weird behavior I cannot explain. I call a function that returns a [System.Collections.ArrayList] but when I print my variable that receives the content of the array, if I have one value(for example: logXXX_20210222_075234355.txt), then I get 0 logXXX_20210222_075234355.txt. The value 0 gets added for some reason as if it has the index of the value.
If I have 4 values, it will look like this:
0 1 2 3 logXXX_20210222_075234315.txt logXXX_20210225_090407364.txt
logXXX_20210204_120318221.txt logXXX_20210129_122737751.txt
Can anyone help?
Here is a simple code that does that:
function returnAnArray{
$arrayToReturn =[System.Collections.ArrayList]::new()
$arrayToReturn.Add('logICM_20210222_075234315.txt')
return $arrayToReturn
}
$fileNames = returnAnArray
Write-Host $fileNames
0 logICM_20210222_075234315.txt
It's characteristic of the ArrayList class to output the index on .Add(...). However, PowerShell returns all output, which will cause it to intermingle the index numbers with the true or other intended output.
My favorite solution is to simply cast the the output from the .Add(...) method to [Void]:
function returnAnArray{
$arrayToReturn = [System.Collections.ArrayList]::new()
[Void]$arrayToReturn.Add('logICM_20210222_075234315.txt')
return $arrayToReturn
}
You can also use Out-Null for this purpose but in many cases it doesn't perform as well.
Another method is to assign it to $null like:
function returnAnArray{
$arrayToReturn = [System.Collections.ArrayList]::new()
$null = $arrayToReturn.Add('logICM_20210222_075234315.txt')
return $arrayToReturn
}
In some cases this can be marginally faster. However, I prefer the [Void] syntax and haven't observed whatever minor performance differential there may be.
Note: $null = ... works in all cases, while there are some cases where [Void] will not; See this answer (thanks again mklement0) for more information.
An aside, you can use casting to establish the list:
$arrayToReturn = [System.Collections.ArrayList]#()
Update Incorporating Important Comments from #mklement0:
return $arrayToReturn may not behave as intended. PowerShell's output behavior is to enumerate (stream) arrays down the pipeline. In such cases a 1 element array will end up returning a scalar. A multi-element array will return a typical object array [Object[]], not [Collection.ArrayList] as seems to be the intention.
The comma operator can be used to guarantee the return type by making the ArrayList the first element of another array. See this answer for more information.
Example without ,:
Function Return-ArrayList { [Collections.ArrayList]#(1,2,3,4,5,6) }
$ArrReturn = Return-ArrayList
$ArrReturn.gettype().FullName
Returns: System.Object[]
Example with ,:
Function Return-ArrayList { , [Collections.ArrayList]#(1,2,3,4,5,6) }
$ArrReturn = Return-ArrayList
$ArrReturn.gettype().FullName
Returns: System.Collections.ArrayList
Of course, this can also be handled by the calling code. Most commonly by wrapping the call in an array subexpression #(...). a call like: $filenames = #(returnAnArray) will force $filenames to be a typical object array ([Object[]]). Casting like $filenames = [Collections.ArrayList]#(returnArray) will make it an ArrayList.
For the latter approach, I always question if it's really needed. The typical use case for an ArrayList is to work around poor performance associated with using += to increment arrays. Often this can be accomplished by allowing PowerShell to return the array for you (see below). But, even if you're forced to use it inside the function, it doesn't mean you need it elsewhere in the code.
For Example:
$array = 1..10 | ForEach-Object{ $_ }
Is preferred over:
$array = [Collections.ArrayList]#()
1..10 | ForEach-Object{ [Void]$array.Add( $_ ) }
Persisting the ArrayList type beyond the function and through to the caller should be based on a persistent need. For example, if there's a need easily add/remove elements further along in the program.
Still More Information:
Notice the Return statement isn't needed either. This very much ties back to why you were getting extra output. Anything a function outputs is returned to the caller. Return isn't explicitly needed for this case. More commonly, Return can be used to exit a function at desired points...
A function like:
Function Demo-Return {
1
return
2
}
This will return 1 but not 2 because Return exited the function beforehand. However, if the function were:
Function Demo-Return
{
1
return 2
}
This returns 1, 2.
However, that's equivalent to Return 1,2 OR just 1,2 without Return
Update based on comments from #zett42:
You could avoid the ArrayList behavior altogether by using a different collection type. Most commonly a generic list, [Collections.Generic.List[object]]. Technically [ArrayList] is deprecated already making generic lists a better option. Furthermore, the .Add() method doesn't output anything, thus you do not need [Void] or any other nullification method. Generic lists are slightly faster than ArrayLists, and saving the nullification operation a further, albeit still small performance advantage.
ArrayList appears to store alternating indexes and values:
PS /home/alistair> $filenames[0]
0
PS /home/alistair> $filenames[1]
logICM_20210222_075234315.txt

Raku: Trouble Accessing Value of a Multidimensional Hash

I am having issues accessing the value of a 2-dimensional hash. From what I can tell online, it should be something like: %myHash{"key1"}{"key2"} #Returns value
However, I am getting the error: "Type Array does not support associative indexing."
Here's a Minimal Reproducible Example.
my %hash = key1-dim1 => key1-dim2 => 42, key2-dim1 => [42, 42];
say %hash{'key1-dim1'}{'key1-dim2'}; # 42
say %hash{'key2-dim1'}{'foo bar'}; # Type Array does not support associative indexing.
Here's another reproducible example, but longer:
my #tracks = 'Foo Bar', 'Foo Baz';
my %count;
for #tracks -> $title {
$_ = $title;
my #words = split(/\s/, $_);
if (#words.elems > 1) {
my $i = 0;
while (#words.elems - $i > 1) {
my %wordHash = ();
%wordHash.push: (#words[$i + 1] => 1);
%counts.push: (#words[$i] => %wordHash);
say %counts{#words[$i]}{#words[$i+1]}; #===============CRASHES HERE================
say %counts.kv;
$i = $i + 1;
}
}
}
In my code above, the problem line where the 2-d hash value is accessed will work once in the first iteration of the for-loop. However, it always crashes with that error on the second time through. I've tried replacing the array references in the curly braces with static key values in case something was weird with those, but that did not affect the result. I can't seem to find what exactly is going wrong by searching online.
I'm very new to raku, so I apologize if it's something that should be obvious.
After adding the second elements with push to the same part of the Hash, the elment is now an array. Best you can see this by print the Hash before the crash:
say "counts: " ~ %counts.raku;
#first time: counts: {:aaa(${:aaa(1)})}
#second time: counts: {:aaa($[{:aaa(1)}, {:aaa(1)}])}
The square brackets are indicating an array.
Maybe BagHash does already some work for you. See also raku sets without borders
my #tracks = 'aa1 aa2 aa2 aa3', 'bb1 bb2', 'cc1';
for #tracks -> $title {
my $n = BagHash.new: $title.words;
$n.raku.say;
}
#("aa2"=>2,"aa1"=>1,"aa3"=>1).BagHash
#("bb1"=>1,"bb2"=>1).BagHash
#("cc1"=>1).BagHash
Let me first explain the minimal example:
my %hash = key1-dim1 => key1-dim2 => 42,
key2-dim1 => [42, 42];
say %hash{'key1-dim1'}{'key1-dim2'}; # 42
say %hash{'key2-dim1'}{'key2-dim2'}; # Type Array does not support associative indexing.
The problem is that the value associated with key2-dim1 isn't itself a hash but is instead an Array. Arrays (and all other Positionals) only support indexing by position -- by integer. They don't support indexing by association -- by string or object key.
Hopefully that explains that bit. See also a search of SO using the [raku] tag plus 'Type Array does not support associative indexing'.
Your longer example throws an error at this line -- not immediately, but eventually:
say %counts{...}{...}; # Type Array does not support associative indexing.
The hash %counts is constructed by the previous line:
%counts.push: ...
Excerpting the doc for Hash.push:
If a key already exists in the hash ... old and new value are both placed into an Array
Example:
my %h = a => 1;
%h.push: (a => 1); # a => [1,1]
Now consider that the following code would have the same effect as the example from the doc:
my %h;
say %h.push: (a => 1); # {a => 1}
say %h.push: (a => 1); # {a => [1,1]}
Note how the first .push of a => 1 results in a 1 value for the a key of the %h hash, while the second .push of the same pair results in a [1,1] value for the a key.
A similar thing is going on in your code.
In your code, you're pushing the value %wordHash into the #words[$i] key of the %counts hash.
The first time you do this the resulting value associated with the #words[$i] key in %counts is just the value you pushed -- %wordHash. This is just like the first push of 1 above resulting in the value associated with the a key, from the push, being 1.
And because %wordHash is itself a hash, you can associatively index into it. So %counts{...}{...} works.
But the second time you push a value to the same %counts key (i.e. when the key is %counts{#words[$i]}, with #words[$i] set to a word/string/key that is already held by %counts), then the value associated with that key will not end up being associated with %wordHash but instead with [%wordHash, %wordHash].
And you clearly do get such a second time in your code, if the #tracks you are feeding in have titles that begin with the same word. (I think the same is true even if the duplication isn't the first word but instead later ones. But I'm too confused by your code to be sure what the exact broken combinations are. And it's too late at night for me to try understand it, especially given that it doesn't seem important anyway.)
So when your code then evaluates %counts{#words[$i]}{#words[$i+1]}, it is the same as [%wordHash, %wordHash]{...}. Which doesn't make sense, so you get the error you see.
Hopefully the foregoing has been helpful.
But I must say I'm both confused by your code, and intrigued as to what you're actually trying to accomplish.
I get that you're just learning Raku, and that what you've gotten from this SO might already be enough for you, but Raku has a range of nice high level hash like data types and functionality, and if you describe what you're aiming at we might be able to help with more than just clearing up Raku wrinkles that you and we have been dealing with thus far.
Regardless, welcome to SO and Raku. :)
Well, this one was kind of funny and surprising. You can't go wrong if you follow the other question, however, here's a modified version of your program:
my #tracks = ['love is love','love is in the air', 'love love love'];
my %counts;
for #tracks -> $title {
$_ = $title;
my #words = split(/\s/, $_);
if (#words.elems > 1) {
my $i = 0;
while (#words.elems - $i > 1) {
my %wordHash = ();
%wordHash{#words[$i + 1]} = 1;
%counts{#words[$i]} = %wordHash;
say %counts{#words[$i]}{#words[$i+1]}; # The buck stops here
say %counts.kv;
$i = $i + 1;
}
}
}
Please check the line where it crashed before. Can you spot the difference? It was kind of a (un)lucky thing that you used i as a loop variable... i is a complex number in Raku. So it was crashing because it couldn't use complex numbers to index an array. You simply had dropped the $.
You can use sigilless variables in Raku, as long as they're not i, or e, or any of the other constants that are already defined.
I've also made a couple of changes to better reflect the fact that you're building a Hash and not an array of Pairs, as Lukas Valle said.

Case insensitive [Linq.Enumerable]::SequenceEqual()

I have been using [Linq.Enumerable]::SequenceEqual() to compare the order of items in two arrays, for example
$validOrder = #('one', 'two', 'three')
$provided = #('one', 'two', 'three')
[Linq.Enumerable]::SequenceEqual($validOrder, $provided)
This works but now I realize I want to address capitalization errors independently, so I want to test for order in a case insensitive way.
I found this that documents a different method signature, with IEqualityComparer<T> as the third value. That sure seems like the right direction, but I haven't found anything that helps me implement this in powershell. I tried just using 'OrdinalIgnoreCase' for the last argument, which works for [String].FindIndex() as was pointed out in another thread. But alas not here. I also found this which actually makes a custom comparer for a different object type, but that seems like I am just then implementing manually the thing that I actually want, and I am not sure what the value would be of using [Linq.Enumerable]::SequenceEqual(), I could just pass my arrays to my class method and do the work there directly.
I have also made this approach work
if (-not (Compare-Object -SyncWindow 0 $validOrder $provided)) {
$result = 'ordered'
} else {
$result = 'disordered'
}
And it is already case insensitive. But it is also slower, and I may have a lot of these tests to do, so speed will be of benefit.
Lastly I see that this seems to work, and is SUPER simple, case insensitive (if I want) and seemingly fast. The total number of items in the array will always be small, it is the number of repeats with different array that is the performance issue.
$result = ($provided -join ' ') -eq ($validOrder -join ' ')
So, is this last option viable, or am I missing something obvious that argues against it?
Also, I feel like I will have other situations where IEqualityComparer<T> is a useful argument, so knowing how to do it would be useful. Assuming I am reading this right and IEqualityComparer<T> provides a mechanism for a different form of comparison, other than just rolling my own comparison.
I tried just using 'OrdinalIgnoreCase' for the last argument, ...
You're so close:
$lower = 'a b c'.Split()
$upper = 'A B C'.Split()
$ignoreCaseComparer = [System.StringComparer]::OrdinalIgnoreCase
[Linq.Enumerable]::SequenceEqual($lower, $upper, $ignoreCaseComparer)
The StringComparer class - NOT to be confused with the StringComparison enum type - implements IEqualityComparer<string>, and so satisfies the type constraint.
Please note that the above method invocation only works because I used String.Split() to create the method arguments, and Split() explicitly returns a [string[]] - for most other array-like expressions you may find yourself needing to explicitly type the input arguments in order for PowerShell to pass the correct type parameters:
# This will fail - type of `#()` is [object[]],
# and `SequenceEquals<object>()` doesn't accept stringcomparer
$lower = #(-split 'a b c')
$upper = #(-split 'A B C')
$ignoreCaseComparer = [System.StringComparer]::OrdinalIgnoreCase
[Linq.Enumerable]::SequenceEqual($lower, $upper, $ignoreCaseComparer)
# This will succeed thanks to the cast(s) to [string[]] at the call site
$lower = #(-split 'a b c')
$upper = #(-split 'A B C')
$ignoreCaseComparer = [System.StringComparer]::OrdinalIgnoreCase
[Linq.Enumerable]::SequenceEqual([string[]]$lower, [string[]]$upper, $ignoreCaseComparer)
Probably not the best answer to your exact question, but left here for posterity...
If you want to use your own custom comparer (e.g. reversing values from one array before comparing) you can so something like this:
$expected = #("one", "two", "three");
$actual = #("eno", "owt", "eerht");
# see https://stackoverflow.com/a/32635196/3156906
class ReversingComparer : System.Collections.Generic.IEqualityComparer[string] {
[bool]Equals([string]$x, [string]$y) { return $x -eq ($y[-1..-$y.Length] -join ''); }
[int]GetHashCode([string] $x) { return $x.GetHashCode(); }
}
[ReversingComparer] $comparer = [ReversingComparer]::new();
[Linq.Enumerable]::SequenceEqual([string[]]$expected, [string[]]$actual, $comparer);
# true

How to create a loop to feed back the result as new param of the function? (PowerShell)

My current script is:
function F
{
param ([int]$OF)
$OF / 2
}
$OF = 100
if ((F $OF) -ge 2)
{
F (F $OF)
}
The result is 25 now. But what I want to do is to add a loop to this script. Within the loop, the result F (F $OF) will be fed back as the new $OF of the function. Then the function runs again. It will not stop feeding back the result until the result is -lt 2. The whole process is more like the optimization: keep reducing the value until it meets the objective. However, I am not sure how to create such a loop. Use FOR LOOP? Or DO UNTIL LOOP?
couple of ways to do this:
function F
{
param ([int]$OF)
do
{
$result = $of / 2
$of = $result
$result
}
while( $result -gt 2)
}
f -OF 200
-OR
function F2
{
param ([int]$OF)
$of / 2
}
#Declare the parameter value
$val = 100
do
{
$result = F2 -OF $val
$val = $result
$result
}
while($result -gt 2)
The original description in your question seems to point to a recursive method as a recursive function can run until a condition is met (in the same manner as a loop). In this case a simple while loop or do/while loop would probably work but this also provides an excellent opportunity to learn about recursion for later problems or assignments (such as flattening arrays or sorting/searching problems).
Simple Recursive Method
A note on the code: it is good coding practice to start learning naming standards. In this case, variables created within functions shouldn't have the same name as other functions (unless a $global is needed) and one letter functions are rarely a good idea - both of these can cause confusion and make code hard to parse later.
function CalculateF #changed from F
{
param ([int] $cur_of) #changed from $OF so no confusion with calling variable
$cur_of
if ($cur_of -gt 2)
{
CalculateF -cur_of ($cur_of/2) # calls itself with halved value
}
}
$OF = 100
if (CalculateF -cur_of $OF -ge 2) #changed these function calls to call the parameter directly
{
CalculateF -cur_of $OF
}
This function runs, prints the value of the current OF (cur_of), checks to see if the value of cur_of is greater than 2. Than CalculateF calls itself (that is simple recursion) with cur_of/2. Or if it is not it completes the function.
For Loop Version
As #Kiran already put a version with a do/while loop and function - I'll put the for loop version which does not require or make sense with a function at all. As a for loop can perform simple mathematical calculations on the iterator not just +/- 1, as such:
$val = 100
for ($i=$val; $i -ge 2; $i /= 2)
{
# Sets iterator to value and loops through (dividing by 2 each time)
[int] $i
}
Note: Tested above with powershell -version 2 ./scriptname.ps1 to ensure no versioning issues

Powershell: Multidimensional array as return value of function

I've got some problems with two-dimensional arrays in PowerShell.
Here's what I want to do:
I create a function that is supposed to return a two-dimensional array. When invoking the function I want the return value to be a new two-dimensional array.
For a better understanding I've added an example function, below:
function fillArray() {
$array = New-Object 'object[,]' 2,3
$array[0,0] = 1
$array[0,1] = 2
$array[0,2] = 3
$array[1,0] = 4
$array[1,1] = 5
$array[1,2] = 6
return $array
}
$erg_array = New-Object 'object[,]' 2,3
$erg_array = fillArray
$erg_array[0,1] # result is 1 2
$erg_array[0,2] # result is 1 3
$erg_array[1,0] # result is 2 1
The results are not what I expect. I want to return the information in the same way as declared in the function. So I would expect $erg_array[0,1] to give me 2 instead of the 1,2 I receive with the code above. How can I achieve this?
In order to return the array exactly as it is without "unrolling" use the comma operator (see help about_operators)
function fillArray() {
$array = New-Object 'object[,]' 2, 3
$array[0,0] = 1
$array[0,1] = 2
$array[0,2] = 3
$array[1,0] = 4
$array[1,1] = 5
$array[1,2] = 6
, $array # 'return' is not a mistake but it is not needed
}
# get the array (we do not have to use New-Object now)
$erg_array = fillArray
$erg_array[0,1] # result is 2, correct
$erg_array[0,2] # result is 3, correct
$erg_array[1,0] # result is 4, correct
The , creates an array with a single item (which is our array). This 1-item array gets unrolled on return, but only one level, so that the result is exactly one object, our array. Without , our array itself is unrolled, its items are returned, not the array. This technique with using comma on return should be used with some other collections as well (if we want to return a collection instance, not its items).
What is really missing in this port is what everyone is looking for. How to get more than one thing out of a function. Well I am going to share what everyone wants to know who has searched and found this hoping it will answer the question.
function My-Function([string]$IfYouWant)
{
[hashtable]$Return = #{}
$Return.Success = $False
$Return.date = get-date
$Return.Computer = Get-Host
Return $Return
}
#End Function
$GetItOut = My-Function
Write-host “The Process was $($GetItOut.Success) on the date $($GetItOut.date) on the host $($GetItOut.Computer)”
#You could then do
$var1 = $GetItOut.Success
$Var2 =$GetItOut.date
$Var3 = $GetItOut.Computer
If ($var1 –like “True”){write-host “Its True, Its True”}