Powershell Array Issue when one item found

80 views Asked by At

I have created a script to automate some AD User account modification tasks. The user is prompted to enter a partial last name which is used to look up any user account that matches the search criteria. I found my code has a mistake in my logic. It works fine if it finds multiple matches - it will list each with the appropriate info, and it works ine if there are 0 matches - it returns a "User Not Found" message. An issue only occurs if there is only 1 match found - in this case it returns an array of every letter of the users name and UserID. I suspect it how I constructed my array but don't know for sure. Any suggestions?

One Match (Issue):strong text enter image description here

Multiple Matches (Works Fine): enter image description here

No Matches (Works Fine): enter image description here

Where I suspect issue is: enter image description here

Code:

#prompt for last name (or at least portion of last name, then conver to a string with wild card for use in filter
$ticketNum = Read-Host "`nEnter the ServiceNow Ticket Number"
$lname = Read-Host "`nEnter at least the first three chars of users last name (you may enter full last name)"
$search = $lname + "*"

#Empty arrays
$ResultsArray=@("")
$ADUsersList=@("")

#add aduser properties to array, return error and exit if no user found
$ADUsersList += Get-ADUser -filter 'surname -like $search' -Properties * | select name,samaccountname,DistinguishedName,Enabled
if ($ADUsersList.count -le 1){
    Write-host "USER NOT FOUND"
    return
}

#populate a 2D array with NAM and with UserID
$ResultsArray = @(($ADUsersList.name),($ADUsersList.samaccountname),($ADUsersList.enabled))

Write-host "`t NAME `t`t`t USER ID `t`t ACCOUNT ENABLED"
#return list of found users and user ids. Preface with index number to use for selection menu
for($i=0;$i-le $ResultsArray[0].length-1;$i++){
    “[{0}] = {1} `t `t {2} `t `t {3}” -f $i,$ResultsArray[0][$i],$ResultsArray[1] [$i],$ResultsArray[2][$i] 
}

#Prompt 
$selection = (Read-Host "`nEnter number for user you want to modify") -as [uint32]

#Input validation - Only allow a valid index #.  If valid, write user selected to screen before proceeding 
if ($selection -notmatch '^[0-9]+$' -or $selection -gt $ResultsArray[0].length -or $selection -lt 0){
    Write-host "INVALID SELECTION!!!" -ForegroundColor Red
} 
else {
    Write-Host "`nYou selected user " -nonewline 
    Write-Host $ResultsArray[0][$selection] -nonewline -ForegroundColor Green
    Write-Host " with user ID a of " -nonewline 
    Write-Host $ResultsArray[1][$selection] -nonewline -ForegroundColor Green
    $proceed = Read-Host "`nProceed with disabling for Leave Of Absence? (y or n)"
    if($proceed -eq "y"){
        Write-Host "proceeding ..."
        $target = $ResultsArray[1][$selection]
        $aduser = Get-ADUser -Identity $target -Properties *
        $prepend = "DISABLED per HR for LOA ($ticketNum) - "
        $desc = $aduser.description
        $loaDesc = $prepend + $desc
        Set-ADUser -Identity $target -description $loaDesc
        Disable-ADAccount -Identity $target
    }
    else{
        Write-host "no changes will be made"
        break
    }
}
2

There are 2 answers

3
Mathias R. Jessen On BEST ANSWER

The problem is this statement right here:

#populate a 2D array with NAM and with UserID
$ResultsArray = @(($ADUsersList.name),($ADUsersList.samaccountname),($ADUsersList.enabled))

If $ADUsersList contains 1 object then $ResultsArray will no longer be a nested array - it'll be a flat array consisting of the three property values from that 1 user.

You can force the inner expressions to evaluate to arrays with the array subexpression operator @(...):

$ResultsArray = @(@($ADUsersList.name), @($ADUsersList.samaccountname), @($ADUsersList.enabled))

... but a better way would be to forego the $ResultsArray step completely - you already have an array of objects with the relevant properties attached stored in $ADUsersList:

# ...

#add aduser properties to array, return error and exit if no user found
$ADUsersList = @(Get-ADUser -filter 'surname -like $search' -Properties * | select name,samaccountname,DistinguishedName,Enabled)

if ($ADUsersList.count -le 1){
    Write-host "USER NOT FOUND"
    return
}

# skip the `$ResultsArray` step completely

Write-host "`t NAME `t`t`t USER ID `t`t ACCOUNT ENABLED"
#return list of found users and user ids. Preface with index number to use for selection menu
for ($i = 0; $i -le $ADUsersList.Count; $i++) {
    "[{0}] = {1} `t `t {2} `t `t {3}" -f $i,$ADUserlist[$i].Name,$ADUserlist[$i].SAMAccountName,$ADUserlist[$i].Enabled
}
0
mklement0 On

To provide some background information to Mathias' helpful answer (which is rightfully the accepted one):

Even though you've explicitly constructed $ADUsersList as an array (@(...)), extracting property values from the elements of the array via member-access enumeration (e.g. $ADUsersList.Name) is not guaranteed to return an array of property values, namely if the array happens to contain just one element.

That is, - perhaps surprisingly, see GitHub issue #6802 for a discussion - member-access enumeration applies pipeline-like logic to the property values it retrieves:

  • a single value is output as-is
  • two or more values are returned as a regular PowerShell array, i.e. of type [object[]]

Therefore, if the array $ADUsersList happens to have only ONE element:

  • $ADUsersList.Name returns a single [string] instance rather than a (single-element) array of strings,

  • Indexing into a [string] instance returns characters from that string; e.g., 'abc'[1] is b - this is what you saw.

Therefore, in order to ensure that a member-access enumeration returns an array of values, you must either:

  • either: wrap it in @(...), the array-subexpression operator, e.g. @($ADUsersList.Name)
  • or, preferably, cast it to [array], e.g. [array] $ADUsersList.Name - this is actually more efficient, because it doesn't involve enumeration and re-collection of the results in a new array if the result already is an array, which is what @(...) does.

A self-contained, simplified example:

# Create a single-element array.
$ADUsersList = @([pscustomobject] @{ Name = 'J.Doe' })

# Apply member-access enumeration:

# WITHOUT ensuring that that result is an array: 
#  `$ADUsersList.Name` is a single [string], 
#  and [0] extract its first *character*
# -> !! Outputs just 'J'.
$ADUsersList.Name[0]

# WITH ensuring that that result is an array.
#  `[array] $ADUsersList.Name` is a single-element array
#   and [0] extract its first *element*, i.e the contained [string].
# -> OK: 'J.Doe'
([array] $ADUsersList.Name)[0]