Issue with using properties in an array and custom calculated property - powershell

So I have the following code that ingests AD users on a domain controller. The following throws an error:
# User Props to select
$user_props = #(
'Name',
'DistinguishedName',
'SamAccountName',
'Enabled',
'SID'
)
# Get AD groups an AD user is a member of
$user_groups = #{ label = 'GroupMemberships'; expression = { (Get-ADPrincipalGroupMembership -Identity $_.DistinguishedName).Name } }
# Get AD Users
$users = Get-ADUser -Filter * -Property $user_props | Select-Object $user_props, $user_groups -ErrorAction Stop -ErrorVariable _error
However, if I were to change $users to the following:
$users = Get-ADUser -Filter * -Property $user_props | Select-Object Name, DistinguishedName, SamAccountName, Enabled, SID, $user_groups -ErrorAction Stop -ErrorVariable _error
I no longer get this error. Is there a way I can define $user_props such that I don't need to type out each property and still use my custom calculated property $user_groups?
I believe the issue has to do with mixing an array ($user_props) with a hashtable ($user_groups) but I'm unsure how to best write this. Thank you for the help!

The easiest way to "concatenate" two variables into one flat array is to use the #(...) array subexpression operator:
... |Select-Object #($user_props;$user_groups) ...

Since this issue keeps coming up, let me complement Mathias R. Jessen's effective solution with some background information:
Select-Object's (potentially positionally implied) -Property parameter requires a flat array of property names and/or calculated properties (for brevity, both referred to as just property names below).
Therefore, if you need to combine two variables containing arrays of property names, or combine literally enumerated property name(s) with such variables, you must explicitly construct a flat array.
Therefore, simply placing , between your array variables does not work, because it creates a jagged (nested) array:
# Two arrays with property names.
$user_props = #('propA', 'propB')
$user_groups = #('grpA', 'grpB')
# !! WRONG: This passes a *jagged* array, not a flat one.
# -> ERROR: "Cannot convert System.Object[] to one of the following types
# {System.String, System.Management.Automation.ScriptBlock}."
'foo' | Select-Object $user_props, $user_groups
# !! WRONG:
# The same applies if you tried to combine one or more *literal* property names
# with an array variable.
'foo' | Select-Object 'propA', $user_groups
'foo' | Select-Object $user_groups, 'propA', 'propB'
That is, $user_props, $user_groups effectively passes
#( #('propA', 'propB'), #('grpA', 'grpB') ), i.e. a jagged (nested) array,
whereas what you need to pass is
#('propA', 'propB', 'grpA', 'grpB'), i.e. a flat array.
Mathias' solution is convenient in that you needn't know or care whether $user_props and $user_groups contain arrays or just a single property name, due to how #(...), the array-subexpression operator works - the result will be a flat array:
# Note how the variable references are *separate statements*, whose
# output #(...) collects in an array:
'foo' | Select-Object #($user_props; $user_groups)
# Ditto, with a literal property name.
# Note the need to *quote* the literal name in this case.
'foo' | Select-Object #('propA'; $user_groups)
In practice it won't make a difference for this use case, so this is a convenient and pragmatic solution, but generally it's worth noting that #(...) enumerates array variables used as statements inside it, and then collects the results in a new array. That is, both $user_props and $user_groups are sent to the pipeline element by element, and the resulting, combined elements are collected in a new array.
A direct way to flatly concatenate arrays (or append a single element to an array) is to use the + operator with (at least) an array-valued LHS. This, of necessity, returns a new array that is copy of the LHS array with the element(s) of the RHS directly appended:
# Because $user_props is an *array*, "+" returns an array with the RHS
# element(s) appended to the LHS element.
'foo' | Select-Object ($user_props + $user_groups)
If you're not sure if $user_props is an array, you can simply cast to [array], which also works with a single, literal property name:
# The [array] cast ensures that $user_props is treated as an array, even if it isn't one.
# Note:
# 'foo' | Select-Object (#($user_props) + $user_groups)
# would work too, but would again needlessly enumerate the array first.
'foo' | Select-Object ([array] $user_props + $user_groups)
# Ditto, with a single, literal property name
'foo' | Select-Object ([array] 'propA' + $user_groups)
# With *multiple* literal property names (assuming they come first), the
# cast is optional:
'foo' | Select-Object ('propA', 'propB' + $user_groups)
Note:
The above uses (...), the grouping operator, in order to pass expressions as arguments to a command - while you frequently see $(...), the subexpression operator used instead, this is not necessary and can have unwanted side effects - see this answer.
#(...) isn't strictly needed to declare array literals, such as #('foo', 'bar') - 'foo', 'bar' is sufficient, though you may prefers enclosing in #(...) for visual clarity. In argument parsing mode, quoting is optional for simple strings, so that Write-Output foo, name is the simpler alternative to Write-Output #('foo', 'name')
,, the array constructor operator, has perhaps surprisingly high precedence, so that 1, 2 + 3 is parsed as (1, 2) + 3, resulting in 1, 2, 3

Related

Use Variable for Select-Object or Format-Table

I have a a similar question like this: Question 44460843
I want to use variables for the fields.
This example works well:
$x = ("Id,ProcessName,CPU").split(",")
Get-Process | ft -a $x
But how do i get it, if i want custom fields like this?
Get-Process | ft -a Id,ProcessName,#{Name = 'CPUx' ; Expression = {$_.CPU}}
I tried this, but it don't works:
$x = ("Id,ProcessName,#{Name = 'CPUx' ; Expression = {$_.CPU}}").split(",")
Get-Process | ft -a $x
Does anybody know how to do this?
I am a lazy guy and want to avoid this copy/paste orgies with long text.
Note:
The answer applies analogously to the Select-Object cmdlet, which is what should be used to extract properties for later programmatic processing.
Format-* cmdlets, such as Format-Table in this case (whose built-in alias is ft), are only intended to produce for-display formatting; see this answer for more information.
Theo has provided the solution in a comment:
$x = 'Id', 'ProcessName', #{ Name = 'CPUx' ; Expression = { $_.CPU } }
Get-Process | Format-Table -AutoSize $x # same as: ft -a $x
That is, the answer to the linked question applies in your case as well, even though it happens to use only literal strings as property names:
Directly construct an array of property names and calculated properties, using ,, the array constructor operator, which allows you to use any mix of literals and variables; e.g.:
$pName = 'processName' # property name
$pCpu = #{ Name = 'CPUx' ; Expression = { $_.CPU } } # calculated property
$x = 'Id', $pName, $pCpu
Do not start with a single string that you split into an array of tokens with .Split(), because it will limit you to property names, given that the tokens are invariably strings.
As an aside:
The array binds positionally to Format-Table's (ft's) -Property parameter, and you can easily discover that as well as the fact that an array of property names / calculated properties is discovered as follows:
PS> Format-Table -?
...
SYNTAX
Format-Table [[-Property] <System.Object[]>] ...
DESCRIPTION
...
The outer [...] tells you that the parameter is optional as a whole.
The [...] around -Property tells you that specifying the parameter name explicitly is optional, i.e. that positional binding is supported.
<System.Object[]> tells you that an array ([]) of System.Object instances is expected as the type of the argument(s).
To get more information about what objects, specifically, may be passed, inspect the parameter individually:
PS> Get-Help Format-Table -Parameter Property
-Property <System.Object[]>
Specifies the object properties that appear in the display and the order
in which they appear. Type one or more property names, separated
by commas, or use a hash table to display a calculated property.
Wildcards are permitted.
...

Powershell eq operator saying hashes are different, while Write-Host is showing the opposite

I have a script that periodically generates a list of all files in a directory, and then writes a text file of the results to a different directory.
I'd like to change this so it checks the newest text file in the output directory, and only makes a new one if there's differences. It seemed simple enough.
Here's what I tried:
First I get the most recent file in the directory, grab the hash, and write my variable values to the console:
$lastFile = gci C:\ReportOutputDir | sort LastWriteTime | select -last 1 | Select-Object -ExpandProperty FullName
$oldHash = Get-FileHash $lastFile | Select-Object Hash
Write-Host 'lastFile = '$lastFile
Write-Host 'oldHash = '$oldHash
Output:
lastFile = C:\ReportOutputDir\test1.txt
oldHash = #{Hash=E7787C54F5BAE236100A24A6F453A5FDF6E6C7333B60ED8624610EAFADF45521}
Then I do the exact same gci on the FileList dir, and create a new file (new_test.txt), then grab the hash of this file:
gci -Path C:\FileLists -File -Recurse -Name -Depth 2 | Sort-Object | out-file C:\ReportOutputDir\new_test.txt
$newFile = gci C:\ReportOutputDir | sort LastWriteTime | select -last 1 | Select-Object -ExpandProperty FullName
$newHash = Get-FileHash $newFile | Select-Object Hash
Write-Host 'newFile = '$newFile
Write-Host 'newHash = '$newHash
Output:
newFile = C:\ReportOutputDir\new_test.txt
newHash = #{Hash=E7787C54F5BAE236100A24A6F453A5FDF6E6C7333B60ED8624610EAFADF45521}
Finally, I attempt my -eq operator where I'd usually simply remove the newFile if it's equal. For now, I'm just doing a simple :
if ($newHash -eq $oldHash){
'files are equal'
}
else {'files are not equal'}
And somehow, I'm getting
files are not equal
What gives? Also, for the record I was originally trying to save the gci output to a variable and comparing the contents of the last file to the gci output, but was also having trouble with the -eq operator. Fairly new to powershell stuff so I'm sure I'm doing something wrong here.
Select-Object Hash creates an object with a .Hash property and it is that property that contains the hash string.
The object returned is of type [pscustomobject], and two instances of this type never compare as equal - even if all their property names and values are equal:
The reason is that reference equality is tested, because [pscustomobject] is a .NET reference type that doesn't define custom equality-testing logic.
Testing reference equality means that only two references to the very same instance compare as equal.
A quick example:
PS> [pscustomobject] #{ foo = 1 } -eq [pscustomobject] #{ foo = 1 }
False # !! Two distinct instances aren't equal, no matter what they contain.
You have two options:
Compare the .Hash property values, not the objects as a whole:
if ($newHash.Hash -eq $oldHash.Hash) { # ...
If you don't need a [pscustomobject] wrapper for the hash strings, use Select-Object's -ExpandProperty parameter instead of the (possibly positionally implied) -Property parameter:
Select-Object -ExpandProperty Hash
As for why the Write-Host output matched:
When you force objects to be converted to string representations - essentially, Write-Host calls .ToString() on its arguments - the string representations of distinct [pscustomobject] instances that have the same properties and values will be the same:
PS> "$([pscustomobject] #{ foo = 1 })" -eq "$([pscustomobject] #{ foo = 1 })"
True # Same as: '#{foo=1}' -eq '#{foo=1}'
However, you should not rely on these hashtable-like string representations to determine equality of [pscustomobject]s as a whole, because of the inherent limitations of these representations, which can easily yield false positives.
This answer shows how to compare [pscustomobject] instances as a whole, by comparing all of their property values, by passing all property names to Compare-Object -Property - but note that this assumes that all property values are either strings or instances of .NET value types or corresponding properties must again either reference the very same instance of a .NET reference type or be of a type that implements custom equality-comparison logic.

How to get the first row from a PowerShell datatable

In PowerShell, if $dt is a datatable, I am used to using foreach() to do row-by-row operations. For example...
foreach ($tmpRow in $dt) {
Write-Host $tmpRow.blabla
}
I just want to get the first row (of n columns). I could introduce a counter $i and just break the foreach loop on the first iteration, but that seems clunky. Is there a more direct way of achieving this?
For a collection (array) that is already in memory, use indexing, namely [0]:
Note: Normally, $dt[0] should suffice, but in this case the index must be applied to the .Rows property, as Theo advises:
$dt.Rows[0].blabla
Given that PowerShell automatically enumerates a System.Data.DataTable by enumerating the System.Data.DataRow instances stored in its .Rows property - both in the pipeline and in a foreach loop, as in your code - the need to specify .Rows explicitly for indexing is surprising.
With $dt containing a System.Data.DataTable instance, $dt[0] is actually the same as just $dt itself, because PowerShell in this context considers $dt a single object, and generally supports indexing even into such single objects, in the interest of unified treatment of scalars and arrays - see this answer for background information.
For command output, use Select-Object -First 1. Using the example of Invoke-SqlCmd
(Invoke-SqlCommand ... | Select-Object -First 1).blabla
Note: Since Invoke-SqlCommand by default outputs individual System.Data.DataRow instances (one by one), you can directly access property .blabla on the result.
The advantage of using Select-Object -First 1 is that short-circuits the pipeline and returns once the first output object has been received, obviating the need to retrieve further objects.
PowerShell automatically enumerates all rows when you pipe a DataTable, so you could pipe it to Select-Object -First 1:
# set up sample table
$dt = [System.Data.DataTable]::new()
[void]$dt.Columns.Add('ID', [int])
[void]$dt.Columns.Add('Name', [string])
# initialize with 2 rows
[void]$dt.Rows.Add(1, "Clive")
[void]$dt.Rows.Add(2, "Mathias")
# enumerate only 1 row
foreach ($tmpRow in $dt |Select-Object -First 1) {
Write-Host "Row with ID '$($tmpRow.ID)' has name '$($tmpRow.Name)'"
}
Expected screen buffer output:
Row with ID '1' has name 'Clive'

Sort-Object ignores substituted switch

I'm trying to adapt the sorting order of Sort-Object to a situation.
However, it seems to ignore my variable as the sort order switch -Descending
The following code shows my problem:
I've also tried some variants on the actual substitution, e.g. $($SortOrder) but haven't found a combination that works.
$dlist = ("Zeta", "Beta", "Foo", "Alpha","Yada" )
$sorted1 = $dlist | Sort-Object -Descending
Write-Host $sorted1
$sortOrder = "-Descending"
$sorted2 = $dlist | Sort-Object $sortOrder
Write-Host $sorted2
This produces the following two lines: The first is sorted the second is not
Zeta Yada Foo Beta Alpha
Zeta Beta Foo Alpha Yada
What am I failing to do?
To control the direction of sorting programmatically, use a [bool] variable and pass it to the -Descending switch; $true sorts in descending order, $false in ascending order:
$descending = $true # Setting this to $false will sort in ascending order.
'Zeta', 'Beta', 'Foo', 'Alpha', 'Yada' | Sort-Object -Descending:$descending
Note how : rather than the usual space must be used to separate the parameter name from the value in this case (this syntax is generally supported, but otherwise rarely used).[1]
Alternatively, use splatting:
$params = #{ Descending = $true }
'Zeta', 'Beta', 'Foo', 'Alpha', 'Yada' | Sort-Object #params
Both commands yield the desired (descending) order:
Zeta
Yada
Foo
Beta
Alpha
As for what you tried:
You cannot pass a switch parameter, such as -Descending, as a string (variable).
If you do, Sort-Object will consider it a positional argument that binds to the -Property parameter, indicating what property/ies on the input objects to sort by.
If no such property exists, all input objects compare the same (the value they're compared by is $null for all of them), with no guaranteed output order.
[1] The : unconditionally tells PowerShell that the next token is the argument for that parameter. If spaces were used, PowerShell would consider the next token a separate positional argument, given that switch parameters normally do not take arguments (the mere presence of a switch parameter implies that its value is $true).
There is a possibility to do it with Strings:
$desc='-descending'
Invoke-Expression ls | sort $desc
But the solution above is better

PowerShell update nested value

I have the following output from a PowerShell command and want to update the value for EmployeeID
I can filter the output with $test.identifiers | where {$_.name -like "EmployeeID" }
But if I try to update the value with
$test.identifiers | where {$_.name -like "EmployeeID" } | foreach {$_.values.value = "098324"}
I get an error
How can I update this nested value?
$_.values contains an array (or collection) objects, which explains why you can get (read) the .value property, but not set (write) it (see below).
If you expect the array to have just one element, simply use [0] to access that element directly:
$test.identifiers | where {$_.name -like "EmployeeID" } | foreach {
$_.values[0].value = '098324'
}
If there are multiple elements, use
$_.values | foreach { $_.value = '098324' } to assign to them all, or, alternatively in PSv4+,
$_.values.ForEach('value', '098324')
In PSv3+ a feature called member-access enumeration allows you to access a property on a collection and have the property values from the individual elements returned as an array.
However, that only works for getting properties, not for setting them.
When you try to set, only the collection's own properties are considered, which explains the error you saw - an array itself has no .value property.
While this asymmetry is by design, to avoid potentially unwanted bulk modification, the error message could certainly be more helpful.
Simple reproduction of the problem:
Create an object with property one containing a single-element array with another object, with property two:
$obj = [pscustomobject] #{ one = , [pscustomobject] #{ two = 2 } }
The default output looks as follows:
PS> $obj
one
---
{#{two=2}}
The outer {...} indicate an array, as in your case, and what's inside is a
hashtable-like notation that PowerShell uses to represent custom objects.
Getting the nested-inside-an-array object's two property works as intended:
PS> $obj.two
2
Trying to set it fails:
PS> $obj.two = 2.1
The property 'two' cannot be found on this object. Verify that the property exists and can be set.
To set, use .ForEach(), for instance:
PS> $obj.ForEach('two', 2.1); $obj
one
---
{#{two=2.1}}
Have you tried it this way with the full object path:
$test.identifiers | where {$_.name -like "EmployeeID" } | foreach {$_.identifiers.values.value = "098324"}