I have the following dataset:
id|selectedquery|
1|SELECT fieldX FROM tableA|
2|SELECT fieldY FROM tableB|
that dataset is used in the following code
$rows=($dataSet.Tables | Select-Object -Expand Rows)
$i=0
foreach ($row in $rows)
{
#Write-Output $rows.selectquery[$i].length
$query = $rows.selectquery[$i]
#Write-Output $rows.selectquery[$i]
--doing some stuff--
$i++
}
Often $rows.selectquery[$i] only gives me the first character of the value in the field selectedquery being the 'S'.
When I remove the [$i] from $rows.selectquery it gives me (understandably) multiple records back. If I then put the [$i] back after $rows.selectquery[$i] things woerk fine.
Can anyone explain this behaviour?
You'll want to reference the SelectQuery property on either $row or $rows[$i] - not the entire $rows collection:
$row.SelectQuery
# or
$rows[$i].SelectQuery
Mathias' helpful answer shows the best way to solve your particular problem.
As for what happened:
You - inadvertently - used PowerShell's member-access enumeration feature when you used $rows.selectquery; that is, even though $rows is a collection that itself has no .selectquery property, PowerShell accessed that property on every element of the collection and returned the resulting values as an array.
The pitfall is that if the collection only has one element, the return value is not an array - it is just the one and only element's property value itself.
While this is analogous to how the pipeline operates (a single output object is captured by itself if assigned to a variable, while two or more are implicitly collected in an array), it is somewhat counterintuitive in the context of member-access enumeration:
In other words, $collection.SomeProperty is equivalent to $collection | ForEach-Object { $_.SomeProperty } and not, as would make more sense, because it always returns an array (collection), $collection.ForEach('SomeProperty')
GitHub issue #6802 discusses this problem.
While this behavior is often unproblematic, because PowerShell offers unified handling of scalars and collections (e.g. (42)[0], is the same as 42 itself; see this answer), a problem arises if the single value returned happens to be a string, because indexing into a string returns its characters.
Workaround: Cast to [array] before applying the index:
([array] $rows.selectquery)[0]
A simple example:
# Multi-element array.
[array] $rows1 = [pscustomobject] #{ selectquery = 'foo' },
[pscustomobject] #{ selectquery = 'bar' }
# Single-element array:
[array] $rows2 = [pscustomobject] #{ selectquery = 'baz' }
# Contrast member-access enumeration + index access between the two:
[pscustomobject] #{
MultiElement = $rows1.selectquery[0]
SingleElement = $rows2.selectquery[0]
SinglElementWithWorkaround = ([array] $rows2.selectquery)[0]
}
The above yields the following:
MultiElement SingleElement SinglElementWithWorkaround
------------ ------------- --------------------------
foo b baz
As you can see, the multi-element array worked as expected, because the member-access enumeration returned an array too, while the single-element array resulted in single string 'baz' being returned and 'baz'[0] returns its first character, 'b'.
Casting to [array] first avoids that problem (([array] $rows2.selectquery)[0]).
Using #(...), the array-subexpression operator - #($rows.selectquery)[0] - is another option, but, for the sake of efficiency, it should only be used on commands (e.g., #(Get-ChildItem -Name *.txt)[0]) not expressions, as in the case at hand.)
Related
I have a nested hashtable with an array and I want to loop through the contents of another array and add that to the nested hashtable. I'm trying to build a Slack message block.
Here's the nested hashtable I want to add to:
$msgdata = #{
blocks = #(
#{
type = 'section'
text = #{
type = 'mrkdwn'
text = '*Services Being Used This Month*'
}
}
#{
type = 'divider'
}
)
}
$rows = [ ['azure vm', 'centralus'], ['azure sql', 'eastus'], ['azure functions', 'centralus'], ['azure monitor', 'eastus2'] ]
$serviceitems = #()
foreach ($r in $rows) {
$servicetext = "*{0}* - {1}" -f $r[1], $r[0]
$serviceitems += #{'type'='section'}
$serviceitems += #{'text'= ''}
$serviceitems.text.Add('type'='mrkdwn')
$serviceitems.text.Add('text'=$servicetext)
$serviceitems += #{'type'='divider'}
}
$msgdata.blocks += $serviceitems
The code is partially working. The hashtables #{'type'='section'} and #{'type'='divider'} get added successfully. Trying to add the nested hashtable of #{'text' = #{ 'type'='mrkdwn' 'text'=$servicetext }} fails with this error:
Line |
24 | $serviceitems.text.Add('type'='mrkdwn')
| ~
| Missing ')' in method call.
I tried looking through various Powershell posts and couldn't find one that applies to my specific situation. I'm brand new to using hashtables in Powershell.
Complementing mklement0's helpful answer, which solves the problem with your existing code, I suggest the following refactoring, using inline hashtables:
$serviceitems = foreach ($r in $rows) {
#{
type = 'section'
text = #{
type = 'mrkdwn'
text = "*{0}* - {1}" -f $r[1], $r[0]
}
}
#{
type = 'divider'
}
}
$msgdata.blocks += $serviceitems
This looks much cleaner and thus easier to maintain in my opinion.
Explanations:
$serviceitems = foreach ... captures all output (to the success stream) of the foreach loop in variable $serviceitems. PowerShell automatically creates an array from the output, which is more efficient than manually adding to an array using the += operator. Using += PowerShell has to recreate an array of the new size for each addition, because arrays are actually of fixed size. When PowerShell automatically creates an array, it uses a more efficient data structure internally.
By writing out an inline hash table, without assigning it to a variable, PowerShell implicitly outputs the data, in effect adding it to the $serviceitems array.
We output two hash tables per loop iteration, so PowerShells adds two array elements to $serviceitems per loop iteration.
Note:
This answer addresses your question as asked, specifically its syntax problems.
For a superior solution that bypasses the original problems in favor of streamlined code, see zett42's helpful answer.
$serviceitems.text.Add('type'='mrkdwn') causes a syntax error.
Generally speaking, IF $serviceitems.text referred to a hashtable (dictionary), you need either:
method syntax with distinct, ,-separated arguments:
$serviceitems.text.Add('type', 'mrkdwn')
or index syntax (which would quietly overwrite an existing entry, if present):
$serviceitems.text['type'] = 'mrkdwn'
PowerShell even lets you access hashtable (dictionary) entries with member-access syntax (dot notation):
$serviceitems.text.type = 'mrkdwn'
In your specific case, additional considerations come into play:
You're accessing a hashtable via an array, instead of directly.
The text entry you're trying to target isn't originally a nested hashtable, so you cannot call .Add() on it; instead, you must assign a new hashtable to it.
Therefore:
# Define an empty array
$serviceItems = #()
# "Extend" the array by adding a hashtable.
# Note: Except with small arrays, growing them with +=
# should be avoided, because a *new* array must be allocated
# every time.
$serviceItems += #{ text = '' }
# Refer to the hashtable via the array's last element (-1),
# and assign a nested hashtable to it.
$serviceItems[-1].text = #{ 'type' = 'mrkdwn' }
# Output the result.
$serviceItems
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
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)
There's an array of objects, where each has a collection of objects, where each has a string property. When I do a nested iteration:
$TheArray | %{$_.TheCollection | %{$_.TheProperty}}
it seems like I end up not with an array of string arrays, but with a 1D array of strings. Is that by design? That is the desired behavior in the first place, but utterly unexpected.
Yes, that output makes sense to me, at least on an intuitive level. I can't explain in accurate technical detail, but the only object written to the pipeline in your expression
$TheArray | %{$_.TheCollection | %{$_.TheProperty} }
is the inner-most
$_.TheProperty
Since this evaluates to a String, a number of Strings are accumulated in the pipeline and returned in an array.
Here's some sample code that mocks-up what you've described:
class HasProperty {
[String] $TheProperty;
HasProperty ([String] $prop){
$this.TheProperty = $prop
}
}
class SomeObject {
[HasProperty[]] $TheCollection
SomeObject ([HasProperty[]] $array) {
$this.TheCollection = $array
}
}
[SomeObject[]]$TheArray = #()
$TheArray = foreach ($i in (0..9)) {
[HasProperty[]]$tempArray = foreach ($n in (0..3)) { [HasProperty]::new("Property$i-$n") }
[SomeObject]::new($tempArray)
}
$TheArray | %{$_.TheCollection | %{$_.TheProperty} }
PowerShell's object-oriented pipeline makes it easy to extract values from some collection of objects. I've used it to get the group membership of a collection of users to determine how their memberships overlap, for instance.
PowerShell by default enumerates (unrolls) collections when outputting them to the pipeline: that is, instead of outputting the collection itself, its elements are output, one by one.
PowerShell collects all output objects from a pipeline in a flat array.
Therefore, even outputting multiple arrays creates a single, flat output array by default, which is the concatenation of these arrays.
A simpler example:
# Output 2 2-element arrays.
> 1..2 | % { #(1, 2) } | Measure-Object | % Count
4 # i.e., #(1, 2, 1, 2).Count
In order to produce nested arrays, you must suppress enumeration, which can be achieved in two ways:
Simplest option: Wrap the output array in an aux. outer array so that enumerating the outer array yields the embedded one as a single output object:
# Use unary , to wrap the RHS in a single-element array.
> 1..2 | % { , #(1, 2) } | Measure-Object | % Count
2 # i.e., #(#(1, 2), #(1, 2)).Count
Alternative, using Write-Output -NoEnumerate (PSv4+):
> 1..2 | % { Write-Output -NoEnumerate #(1, 2) } | Measure-Object | % Count
2 # i.e., #(#(1, 2), #(1, 2)).Count
Note: While use of #(...) is not necessary to create array literals (as used above) - for literals, separating elements with , is sufficient - you still need #(...) to ensure that output from enclosed expressions or commands is treated as an array, in case only a single object happens to be output.
How can i delete row from array which is pscustomobject in a loop?
Getting errors if i use this in the loop:
$a = $a | where {condition to remove lines}
Getting the below error
Method invocation failed because [System.Management.Automation.PSObject] does not contain a method named 'op_Addition'.
any suggetion to remove row from array.
Let me make some general points, given the generic title of the question:
Arrays (in .NET, which underlies PowerShell) are fixed-size data structures. As such, you cannot delete elements from them directly.
However, you can create a new array that is a copy of the original one with the unwanted elements omitted, and that is what the pipeline approach facilitates:
# Sample array.
$a = 1, 2, 3
# "Delete" element 2 from the array, which yields #(1, 3).
# #(...) ensures that the result is treated as an array even if only 1 element is returned.
$a = #($a | Where-Object { $_ -ne 2 })
PowerShell automatically captures the output from a pipeline in an array (of type [System.Object[]]) when you assign it to a variable.
However, since PowerShell automatically unwraps a single-element result, you need #(...), the array-subexpression operator to ensure that $a remains an array even if only a single element was returned - the alternative would be to type-constrain the variable as an array:
[array] $a = $a | Where-Object { $_ -ne 2 }
Note that even though the result is assigned back to input variable $a, $a now technically contains a new array (and the old one, if it is not being referenced elsewhere, will eventually be garbage-collected).
As for what you tried:
How can i delete row from array which is pscustomobject
As wOxxOm points out, [pscustomobject] are not arrays, but perhaps you meant to say that you have an array whose elements are custom objects, in which case the above approach applies.
Alternatively, if the array to delete elements from is stored in a property of a custom object, send that property's value through the pipeline instead, and assign the results back to that property.
The error message occurs when you try to use the + operator with a [pscustomobject] instance as the LHS, which is not supported; e.g.:
PS> ([pscustomobject] #{ foo = 'bar' }) + 1
Method invocation failed because [System.Management.Automation.PSObject] does not contain a method named 'op_Addition'.
...
PowerShell doesn't know how to "add" something to a custom object, so it complains.