Joshua Lapchuk
Joshua Lapchuk

Reputation: 33

Dynamically an add element to an array in PowerShell

I'm attempting to run a query against AD to give me a list of users. Since our organisation adds the tag "LastName, FirstName" to the description field of computer objects, I also want to list out the assigned machines per user. This is what I have so far:

$ADGroup = "SomeADGroup"
$UserList = Get-ADGroupMember -Identity $ADGroup -Recursive `
  | Select SamAccountName `
  | foreach-object {(Get-ADUser -Identity $_.SamAccountName `
  | Select @{n='AssignedUser';e={$_.Surname + ", " + $_.GivenName}}).AssignedUser}


$Results = New-Object System.Collections.Generic.List[System.Object]

foreach ($User in $UserList)
{
    $MachineList = @(((Get-ADComputer -Filter "Description -like `"*$User*`"" | Select Name).Name) -join $_ + ",").ToUpper()

    foreach ($Machine in $MachineList)
    {
        $Results +=
            @{
                'User' = $User
                'Machine' = $Machine
            }
    }
}

$Results | ForEach-Object { [pscustomobject] $_ } | Format-Table

The result is something like this:

User                 Machine
----                 -------

White, Scott         W107175
Jones, Henry         W107195
Flinstone, Fred      L109531,W108812

Since it's possible to have multiple machines assigned to each user, I'm trying to figure out a way to dynamically add "columns" to the array so that each machine name element resides in a column. This will eventually make it easier to export to CSV. I'm looking for output similar to this:

User                 Machine   Machine2
----                 -------   -------

White, Scott         W107175
Jones, Henry         W107195
Flinstone, Fred      L109531   W108812

Upvotes: 2

Views: 10700

Answers (3)

Joshua Lapchuk
Joshua Lapchuk

Reputation: 33

$ADGroup = "Some AD Group"
$UserList = Get-ADGroupMember -Identity $ADGroup -Recursive | Get-ADUser

$Results = @()
$MaxMachineCount = 0

foreach ($User in $UserList)
{
    if ($User.SurName -and $User.GivenName -ne $null)
    {
        $CurrentUser = [ordered]@{ 'User' = $User.SurName + ", " + $User.GivenName }
        $MachineList = @(((Get-ADComputer -Filter "Description -like `"*$($CurrentUser.User)*`"") | Select -ExpandProperty Name))

        for ($i=0; $i -lt $MachineList.Count; $i++)
        {
            $CurrentUser.Add("Machine $i","$($MachineList[$i].ToUpper())")
            if ($MaxMachineCount -le $i) {$MaxMachineCount++}
        }
        $Results += [PSCustomObject]$CurrentUser
    }
}

for ($i=0; $i -lt $MaxMachineCount; $i++){$Results[0] | Add-Member -Name "Machine $i" -Value $null -MemberType NoteProperty -ErrorAction Ignore}

$Results | Format-Table -AutoSize

The end piece of code Format-Table -AutoSize can also be substituted for ConvertTo-CSV | Out-File "C:\Some\Directory\File.csv"

I added this to create additional columns in my result. Previously, only a maximum of two machines per user would show. Now, the script returns ALL assigned machines per user.

for ($i=0; $i -lt $MachineList.Count; $i++){$Results[0] | Add-Member -Name "Machine $i" -Value $null -MemberType NoteProperty -ErrorAction Ignore

This is my result:

User                Machine 0 Machine 1 Machine 2 Machine 3 Machine 4 Machine 5 Machine 6 Machine 7 Machine 8
----                --------- --------- --------- --------- --------- --------- --------- --------- ---------
Froeman, Abe        L110911
Maclatchie, Matthew W109370   L108518
White, Walter       V999100   L107800   V999109   V999108   V999107   V999102   V999106   V999105   V999101

Thank you to @TessellatingHeckler and @TheMadTechnician for your input.

Upvotes: 0

TessellatingHeckler
TessellatingHeckler

Reputation: 29048

You're well into "variable variable" question territory here; fundamentally you would never write

$User.Machine1
$User.Machine2
$User.Machine...

because you don't know how many machines there are, so you cannot write the property names in your code. Arrays are the answer to this, and you have one variable in your code which references an array, with all the machines in it. Then your code makes sense, it's easy and sensible and works, but it doesn't align with your dream of CSV - because your data is not CSV-shaped.

If you add one property per machine to each user, you get some users with one machine and some with four ... and that trips up the likes of Format-Table, ConvertTo-Csv, Out-GridView, because they take the first object they see as the template, and then any columns on other objects which weren't on the first object, they drop.

So you have to do what TheMadTechnician is doing, and go back and dynamically adding all the blank MachineN properties to all the objects, and it is enormously simpler if you just say up front "there will be N columns".

Then you can count through them, and I get this:

$ADGroup = "SomeADGroup"
$ColumnCount = 10

Get-ADGroupMember -Identity $ADGroup -Recursive | Get-ADUser | ForEach-Object {

    $User = [ordered]@{ 'User' = $_.SurName + ", " + $_.GivenName }

    $Machines = @((Get-ADComputer -Filter "Description -like '*$($User.User)*'").Name)

    for ($i=0; $i -lt $ColumnCount; $i++)
    {
        $User["Machine$i"] = $Machines[$i]
    }

    [PSCustomObject]$User

} | ConvertTo-Csv

NB. I'm relying on array out of bounds behaviour. If you need to avoid that, then add 10 columns of empty string onto the end of the Machines list as a buffer.

But I'd still be very tempted to just mash out a CSV myself, with string manipulation, and not go through objects at all, since you're not using them as they 'should' be used, but only going a long way around making the right properties only to throw them away to get a CSV.


Off-topic, your code is very convoluted:

$UserList = Get-ADGroupMember -Identity $ADGroup -Recursive `
| Select SamAccountName `
| foreach-object {(Get-ADUser -Identity $_.SamAccountName `
| Select @{n='AssignedUser';e={$_.SurName + ", " + $_.GivenName}}).AssignedUser}

You get the AD Group Member, then you select the SamAccountName in a way that does absolutely nothing, then you loop over the results instead of pipelining them directly into Get-ADUser, for no benefit, then you select a calculated property and then throw it away and get the value. Instead:

Get-ADGroupMember -Identity $ADGroup -Recursive | Get-ADUser | ForEach-Object { $_.SurName + ', ' + $_.GivenName }

Get the group members, get their AD Users, and compute your string from those results.

and

$Results = New-Object System.Collections.Generic.List[System.Object]
->
$Results = @()

or, better yet:

$Results = @()
foreach ($x in $y) {
    $Results += ...
}

->

$Results = foreach ($x in y ) {
    ...
}

and in

| ForEach-Object { [pscustomobject] $_ } |

You ... could have made them PSCustomObjects when you were making them, and then you wouldn't need this.

Upvotes: 1

TheMadTechnician
TheMadTechnician

Reputation: 36332

Ok, so you want to add an unknown number of properties to an existing object. That's not that hard to do using the Add-Member cmdlet. Next you just need to structure a loop so that adding those properties is easy to understand, which we can do using a For loop. Lastly you need to make sure that the first record in your array has all of the potential properties. I'll explain that a little more when we get there.

First, we get the users. I changed what you had a little, because I'm strictly opposed to using the backtick as a line continuation character since it is so easily foiled by whitespace. I also changed $UserList from an array of strings to an array of objects, since you want objects later anyway. We capture the first and last name still, but as the User property on a [PSCustomObject].

$ADGroup = "SomeADGroup"
$UserList = Get-ADGroupMember -Identity $ADGroup -Recursive |
    Select SamAccountName |
    foreach-object {
        Get-ADUser -Identity $_.SamAccountName | 
            Select @{n='User';e={$_.Surname + ", " + $_.GivenName}}
    }

So now we have an array of objects with one property. Now we loop through that, do the search for computers, and capture the machines as an array of strings. That's just slightly modified code from what you had already.

foreach ($User in $UserList)
    {         
        $MachineList = Get-ADComputer -Filter {Description -like "*$($User.User)*"} | Select -ExpandProperty Name

Now is where we change things up a little. For each machine that we found we need to add a property to that object we already have. Since we don't know how many that is we can just use a For loop and number the properties accordingly.

        for($i=0;$i -le $MachineList.Count;$i++)
            {
                Add-Member -InputObject $User -NotePropertyName "Machine$($i+1)" -NotePropertyValue $MachineList[$i]
            }
    }

So now $UserList is an array of objects, each of which has the User property, and however many Machine# properties as is appropriate to that user. The problem is that PowerShell outputs arrays of objects based on the first object in the array. So in your example:

User                 Machine   Machine2           
----                 -------   -------    
White, Scott         W107175           
Jones, Henry         W107195           
Flinstone, Fred      L109531   W108812

The Machine2 column will never be displayed because that property doesn't exist on the first record for Scott White. So to get around this we need to find out all of the possible properties for any given object in the array, and add any missing ones to the first record. This is easily done using the hidden PSObject property that each object in PowerShell has.

First we get a list of all possible property names:

$AllProps = $UserList | ForEach{$_.PSObject.Properties.Name} | Select -Unique

Then we filter out any properties already on the first record, and anything left gets added to the record as a new property using the Add-Member cmdlet again.

$AllProps | 
    Where{ $_ -notin $UserList[0].psobject.properties.name } | 
    ForEach{ Add-Member -InputObject $UserList[0] -NotePropertyName $_ -NotePropertyValue $null }

Then there's nothing to do but output the results, which are already stored in $UserList since we made our list of users, and simply updated that existing list with each user's machines, rather than making a list of users, and then a new list of users plus machines.

$UserList |  Format-Table

Upvotes: 3

Related Questions