Weekend Scripter: Use PowerShell for Conditional User Profile Removal

Doctor Scripto

Summary: Guest blogger, Bob Stevens, talks about how to use Windows PowerShell to perform conditional user profile removal.

Microsoft Scripting Guy, Ed Wilson, is here. Today we are lucky to have guest blogger, Bob Stevens, return for another exciting and useful blog post. Be sure you check out Bob’s other Hey, Scripting Guy! Blog posts. Bob is a member of the Twin Cities PowerShell User Group. Here is his contact information:

Blog: Help! I’m Stuck in My PowerShell!
Twitter: @B_stevens6
LinkedIn: Robert Stevens

Note   This is the second post in a two part series about using Windows PowerShell to work with user profiles. You should read yesterday’s post, Use PowerShell to Generate a Recent Profile Report, prior to reading today’s.

And now, here’s Bob…

In my last post I discussed how to use Windows PowerShell to return a list of profiles that have not been accessed in the last 30 days by using the metadata of the ntuser.dat file:

Image of command output

In response to input from Ed Wilson, I decided to modify this script heavily to not only return the opposite (profiles that have not been accessed in the past 30 days), but also to provide the user the option of removing such profiles. Although this seems rather straight forward, a number of issues do exist. Primary among them is the built-in profiles, such as Administrator. As you may well know, removing the local Administrator profile can cause some issues, especially if the Group Policy Objects (GPOs) for your organization alter this profile.

Before we address that, we need to alter the script from my previous blog post. In that post, we used the ntuser.dat file to define the last time the profile was used. This is not going to be necessary now because we are looking for the last time the profile has been used at all, not the last time the user’s registry hive was loaded. Because of this, we can confidantly reduce the complexity of the first part of our script by removing the wildcard character.

Get-Childitem -force “C:\Documents and Settings”

Here you see the Get-Childitem cmdlet followed by the -force switch. (This is followed by the profile path for the version 5 Windows systems, whichI will address later in this post). This returns the content of our defined directory, including hidden and system objects, C:\Documents and Settings or C:\Users.

Now we need to pipe the output from that command to a conditional statement that is very similar to the previous script. The only alterations we are going to make are with the inequality statement and the value. These we change from less than (-le) to greater than (-ge), and we change the number of days from 31 to 30.

Get-ChildItem -force “C:\Documents and Settings” | Where {

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

}

You can stop here if you simply want the list of profiles that were not accessed in the last 30 days. If you want to move on, we need to alter that first line to pipe it to an array. An array is similar to a variable; however, with an array you are defining multiple values within a single variable. This is done by encapsulating the values in parentheses and preceding the left parentheses with an ampersand (@).

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

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

} )

We need to use a pair of IF statements to avoid having to change the script on a case-by-case basis. The structure of these commands are explained in detail in my previous post, Use PowerShell to Generate a Recent Profile Report, so feel free to backtrack if you want to furthur understand them.

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

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

Inside the braces that follow the first IF statement’s condition, we need to nest our variable declaration $over30dayprofiles.

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

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

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

} )

}

We now nest the same variable in the second IF statement’s declaration with one minor variation. We replace “Documents and Settings” with “Users.”

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

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

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

} )

}

Roll them together to have a complete picture of our array declaration:

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

} )

}

Combined, our pair of IF statements declare our variable, $over30dayprofiles, as all items with a last write time of greater than 30 days. To manipulate this data into the correct format, we use the Set-Content cmdlet. Set-content will replace the context of a text file, and it natively does so without all of the metadata, which is exactly what we need. (If you want to append data to the text file, use Add-Content.) With that, we have the following command:

Set-Content “.\over30dayprofiles.txt” $over30dayprofiles

Here is the result:

Image of command output

Notice how Administrator, LocalService and NetworkService are present. We need those to remain, so we are going to actually remove them, and set the content to another file. To do this, we first need to get the content of this file with the Get-Content cmdlet:

Get-Content “.\over30dayprofiles.txt”

Now we use the pipe operator (|) to funnel the content of over30dayprofiles.txt  into the ForEach-Object cmdlet.

Get-Content “.\over30dayprofiles.txt” | ForEach-Object

ForEach-Object is simply stating, “For each object, do this.” The “do this” must be encapsulated in braces because it is followed by actual script.

Get-Content “.\over30dayprofiles.txt” | ForEach-Object{}

Inside the braces, we replace Administrator, LocalService, and NetworkService (with any other profiles you want to exempt) with  a null value by using two single quotation marks with nothing between them (‘’). This is done with the -replace operator.

Note   Remember that you need to have the placeholder, $_, on the first line because it is necessary for the operation of ForEach-Object. The placeholder, $_, simply tells Windows PowerShell to address each individual object within the data provided.

$_ -replace ‘Administrator’, ” `
  -replace ‘LocalService’, ” `
  -replace ‘NetworkService’, ” `

It is important to clarify the structure of the -replace operator. -Replace must be followed by the item to be replaced, which is encapsulated in single quotation marks() and followed by a comma (,). The comma is followed by what is being replaced, and it is also encapsulated in single quotation marks.

Note   Because we are replacing these items with nothing, the single quotation marks look like double quotation marks. Be careful to make this distinction because this code requires single quotation marks.

Finally, we use the accent (`) character to signify “next line.” As you can see, only the first line requires the placeholder.

So far, this set of script looks like this:

Get-Content “.\over30dayprofiles.txt” | ForEach-Object {

$_ -replace ‘Administrator’, ” `
  -replace ‘LocalService’, ” `
  -replace ‘NetworkService’, ” `

  }

Now that we have removed those important profiles, we need to remove any empty lines (also known as whitespace) that may be in our file. To do this, we do not look for actual white space because that is tantamount to looking for nothing. Rather, we look for lines that have an alpha character in them. This is accomplished by piping the results of the previous code to the Select-String cmdlet and a regular expression. We start with the pipe operator and the Select String cmdlet:

| Select-String

Select-String requires two things to operate properly: the path and the pattern. Thankfully, the path (location of the data) is taken care of by the pipe operator. We add the -pattern definition followed by the Word regular expression (\w). The Word regular expression defines an alphabetical character. The following script states, “Select any alpha character.”

Select-String -pattern “\w”

Now we pipe the results of that to yet another Foreach-Object. This time we are looking for the lines that the previous command returned. Attach .line to the $_ in all ForEach-Object commands and encapsulate that in braces:

| ForEach-Object { $_.line }

Now that we have all the commands necessary to format our data, we are going to roll them together:

Get-Content “.\over30dayprofiles.txt” | ForEach-Object {

$_ -replace ‘Administrator’, ” `
   -replace ‘LocalService’, ” `
   -replace ‘NetworkService’, ” `

} | Select-String -Pattern “\w” | ForEach-Object { $_.line }

Now, we redefine the variable $over30dayprofiles. We do this by encapsulating the entire previous script in paretheses, and preceding that with the “at” sign (@) to create an array:

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

$_ -replace ‘Administrator’, ” `
   -replace ‘LocalService’, ” `
   -replace ‘NetworkService’, ” `

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

If you want to check your work, type $over30dayprofiles. It should return a list of profiles that have not been accessed within the past 30 days, with the exception of our defined profiles.  

Now that we have our input, we can proceed with our script. To sequentially delete profiles, we need to set up a Do-Until loop. This kind of loop simply states, “Do X, until Y is True.” For our loop we need to define two variables. The first is the counter, $i, and the second is the Until condition (Y).

Note   A counter is a value in programming that is used to faciliate an end to a programming loop. Without it, the program would constantly “Do” something until the computer is shut off or the process is ended somehow.

I always start by defining $i as 0:

$i = 0

The Count variable is merely the number of profiles in our $over30dayprofiles array. To define the number of items in an array, we append the count array property, .count, to $over30dayprofiles, and we use that to define the $count variable:

$count = $over30dayprofiles.count

We also need to make sure that our path actually contains profiles by changing our logical location on the hard drive to C:\Documents and Settings. We do this with the Set-Location cmdlet:

Set-Location ‘C:\Documents and Settings’

Now we start our Do-Until loop. As explained earlier, there are two parts to this loop: the Do section and the Until section. Because Do is followed by an action (and functional code, not simply values), it is followed by braces. Until is followed by a condition, so we use parentheses instead of braces:

Do{}

Until()

To remove the subsequent directories, we are going to use the Remove-Item cmdlet. When we remove these profiles, it is important  that the profile and all child folders and items (regardless of special properties such as Read-only, Hidden, or System) are removed. We do this by using the -force and the -recurse switches. The -force switch ignores special properties, and the -recurse switch removes all of the child directories and their content.

Remove-Item -force -recurse

By itself, all this command does is indiscriminately delete the following item and all of its content, regardless of properties. What it does not say is actually what to delete. That is where we use the $over30dayprofiles array. As displayed earlier in this post, this array holds the names of the profiles that have not been accessed within the past 30 days, minus those that we specified.

Remove-Item -force -recurse $over30dayprofiles

We want to do this sequentially, so we are using the counter that we defined earlier to identify the element in the array that we want to remove. Remember that with an array, you are defining multiple values within a single variable. The values in the array are called elements, and each element is numbered (beginning with 0) to differentiate one value from the next.

Specifing an element within an array is done by appending brackets to the end of an array variable, and putting the element number within those brackets. You can use a variable in place of an actual number. This is where we are using our counter, which conveniently starts at 0 and increments by 1 every time the Do statement loops.

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

We now add the counter. Simply appending two addition symbols (++) to the $i variable will increment it by one:

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

Now that we have completed our statement, we need to nest it in the braces following Do:

Do{

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

}

Until()

Without defining the Until condition, this loop will continue until it starts generating errors (when it runs out of elements in the array to perform the Do action against). The following error message tells you where the issue is: “Missing While or Until in Do loop.” Always read your error messages.

Image of message

Right now our Until statement looks like this:

Until()

Our condition is going to say Do the above Until the counter is greater than the number of items in $over30dayprofiles ($count).” Here is where Until, our counter, and $count come together. First we express the counter:

Until($i)

Then we use the greater than (-ge) inequality:

Until($i -ge)

And finally, we insert the $count directly after the greater than operator for the complete conditon that reads, “Until our counter is greater than the count.”

Until($i -ge $count)

Combined, the script looks like this:

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 beauty of technology is its fluid nature. This script, much like the script in Use PowerShell to Generate a Recent Profile Report will need to be altered as time goes by, new versions of operating systems are released, and old versions lose support. I stand by the following statement: “There is always another better way to do things.” That is especially true with Windows PowerShell. Thank you for reading and, as always, please post your comments in the following Leave a Comment text box!

~Bob

Awesome job, Bob. These are great posts. Thank you for taking time to share with the Windows PowerShell community. Join me tomorrow when I will have a guest blog written by Brian Wilhite. Have a look at his previous Hey, Scripting Guy! Blog posts.

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

1 comment

Discussion is closed. Login to edit/delete existing comments.

  • fyrman 24 0

    we try to do something like this at work but find the sccm managed windows defender scans change the last accessed date for the users ntuser.dat

Feedback usabilla icon