How to remove remote or local user profiles using PowerShell
Last week we took a look at using Delprof2 to enumerate remote or local user profiles. If you aren’t familiar with that post, I would highly recommend you read it before this one to understand the background. We are essentially building a partner for the previous cmdlet to remove user profiles when needed.
Delprof2
We already covered the important aspects of DelProf2 in last’s week’s post, but since this is a free piece of software that I do not develop or contributor to, be sure to check out Helge Klein and the download page for Delprof2. They release this software for free and, best I can tell, keep it updated regularly.
Syntax
After I say we’ve already covered everything, I realized that we actually haven’t yet! To actually remove a profile using delprof2 we need to use the removal switch:
delprof2 /u
This will remove all profiles that meet the default criteria for delprof2. Though you can still use all of the other parameters that I mentioned in last week’s post.
Wrapping Delprof2: The sequel
Believe it or not, we are going to be doing something very similar to what we did last week. It will get a little more complicated because we’ll end up with 2 different parameter sets, but the parsing of the output is going to reuse some of the regex. This is because the output of delprof2 is going to be the same except for the output designating deletion.
Something to note, before we get too deep into this, is if these functions were a part of a module, I would write a separate private helper function to parse the output of delprof2 and reference that helper function inside both Get-InactiveUserProfiles
and Remove-InactiveUserProfiles
.
Profile deletion output
So we’ve already got the parsing of profile information from delprof2, we need to see what the output looks like when it actually deletes a profile:
DelProf2 by Helge Klein (http://helgeklein.com)
Listing inactive profiles on 'VHOST01'.
Including only directories matching: temp
Ignoring profile '\\VHOST01\C$\Users\Default' (reason: special profile)
Ignoring profile '\\VHOST01\C$\Users\Public' (reason: special profile)
Ignoring profile '\\VHOST01\C$\Users\Administrator' (reason: directory inclusion)
Ignoring profile '\\VHOST01\C$\Users\D' (reason: directory inclusion)
Ignoring profile '\\VHOST01\C$\Users\L' (reason: directory inclusion)
The following user profiles match the deletion criteria:
\\VHOST01\C$\Users\Temp
Deleting profile '\\VHOST01\C$\Users\Temp' ...
... done.
So those last two lines are what we are looking for in the output to know if a user profile deletion was attempted and then if it was successful. So instead of a foreach
loop, we’ll use a for
loop and reference that line and that line + 1:
for ($x=0; $x -lt $return.Count; $x++){
# Removed for brevity
elseif ($return[$x] -match "^Deleting profile \'(?<profilePath>\\\\([^\\]+\\)+(?<profile>[^\']+))\'") {
# Profile to delete
$firstMatch = $Matches
$deleted = $false
if ($return[$x+1] -match '\.\.\. done\.'){
$deleted = $true
}
[pscustomobject]@{
Ignored = $false
Reason = $null
ProfilePath = $firstMatch.ProfilePath
Profile = $firstMatch.Profile
ComputerName = $Name
Deleted = $deleted
}
$x++
}
}
Dealing with the pipeline
Since I want Remove-InactiveUserProfiles
to be able to accept pipeline input from Get-InactiveUserProfiles
and be able to run the command standalone, we’ll need 2 parameter sets. FromSelf
contains all the same parameters as Get-InactiveUserProfiles
and FromGet
contains a single parameter to encapsulate the properties of the output from Get-InactiveUserProfiles
:
[cmdletbinding(
DefaultParameterSetName = 'FromGet'
)]
param (
[Parameter(
ParameterSetName = 'FromSelf'
)]
[ValidateNotNullOrEmpty()]
[string]$Name = $env:COMPUTERNAME
,
[Parameter(
ParameterSetName = 'FromSelf'
)]
[string[]]$include # Supports ? and *
,
[Parameter(
ParameterSetName = 'FromSelf'
)]
[string[]]$exclude # Supports ? and *
,
[Parameter(
ParameterSetName = 'FromSelf'
)]
[int]$olderThan
,
[Parameter(
ParameterSetName = 'FromGet',
ValueFromPipeline = $true
)]
[PSObject]$ProfileObject
,
[string]$delprof2 = "c:\windows\system32\delprof2.exe"
)
With that, I’ve also added some logic to take different action based on the parameter set. For instance, if the parameter set is FromGet
, it will only output the deleted profiles, not all the ignored ones.
Shortcomings
One of the sacrifices I have to make to accept pipeline input from Get-InactiveUserProfiles
is that I have to declare the parameter as a generic PSOject
. It would be better to create an inactive user profile class and declare the input object as that class so that I could assume that the object would have certain properties. Again, if this were it’s own module I would likely do that.
The functioning function
This entire function ends up looking like:
Function Remove-InactiveUserProfiles {
[cmdletbinding(
DefaultParameterSetName = 'FromGet'
)]
param (
[Parameter(
ParameterSetName = 'FromSelf'
)]
[ValidateNotNullOrEmpty()]
[string]$Name = $env:COMPUTERNAME
,
[Parameter(
ParameterSetName = 'FromSelf'
)]
[string[]]$include # Supports ? and *
,
[Parameter(
ParameterSetName = 'FromSelf'
)]
[string[]]$exclude # Supports ? and *
,
[Parameter(
ParameterSetName = 'FromSelf'
)]
[int]$olderThan
,
[Parameter(
ParameterSetName = 'FromGet',
ValueFromPipeline = $true
)]
[PSObject]$ProfileObject
,
[string]$delprof2 = "c:\windows\system32\delprof2.exe"
)
Begin {
Write-Verbose $PSCmdlet.ParameterSetName
$switches = & {
"/u"
if ($include) {
foreach ($inc in $include) {
"/id:$inc"
}
}
if ($exclude) {
foreach ($exc in $exclude) {
"/ed:$exc"
}
}
if ($olderThan) {
"/d:$olderThan"
}
}
}
Process {
if ($PSCmdlet.ParameterSetName -eq 'FromSelf'){
if ($PSBoundParameters.ContainsKey('Name') -and ($Name -ne $env:COMPUTERNAME)) {
if (Test-Connection $Name -Count 1 -Quiet) {
$computer = "/c:$Name"
} else {
Throw "Cannot ping $Name"
}
}
Write-Verbose "CMD: $delprof2 $computer $switches"
$return = & $delprof2 $computer $switches
} elseif ($PSCmdlet.ParameterSetName -eq 'FromGet'){
if ( -not $profileObject.Ignored) {
$computer = $null
if ($null -ne $profileObject.Computer){
$computer = "/c:$($profileObject.Computer)"
}
Write-Verbose "CMD: $delprof2 $computer $switches /id:$($profileObject.Profile)"
$return = & $delprof2 $computer $switches "/id:$($profileObject.Profile)"
}
}
for ($x=0; $x -lt $return.Count; $x++){
if ($return[$x] -match "^Ignoring profile \'(?<profilePath>\\\\([^\\]+\\)+(?<profile>[^\']+))\' \(reason\: (?<reason>[^\)]+)\)") {
# Ignored profile
If($PSCmdlet.ParameterSetName -ne 'FromGet'){
[pscustomobject]@{
Ignored = $true
Reason = $Matches.reason
ProfilePath = $Matches.profilePath
Profile = $Matches.profile
ComputerName = $Name
Deleted = $false
}
}
} elseif ($return[$x] -match "^Deleting profile \'(?<profilePath>\\\\([^\\]+\\)+(?<profile>[^\']+))\'") {
# Profile to delete
$firstMatch = $Matches
$deleted = $false
if ($return[$x+1] -match '\.\.\. done\.'){
$deleted = $true
}
[pscustomobject]@{
Ignored = $false
Reason = $null
ProfilePath = $firstMatch.ProfilePath
Profile = $firstMatch.Profile
ComputerName = $Name
Deleted = $deleted
}
$x++
} elseif ($return[$x] -match "^Access denied to profile \'(?<profilePath>\\\\([^\\]+\\)+(?<profile>[^\']+))\'") {
# Access denied
if ($PSCmdlet.ParameterSetName -ne 'FromGet'){
[pscustomobject]@{
Ignored = $true
Reason = 'Access denied'
ProfilePath = $Matches.ProfilePath
Profile = $Matches.Profile
ComputerName = $Name
Deleted = $false
}
}
}
}
}
End{}
}
Like usual, you can also find this in my Github Utilities repo.
Usage
So now with this lovely addition, we can remove user profiles! You don’t have to use Get-InactiveUserProfiles
, but that is super handy for running tests to see what will be deleted.
Simple usages
To remove all local user profiles that meet the default criteria:
Remove-InactiveUserProfiles
Or we can go back to an example from last week and remove all the profiles that start with a certain letter:
Remove-InactiveUserProfiles -Include T* | Format-Table
This outputs:
Ignored Reason ProfilePath Profile ComputerName Deleted
------- ------ ----------- ------- ------------ -------
True special profile \\VHOST01\C$\Users\Default Default VHOST01 False
True special profile \\VHOST01\C$\Users\Public Public VHOST01 False
True directory inclusion \\VHOST01\C$\Users\Administrator Administrator VHOST01 False
True directory inclusion \\VHOST01\C$\Users\D D VHOST01 False
True directory inclusion \\VHOST01\C$\Users\L L VHOST01 False
False \\VHOST01\C$\Users\Temp Temp VHOST01 True
Advanced usage
And then we can, of course, pipe the output from Get-InactiveUserProfiles
to it as well:
Get-InactiveUserProfiles -include T* | Remove-InactiveUserProfiles
And that will give us some abbreviated output:
Ignored : False
Reason :
ProfilePath : \\VHOST01\C$\Users\Temp
Profile : Temp
ComputerName : VHOST01
Deleted : True
Though if we wanted to get a list of computers from a text file or Active Directory we’d either need to pipe in to Get-InactiveUserProfiles
first or use a Foreach-Object
loop:
Get-ADComputer -Filter {Name -like 'RDS*'} | Get-InactiveUserProfiles -OlderThan 7 | Remove-InactiveUserProfiles
# OR
Get-ADComputer -Filter {Name -like 'RDS*'} | Foreach-Object {Remove-InactiveUserProfiles -Name $_.Name -OlderThan 7}
And both of those are about the same length, though I personally prefer the pipeline usage.
Conclusion
I hope this has been an inciteful read for you! This is cmdlet that has served me well over the years, so I’m happy to share.
Be sure to check out my Utilities repo in Github, post an issue if you find a problem with anything there. I’ll definitely fix it!
Leave a Comment