Home > Enterprise >  Issue with using properties in an array and custom calculated property
Issue with using properties in an array and custom calculated property

Time:08-18

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 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