PowerShell drops the trailing zero of an array element's value when that value contains a dot. Unless I wrap the value in quotes. Unfortunately I need to retain the trailing zero and the script users fail to use quotes reliably.
How can I coerce PowerShell to preserve the trailing zero?
The array element format is Number.Number
The array element may have 1 to n trailing zeros
Single element arrays retain trailing zeros
Multi element arrays drop trailing zeros
Strongly typing the parameter as [string[]] does not resolve the issue.
Example:
function Get-Customer {
Param
(
[string[]]$CustomerId
)
$CustomerId
}
> Get-Customer -CustomerId 654.1,654.10,654.1721,654.1720
654.1 #CORRECT
654.1 #INVALID
654.1721 #CORRECT
654.172 #INVALID
You cannot preserve that 0 if the caller does not put it in quotes. The fact is that those values will be interpreted as numbers because they fit the pattern of number literals.
So if you can't change your caller's behavior then they will be numbers before they ever enter your function. Your [string[]] cast will convert the number to a string, but it's already a number at that point and will follow number -> string rules.
PowerShell is very lenient when it comes to type conversion, or rather, it tries hard to successfully convert types when there is a mismatch, so it will be difficult to throw an error in this case too (you have no access to the original values to know anything is wrong as this happens during parameter binding).
You could do this:
function Get-Customer {
Param
(
[ValidateScript({
$_ -is [string]
})]
[object[]]$CustomerId
)
$CustomerId
}
It would force the values passed in to already be a [string], which kind of sucks for all the other cases where string conversion would have been useful though.
I'm with briantist, to reduce quoting, you could split one string:
function Get-Customer {
Param
(
[ValidateScript({
$_ -is [string]
})]
[object[]]$CustomerId
)
$CustomerId -split ','
}
> Get-Customer -CustomerId '654.1,654.10,654.1721,654.1720',"1.000,2.00"
654.1
654.10
654.1721
654.1720
1.000
2.00
How about this trick. No commas, no quotes, still getting an array and maintaining all your items as is, like this...
function Get-CustomerId
{ $args }
New-Alias -Name cid -Value Get-CustomerId
cid 654.1 654.10 654.1721 654.1720
# Results
654.1
654.10
654.1721
654.1720
Related
I have an array which contains n number of elements. So there might be chances the each element could have spaces in the beginning or at the end. So I want to remove the space in one shot. Here is my code snippet which is working and which is not working (The one which not working is able to trim at the end but not from the front side of the element).
Not Working:
....
use Data::Dumper;
my #a = ("String1", " String2 ", "String3 ");
print Dumper(\#a);
#a = map{ (s/\s*$//)&&$_}#a;
print Dumper(\#a);
...
Working:
...
use Data::Dumper;
my #a = ("String1", " String2 ", "String3 ");
print Dumper(\#a);
my #b = trim_spaces(#a);
print Dumper(\#b);
sub trim_spaces
{
my #strings = #_;
s/\s+//g for #strings;
return #strings;
}
...
No idea whats the difference between these two.
If there is any better please share with me!!
Your "not working example" only removes spaces from one end of the string.
The expression s/^\s+|\s+$//g will remove spaces from both ends.
You can improve your code by using the /r flag to return a modified copy:
#a = map { s/^\s+|\s+$//gr } #a;
or, if you must:
#a = map { s/^\s+|\s+$//g; $_ } #a;
This block has two problems:
{ (s/\s*$//)&& $_ }
The trivial problem is that it's only removing trailing spaces, not leading, which you said you wanted to remove as well.
The more insidious problem is the misleading use of &&. If the regex in s/// doesn't find a match, it returns undef; on the left side of a &&, that means the right side is never executed, and the undef becomes the value of the whole block. Which means any string that the regex doesn't match will be removed and replaced with a undef in the result array returned by map, which is probably not what you want.
That won't actually happen with your regex as written, because every string matches \s*, and s/// still returns true even if it doesn't actually modify the string. But that's dependent on the regex and a bad assumption to make.
More generally, your approach mixes and matches two incompatible methods for modifying data: mutating in place (s///) versus creating a copy with some changes applied (map).
The map function is designed to create a new array whose elements are based in some way on an input array; ideally, it should not modify the original array in the process. But your code does – even if you weren't assigning the result of map back to #a, the s/// modifies the strings inside #a in place. In fact, you could remove the #a = from your code and get the same result. This is not considered good practice; map should be used for its return value, not its side effects.
If you want to modify the elements of an array in place, your for solution is actually the way to go. It makes it clear what you're doing and side effects are OK.
If you want to keep the original array around and make a new one with the changes applied, you should use the /r flag on the substitutions, which causes them to return the resulting string instead of modifying the original in place:
my #b = map { s/^\s+|\s+$//gr } #a;
That leaves #a alone and creates a new array #b with the trimmed strings.
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.
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;
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.
I need help filtering a big .CSV file for which a certain row must only contain strings from a predetermined set of strings held in a array returned by another Powershell function.
For example, suppose I have the following to filter:
datastore3
datastore1 vl_datastore2 datastore3
datastore1 vl_datastore2 datastore3
datastore1 datastore3
with the following array of strings through which I must discard any bad row:
datastore1 datastore3 (datastore1 in index 0, datastore3 in index 1)
In other words, my function should automatically get rid of any row that has the "vl_datastore2" substring in it, therefore only the first and last row would remain.
How can I go about this? For now I am able to split the rows to filter into an array of strings ("datastore1 vl_datastore2 datastore3" would thus be an array with 3 strings in it),but I'm having trouble finding the correct way to use any Powershell operator to filter my list correctly.
Thanks in advance!
Don't know if this helps or not, but:
$TestArray = #(
'datastore3'
'datastore1 vl_datastore2 datastore3',
'datastore1 vl_datastore2 datastore3',
'datastore1 datastore3'
)
$Filters = #(
'datastore1',
'datastore3'
)
[regex]$regex = ‘(?i)(‘ + (($Filters |foreach {[regex]::escape($_)}) –join “|”) + ‘)’
$TestArray | Where {-not ($_.split() -notmatch $regex)}
datastore3
datastore1 datastore3
That builds an alternating regex from the strings in the $Filter array so that you can essentially match multiple lines to multiple strings in one operation.
The bit that's building the regex is explained here:
http://blogs.technet.com/b/heyscriptingguy/archive/2011/02/18/speed-up-array-comparisons-in-powershell-with-a-runtime-regex.aspx
I think I'd go another route and use a flag variable and -notcontains. Run the array to test line by line, split each line, check each piece of the split to see if it's contained in the list of approved terms, and if not set a flag so that the line will not be passed down the pipe.
$TestArray = #("datastore3",
"datastore1 vl_datastore2 datastore3",
"datastore1 vl_datastore2 datastore3",
"datastore1 datastore3")
$Filter = #("datastore1","datastore3")
$TestArray|%{
$SetValid = $True
$_ -split " "|?{$Filter -notcontains $_}|%{$SetValid=$false}
if($SetValid){$_}
}
When run that results in:
datastore3
datastore1 datastore3