Conditional User Profile Deletion Revisited

Conditional User Profile Deletion Revisited

  • Comments 8
  • Likes

Summary: Guest blogger, Bob Stevens, revises his script for conditional user profile removal.

Microsoft Scripting Guy, Ed Wilson, is here. I am happy to welcome back guest blogger, Bob Stevens. Take it away Bob…

Previously I released a blog post entitled Weekend Scripter: Use PowerShell for Conditional User Profile Removal. If you have not read this post, please do because it is necessary so that you’ll understand what I am talking about today.

It was pointed out to me that Windows Vista and later operating systems create temporary user directories instead of re-creating the directories when the user next signs in. To start at a reference point, here is the original script that I created:

IF ((Get-WmiObject Win32_OperatingSystem).version -like “5*”) {

$over30dayprofiles = @(Get-ChildItem -force "C:\Documents and Settings" | Where {

((Get-Date)-$_.lastwritetime).days -ge 30

} )

}

IF ((Get-WmiObject Win32_OperatingSystem).version -like “6*”) {

$over30dayprofiles = @(Get-ChildItem -force "C:\Users" | Where {

((Get-Date)-$_.lastwritetime).days -ge 30

} )

Set-Content ".\over30dayprofiles.txt" $over30dayprofiles

$over30dayprofiles = @(Get-Content ".\over30dayprofiles.txt" | Foreach-Object {

  $_ -replace 'Administrator', '' `
   -replace 'LocalService', '' `
   -replace 'NetworkService', ''

} | Select-String -pattern "\w" | ForEach-Object { $_.line })

$count = $over30dayprofiles.count

$i = 0

Set-Location 'C:\Documents and Settings'

Do {

 Remove-Item -force -recurse $over30dayprofiles[$i]

 $i++

}

Until($i -ge $count)

The first alteration that we need is to add a message to the user in the event that there are no profiles to remove. To do so, directly after we remove the desired accounts and white space from the variable $over30dayprofiles, we add the following:

If ($over30dayprofiles.count -eq 0){Write-Host " all profiles have been accessed in the last 30 days"}

If ($over30dayprofiles.count -gt 0){

Because of the brace that is located at the end of the second IF statement, we need to add a closing brace at the end of the script. This will run the command inside the script only when there are profiles that have not been accessed within the past 30 days.

Directly after that, we add the command to determine the SID of each profile that we are going to remove. First we need to define in a variable the number of profiles that we are going to remove:

$count = $over30dayprofiles.count

The Count property simply counts the items in the $over30dayprofiles array. Next we need to state that this is only to be run in any version of Windows 6 and later. I explained how to do this in another previous post: Weekend Scripter: Use PowerShell for Conditional User Profile Removal.

IF ((Get-WmiObject Win32_OperatingSystem).version -like “6*”) {}

Inside the braces, we are going to use the WMI object Win32_userprofile. To do this, we need to use the Get-WMIobject cmdlet to call Win32_userprofile:

Get-WMIobject win32_userprofile

Because we only want two properties (SID and Path), we are going to filter it by using the pipe operator (|) followed by the Select cmdlet:

Get-WMIobject win32_userprofile | select

The property types are the same for all profiles, so no quotations are needed. We simply need to separate them by a comma and a space:

Get-WMIobject win32_userprofile | Select SID, Localpath

We are going to manipulate this further, so we pipe it into the file named ProfileInfo.txt:

Get-WMIobject win32_userprofile | Select SID, Localpath | Out-File .\ProfileInfo.txt

The next portion of the script utilizes a Do-Until loop, so we set a counter at 0:

$i = 0

Now let’s create the item that we are going to be manipulating:

New-Item -path .\SID.txt -itemtype File

And finally, we can start the Do-Until loop:

Do{}

Until()

Inside the braces that follow the Do statement, we are going to pull the contents of ProfileInfo.txt, select any string that has the currently selected value from the $over30dayprofiles array, and split the line at the first whitespace that occurs. This is done by first retrieving the data, and then selecting the line:

Get-Content .\ProfileInfo.txt | Select-String -pattern $over30dayprofiles[$i] |

Now we split the line at the first white space by using a regular expression:

Foreach-Object { $_ -split ’\s+’ }

And we select the first half of the string:

ForEach-Object { $_ -split '\s+' | select -f 1}

If we append this command to the end of the content and set the output location, this is the result:

Get-Content .\ProfileInfo.txt | Select-String -pattern $over30dayprofiles[$i] | ForEach-Object { $_ -split '\s+' | select -f 1} | Add-Content .\SID.txt

Now we nest this into the Do portion of our Do-Until loop, and add the escape value. An escape value is a predetermined number that will end the loop when the counter meets the inequality presented by the Until or While statements.

Do {

Get-Content .\ProfileInfo.txt | Select-String -pattern $over30dayprofiles[$i] | ForEach-Object { $_ -split '\s+' | select -f 1} | Add-Content .\SID.txt

$i++}

Until($i -ge $count)

As you can see, the escape value is $i when our counter reaches a value greater than $count. $count is the number of elements in the $over30daysprofiles array.

IF ((Get-WmiObject Win32_OperatingSystem).version -like “6*”) {

Get-WMIobject win32_userprofile | select SID, Localpath | Out-File .\ProfileInfo.txt

$i = 0

New-Item -path .\SID.txt -itemtype File

DO {

get-content .\ProfileInfo.txt | Select-String -pattern $over30dayprofiles[$i] | ForEach-Object {

                        $_ -split '\s+' | select -f 1

            } | Add-Content .\SID.txt

$i++

}

Until($i -ge $count)

$SID = @(Get-Content .\SID.txt)

Now we are going to take the contents of the SID.txt file, and set them as the values for another array, $SID:

$SID = @(Get-Content .\SID.txt)

Because this script is to be run on any current computer, we need to add the following line so that we are working out of the Documents and Settings folder:

IF ((Get-WmiObject Win32_OperatingSystem).version -like “5*”) {Set-Location 'C:\Documents and Settings'}

For the sake of clarity, the next set of script will be explained in its entirety.

First we set our counter and Do-Until statement:

$i = 0

Do {}

Until()

Now we add the conditional IF statements:

$i = 0

Do {

            IF(){}

            IF(){}

}

Until()

Next we add our operating system identifiers by using the WMI object we have already been using:

$i = 0

Do {

            IF((Get-WmiObject Win32_OperatingSystem).version -like “5*”){}

            IF((Get-WmiObject Win32_OperatingSystem).version -like “6*”){}

}

Until()

Here is where the script changes depending on the operating system. For version 5 operating systems, we need to simply remove the profile folders because they will re-create if the user signs in again. It was suggested that I use the utility Delprof for this, but because there is no way to differentiate between administrator accounts and user accounts, we will go with this:

$i = 0

Do {

            IF((Get-WmiObject Win32_OperatingSystem).version -like “5*”){

Remove-Item -force -recurse $over30dayprofiles[$i]}
            IF((Get-WmiObject Win32_OperatingSystem).version -like “6*”){}

}

Until()

Now we move on to what actually changed. As expressed in the comments from the previous post, when you delete the profile folder from the Users folder in Windows Vista and later, the operating system will continually generate a temporary profile because the registry key located at:

“HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList”

So instead of removing the profile folder or doing some truly interesting stuff with the registry, we are going to use the win32_userprofile WMI object, this bit of script is courtesy of the user, JRV. Specifically, we are going to filter it for the SID in the $SID array that corresponds with the counter, and we are going to set this as the value of the $profile variable:

$profile = Get-WmiObject win32_userprofile -filter "SID='$($SID[$i])'"

Now that we have the $profile variable defined with the desired SID, we are going to delete the profile that it refers to. This is done with the Delete() property (this will NOT work without the empty parentheses).

$profile.delete()

Now we add these two lines together and nest them in our second IF statement:

$i = 0

Do {

IF((Get-WmiObject Win32_OperatingSystem).version -like “5*”){Remove-Item -force -recurse $over30dayprofiles[$i]}

IF((Get-WmiObject Win32_OperatingSystem).version -like “6*”){ $profile = Get-WmiObject win32_userprofile -filter "SID='$($SID[$i])'"

 

$profile.delete()}

}

Until()

We need to add the rest of the script to increment the counter:

Do {

IF((Get-WmiObject Win32_OperatingSystem).version -like “5*”){Remove-Item -force -recurse $over30dayprofiles[$i]}

IF((Get-WmiObject Win32_OperatingSystem).version -like “6*”){ $profile = Get-WmiObject win32_userprofile -filter "SID='$($SID[$i])'"

 

$profile.delete()}

 $i++

}

Until($i -ge $count)

And finally, we need to close the brace that we left open with If “($over30dayprofiles.count -gt 0){“ and clean up our temporary files:

Remove-Item .\over30dayprofiles.txt

Remove-Item .\ProfileInfo.txt

Remove-Item .\SID.txt

}

Following is the complete and working script. Although it will generate errors if there is an issue with the accounts being removed, it will function on all others. These errors are reported, and the script will continue to run to completion. I have used Bold formatting to highlight the sections that have been added. _____________________________________________________________

Set-Location $home\desktop

New-Item -path .\over30dayprofiles.txt -itemtype File

IF ((Get-WmiObject Win32_OperatingSystem).version -like “5*”) {

$over30dayprofiles = @(Get-ChildItem -force "C:\Documents and Settings" | Where {

((Get-Date)-$_.lastwritetime).days -ge 30

} )

}

IF ((Get-WmiObject Win32_OperatingSystem).version -like “6*”) {

$over30dayprofiles = @(Get-ChildItem -force "C:\Users" | Where {

((Get-Date)-$_.lastwritetime).days -ge 30

} )

}

Set-Content ".\over30dayprofiles.txt" $over30dayprofiles

 

$over30dayprofiles = @(Get-Content ".\over30dayprofiles.txt" | Foreach-Object {

 

  $_ -replace 'Administrator', '' `

  -replace 'LocalService', '' `

  -replace 'NetworkService', ''`

  -replace 'desktop.ini', ''`

  -replace 'temp', ''`

  -replace 'All Users', ''`

  -replace 'Default', ''`

  -replace 'Default User', ''`

  -replace 'Public', ''`

  -replace ' User', ''

 

} | Select-String -Pattern "\w" | ForEach-Object { $_.line })

If ($over30dayprofiles.count -eq 0){Write-Host "all profiles have been accessed in the last 30 days"}

If ($over30dayprofiles.count -gt 0){

$count = $over30dayprofiles.count

IF ((Get-WmiObject Win32_OperatingSystem).version -like “6*”) {

get-WMIobject win32_userprofile | select SID, Localpath | Out-File .\ProfileInfo.txt

$i = 0

New-Item -path .\SID.txt -itemtype File

DO {

Get-Content .\ProfileInfo.txt | Select-String -pattern $over30dayprofiles[$i] | ForEach-Object {

                        $_ -split '\s+' | select -f 1

            } | Add-Content .\SID.txt

$i++

}

 

Until($i -ge $count)

$SID = @(Get-Content .\SID.txt)}

IF ((Get-WmiObject Win32_OperatingSystem).version -like “5*”) {Set-Location 'C:\Documents and Settings'}

 

$i = 0

Do {

  IF((Get-WmiObject Win32_OperatingSystem).version -like “5*”){

   Remove-Item -force -recurse $over30dayprofiles[$i]

  }

 

  IF((Get-WmiObject Win32_OperatingSystem).version -like “6*”) {

   $profile = Get-WmiObject win32_userprofile -filter "SID='$($SID[$i])'"

   $profile.Delete()

  }

 $i++

}

Until($i -ge $count)

 

Remove-Item .\over30dayprofiles.txt

Remove-Item .\ProfileInfo.txt

Remove-Item .\SID.txt

}

The complete script is uploaded to the Script Center Repository: Over 30 Day Removal. Please download it from there, rather than attempting to copy from this web page. As with my previous posts, I welcome all critiques. It is my firm belief that anything can be improved by the collective knowledge of the community.

~Bob

Thank you, Bob, for your time and efforts in writing this blog to share with our readers.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy 

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • The following is likely to fail on many systems:

    Set-Location $home\desktop

    New-Item -path .\over30dayprofiles.txt -itemtype File

  • @JRV

    Why would a user profile location fail?

  • Maybe because many peoples desktop folder is not necessarily in that location?

    Regarding the script, would it have been better to set the removal limit via a parameter passed in when you run it rather than this fixed number of days. If you want a different limit you have to rewrite the whole script?

    Pete

  • @Bob - think about it.  You are assuming that $home always points to the profile.  Infact is does not inmost cases.  It can be set to point to the users documents or anywhere else.  I point mine in the PowerShell profile to the scripts folder.

    This:

    $env:USERPROFILE

    Points to the current users profile always.  However Desktop may be redirected and on all of my domains it is redirected to a network share.

    Here is where we always find the desktop:

    [environment]::GetFolderPath('desktop')

    I recommend not using the desktop as on many systems it is read-only for non-admins.  Use a temp location.

    $env:TEMP

    This always gets the users temp folder even if it has been redirected as I do on most Terminal Server systems to move the junk onto a spare drive and not allow it so clog the system drive.

  • Thank you for all of your input!

    @JRV,

    very good point on the desktop location.

    @PGomersall

    The script was written with 30 days in mind.  it would be rather easy to alter it to accept user input, but I specifically chose not to to keep the script simple.  That is, I would have to address negative, and absurdly large values before I put it into production.

    No matter how you look at it, it looks good until someone else looks at it.  That is how you get a stellar product.

    Thank You,

    Bob Stevens

  • @Bob

    Personally I look at the script this way in a production environment.  I just think "Did it meet the short term need and does it take some work off my desk?"

    If it did, sometimes (sometimes) it doesn't matter how you wrote it or it's limitations.    With the time saved, you can ALWAYS go back and improve upon it. :)

    Don't ya just LOVE PowerShell?

    Sean

  • @Sean - good point.

    We tend to forget that scripting for admins is a tool and not a discipline.  It is not intended to be formal programming.  It's an efficient utility that can be used ad-hoc as needed just like the old CMD.EXE shell.  Learning to efficiently use this shell in everyday administration is very valuable. The ability to further refine and extend a simple script is just icing on the cake.

  • Sorry I am a little new to powershell but, why not store the list of values in a array instead of a txt file?