Daniel
Daniel

Reputation: 590

Export-Csv Producing Empty Output File

I've got a script to audit local user accounts. Everything works, except exporting to CSV. The output file is blank, except for the column headers:

Computer Name Enabled PasswordChangeableDate PasswordExpires UserMayChangePassword PasswordRequired PasswordLastSet LastLogon

What am I doing wrong here? The export portion is towards the bottom.

$Result = New-Object System.Collections.Generic.List[System.Object] #store local user data
$Errors = New-Object System.Collections.Generic.List[System.Object] #store any devices that could not be reached

$devices = Get-Content "list.txt" #read in all devices to check

#for progress bar - store current # and total
$length = $devices.Count
$i = 1

$displayOutputReport = $true

foreach ($device in $devices){
    Write-Progress -Activity "Retrieving data from $device ($i of $length)" -percentComplete  ($i++*100/$length) -status Running

    if (Test-Connection -ComputerName $device -Count 1 -Quiet){
        #Get account properties and add to list

         $temp = Invoke-Command -ComputerName $device -ScriptBlock {
             Get-LocalUser |  Select-Object -Property @{N="Computer"; E={$env:COMPUTERNAME}}, Name, Enabled, PasswordChangeableDate, PasswordExpires, UserMayChangePassword, PasswordRequired, PasswordLastSet, LastLogon
         }
         $Result.Add($temp)

    }else{
        $errors.Add($device)
        Write-Host "Cannot connect to $device, skipping." -ForegroundColor Red
    }
}

#display results
if ($displayOutputReport){
    if ($Result.Count -gt 0){
        $Result | Format-Table Computer,Name, Enabled, PasswordChangeableDate, PasswordExpires, UserMayChangePassword, PasswordRequired, PasswordLastSet, LastLogon -AutoSize
    }

    Write-Host ""

    if ($Errors.Count -gt 0){
        Write-Host "Errors:" -ForegroundColor Red
        $Errors
        Write-Host ""
    }
}

#export to CSV
$ScriptPath = $pwd.Path
$ReportDate = (Get-Date).ToString('MM-dd-yyyy hh-mm-ss tt')

if($Result.Count -gt 0){
    $reportFile = $ScriptPath + "\LocalUserAudit $ReportDate.csv"
    $Result | Select-Object Computer, Name, Enabled, PasswordChangeableDate, PasswordExpires, UserMayChangePassword, PasswordRequired, PasswordLastSet, LastLogon | Export-Csv $reportFile -NotypeInformation
    Write-Host "Report saved to $reportFile"
}

if ($Errors.Count -gt 0){
    $errorFile = $ScriptPath + "\LocalUserAudit ERRORS $ReportDate.txt"
    $Errors | Out-File $errorFile
    Write-Host "Errors saved to $errorFile"
}

Upvotes: 1

Views: 1180

Answers (2)

mklement0
mklement0

Reputation: 440431

Change

$Result.Add($temp)

to

$Result.AddRange(@($temp))

to ensure that $Result ends up as a flat collection of objects.


As for what you tried:

$temp is likely to be an array of objects, as Get-LocalUser likely reports multiple users.

I you pass an array to List`1.Add(), it is added as a whole to the list, which is not your intent.

Therefore you ended up with a list of arrays rather than a flat list of individual objects. And because an array doesn't have the properties you were trying to select (Name, Enabled, ...) the column values for those properties in the output CSV ended up empty.

By contrast, List`1.AddRange() accepts an enumerable of objects (which an array implicitly is), and adds each enumerated object directly to the list.

The use of @(...) around $temp guards against the case where Get-LocalUser happens to return just one object - @(), the array-subexpression operator, then ensures that it is treated as an array.


A better alternative:

If you use the foreach loop as an expression, you can let PowerShell do the work of collecting all output objects in flat array, thanks to the pipeline's automatic enumeration of collections:

# Assign directly to $Result
$Result = foreach ($device in $devices){
    Write-Progress -Activity "Retrieving data from $device ($i of $length)" -percentComplete  ($i++*100/$length) -status Running

    if (Test-Connection -ComputerName $device -Count 1 -Quiet){
        #Get account properties and add to list

         # Simply pass the output of the Invoke-Command call through.
         Invoke-Command -ComputerName $device -ScriptBlock {
             Get-LocalUser |  Select-Object -Property @{N="Computer"; E={$env:COMPUTERNAME}}, Name, Enabled, PasswordChangeableDate, PasswordExpires, UserMayChangePassword, PasswordRequired, PasswordLastSet, LastLogon
         }

    }else{
        $errors.Add($device)
        Write-Host "Cannot connect to $device, skipping." -ForegroundColor Red
    }
}

This is not only more convenient, but also performs better.

Note: If you need to ensure that $Result is always an array, use [array] $Result = foreach ...

Upvotes: 1

Daniel
Daniel

Reputation: 590

Update: I've got it working, but can someone explain why this works instead? I'd prefer to avoid concatenating arrays when this will be run thousands of times.

I changed

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

to

$Result = @()

And

$temp = Invoke-Command -ComputerName $device -ScriptBlock {
         Get-LocalUser |  Select-Object -Property @{N="Computer"; E={$env:COMPUTERNAME}}, Name, Enabled, PasswordChangeableDate, PasswordExpires, UserMayChangePassword, PasswordRequired, PasswordLastSet, LastLogon
     }
     $Result.Add($temp)

To

 $Result += Invoke-Command -ComputerName $device -ScriptBlock {
         Get-LocalUser |  Select-Object -Property @{N="Computer"; E={$env:COMPUTERNAME}}, Name, Enabled, PasswordChangeableDate, PasswordExpires, UserMayChangePassword, PasswordRequired, PasswordLastSet, LastLogon
    }

Upvotes: 0

Related Questions