I have an array
$a = (Invoke-RestMethod -Uri "...." -Method Get) | select X,Y,Z # Object[] Z has type of datetime
$a has X, Y, Z.
Now I need to check if a row is in $a
$x,$y,$z = ....
if ($a -contains $x,$y, $x) { ... } # doesn't work
How to do it?
It sounds like you want to test array $a for containing an object that has a given set of property values ($x, $y, $z) for a given set of property names (.X, .Y, .Z):
$hasObjectWithValues = [bool] $(foreach ($o in $a) {
if ($o.X -eq $x -and $o.Y -eq $y -and $o.Z -eq $z) {
$true
break
}
})
Note: The cleaner form [bool] $hasObjectWithValues = foreach ... should work, but, as of PowerShell Core 7.0.0-preview.4, doesn't, due to this bug
As for what you tried:
$a -contains $x,$y, $z
The RHS of PowerShell's -contains operator only supports a scalar (single value), to be tested for equality with the elements in the the array-valued LHS.
However, even if you wrapped your RHS into a single object - [pscustomobject] #{ X = $x, Y = $y, Z = $z }, that approach wouldn't work, because [pscustomobject]s, as also returned by Invoke-RestMethod, are reference types, which - in the absence of custom equality comparison behavior - are compared by reference equality, meaning that they're only considered equal if they refer to the very same object in memory.
Related
Using PowerShell Core 6.1 on Mac. It appears that piping an array to ForEach-Object is modifying or wrapping each element such that the -is operator considers all of them to be PSCustomObjects.
Let me demonstrate:
Set up an array of four items of different types (use of JSON because that's where the data is coming from in my real use case):
$a = '[4, "Hi", {}, true]' | ConvertFrom-Json
Iterate the list by index and determine which ones are PSCustomObjects:
0..3 | ForEach-Object {
$v = $a[$_]
$t = $v.GetType().FullName
$is = $v -is [PSCustomObject]
"$t - $is"
}
The output (for me) is exactly what I would expect:
System.Int64 - False
System.String - False
System.Management.Automation.PSCustomObject - True
System.Boolean - False
But if I just pipe the array to ForEach-Object:
$a | ForEach-Object {
$v = $_
$t = $v.GetType().FullName
$is = $v -is [PSCustomObject]
"$t - $is"
}
Now the output claims that all four are PSCustomObjects:
System.Int64 - True
System.String - True
System.Management.Automation.PSCustomObject - True
System.Boolean - True
Could anyone explain what is happening here?
PetSerAl, as he frequently does, has provided the crucial pointer in a comment:
Piping objects to ForEach-Object wraps them in a [psobject] instance (as reflected in $_ / $PSItem), which causes -is [pscustomobject] / -is [psobject] to return $True for any input object, because - confusingly - [pscustomobject] is the same as [psobject]: they're both type accelerators for [System.Management.Automation.PSObject] - against what one would expect,[pscustomobject] is not short for [System.Management.Automation.PSCustomObject].
Therefore, test the input objects for being instances of [System.Management.Automation.PSCustomObject] rather than [pscustomobject]:
$a | ForEach-Object {
$_ -is [System.Management.Automation.PSCustomObject]
}
Note that if you use a foreach loop, even -is [pscustomobject] would work, because the objects being enumerated are then not wrapped in an extra [psobject] instance:
foreach ($element in $a) {
$element -is [pscustomobject]
}
This works, because even a bona fide [System.Management.Automation.PSCustomObject] is technically also a [System.Management.Automation.PSObject] behind the scenes.
I'm a bit confused about some behavior I'm seeing.
The following code is designed to find and replace several strings in the registry.
$keys = #(gci -Path hkcu:\ -recurse -ErrorAction SilentlyContinue)
foreach ($key in $keys)
{
foreach ($vname in $key.GetValueNames())
{
$val = $key.GetValue($vname, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
if ($val -like "c:\users\administrator*")
{
$nval = $val -replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%"
write-host "$key\$vname=$val -> $nval"
((Get-Item $Key.PSParentPath).OpenSubKey($Key.PSChildName, "True")).SetValue($vname, $nval, $key.GetValueKind($vname))
}
}
}
I continue to get the following error message when the registry value type is REG_MULTI_SZ. Exception calling "SetValue" with "3" argument(s): "The type of the value object did not match the specified RegistryValueKind or the object could not be properly converted."
If I comment out the -replace portion, i.e.:
$nval = $val #-replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%"
The registry keys are updated without errors (obviously with the same value they were).
So, something in the -replace operation is changing the data type. It appears to change in to type System.Object[].
The registryKey.SetValue method requires a string[] to set REG_MULTI_SZ.
Why is it changing the type? And how do I work around this behavior?
UPDATE:
Applying Option #1 in the answer did not work. $nval was still of type System.Object[] even after adding the cast. Applying option #3 did work.
Here is the final code that correctly searches and replaces a string found at the beginning of a string in the registry.
$keys = #(gci -Path hkcu:\ -recurse -ErrorAction SilentlyContinue)
foreach ($key in $keys)
{
foreach ($vname in $key.GetValueNames())
{
$val = $key.GetValue($vname, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
if ($val -like "c:\users\administrator*")
{
if ($key.GetValueKind($vname) -eq [Microsoft.Win32.RegistryValueKind]::MultiString)
{ $nval = $val -replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%" -as [string[]] }
else
{ $nval = $val -replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%" }
write-host "$key\$vname=$val -> $nval"
((Get-Item $Key.PSParentPath).OpenSubKey($Key.PSChildName, "True")).SetValue($vname, $nval, $key.GetValueKind($vname))
}
}
}
Most things in PowerShell that return multiple objects return them as an [object[]]; most likely because you could return any number of different objects, and because PowerShell wraps most objects in [PSObject] whether you know it or not.
So although you may start with a [string[]], modifying it would result in an [object[]].
When you need an array of a specific type, the simple way is to just cast it.
You have 3 options for casting it: cast the value of the assignment or cast the variable, or use the -as operator.
Casting the value:
$nval = [string[]]($val -replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%")
Now $nval will be a [string[]] unless you re-assign something else to it later.
Casting the variable:
[string[]]$nval = $val -replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%"
This works a little bit differently. Putting the cast on the variable in PowerShell applies that cast to all values assigned to it.
This is a good option when you know $nval always needs to be [string[]] and you don't want to cast it on every assignment.
-As operator:
$nval = $val -replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%" -as [string[]]
This is similar to casting the value; the difference is that it doesn't throw an exception if the cast is unsuccessful, it just returns $null.
To deal with the different registry kinds and the casts needed, I recommended a switch statement in the comments, but I came up with a better idea: just use a hashtable:
$kindToType = #{
[Microsoft.Win32.RegistryValueKind]::MultiString = [string[]]
[Microsoft.Win32.RegistryValueKind]::ExpandString = [string]
[Microsoft.Win32.RegistryValueKind]::String = [string]
[Microsoft.Win32.RegistryValueKind]::DWord = [int]
# etc.
}
Then:
$nval = $val -replace "(?i:^(C:\\users\\Administrator))", "%USERPROFILE%" -as $kindToType[$key.GetValueKind($vname)]
This creates a lookup table where you can directly get a type from a registry kind.
To complement briantist's helpful answer with a focused summary:
If the LHS of a -replace expression is a collection[1]
, the replacement is performed on each of its elements.
The results are returned as a regular PowerShell array, which is of type [System.Object[]] - irrespective of the specific type of the input collection.
To preserve the input collection type, you must use an explicit cast or call an appropriate constructor - see briantist's answer.
Example:
# Apply -replace to a LHS of type [System.Collections.ArrayList]
$result = ([System.Collections.ArrayList] ('one', 'two')) -replace 'o', '#'
> $result
#ne
tw#
> $result.GetType().FullName # inspect the result's type
System.Object[]
[1] Without having looked at the source code, it seems that "collection" in this sense refers to any type that implements interface [System.Collections.IEnumerable] that doesn't also implement [System.Collections.IDictionary]; for a given instance $coll, you can test as follows:
$coll -is [System.Collections.IEnumerable] -and $coll -isnot [System.Collections.IDictionary].
Notably, this excludes hashtable-like collections, such as [hashtable] (literal syntax #{ ... }) and [System.Collections.Specialized.OrderedDictionary] (literal syntax [ordered] #{ ... }).
If I have:
$a=$null
$b=''
$c=#($null,$null)
$d='foo'
write-host $a
write-host $b
write-host $c
write-host $d
the output is
foo
I'd really like to be able to easily get output that shows the variable values, e.g.,
$Null
''
#($Null,$Null)
'foo'
I can write a function to do this, but I'm guessing/hoping there's something built-in that I'm missing. Is there, or does everyone just roll their own function for something like this?
At the moment the quickest thing I've come up with is running a value through ConvertTo-Json before printing it. It doesn't handle a plain $null, but it shows me the other values nicely.
What you're looking for is similar to Ruby's .inspect method. It's something I always loved in Ruby and do miss in PowerShell/.Net.
Unfortunately there is no such thing to my knowledge, so you will somewhat have to roll your own.
The closest you get in .Net is the .ToString() method that, at a minimum, just displays the object type (it's inherited from [System.Object]).
So you're going to have to do some checking on your own. Let's talk about the edge case checks.
Arrays
You should check if you're dealing with an array first, because PowerShell often unrolls arrays and coalesces objects for you so if you start doing other checks you may not handle them correctly.
To check that you have an array:
$obj -is [array]
1 -is [array] # false
1,2,3 -is [array] # true
,1 -is [array] #true
In the case of an array, you'll have to iterate it if you want to properly serialize its elements as well. This is basically the part where your function will end up being recursive.
function Format-MyObject {
param(
$obj
)
if ($obj -is [array]) {
# initial array display, like "#(" or "["
foreach ($o in $obj) {
Format-MyObject $obj
}
# closing array display, like ")" or "]"
}
}
Nulls
Simply check if it's equal to $null:
$obj -eq $null
Strings
You can first test that you're dealing with a string by using -is [string].
For empty, you can compare the string to an empty string, or better, to [string]::Empty. You can also use the .IsNullOrEmpty() method, but only if you've already ruled out a null value (or checked that it is indeed a string):
if ($obj -is [string) {
# pick one
if ([string]::IsNullOrEmpty($obj)) {
# display empty string
}
if ($obj -eq [string]::Empty) {
# display empty string
}
if ($obj -eq "") { # this has no advantage over the previous test
# display empty string
}
}
Alternative
You could use the built-in XML serialization, then parse the XML to get the values out of it.
It's work (enough that I'm not going to do it in an SO answer), but it removes a lot of potential human error, and sort of future-proofs the approach.
The basic idea:
$serialized = [System.Management.Automation.PSSerializer]::Serialize($obj) -as [xml]
Now, use the built in XML methods to parse it and pull out what you need. You still need to convert some stuff to other stuff to display the way you want (like interpreting <nil /> and the list of types to properly display arrays and such), but I like leaving the actual serialization to an official component.
Quick example:
[System.Management.Automation.PSSerializer]::Serialize(#(
$null,
1,
'string',
#(
'start of nested array',
$null,
'2 empty strings next',
'',
([string]::Empty)
)
)
)
And the output:
<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
<Obj RefId="0">
<TN RefId="0">
<T>System.Object[]</T>
<T>System.Array</T>
<T>System.Object</T>
</TN>
<LST>
<Nil />
<I32>1</I32>
<S>string</S>
<Obj RefId="1">
<TNRef RefId="0" />
<LST>
<S>start of nested array</S>
<Nil />
<S>2 empty strings next</S>
<S></S>
<S></S>
</LST>
</Obj>
</LST>
</Obj>
</Objs>
I shared two functions that reveal PowerShell values (including the empty $Null's, empty arrays etc.) further than the usually do:
One that the serializes the PowerShell objects to a PowerShell
Object Notation (PSON)
which ultimate goal is to be able to reverse everything with the
standard command Invoke-Expression and parse it back to a
PowerShell object.
The other is the ConvertTo-Text (alias CText) function that I used in
my Log-Entry
framework. note the
specific line: Log "Several examples that usually aren't displayed
by Write-Host:" $NotSet #() #(#()) #(#(), #()) #($Null) that I wrote
in the example.
Function Global:ConvertTo-Text1([Alias("Value")]$O, [Int]$Depth = 9, [Switch]$Type, [Switch]$Expand, [Int]$Strip = -1, [String]$Prefix, [Int]$i) {
Function Iterate($Value, [String]$Prefix, [Int]$i = $i + 1) {ConvertTo-Text $Value -Depth:$Depth -Strip:$Strip -Type:$Type -Expand:$Expand -Prefix:$Prefix -i:$i}
$NewLine, $Space = If ($Expand) {"`r`n", ("`t" * $i)} Else{"", ""}
If ($O -eq $Null) {$V = '$Null'} Else {
$V = If ($O -is "Boolean") {"`$$O"}
ElseIf ($O -is "String") {If ($Strip -ge 0) {'"' + (($O -Replace "[\s]+", " ") -Replace "(?<=[\s\S]{$Strip})[\s\S]+", "...") + '"'} Else {"""$O"""}}
ElseIf ($O -is "DateTime") {$O.ToString("yyyy-MM-dd HH:mm:ss")}
ElseIf ($O -is "ValueType" -or ($O.Value.GetTypeCode -and $O.ToString.OverloadDefinitions)) {$O.ToString()}
ElseIf ($O -is "Xml") {(#(Select-XML -XML $O *) -Join "$NewLine$Space") + $NewLine}
ElseIf ($i -gt $Depth) {$Type = $True; "..."}
ElseIf ($O -is "Array") {"#(", #(&{For ($_ = 0; $_ -lt $O.Count; $_++) {Iterate $O[$_]}}), ")"}
ElseIf ($O.GetEnumerator.OverloadDefinitions) {"#{", (#(ForEach($_ in $O.Keys) {Iterate $O.$_ "$_ = "}) -Join "; "), "}"}
ElseIf ($O.PSObject.Properties -and !$O.value.GetTypeCode) {"{", (#(ForEach($_ in $O.PSObject.Properties | Select -Exp Name) {Iterate $O.$_ "$_`: "}) -Join "; "), "}"}
Else {$Type = $True; "?"}}
If ($Type) {$Prefix += "[" + $(Try {$O.GetType()} Catch {$Error.Remove($Error[0]); "$Var.PSTypeNames[0]"}).ToString().Split(".")[-1] + "]"}
"$Space$Prefix" + $(If ($V -is "Array") {$V[0] + $(If ($V[1]) {$NewLine + ($V[1] -Join ", $NewLine") + "$NewLine$Space"} Else {""}) + $V[2]} Else {$V})
}; Set-Alias CText ConvertTo-Text -Scope:Global -Description "Convert value to readable text"
ConvertTo-Text
The ConvertTo-Text function (Alias CText) recursively converts PowerShell object to readable text this includes hash tables, custom objects and revealing type details (like $Null vs an empty string).
Syntax
ConvertTo-Text [<Object>] [[-Depth] <int>] [[-Strip] <int>] <string>] [-Expand] [-Type]
Parameters
<Object>
The object (position 0) that should be converted a readable value.
-Depth <int>
The maximal number of recursive iterations to reveal embedded objects.
The default depth for ConvertTo-Text is 9.
-Strip <int>
Truncates strings at the given length and removes redundant white space characters if the value supplied is equal or larger than 0. Set -Strip -1 prevents truncating and the removal of with space characters.
The default value for ConvertTo-Text is -1.
-Expand
Expands embedded objects over multiple lines for better readability.
-Type
Explicitly reveals the type of the object by adding [<Type>] in front of the objects.
Note: the parameter $Prefix is for internal use.
Examples
The following command returns a string that describes the object contained by the $var variable:
ConvertTo-Text $Var
The following command returns a string containing the hash table as shown in the example (rather then System.Collections.DictionaryEntry...):
ConvertTo-Text #{one = 1; two = 2; three = 3}
The following command reveals values (as e.g. $Null) that are usually not displayed by PowerShell:
ConvertTo-Text #{Null = $Null; EmptyString = ""; EmptyArray = #(); ArrayWithNull = #($Null); DoubleEmptyArray = #(#(), #())} -Expand
The following command returns a string revealing the WinNT User object up to a level of 5 deep and expands the embedded object over multiple lines:
ConvertTo-Text ([ADSI]"WinNT://./$Env:Username") -Depth 5 -Expand
A quick self-rolled option good for some datatypes.
function Format-MyObject {
param(
$obj
)
#equality comparison order is important due to array -eq overloading
if ($null -eq $obj)
{
return 'null'
}
#Specify depth because the default is 2, because powershell
return ConvertTo-Json -Depth 100 $obj
}
This question already has answers here:
Powershell create array of arrays
(3 answers)
Closed 5 years ago.
This is building $ret into a long 1 dimensional array rather than an array of arrays. I need it to be an array that is populated with $subret objects. Thanks.
$ret = #()
foreach ($item in $items){
$subret = #()
$subRet = $item.Name , $item.Value
$ret += $subret
}
there might be other ways but arraylist normally works for me, in this case I would do:
$ret = New-Object System.Collections.ArrayList
and then
$ret.add($subret)
The suspected preexisting duplicate question is indeed a duplicate:
Given that + with an array as the LHS concatenates arrays, you must nest the RHS with the unary form of , (the array-construction operator) if it is an array that should be added as a single element:
# Sample input
$items = [pscustomobject] #{ Name = 'n1'; Value = 'v1'},
[pscustomobject] #{ Name = 'n2'; Value = 'v2'}
$ret = #() # create an empty *array*
foreach ($item in $items) {
$subret = $item.Name, $item.Value # use of "," implicitly creates an array
$ret += , $subret # unary "," creates a 1-item array
}
# Show result
$ret.Count; '---'; $ret[0]; '---'; $ret[1]
This yields:
2
---
n1
v1
---
n2
v2
The reason the use of [System.Collections.ArrayList] with its .Add() method worked too - a method that is generally preferable when building large arrays - is that .Add() only accepts a single object as the item to add, irrespective of whether that object is a scalar or an array:
# Sample input
$items = [pscustomobject] #{ Name = 'n1'; Value = 'v1'},
[pscustomobject] #{ Name = 'n2'; Value = 'v2'}
$ret = New-Object System.Collections.ArrayList # create an *array list*
foreach ($item in $items) {
$subret = $item.Name, $item.Value
# .Add() appends whatever object you pass it - even an array - as a *single* element.
# Note the need for $null = to suppress output of .Add()'s return value.
$null = $ret.Add($subret)
}
# Produce sample output
$ret.Count; '---'; $ret[0]; '---'; $ret[1]
The output is the same as above.
Edit
It is more convoluted to create an array of tuples than fill an array with PsObjects containing Name Value as the two properties.
Select the properties you want from $item then add them to the array
$item = $item | select Name, Value
$arr = #()
$arr += $item
You can reference the values in this array by doing this
foreach($obj in $arr)
{
$name = $obj.Name
$value = $obj.Value
# Do actions with the values
}
I am trying to understand the behavior of the #() array constructor, and I came across this very strange test.
It seems that the value of an empty pipeline is "not quite" the same as $null, even though it is -eq $null
The output of each statement is shown after the ###
$y = 1,2,3,4 | ? { $_ -ge 5 }
$z = $null
if ($y -eq $null) {'y is null'} else {'y NOT null'} ### y is null
if ($z -eq $null) {'z is null'} else {'z NOT null'} ### z is null
$ay = #($y)
$az = #($z)
"ay.length = " + $ay.length ### ay.length = 0
"az.length = " + $az.length ### az.length = 1
$az[0].GetType() ### throws exception because $az[0] is null
So the $az array has length one, and $az[0] is $null.
But the real question is: how is it possible that both $y and $z are both -eq $null, and yet when I construct arrays with #(...) then one array is empty, and the other contains a single $null element?
Expanding on Frode F.'s answer, "nothing" is a mostly magical value in PowerShell - it's called [System.Management.Automation.Internal.AutomationNull]::Value. The following will work similarly:
$y = 1,2,3,4 | ? { $_ -ge 5 }
$y = [System.Management.Automation.Internal.AutomationNull]::Value
PowerShell treats the value AutomationNull.Value like $null in most places, but not everywhere. One notable example is in a pipeline:
$null | % { 'saw $null' }
[System.Management.Automation.Internal.AutomationNull]::Value | % { 'saw AutomationNull.Value' }
This will only print:
saw $null
Note that expressions are themselves pipelines even if you don't have a pipeline character, so the following are roughly equivalent:
#($y)
#($y | Write-Output)
Understanding this, it should be clear that if $y holds the value AutomationNull.Value, nothing is written to the pipeline, and hence the array is empty.
One might ask why $null is written to the pipeline. It's a reasonable question. There are some situations where scripts/cmdlets need to indicate "failed" without using exceptions - so "no result" must be different, $null is the obvious value to use for such situations.
I've never run across a scenario where one needs to know if you have "no value" or $null, but if you did, you could use something like this:
function Test-IsAutomationNull
{
param(
[Parameter(ValueFromPipeline)]
$InputObject)
begin
{
if ($PSBoundParameters.ContainsKey('InputObject'))
{
throw "Test-IsAutomationNull only works with piped input"
}
$isAutomationNull = $true
}
process
{
$isAutomationNull = $false
}
end
{
return $isAutomationNull
}
}
dir nosuchfile* | Test-IsAutomationNull
$null | Test-IsAutomationNull
The reason you're experiencing this behaviour is becuase $null is a value. It's a "nothing value", but it's still a value.
PS P:\> $y = 1,2,3,4 | ? { $_ -ge 5 }
PS P:\> Get-Variable y | fl *
#No value survived the where-test, so y was never saved as a variable, just as a "reference"
Name : y
Description :
Value :
Visibility : Public
Module :
ModuleName :
Options : None
Attributes : {}
PS P:\> $z = $null
PS P:\> Get-Variable z | fl *
#Our $null variable is saved as a variable, with a $null value.
PSPath : Microsoft.PowerShell.Core\Variable::z
PSDrive : Variable
PSProvider : Microsoft.PowerShell.Core\Variable
PSIsContainer : False
Name : z
Description :
Value :
Visibility : Public
Module :
ModuleName :
Options : None
Attributes : {}
The way #() works, is that it guarantees that the result is delievered inside a wrapper(an array). This means that as long as you have one or more objects, it will wrap it inside an array(if it's not already in an array like multiple objects would be).
$y is nothing, it's a reference, but no variable data was stored. So there is nothing to create an array with. $z however, IS a stored variable, with nothing(null-object) as the value. Since this object exists, the array constructor can create an array with that one item.