PowerShell .Count returns 1 on empty array - arrays

If I use .Count to check the number of items in an empty array, like this:
$theNames = #()
$theTotalNames = $theNames.Count
it finds nothing, and the numeric variable $theTotalNames is 0, as expected
But I have a situation where if I use .Count to check the contents of seemingly empty array, it is returning 1.
I'm populating the array with the results returned from the Invoke-RestMethod query like this:
$responseData = Invoke-RestMethod -Uri $url -Method Get -Headers $headers
$theNames = #($responseData.PsObject.Properties["the_field"].value.field_name)
$theTotalNames = $theNames.Count
If the query returns nothing because there were no fields found, $theTotalNames somehow equals 1. If the query returns one or more items, $theTotalItems will correctly equal 1.. or higher
When I display the contents of $theNames array after the query that returned nothing, the array seems empty.
If I check what's in the array, like this:
if ($theNames) {
"The array contains something"
}
else {
"The array contains nothing"
}
the console always says the array contains nothing.
So, why does .Count think there's at lease one item in the array?

As PetSerAl implies in a comment, the array may not be empty but may have a single element that happens to be $null; the .Count property reports the number of elements irrespective of the value of the elements:
#($null).Count # -> 1
Work around the problem as follows (assuming actual values are never the empty string):
$theTotalNames = if ($theNames) { $theNames.Count } else { 0 }
This relies on the fact that a single-element array that contains a "falsy" value is regarded as $False in a Boolean context.
Optional background information
In PowerShell, $null is generally a "something" (the "null scalar"), whereas there is also a "null collection", which is closer to "nothing", namely the [System.Management.Automation.Internal.AutomationNull]::Value singleton, which is "returned" by commands that have no output at all.
The simplest way to produce it is to call an empty script block: & {}
Trying to wrap that in an array indeed yields an empty array:
#(& {}).Count # -> 0
However, in PSv3+ there is (at least?) one context in which $null too is considered "nothing":
foreach ($el in $null) { "loop entered" } # loop is NOT entered.
I presume that the rationale for this intentional inconsistency is that uninitialized variables default to $null and that entering the loop for uninitialized variables would be undesirable.
For more, see this GitHub discussion.

Related

Powershell: Array receiving the return of a function contains incremental numbers for each value in the array [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

Not able to divide each row of a csv file in the form of array using perl

I am stucked in a problem wherein I am parsing a csv file. The CSV file looks like-
CPU Name,DISABLE,Memory,Encoding,Extra Encoding
,b,d,,
String1,YES,1TB,Enabled,Enabled
String2,NO,1TB,Enabled,Enabled
String3,YES,1TB,Enabled,Enabled
I want to capture the first two rows in two different arrays. The code that I am using to do it is-
my $row_no =0;
while(my $row=<$fi>){
chomp($row);
$row=~ s/\A\s+//g;
$row=~s/\R//g;
#say $row;
if($row_no==0)
{
#say $row;
my #name_initial = split(',',$row);
say length(#name_initial);
say #name_initial;
}
elsif($row_no==1)
{
#say $row;
#data_type_initial =split(',',$row);
say length(#data_type_initial);
say #data_type_initial;
}
$row_no++;
}
Now I formed two arrays from topmost two lines in file (#name_initial and #data_type_initial respectively).When I am printing these array I can see all the 5 values but when I am printing the length of array it is showing length of each array as 1. When I am printing the element using index of arrays I find each element in place then why it is showing length as 1. Also second array which is formed from second line of csv file is printed as "bd". All the null values are gone and although it is containing two values 'b' and 'd'. Its length is printed as 1.
I want to convert the row of csv file in array with all the null and non_NULL values so that I can iterate on the array elements and can give conditions based on null and non null values.How can I do that???
Have a look at perldoc length. It says this:
length EXPR
length
Returns the length in characters of the value of
EXPR. If EXPR is omitted, returns the length of $_. If EXPR is
undefined, returns undef.
This function cannot be used on an entire array or hash to find out
how many elements these have. For that, use scalar #array and scalar
keys %hash, respectively.
Like all Perl character operations, length normally deals in logical
characters, not physical bytes. For how many bytes a string encoded as
UTF-8 would take up, use length(Encode::encode('UTF-8', EXPR)) (you'll
have to use Encode first). See Encode and perlunicode.
In particular, the bit that says "This function cannot be used on an entire array or hash to find out how many elements these have. For that, use scalar #array and scalar keys %hash, respectively".
So you're using the wrong approach here. Instead of say length(#array), you need say scalar(#array).
To explain the results you're getting. length() expects to be given a scalar value (a string) to measure. So it treats your array as a scalar (effectively adding an invisible call to scalar()) and gets back the number of elements in the array (which is "5") and length() then tells you the number of elements in that string - which is 1.
It's also worth pointing out that you don't need to keep track of your own $row_no variable. Perl has a built-in variable called $. which contains the current record number.
Using that knowledge (and adding little whitespace) gives us something like this:
while (my $row = <$fi>) {
chomp($row);
$row =~ s/\A\s+//g;
$row =~s/\R//g;
#say $row;
if ($. == 0) {
#say $row;
my #name_initial = split(/,/, $row);
say scalar(#name_initial);
say #name_initial;
} elsif ($. == 1) {
#say $row;
#data_type_initial = split(/,/, $row);
say scalar(#data_type_initial);
say #data_type_initial;
}
}
Update: You sneaked a couple of extra questions in at the end of this one. I'd suggest that you raise those separately.

Use of uninitialized value in array value

I am having an array named #Lst_1. One of my elements is 0 in array.
Whenever I call that element. In example below the value stored in second index of an array is 0.
$Log_Sheet->write(Row,0,"$Lst_1[2]",$Format);
I am getting a warning saying
Use of uninitialized value within #Lst_1 in string.
Please help me do that.
First index of array is 0. Second element will be $List_1[1];
#!/usr/bin/env perl
use v5.22;
use warnings;
my #array = qw(foo bar);
# number of elements in array
say scalar(#array);
# last index of array
say $#array;
# undefined element (warn)
say $array[ $#array + 1];
If you just want to silence the error,
$Log_Sheet->write(Row, 0, $Lst_1[2] // 0, $Format);
This does use a feature of perl 5.10, but that's ancient enough you really should be using a sufficiently new perl to have it. I mean, there's a lot of ancient perl bugs, so it behooves you to be using a newer version.
As far as understanding the issue, no, $Lst_1[2] doesn't contain a 0. It contains an undef, which just happens to be treated mostly like 0 in numeric contexts.
Yes, I did remove the quotes around $Lst_1[2] - that was necessary, because "$Lst_1[2]" treats that undef like a string, so it's become the empty string for the purpose of a "$Lst_1[2]" // 0 test. (The empty string also happens to be treated like 0, so that doesn't change the behavior in a numeric context.)
It's not clear from your short excerpt whether #Lst_1 is less than 3 elements long, or if there's an explicit undef in #Lst_1. You'd need to show a larger excerpt of your code - or possibly even the whole thing and the data it is processing - for us to be able to determine that by looking. You could determine it, however, by adding something like the following in front of the line you gave:
if (#Lst_1 < 3) {
print "\#Lst_1 only has " . #Lst_1 . " elements\n"
} elsif (not defined($Lst_1[2])) {
print "\$Lst_1[2] is set to undef\n";
}
There are two basic ways a list can have an explicit undef element in it. The following code demonstrates both:
my #List = map "Index $_", 0 .. 3;
$List[2] = undef;
$List[5] = "Index 5";
use Data::Dump;
dd #List;
This will output
("Index 0", "Index 1", undef, "Index 3", undef, "Index 5")
The first undef was because I set it, the second was because there wasn't a fifth element but I put something in the sixth slot.

Perl unit testing: check if a string is an array

I have this function that I want to test:
use constant NEXT => 'next';
use constant BACK => 'back';
sub getStringIDs {
return [
NEXT,
BACK
];
}
I've tried to write the following test, but it fails:
subtest 'check if it contains BACK' => sub {
use constant BACK => 'back';
my $strings = $magicObject->getStringIDs();
ok($strings =~ /BACK/);
}
What am I doing wrong?
Your getStringIDs() method returns an array reference.
The regex binding operator (=~) expects a string on its left-hand side. So it converts your array reference to a string. And a stringified array reference will look something like ARRAY(0x1ff4a68). It doesn't give you any of the contents of the array.
You can get from your array reference ($strings) to an array by dereferencing it (#$strings). And you can stringify an array by putting it in double quotes ("#$strings").
So you could do something like this:
ok("#$strings" =~ /BACK/);
But I suspect, you want word boundary markers in there:
ok("#$strings" =~ /\bBACK\b/);
And you might also prefer the like() testing function.
like("#$strings", qr[\bBACK\b], 'Strings array contains BACK');
Update: Another alternative is to use grep to check that one of your array elements is the string "BACK".
# Note: grep in scalar context returns the number of elements
# for which the block evaluated as 'true'. If we don't care how
# many elements are "BACK", we can just check that return value
# for truth with ok(). If we care that it's exactly 1, we should
# use is(..., 1) instead.
ok(grep { $_ eq 'BACK' } #$strings, 'Strings array contains BACK');
Update 2: Hmm... the fact that you're using constants here complicates this. Constants are subroutines and regexes are strings and subroutines aren't interpolated in strings.
The return value of $magicObject->getStringIDs is an array reference, not a string. It looks like the spirit of your test is that you want to check if at least one element in the array pattern matches BACK. The way to do this is to grep through the dereferenced array and check if there are a non-zero number of matches.
ok( grep(/BACK/,#$strings) != 0, 'contains BACK' );
At one time, the smartmatch operator promised to be a solution to this problem ...
ok( $strings ~~ /BACK/ )
but it has fallen into disrepute and should be used with caution (and the no warnings 'experimental::smartmatch' pragma).
The in operator is your friend.
use Test::More;
use syntax 'in';
use constant NEXT => 'next';
use constant BACK => 'back';
ok BACK |in| [NEXT, BACK], 'BACK is in the arrayref';
done_testing;

How to reference a split expression in Perl?

I want to create a reference to an array obtained by a split in Perl.
I'm thinking something like:
my $test = \split( /,/, 'a,b,c,d,e');
foreach $k (#$test) {
print "k is $k\n";
}
But that complains with Not an ARRAY reference at c:\temp\test.pl line 3.
I tried a few other alternatives, all without success.
Background explanation:
split, like other functions, returns a list. You cannot take a reference to a list. However, if you apply the reference operator to a list, it gets applied to all its members. For example:
use Data::Dumper;
my #x = \('a' .. 'c');
print Dumper \#x
Output:
$VAR1 = [
\'a',
\'b',
\'c'
];
Therefore, when you write my $test = \split( /,/, 'a,b,c,d,e');, you get a reference to the last element of the returned list (see, for example, What’s the difference between a list and an array?). Your situation is similar to:
Although it looks like you have a list on the righthand side, Perl actually sees a bunch of scalars separated by a comma:
my $scalar = ( 'dog', 'cat', 'bird' ); # $scalar gets bird
Since you’re assigning to a scalar, the righthand side is in scalar context. The comma operator (yes, it’s an operator!) in scalar context evaluates its lefthand side, throws away the result, and evaluates it’s righthand side and returns the result. In effect, that list-lookalike assigns to $scalar it’s rightmost value. Many people mess this up becuase they choose a list-lookalike whose last element is also the count they expect:
my $scalar = ( 1, 2, 3 ); # $scalar gets 3, accidentally
In your case, what you get on the RHS is a list of references to the elements of the list returned by split, and the last element of that list ends up in $test. You first need to construct an array from those return values and take a reference to that. You can make that a single statement by forming an anonymous array and storing the reference to that in $test:
my $test = [ split( /,/, 'a,b,c,d,e') ];
Surround split command between square brackets to make an anonymous reference.
my $test = [ split( /,/, 'a,b,c,d,e') ];
Giving it a name has different semantics in that changes to the named variable then change what was referenced while each anonymous array is unique. I discovered this the hard way by doing this in a loop.

Resources