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!
CodePudding user response:
The easiest way to "concatenate" two variables into one flat array is to use the @(...)
array subexpression operator:
... |Select-Object @($user_props;$user_groups) ...
CodePudding user response:
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 thatWrite-Output foo, name
is the simpler alternative toWrite-Output @('foo', 'name')
,
, the array constructor operator, has perhaps surprisingly high precedence, so that1, 2 3
is parsed as(1, 2) 3
, resulting in1, 2, 3