Search code examples
powershellforeachuser-profileshortcut-fileexport-csv

Shortcut List script


I've created a script that will read a list of usernames in a file named Users.csv and then search a network share for their user profile. It then reports on the shortcut target paths of said user profile and exports that to another .csv.

It works great when I use it per user but when I get it to report the data to the .csv file I can only seem to get it to report on the last user in my Users.csv and not each one. I was thinking it is because the export part of the script overwrites the report.csv for each user it runs and I need it to create a unique report.csv for each username. Anyone have any ideas?

$Users = (Get-Content C:\temp\Users.csv) -notmatch '^\s*$'

foreach ($User in $Users) {
    $Shortcuts = Get-ChildItem -Recurse \\UNCPATHTOUSERPROFILES\$User -Include *.lnk
    $Shell = New-Object -ComObject WScript.Shell
    $data = foreach ($Shortcut in $Shortcuts) {
        [PSCustomObject]@{
            ShortcutName = $Shortcut.Name;
                  Target = $Shell.CreateShortcut($Shortcut).targetpath
                    User = $User
        }        
    }
    [Runtime.InteropServices.Marshal]::ReleaseComObject($Shell) | Out-Null
}
foreach ($User in $Users) {
    $data | Export-Csv c:\temp\report.csv -NoTypeInformation 
} 

Solution

  • You should combine your two outer foreach loops, not only since they're iterating the same collection, but because the second loop is trying to use a variable, $data, created in the first loop. Further, since Export-Csv is being called in a loop, you will need to pass the -Append parameter to prevent the output file from being overwritten each time.

    $Users = (Get-Content C:\temp\Users.csv) -notmatch '^\s*$'
    
    foreach ($User in $Users) {
        $Shortcuts = Get-ChildItem -Recurse \\UNCPATHTOUSERPROFILES\$User -Include *.lnk
        $Shell = New-Object -ComObject WScript.Shell
        $data = foreach ($Shortcut in $Shortcuts) {
            [PSCustomObject]@{
                ShortcutName = $Shortcut.Name;
                      Target = $Shell.CreateShortcut($Shortcut).targetpath
                        User = $User
            }        
        }
        $data | Export-Csv c:\temp\report.csv -Append -NoTypeInformation
    
        [Runtime.InteropServices.Marshal]::ReleaseComObject($Shell) | Out-Null
    }
    

    You could also eliminate the $Shortcuts and $data variables in favor of using the pipeline...

    $Users = (Get-Content C:\temp\Users.csv) -notmatch '^\s*$'
    
    foreach ($User in $Users) {
        $Shell = New-Object -ComObject WScript.Shell
    
        Get-ChildItem -Recurse \\UNCPATHTOUSERPROFILES\$User -Include *.lnk `
            | ForEach-Object -Process {
                [PSCustomObject]@{
                    ShortcutName = $_.Name;
                    Target = $Shell.CreateShortcut($_).targetpath
                    User = $User
                }
            } `
            | Export-Csv c:\temp\report.csv -Append -NoTypeInformation
    
        [Runtime.InteropServices.Marshal]::ReleaseComObject($Shell) | Out-Null
    }
    

    ...but note that -Append is still required. Finally, you could rewrite the whole thing using the pipeline...

    $Shell = New-Object -ComObject WScript.Shell
    try
    {
        Get-Content C:\temp\Users.csv `
            | Where-Object { $_ -notmatch '^\s*$' } -PipelineVariable 'User' `
            | ForEach-Object -Process { "\\UNCPATHTOUSERPROFILES\$User" } `
            | Get-ChildItem -Recurse -Include *.lnk `
            | ForEach-Object -Process {
                [PSCustomObject]@{
                    ShortcutName = $_.Name;
                    Target       = $Shell.CreateShortcut($_).targetpath
                    User         = $User
                } `
            } `
            | Export-Csv c:\temp\report.csv -NoTypeInformation
    }
    finally
    {
        [Runtime.InteropServices.Marshal]::ReleaseComObject($Shell) | Out-Null
    }
    

    ...so that report.csv is only opened for writing once, with no -Append needed. Note that I am creating a single WScript.Shell instance and using try/finally to ensure it gets released.

    In the event a user in Users.csv has no directory under \\UNCPATHTOUSERPROFILES, any of the above solutions and the code in the question will throw an error when Get-ChildItem tries to enumerate that directory. You could fix that by checking that the directory exists first or passing -ErrorAction SilentlyContinue to Get-ChildItem, or you could enumerate \\UNCPATHTOUSERPROFILES and filter on those that occur in Users.csv...

    $Shell = New-Object -ComObject WScript.Shell
    try
    {
        # Performs faster filtering and eliminates duplicate user rows
        $UsersTable = Get-Content C:\temp\Users.csv `
            | Where-Object { $_ -notmatch '^\s*$' } `
            | Group-Object -AsHashTable
    
        # Get immediate child directories of \\UNCPATHTOUSERPROFILES
        Get-ChildItem -Path \\UNCPATHTOUSERPROFILES -Directory -PipelineVariable 'UserDirectory' `
            <# Filter for user directories specified in Users.csv #> `
            | Where-Object { $UsersTable.ContainsKey($UserDirectory.Name) } `
            <# Get *.lnk descendant files of the user directory #> `
            | Get-ChildItem -Recurse -Include *.lnk -File `
            | ForEach-Object -Process {
                [PSCustomObject]@{
                    ShortcutName = $_.Name;
                    Target       = $Shell.CreateShortcut($_).targetpath
                    User         = $UserDirectory.Name
                } `
            } `
            | Export-Csv c:\temp\report.csv -NoTypeInformation
    }
    finally
    {
        [Runtime.InteropServices.Marshal]::ReleaseComObject($Shell) | Out-Null
    }