Search code examples
powershellactive-directory

Powershell - Outputting, ADuser SamAccountName, UserPrincipalName, and ADcomputer Name same table


Total n00b to Powershell. I've decided to start learning Powershell since there are some standard processes I do at work that I would like to automate/simplify. I'm familiar with other scripting/programming languages such as Python and Bash but not very proficient. The process I'm trying to simplify is getting basic user information in AD (ADuser SamAccountName, UserPrincipalName, and ADcomputer Name) all in the same table. I am trying to get the computer name through the computers description that matches the users login name and output all information into a neat and tidy table.

At work we use TeamViewer to remote into our users systems. I prefer to use our users computer name to remote in versus their ID because I'm sick of explaining that TeamViewer is not MS Teams and where to find the ID. Anyway, to find the pc name I need to get the users login name in AD first then search for the computer via computer description. We have ~25 OUs all with ~100 users and computers in them. So manually going through each OU to find it is out of the question. It becomes very tedious when I need to do it 60 times a day while on the phone with users and listening to what issue they're having.

Our newer users have logins that are "firstlastname". Older employees have logins that all start the same but end in their initials "companyname-initials". I'm trying to write a script that, when ran prompts for the users real name regardless of login name, then have it spit out all associated information. It's okay if it outputs multiple users information that have similar names such as "Sarah" or "John". As long it can organize the information neatly and in the same lines my team and I will be happy. I've been trying this for ~month now and am failing to grasp some concepts of how certain cmdlets actually work and best practices.

Any input is appreciated, but please take it easy on me as I don't understand higher level concepts of Powershell quite yet and I know I write garbage code. I'm trying to get better.

I have tried so many different things I would need to write a 10 chapter book on it. However, the closest I have come to getting it is creating a new object. When the script runs it gets all the right information but places each piece of information under each object member but not separated. It's all mashed into one long string. Not sure how to explain it exactly. But this is the entire script so far. I'm assuming I need to use ForEach somewhere but not sure how I do that correctly. I think I don't need the object creation part left in so that is deleted but I put it here for all to see it what it looks like. Maybe an if statement isn't the way to go, I'm not sure.

$Name = Read-Host -Prompt " " 

$UserAccount = Get-ADUser -Filter "Name -like '*$Name*'" 

if($UserAccount.SamAccountName -like "companyname-initials*") {
    
    $UserCpu = Get-ADComputer -Filter "Description -like 'companyname-initials*'"
    

} else { 

    $UserCpu = Get-ADComputer -Filter "Description -like '*$Name*'" 

}

$Output = [pscustomobject]@{
    Email = $UserAccount.UserPrincipalName
    Login = $UserAccount.SamAccountName
    PCName = $UserCpu.Name
    }
$Output | Select-Object Email,Login,PCName

Please let me know if anything needs a better explanation.

Thanks!


Solution

  • When the script runs it gets all the right information but places each piece of information under each object member but not separated. It's all mashed into one long string.

    Member Access Enumeration

    Reading between the lines, I think you're inadvertently using a PowerShell feature called Member Access Enumeration.

    # set up some test data
    $users = @(
        [pscustomobject] @{ 
            "firstname" = "vic"
            "lastname"  = "reeves"
        },
        [pscustomobject] @{
            "firstname" = "bob"
            "lastname"  = "mortimer"
        }
    );
    
    # build the result object
    $output = [pscustomobject] @{
        "firstname" = $users.firstname
        "lastname"  = $users.lastname
        "pcname"    = "..."
    }
    
    # display the result
    $output | format-table
    

    This shows the following output:

    firstname  lastname           pcname
    ---------  --------           ------
    {vic, bob} {reeves, mortimer} ...
    

    Because $users is an array that contains multiple values, what $users.firstname is doing is using Member Access Enumeration to build an array of firstname properties of all the objects in the $users array and putting them into a single object in $output - the result is equivalent to @("vic, "bob"), and the same for $users.lastname which gives @("reeves", "mortimer").

    When these values are rendered onto the screen they get serialised as above.

    Workaround

    To fix it you need to iterate over the $users array and convert users one at a time:

    $output = @()
    foreach( $user in $users )
    {
        $output += [pscustomobject] @{
            "firstname" = $user.firstname;
            "lastname"  = $user.lastname
            "pcname"    = "..."
        }
    }
    
    # display the result
    $output | format-table
    

    which now gives this output:

    firstname lastname pcname
    --------- -------- ------
    vic       reeves   ...
    bob       mortimer ...
    

    In your case, the root cause is that $UserAccount = Get-ADUser -Filter "Name -like '*$Name*'" is returning multiple accounts, so you need to iterate over them:

    $Name = Read-Host -Prompt " " 
    
    # might contain multiple accounts
    $UserAccounts = Get-ADUser -Filter "Name -like '*$Name*'" 
    
    # loop over each account adding it to the $output array
    $output = @()
    foreach( $UserAccount in $UserAccounts )
    {
        if($UserAccount.SamAccountName -like "companyname-initials*") {
            $UserCpu = Get-ADComputer -Filter "Description -like 'companyname-initials*'"
        } else { 
            $UserCpu = Get-ADComputer -Filter "Description -like '*$Name*'" 
        }
    
        $Output += [pscustomobject]@{
            Email = $UserAccount.UserPrincipalName
            Login = $UserAccount.SamAccountName
            PCName = $UserCpu.Name
        }
    }
    
    # show the results
    $Output | Format-Table
    

    And you should now get one account per row...

    Note in the above for simplicity I've used something called the assignment by addition operator - the += in the line $Output += [pscustomobject]@{ ... } - in simple terms this appends an item to the $Output array, but it can have performance issues if you're working with very large datasets - it's fine for the sake of this answer and it'll work fine for small datasets, but if you see your code starting to run slowly for large sets of results that's the first place to look...

    Pipeline

    To avoid performance issues with the "assignment by addition" operator you can rewrite the example using the pipeline to collect together all the results for each user account:

    # loop over each account adding it to the $output array
    $output = $users | foreach-object {
        if($_.SamAccountName -like "companyname-initials*") {
            $UserCpu = Get-ADComputer -Filter "Description -like 'companyname-initials*'"
        } else { 
            $UserCpu = Get-ADComputer -Filter "Description -like '*$Name*'" 
        }
    
        # this implicitly returns the values to the pipeline to be
        # collected into the $output variable as an array
        [pscustomobject]@{
            Email = $_.UserPrincipalName
            Login = $_.SamAccountName
            PCName = $UserCpu.Name
        }
    }
    

    This should give the same results as the previous workaround, but won't suffer from the same performance issues...