I have a PowerShell script that fails if only 1 string is fed to an array because it splits it into characters when using Get-Unique and/or Sort-Object. However, if multiple values are provided then it works as expected. So for example:
Expected behavior:
PS X:\> $t = #("asd","bcd") | Get-Unique
PS X:\> $t[0]
asd
PS X:\> $t[1]
bcd
Unexpected (with 1 value):
PS X:\> $t = #("asd") | Get-Unique
PS X:\> $t[0]
a
PS X:\> $t[1]
s
PS X:\> $t[2]
d
Could someone explain why this is happening and how to prevent it?
I'd appreciate any input as my searches did not bring any luck.
Thanks
Get-Unique doesn't split anything - it just returns the one string value as is, and as a result, $t now contains a scalar string, not an array:
PS ~> $t = "asd" |Get-Unique
PS ~> $t.GetType().FullName
System.String
PS ~> $t
asd
But as soon as you try to access the string value with the index accessor [], it returns the individual [char] value found at the given index.
If you want to ensure the output from Get-Unique (or Sort-Object or any other command that might return 0, 1, or more objects as output), wrap the pipeline in the array subexpression operator #():
PS ~> $t = #( "asd" | Get-Unique )
PS ~> $t[0]
asd
Related
I'm currently learning PowerShell, starting with the basics, and I've gotten to arrays. More specifically, looping arrays. I noticed that when declaring an array by itself, it's just simply written as
$myarray = 1, 2, 3, 4, 5
However, when an array is being declared with the intention of it being looped, it is written as
$myarray = #(1, 2, 3, 4, 5)
Out of curiosity, I tried running the code for looping through the array both with and without the # sign just to see if it would work and it was displayed within the string I created in the exact same way for both.
My question is what is the purpose of the # sign? I tried looking it up, but couldn't find any results.
It's an alternative syntax for declaring static arrays, but there are some key details to understanding the differences in syntax between them.
#() is the array sub-expression operator. This works similarly to the group-expression operator () or sub-expression operator $() but forces whatever is returned to be an array, even if only 0 or 1 element is returned. This can be used inline wherever an array or collection type is expected. For more information on these operators, read up on the Special Operators in the documentation for PowerShell.
The 1, 2, 3, 4 is the list-expression syntax, and can be used anywhere an array is expected. It is functionally equivalent to #(1, 2, 3, 4) but with some differences in behavior than the array subexpression operator.
#( Invoke-SomeCmdletOrExpression ) will force the returned value to be an array, even if the expression returns only 0 or 1 element.
# Array sub-expression
$myArray = #( Get-Process msedge )
Note this doesn't have to be a single cmdlet call, it can be any expression, utilizing the pipeline how you see fit. For example:
# We have an array of fruit
$fruit = 'apple', 'banana', 'apricot', 'cherry', 'a tomato ;)'
# Fruit starting with A
$fruitStartingWithA = #( $fruit | Where-Object { $_ -match '^a' } )
$fruitStartingWithA should return the following:
apple
apricot
a tomato ;)
There's another way to force an array type and I see it often mentioned on Stack Overflow as a cool trick (which it is), but with little context around its behavior.
You can use a quirk of the list-expression syntax to force an array type, but there are two key differences between this and using the array sub-expression operator. Prefixing an expression or variable with a comma , will force an array to be returned, but the behavior changes between the two. Consider the following example:
# Essentially the same as #( $someVar )
$myArray1 = , $someVar
# This behaves differently, read below
$myArray2 = , ( Invoke-SomeCmdletOrExpression )
#() or prefixing a variable with , will flatten (another word often used here is unroll) the resulting elements into a single array. But with an expression you have to use the group-expression operator if you use the comma-prefix trick. Due to how the grouped expression is interpreted you will end up with an array consisting of one element.
It will not flatten any resulting elements in this case.
Consider the Get-Process example above. If you have three msedge processes running, $myArray.Count will show a count of 3, and you can access the individual processes using the array-index accessor $myArray[$i]. But if you do the same with $myArray2 in the second list-expression example above, $myArray2.Count will return a count of 1. This is essentially now a multi-dimensional array with a single element. To get the individual processes, you would now need to do $myArray2[0].Count to get the process count, and use the array-index accessor twice to get an individual process:
$myArray2 = , ( Get-Process msedge )
$myArray2.Count # ======> 1
# I have 32 Edge processes right now
$myArray2[0].Count # ===> 32
# Get only the first msedge process
$myArray[0][0] # =======> Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName
# =======> ------- ------ ----- ----- ------ -- -- -----------
# =======> 430 19 101216 138968 74.52 3500 1 msedge
This can be unclear at first because printing $myArray2 to the output stream will show the same output result as $myArray from the first example, and $myArray1 in the second example.
In short, you want to avoid using the comma-prefix trick when you want to use an expression, and instead use the array sub-expression #() operator as this is what it is intended for.
Note: There will be times when you want to define a static array of arrays but you will be using list-expression syntax anyways, so the comma-prefix becomes redundant. The only counterpoint here is if you want to create an array with an array in the first element to add more arrays to it later, but you should be using a generic List[T] or an ArrayList instead of relying on one of the concatenation operators to expand an existing array (+ or += are almost always bad ideas on non-numeric types).
Here is some more information about arrays in PowerShell, as well as the Arrays specification for PowerShell itself.
The operator you're looking for documentation on consists not only of the # but the ( and ) too - together they make up #(), also known as the array subexpression operator.
It ensures that the output from whatever pipeline or expression you wrap in it will be an array.
To understand why this is useful, we need to understand that PowerShell tends to flatten arrays! Let's explore this concept with a simple test function:
function Test-PowerShellArray {
param(
$Count = 2
)
while($count--){
Get-Random
}
}
This function is going to output a number of random numbers - $Count numbers, to be exact:
PS ~> Test-PowerShellArray -Count 5
652133605
1739917433
1209198865
367214514
1018847444
Let's see what type of output we get when we ask for 5 numbers:
PS ~> $numbers = Test-PowerShellArray -Count 5
PS ~> $numbers.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
Alright, so the resulting output that we've stored in $numbers is of type [Object[]] - this means we have an array which fits objects of type Object (any type in .NET's type system ultimately inherit from Object, so it really just means we have an array "of stuff", it could contain anything).
We can try again with a different count and get the same result:
PS ~> $numbers = Test-PowerShellArray -Count 100
PS ~> $numbers.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
So far so good - we collected multiple output values from a function and ended up with an array, all is as expected.
But what happens when we only output 1 number from the function:
PS ~> $numbers = Test-PowerShellArray -Count 1
PS ~> $numbers.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
Say what? Now we're getting System.Int32 - which is the type of the individual integer values - PowerShell noticed that we only received 1 output value and went "Only 1? I'm not gonna wrap this in an array, you can have it as-is"
For this reason exactly, you might want to wrap output that you intend to loop over (or in other ways use that requires it to be an array):
PS ~> $numbers = Test-PowerShellArray -Count 1
PS ~> $numbers.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Int32 System.ValueType
PS ~> $numbers = #(Test-PowerShellArray -Count 1) # #(...) saves the day
PS ~> $numbers.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
In the context of converting to and from JSON:
perhaps the integer's below are just marking array index? Yet it's entirely possible to send 1,1,1 so it's not an index. the "1" might, perhaps, indicate a "depth" then?
PS /home/nicholas/powershell>
PS /home/nicholas/powershell> ConvertTo-Json #(1)
[
1
]
PS /home/nicholas/powershell>
PS /home/nicholas/powershell> ConvertTo-Json #(1,a)
ParserError:
Line |
1 | ConvertTo-Json #(1,a)
| ~
| Missing expression after ','.
PS /home/nicholas/powershell>
PS /home/nicholas/powershell> ConvertTo-Json #(1,2)
[
1,
2
]
PS /home/nicholas/powershell>
PS /home/nicholas/powershell> ConvertTo-Json #(a)
a: The term 'a' is not recognized as a name of a cmdlet, function, script file, or executable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
[]
PS /home/nicholas/powershell>
why are integers okay:
PS /home/nicholas/powershell>
PS /home/nicholas/powershell> ConvertTo-Json #(1,3,9)
[
1,
3,
9
]
PS /home/nicholas/powershell>
but not even a single char?
Neither it seems are String data acceptable.
PowerShell doesn't have any syntax for defining character literals, and bare words (like the a in your example) are interpreted as commands, as the error indicates.
If you want to pass the a single [char] a as a value, there are a number of options:
# Convert single-char string to char
[char]'a'
# or
'a' -as [char]
# Index into string
'a'[0]
# Convert from numberic value
97 -as [char]
So you could do something like this:
PS ~> ConvertTo-Json #(1,'a'[0])
[
1,
"a"
]
But as you'll notice, the resulting JSON appears to have converted the [char] back into a string - and that's because JSON's grammar doesn't have [char]'s either.
From RFC 8259 ยง3:
A JSON value MUST be an object, array, number, or string [...]
So converting from [string] to [char] is in fact completely redundant:
PS ~> ConvertTo-Json #(1,'a')
[
1,
"a"
]
When working with an array of values, indexof can be used to find the position of the value in the array.
#this returns '1', correctly identifying 'blue' in position '1' of the array
$valueArray = #('cup','blue','orange','bicycle')
[array]::indexof($valueArray,'blue')
I would like to use this command to find the position of a file (image) in an array of objects generated with Get-ChildItem, however the returned position is always '-1' no matter where the object I have called for actually is. Note that image123.jpg is in the middle of the array.
$imageArray = Get-ChildItem "C:\Images"
[array]::indexof($imageArray,'image123.jpg')
I have noticed that if I change the array to filenames only, it works returning the actual position of the filename.
$imageArray = Get-ChildItem "C:\Images" | select -expand Name
[array]::indexof($imagesToReview,'image123.jpg')
Is this just the nature of using indexof or is there a way to find the correct position of the image file in the array without converting?
The easiest solution here is the following:
$imageArray = Get-ChildItem "C:\Images"
[array]::indexof($imageArray.Name,'image123.jpg')
Explanation:
[array]::IndexOf(array array,System.Object value) searches an array object for an object value. If no match is found, it returns the array lower bound minus 1. Since the array's first index is 0, then it returns the result of 0-1.
Get-ChildItem -Path SomePath returns an array of DirectoryInfo and FileInfo objects. Each of those objects has various properties and values. Just using $imageArray to compare to image123.jpg would be comparing a System.IO.FileInfo object to a String object. PowerShell won't automatically convert a FileInfo object into a string while correctly parsing to find your target value.
When you choose to select a property value of each object in the array, you are returning an array of those property values only. Using $imageArray | Select -Expand Name and $imageArray.Name return an array of Name property values. Name contains a string in your example. This means you are comparing a String to a String when using [array]::IndexOf($imageArray.Name,'image123.jpg').
The way that .NET by default compares things is just not as forgiving as PowerShell is!
[array]::IndexOf($array, $reference) will go through the array and return the current index when it encounters an item for which the following is true:
$item.Equals($reference)
... which is NOT necessarily the same as doing
$item -eq $reference
For simple values, like numbers and dates and so on, Equals() works exactly like -eq:
PS C:\> $a = 1
PS C:\> $b = 1
PS C:\> $a.Equals($b) # $true
... which is the reason your first example works as expected!
For more complex objects though, Equals() works a bit differently. Both values MUST refer to the same object, it's not enough that they have similar or even identical values:
PS C:\> $a = New-Object object
PS C:\> $b = New-Object object
PS C:\> $a.Equals($b) # $false
In the example above, $a and $b are similar (if not identical) - they're both empty objects - but they are not the same object.
Similarly, if we test with your input values, they aren't the same either:
PS C:\> $a = Get-Item "C:\"
PS C:\> $b = "C:\"
PS C:\> $a.Equals($b) # $false
One of the reasons they can't be considered the same, as AdminOfThings excellently explains, is type mismatch - but PowerShell's comparison operators can help us here!
You'll notice that this works:
PS C:\> $a = Get-Item "C:\"
PS C:\> $b = "C:\"
PS C:\> $b -eq $a
True
That's because the behavior of -eq depends on the left-hand operand. In the example above, "C:\" is a string, so PowerShell converts $a to a string, and all of a sudden the comparison is more like "C:\".Equals("C:\")!
With this in mind, you could create your own Find-IndexOf function to do $reference -eq $item (or any other comparison mechanism you'd like) with a simple for() loop:
function Find-IndexOf
{
param(
[array]$Array,
[object]$Value
)
for($idx = 0; $idx -lt $Array.Length; $idx++){
if($Value -eq $Array[$idx]){
return $idx
}
}
return -1
}
Now you'd be able to do:
PS C:\> $array = #('','PowerShell is case-insensitive by default')
PS C:\> $value = 'POWERsheLL iS cASe-InSenSItIVe BY deFAuLt'
PS C:\> Find-IndexOf -Array $array -Value $value
1
Or:
PS C:\> $array = Get-ChildItem C:\images
PS C:\> $value = 'C:\images\image123.png'
PS C:\> Find-IndexOf -Array $array -Value $value
5
Adding comparison against a specific property on each of the array items (like the file's Name in your example), we end up with something like this:
function Find-IndexOf
{
param(
[array]$Array,
[object]$Value,
[string]$Property
)
if($Property){
for($idx = 0; $idx -lt $Array.Length; $idx++){
if($Value -eq $Array[$idx].$Property){
return $idx
}
}
}
else {
for($idx = 0; $idx -lt $Array.Length; $idx++){
if($Value -eq $Array[$idx]){
return $idx
}
}
}
return -1
}
Find-IndexOf -Array #(Get-ChildItem C:\images) -Value image123.png -Property Name
I have the following code which works but I am looking for a way to do this all inline without the need for creating the unnecessary variables $myArray1 and $myArray2:
$line = "20190208 10:05:00,Source,Severity,deadlock victim=process0a123b4";
$myArray1 = $line.split(",");
$myArray2 = $myArray1[3].split("=");
$requiredValue = $myArray2[1];
So I have a string $line which I want to:
split by commas into an array.
take the fourth item [3] of the new array
split this by the equals sign into another array
take the second item of this array [1]
and store the string value in a variable.
I have tried using Select -index but I haven't been able to then pipe the result and split it again.
The following works:
$line.split(",") | Select -index 3
However, the following results in an error:
$line.split(",") | Select -index 3 | $_.split("=") | Select -index 1
Error message: Expressions are only allowed as the first element of a pipeline.
$line.Split(',')[3].Split('=')[1]
Try below code:
$requiredValue = "20190208 10:05:00,Source,Severity,deadlock victim=process0a123b4" -split "," -split "=" | select -Last 1
Mudit already provided an answer, here's another about your particular case.
Piping to foreach and accessing 2nd element does the trick:
$line.split(",") | Select -index 3 | % {$_.split("=")[1]}
process0a123b4
That being said, aim for readability and ease of maintenance. There's nothing wrong with having intermediate variables. Memory is cheap nowadays, programmers' time is not. Optimization is due when it's needed and only then after careful profiling to see what's the actual bottleneck.
You could pipe the second split to a foreach
$line.split(",") | Select -index 3 | foreach { $_.split("=") | Select -index 1 }
$array=#("blue","green","black")
[string]$input=Read-host "Input:"
$array[$input]
If I enter the number 1 into the input this gives no output. When I
Write-host $input
I get
System.Collections.ArrayList+ArrayListEnumeratorSimple
What I'm looking to do next is:
$inputlist=#("0")
$inputlist += $array[$input]
But I seem to end up with an array where each element is a single letter. I would like them to be one string in $inputlist[1].
$Input is an automatic variable and shouldn't be used in the way you do. Give your variable a different name and the problem will disappear:
PS C:\> $array = #('blue', 'green', 'black')
PS C:\> $val = Read-host 'Input'
Input: 2
PS C:\> $val
2
PS C:\> $val.GetType().FullName
System.String
PS C:\> $array[$val]
green
Splitting the string into an array of single characters can be handled by casting the string to a character array and then to a string array:
PS C:\> [string[]][char[]]$array[$val]
g
r
e
e
n
You'd still be able to append the characters to an array if you cast the string just to char[] (without casting it to string[] afterwards), but then you'd have an array with mixed types:
PS C:\> $inputList = #('0')
PS C:\> $inputList += [char[]]$array[$val]
PS C:\> $inputList
0
g
r
e
e
n
PS C:\> $inputList[0].GetType().FullName
System.String
PS C:\> $inputList[1].GetType().FullName
System.Char
If you want the entire string as the second element of the array, your existing code should already do that:
PS C:\> $inputList = #('0')
PS C:\> $inputList += $array[$val]
PS C:\> $inputList
0
green