Compare string vs array - powershell

I'm new to PowerShell script. This is the question that comes when I'm creating test using pester. The question is about comparing array vs string as follows:
#('hello', 'world') -eq 'hello world' # returns nothing
'hello world' -eq #('hello', 'world') # returns true
I will appreciate if someone can tell me the difference.

PowerShell operators follow the "left hand rule". In other words, the type of the object on the LHS determines how the comparison is done. If the LHS is an array, then the right hand side will be compared to each element of RHS array. If there are matches, then the operator returns the matching elements. if there are no matches, then the operator returns nothing. On the other hand, if the LHS is a string, then the right hand side will be converted to a string and the comparison is done. An array is converted to a string by doing the equivalent of $array -join $OFS. $OFS (output field separator) is a builtin variable that defaults to " ". This is why your second comparison succeeds #("Hello", "world") gets converted to "Hello world".

-eq returns different things when applied to different types of values.
1,2,3,4,1 -eq 1 # returns #(1, 1)
So when applied to an array on the left-hand side, it returns all elements from that array that are equal to the right-hand side operand. It works like a filter on arrays (the same applies to other comparison operators like -ne, -gt, etc.)
#('hello', 'world') -eq 'hello world' # returns nothing
Naturally.
When -eq is applied to a single value on the left-hend side, it returns $true or $false depending on whether the right-hand side is equal to it.
Type conversion takes place. For example, if the left-hand side is a string, the right-hand side is converted to a string, too.
Arrays are converted to string by converting all their elements to string and joining them with a single space by default (the character used can be changed by using a different $OFS - see MSDN, Powershell Team Blog).
'hello world' -eq #('hello', 'world') # returns true
Naturally. But:
'hello world' -eq #('hello ', 'world') # returns false, note the space
Use .Equals() to prevent that.
'hello world'.Equals( #('hello', 'world') ) # returns false
(1).Equals("1") # false, too

Related

Use an array as key in a hashtable

Can an array be used as the key in a hashtable? How can I reference the hashtable item with an array key?
PS C:\> $h = #{}
PS C:\> $h[#(1,2)] = 'a'
PS C:\> $h
Name Value
---- -----
{1, 2} a # looks like the key is a hash
PS C:\> $h[#(1,2)] # no hash entry
PS C:\> $h.Keys #
1
2
PS C:\> $h[#(1,2)] -eq 'a'
PS C:\> $h[#(1,2)] -eq 'b'
PS C:\> foreach ($key in $h.Keys) { $key.GetType() } # this is promising
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
PS C:\> $PSVersionTable.PSVersion.ToString()
7.1.4
While you can use arrays as hashtable keys, doing so is impractical:
Update: There is a way to make arrays work as hashtable keys, but it requires nontrivial effort during construction of the hashtable - see this answer.
You'll have the use the very same array instances as both the keys and for later lookups.
The reason is that arrays, which are instances of .NET reference types (as opposed to value types such as integers), use the default implementation of the .GetHashCode() method to return a hash code (as used in hashtables), and this default implementation returns a different code for each instance - even for two array instances that one would intuitively think of as "the same".
In other words: you'll run into the same problem trying to use instances of any such .NET reference type as hashtable keys, including other collection types - unless a given type happens to have a custom .GetHashCode() implementation that explicitly considers distinct instances equal based on their content.
Additionally, it makes use of PowerShell's indexer syntax ([...]) awkward, because the array instance must be nested, with the unary form of ,, the array constructor operator. However, dot notation (property access) works as usual.
$h = #{}
# The array-valued key.
$key = 1, 2
$h[$key] = 'a'
# IMPORTANT:
# The following lookups work, but only because
# the *very same array instance* is used for the lookup.
# Nesting required so that PowerShell doesn't think that
# *multiple* keys are being looked up.
$h[, $key]
# Dot notation works normally.
$h.$key
# Does NOT work, because a *different array instance* is used.
$h.#(1,2)
A simple test for whether a given expression results in the same hashtable lookup every time and is therefore suitable as a key is to call the .GetHashCode() method on it repeatedly; only if the same number is returned every time (in a given session) can the expression be used:
# Returns *different* numbers.
#(1, 2).GetHashCode()
#(1, 2).GetHashCode()
To inspect a given object or type for whether it is (an instance of) a .NET reference type vs. value type:
# $false is returned in both cases, confirming that the .NET array
# type is a *reference type*
#(1, 2).GetType().IsValueType
[Array].IsValueType
Workaround:
A workaround would be to use string representations of arrays, though coming up with unique (enough) ones may be a challenge.
In the simplest case, use PowerShell's string interpolation, which represents arrays as a space-separated list of the elements' (stringified) values; e.g. "$(1, 2)" yields verbatim 1 2:
$h = #{}
# The array to base the key on.
$array = 1, 2
# Use the *stringified* version as the key.
$h["$array"] = 'a'
# Works, because even different array instances with equal-valued
# instances of .NET primitive types stringify the same.
# '1 2'
$h["$(1, 2)"]
iRon points out that this simplistic approach can lead to ambiguity (e.g., a single '1 2' string would result in the same key as array 1, 2) and recommends the following instead:
a more advanced/explicit way for array keys would be:
joining their elements with a non-printable character; e.g.
$key = $array -join [char]27
or, for complex object array elements, serializing the array:
$key = [System.Management.Automation.PSSerializer]::Serialize($array)
Note that even the XML (string)-based serialization provided by the System.Management.Automation.PSSerializer class (used in PowerShell remoting and background jobs for cross-process marshaling) has its limits with respect to reliably distinguishing instances, because its recursion depth is limited - see this answer for more information; you can increase the depth on demand, but doing so can result in very large string representations.
A concrete example:
using namespace System.Management.Automation
$ht = #{}
# Use serialization on an array-valued key.
$ht[[PSSerializer]::Serialize(#(1, 2))] = 'a'
# Despite using a different array instance, this
# lookup succeeds, because the serialized representation is the same.
$ht[[PSSerializer]::Serialize(#(1, 2))] # -> 'a'
The primary cause of your problems here is that PowerShell's index access operator [] supports multi-index access by enumerating any array values passed.
To understand why, let's have a look at how the index accessor [...] actually works in PowerShell. Let's start with a simple hashtable, with 2 entries using scalar keys:
$ht = #{}
$ht['a'] = 'This is value A'
$ht['b'] = 'This is value B'
Now, let's inspect how it behaves!
Passing a scalar argument resolves to the value associated with the key represented by said argument, so far so good:
PS ~> $ht['a']
This is value A
But we can also pass an array argument, and all of a sudden PowerShell will try to resolve all items as individual keys:
PS ~> $ht[#('a', 'b')]
This is value A
This is value B
PS ~> $ht[#('b', 'a')] # let's try in reverse!
This is value B
This is value A
Now, to understand what happens in your example, let's try an add an entry with an array reference as the key, along with two other entries where the key is the individual values fround in the array:
$ht = #{}
$keys = 1,2
$ht[$keys[0]] = 'Value 1'
$ht[$keys[1]] = 'Value 2'
$ht[$keys] = 'Value 1,2'
And when we subsequently try to resolve the last entry using our array reference:
PS ~> $ht[$keys]
Value 1
Value 2
Oops! PowerShell unraveled the $keys array, and never actually attempted to resolve the entry associated with the key corresponding to the array reference in $keys.
In other words: The index accessor cannot be used to resolve dictionary entries by key is the key type is enumerable
So, how does one access an entry by array reference without having PowerShell unravel the array?
Use the IList.Item() parameterized property instead:
PS ~> $ht.Item($keys)
Value 1,2

PowerShell create array failed in a loop

Thought I have read enough examples here and elsewhere. Still I fail creating arrays in Power Shell.
With that code I hoped to create slices of pair values from an array.
$values = #('hello','world','bonjour','moon','ola','mars')
function slice_array {
param (
[String[]]$Items
)
[int16] $size = 2
$pair = [string[]]::new($size) # size is 2
$returns = [System.Collections.ArrayList]#()
[int16] $_i = 0
foreach($item in $Items){
$pair[$_i] = $Item
$_i++;
if($_i -gt $size - 1){
$_i = 0
[void]$returns.Add($pair)
}
}
return $returns
}
slice_array($values)
the output is
ola
mars
ola
mars
ola
mars
I would hope for
'hello','world'
'bonjour','moon'
'ola','mars'
Is possible to slice that array to an array of arrays with length 2 ?
Any explenation why it doesn't work as expected ?
How should the code be changed ?
Thanks for any hint to properly understand Arrays in PowerShell !
Here's a PowerShell-idiomatic solution (the fix required for your code is in the bottom section):
The function is named Get-Slices to adhere to PowerShell's verb-noun naming convention (see the docs for more information).
Note: Often, the singular form of the noun is used, e.g. Get-Item rather than Get-Items, given that you situationally may get one or multiple output values; however, since the express purpose here is to slice a single object into multiple parts, I've chosen the plural.
The slice size (count of elements per slice) is passed as a parameter.
The function uses .., the range operator, to extract a single slice from an array.
It uses PowerShell's implicit output behavior (no need for return, no need to build up a list of return values explicitly; see this answer for more information).
It shows how to output an array as a whole from a function, which requires wrapping it in an auxiliary single-element array using the unary form of ,, the array constructor operator. Without this auxiliary array, the array's elements would be output individually to the pipeline (which is also used for function / script output; see this answer for more information.
# Note: For brevity, argument validation, pipeline support, error handling, ...
# have been omitted.
function Get-Slices {
param (
[String[]] $Items
,
[int] $Size # The slice size (element count)
)
$sliceCount = [Math]::Ceiling($Items.Count / $Size)
if ($sliceCount -le 1) {
# array is empty or as large as or smaller than a slice? ->
# wrap it *twice* to ensure that the output is *always* an
# *array of arrays*, in this case containing just *one* element
# containing the original array.
,, $Items
}
else {
foreach ($offset in 0..($sliceCount-1)) {
, $Items[($offset * $Size)..(($offset+1) * $Size - 1)] # output this slice
}
}
}
To slice an array into pairs and collect the output in an array of arrays (jagged array):
$arrayOfPairs =
Get-Slices -Items 'hello','world','bonjour','moon','ola','mars' -Size 2
Note:
Shell-like syntax is required when you call functions (commands in general) in PowerShell: arguments are whitespace-separated and not enclosed in (...) (see this answer for more information)
Since a function's declared parameters are positional by default, naming the arguments as I've done above (-Item ..., -Size ...) isn't strictly necessary, but helps readability.
Two sample calls:
"`n-- Get pairs (slice count 2):"
Get-Slices -Items 'hello','world','bonjour','moon','ola','mars' -Size 2 |
ForEach-Object { $_ -join ', ' }
"`n-- Get slices of 3:"
Get-Slices -Items 'hello','world','bonjour','moon','ola','mars' -Size 3 |
ForEach-Object { $_ -join ', ' }
The above yields:
-- Get pairs (slice count 2):
hello, world
bonjour, moon
ola, mars
-- Get slices of 3:
hello, world, bonjour
moon, ola, mars
As for what you tried:
The only problem with your code was that you kept reusing the very same auxiliary array for collecting a pair of elements, so that subsequent iterations replaced the elements of the previous ones, so that, in the end, your array list contained multiple references to the same pair array, reflecting the last pair only.
This behavior occurs, because arrays are instance of reference types rather than value types - see this answer for background information.
The simplest solution is to add a (shallow) clone of your $pair array to your list, which ensures that each list entry is a distinct array:
[void]$returns.Add($pair.Clone())
Why you got 3 equal pairs instead of different pairs:
.Net (powershell based on it) is object-oriented language and it has consept of reference types and value types. Almost all types are reference types.
What happens in your code:
You create $pair = [string[]] object. $pair variable actually stores memory address of (reference to) [string[]] object, because arrays are reference types
You fill $pair array with values
You add (!) $pair to $returns. Remember that $pair is reference to memory block. And when you add it to $returns, it adds memory address of [string[]] you wrote values to.
You repeat step2: You fill $pair array with different values, but address of this array in memory keeps the same. Doing this you actually replace values from step2 with new values in the same $pair object.
= // = step3
= // = step4
= // = step3
As a result: in $returns there are three same memory addresses: [[reference to $pair], [reference to $pair], [reference to $pair]]. And $pair values were overwritten by code with last pair values.
On output it works like this:
Powershell looks at $results which is array.
Powershell looks to $results[0] which reference to $pair
Powershell outputs reference to $pair[0]
Powershell outputs reference to $pair[1]
Powershell looks to $results[1] which reference to $pair
Powershell outputs reference to $pair[0]
Powershell outputs reference to $pair[1]
Powershell looks to $results[1] which reference to $pair
Powershell outputs reference to $pair[0]
Powershell outputs reference to $pair[1]
So you see, you triple output the object from the same memory address. You overwritten it 3 times in slice_array and now it stores only last pair values.
To fix it in your code, you should create a new $pair in memory: add $pair = [string[]]::new($size) just after $returns.Add($pair)

Powershell color formatting with format operator

I'm using a format operator inside a script with a following example:
$current = 1
$total = 1250
$userCN = "contoso.com/CONTOSO/Users/Adam Smith"
"{0, -35}: {1}" -f "SUCCESS: Updated user $current/$total", $userCN
This excpectedly shows the following output:
SUCCESS: Updated user 1/1250 : contoso.com/CONTOSO/Users/Adam Smith
The format operator is there to keep the targeted output text in place with current / total running numbers varying between 1-99999. Without the format operator I could highlight the success line like this:
Write-Host -BackgroundColor Black -ForegroundColor Green "SUCCESS: Updated user $current/$total: $userCN"
But the question is how could I use the highlight-colors combined with the format operator? There's only the -f parameter and it doesn't allow the color parameters because, well, it's not the same thing as Write-Host in the first place.
Unlike other shells, PowerShell allows you to pass commands and expressions as command arguments simply by enclosing them in parentheses, i.e by using (...), the grouping operator.
When calling PowerShell commands (cmdlets, scripts, functions), the output is passed as-is as an argument, as its original output type.
Therefore, Theo's solution from a comment is correct:
Write-Host -BackgroundColor Black -ForegroundColor Green `
("{0, -35}: {1}" -f "SUCCESS: Updated user $current/$total", $userCN)
That is, the -f expression inside (...) is executed and its output - a single string in this case - is passed as a positional argument to Write-Host (implicitly binds to the -Object parameter).
Note that you do not need, $(...), the subexpression operator, in this case:
(...) is sufficient to enclose an expression or command.
In fact, in certain cases $(...) can inadvertently modify your argument, because it acts like the pipeline; that is, it enumerates and rebuilds array expressions as regular PowerShell arrays (potentially losing strong typing) and unwraps single-element arrays:
# Pass a single-element array to a script block (which acts like a function).
# (...) preserves the array as-is.
PS> & { param($array) $array.GetType().Name } -array ([array] 1)
Object[] # OK - single-element array was passed as-is
# $(...) unwraps it.
PS> & { param($array) $array.GetType().Name } -array $([array] 1)
Int32 # !! Single-element array was unwrapped.
# Strongly-typed array example:
PS> & { param($array) $array.GetType().Name } ([int[]] (1..10))
Int32[] # OK - strongly typed array was passed as-is.
# Strongly-typed array example:
PS> & { param($array) $array.GetType().Name } $([int[]] (1..10))
Object[] # !! Array was *enumerated and *rebuilt* as regular PowerShell array.
The primary use of $(...) is:
expanding the output from expressions or commands inside expandable strings (string interpolation)
To send the output from compound statements such as foreach (...) { ... } and if (...) { ... } or multiple statements directly through the pipeline, after collecting the output up front (which (...) does as well); however, you can alternatively wrap such statements & { ... } (or . { ... } in order to execute directly in the caller's scope rather than a child scope) in order to get the usual streaming behavior (one-by-one passing of output) in the pipeline.
Taking a step back: Given that you already can use compound statements as expressions in variable assignments - e.g., $evenNums = foreach ($num in 1..3) { $num * 2 } - and expressions generally are accepted as the first segment of a pipeline - e.g., 'hi' | Write-Host -Fore Yellow - it is surprising that that currently doesn't work with compound statements; this GitHub issue asks if this limitation can be lifted.
In the context of passing arguments to commands:
Use $(...), the subexpression operator only if you want to pass the output from multiple commands or (one or more) compound statements as an argument and/or, if the output happens to be a single object, you want that object to be used as-is or, if it happens to be a single-element array (enumerable), you want it to be unwrapped (pass the element itself, not the array.
Of course, if you're constructing a string argument, $(...) can be useful inside that string, for string interpolation - e.g., Write-Host "1 + 1 = $(1 + 1)"
Use #(...), the array subexpression operator only if you want to pass the output from multiple commands as an argument and/or you want to ensure that the output becomes a array; that is, the output is returned as a (regular PowerShell) array, of type [object[]], even if it happens to comprise just one object. In a manner of speaking it is the inverse of $(...)'s behavior in the single-object output case: it ensures that single-object output too becomes an array.

Comma Before Parentheses in Variable Assignment

I'm using the S.DS.P PowerShell module in a PowerShell script of mine. In it, I have to create the following object:
$ADUserEntry = #{"distinguishedName"=$null;"objectClass"=$null;"sAMAccountName"=$null;"unicodePwd"=$null;"userAccountControl"=0};
In the documentation of the module, it's stated that I have to do the following assignment to the unicodePwd field of a variable created using this object:
$obj.unicodePwd = ,([System.Text.Encoding]::Unicode.GetBytes("$randomPassword") -as [byte[]]);
Notice how there's a comma before the first parentheses. What is that comma doing there?
As Lee_Dailey has pointed out, what you're seeing is the unary form of the (unfortunately named) "comma operator", i.e., PowerShell's array-construction operator.
The unary form creates a single-element array that wraps its (one and only) operand; the array's type is [object[]], as usual in PowerShell:
$arr = , 'foo' # wrap string 'foo' in a single-element array
$arr.GetType().Name # the array's type -> 'Object[]'
$arr[0].GetType().Name # the type of the array's one and only element -> 'String'
Note that while you can even wrap arrays that way, PowerShell's operator-precedence rules require a literal array operand to be enclosed in (...):
# OK - wraps array 1, 2 in a single-element array.
$arr = , (1, 2)
# !! DOES SOMETHING DIFFERENT:
# Creates a 2-element array whose 1st element is integer 1 wrapped in a
# single-element array
$arr = , 1, 2
The binary form constructs an array from the operands, as expected:
$arr = 1, 2, 3 # 3-element array whose elements are integers 1 and 2 and 3
As an aside, re the specific command shown:
,([System.Text.Encoding]::Unicode.GetBytes("$randomPassword") -as [byte[]])
Neither , nor -as [byte[]] are needed in this scenario, because
[System.Text.Encoding]::Unicode.GetBytes() directly returns a [byte[]] array.

Usage of # in powershell

I often see the usage of # symbol in Powershell coding. Want to ask what is it used for? Example as below
$DistributionPointGroups = #("London")
#() is the array operator, which makes sure that even a single item (or zero) is returned as an array.
THE ARRAY SUB-EXPRESSION OPERATOR
The array sub-expression operator creates an array, even if it
contains zero or one object.
The syntax of the array operator is as follows:
#( ... )
You can use the array operator to create an array of zero or
one object.
PS C:\>$a = #("One")
PS C:\>$a.Count
1
PS C:\>$b = #()
PS C:\>$b.Count
0
Source: about_Arrays