A Profile Scripting Challenge Solution
Because of the Christmas holiday this week, I'm sending this out a little early, and there will only be one issue this week. 🎁 I hope you have a wonderful holiday. 🎄
Last month, I left you with a PowerShell scripting challenge. I hope you took advantage of it because it was a great opportunity to learn more about PowerShell and how to use it to automate tasks. You also don't have to look for a PowerShell solution to meet a requirement; you can create your own.
In this case, the task was to create a PowerShell function to query the Win32_UserProfile class on a local or remote computer and get output like this for every profile.
Name : systemprofile
LastUseTime : 11/19/2024 1:55:44 PM
Path : C:\WINDOWS\system32\config\systemprofile
PathCreated : 4/1/2024 3:26:07 AM
PathModified : 6/25/2024 3:23:58 AM
Size : 165896534
Special : True
Loaded : True
PSComputerName : JEFFDESK
I also gave you bonus challenges in case you wanted to push yourself further.
- If the profile has no files show a size value of 0
- Allow the user to select a profile by name
- Use a type name for the output object so that you can write a custom format file
- Optimize the function for performance
I hope you did, but if you didn't, don't worry. I have a solution for you. As always, my solution is not the only one.
Win32_UserProfile
The first step in building a solution is to see what an instance of the Win32_UserProfile
class looks like. You could use Get-CimClass
.
PS C:\> Get-CimClass -ClassName Win32_UserProfile | Select-Object -ExpandProperty CimClassProperties | Select-Object Name,CimType,Flags
Name CimType Flags
---- ------- -----
AppDataRoaming Instance Property, ReadOnly, NullValue
Contacts Instance Property, ReadOnly, NullValue
Desktop Instance Property, ReadOnly, NullValue
Documents Instance Property, ReadOnly, NullValue
Downloads Instance Property, ReadOnly, NullValue
Favorites Instance Property, ReadOnly, NullValue
HealthStatus UInt8 Property, ReadOnly, NullValue
LastAttemptedProfileDownloadTime DateTime Property, ReadOnly, NullValue
LastAttemptedProfileUploadTime DateTime Property, ReadOnly, NullValue
LastBackgroundRegistryUploadTime DateTime Property, ReadOnly, NullValue
LastDownloadTime DateTime Property, ReadOnly, NullValue
LastUploadTime DateTime Property, ReadOnly, NullValue
LastUseTime DateTime Property, ReadOnly, NullValue
Links Instance Property, ReadOnly, NullValue
Loaded Boolean Property, ReadOnly, NullValue
LocalPath String Property, ReadOnly, NullValue
Music Instance Property, ReadOnly, NullValue
Pictures Instance Property, ReadOnly, NullValue
RefCount UInt32 Property, ReadOnly, NullValue
RoamingConfigured Boolean Property, ReadOnly, NullValue
RoamingPath String Property, ReadOnly, NullValue
RoamingPreference Boolean Property, NullValue
SavedGames Instance Property, ReadOnly, NullValue
Searches Instance Property, ReadOnly, NullValue
SID String Property, Key, ReadOnly, NullValue
Special Boolean Property, ReadOnly, NullValue
StartMenu Instance Property, ReadOnly, NullValue
Status UInt32 Property, ReadOnly, NullValue
Videos Instance Property, ReadOnly, NullValue
Since I told you what the result should look like, you can find some properties immediately like Special
, Loaded
, LastUseTime
, and LocalPath
. I included the flags so that I could identify a key property. This is a property that you can always filter on to get a unique instance of the class. In this case, it is the SID
property.
I mention this, because you will eventually need an instance of the class to see the property values. Instead of running Get-CimInstance
to get all instances of the class, see if you can filter to get a single, sample instance.
I'll use the native whoami
command to get my SID and pass that to Get-CimInstance
.
PS C:\> $me = whoami /user /fo csv | ConvertFrom-Csv
PS C:\> Get-CimInstance Win32_UserProfile -Filter "SID='$($me.sid)'"
AppDataRoaming : Win32_FolderRedirectionHealth
Contacts : Win32_FolderRedirectionHealth
Desktop : Win32_FolderRedirectionHealth
Documents : Win32_FolderRedirectionHealth
Downloads : Win32_FolderRedirectionHealth
Favorites : Win32_FolderRedirectionHealth
HealthStatus : 3
LastAttemptedProfileDownloadTime :
LastAttemptedProfileUploadTime :
LastBackgroundRegistryUploadTime :
LastDownloadTime :
LastUploadTime :
LastUseTime : 12/19/2024 1:09:25 PM
Links : Win32_FolderRedirectionHealth
Loaded : True
LocalPath : C:\Users\Jeff
Music : Win32_FolderRedirectionHealth
Pictures : Win32_FolderRedirectionHealth
RefCount :
RoamingConfigured : False
RoamingPath :
RoamingPreference :
SavedGames : Win32_FolderRedirectionHealth
Searches : Win32_FolderRedirectionHealth
SID : S-1-5-21-746680207-121505554-2675587396-1001
Special : False
StartMenu : Win32_FolderRedirectionHealth
Status : 0
Videos : Win32_FolderRedirectionHealth
PSComputerName :
The object doesn't show the profile name, but that can be inferred from the LocalPath
property. That is much easier than trying to resolve the SID. Given the properties from the sample, I can prototype getting most of them from the instance.
PS C:\> Get-CimInstance Win32_UserProfile -Filter "SID='$($me.sid)'" -computername $ENV:COMPUTERNAME | Select-Object -Property @{Name = 'Name'; Expression = { Split-Path $_.LocalPath -Leaf }},LastUseTime,Special,Loaded,PSComputername
Name : Jeff
LastUseTime : 12/19/2024 1:22:12 PM
Special : False
Loaded : True
PSComputerName : THINKX1-JH
That's a good start.
Getting Path Information
Since I have a Path
, I should be able to use Get-Item
which will give me the CreationTime
and LastWriteTime
. I can also get the Size
property from the Length
property. I can get an approximate size of the folder by measuring all of the files with Get-ChildItem
and Measure-Object
.
Here's my revised prototype code.
Get-CimInstance Win32_UserProfile -Filter "SID='$($me.sid)'" -computername $ENV:COMPUTERNAME |
Select-Object @{Name = 'Name'; Expression = { Split-Path $_.LocalPath -Leaf } },LastUseTime,
@{Name = 'Path'; Expression = { $_.LocalPath } },
@{Name = 'PathCreated'; Expression = { (Get-Item -LiteralPath $_.LocalPath).CreationTime } },
@{Name = 'PathModified'; Expression = { (Get-Item -LiteralPath $_.LocalPath).LastWriteTime } },
@{Name = 'FileSize'; Expression = { Get-ChildItem -LiteralPath $_.LocalPath -Recurse | Measure-Object -Property Length -Sum | Select-Object -ExpandProperty Sum } },
Special, Loaded, PSComputerName
This might take a moment to run depending on the profile size.
Name : Jeff
LastUseTime : 12/19/2024 1:26:19 PM
Path : C:\Users\Jeff
PathCreated : 5/17/2022 2:52:03 PM
PathModified : 12/17/2024 11:00:16 AM
FileSize : 25853257637
Special : False
Loaded : True
PSComputerName : THINKX1-JH
Note: When it comes to querying all profiles, the command will have to run in an elevated session.
I can re-run my command without the filter to get information from all profiles.

Creating a Function
You may be wondering why I didn't begin with a function? In most cases, a function is a wrapper around a few underlying PowerShell commands. The function makes the code easy to use and can add features like parameter. I like knowing that I have a core set of commands that works and produces the desired result. Then I can build the function around it. Otherwise, I might struggle trying to determine if a problem is with the PowerShell core code or the function code.
Here's my initial function that lets me filter on a profile name.
Function Get-UserProfileInfo {
[cmdletbinding()]
Param(
[Parameter(Position = 0, HelpMessage = 'The name of the profile to query')]
[ValidateNotNullOrEmpty()]
[string]$ProfileName
)
$splat = @{
classname = 'Win32_UserProfile'
}
If ($ProfileName) {
Write-Verbose "[$((Get-Date).TimeOfDay)] Filtering on profile: $ProfileName"
$splat.Add('Filter', "LocalPath LIKE '%$ProfileName'")
}
[object[]]$data = Get-CimInstance @splat
Write-Verbose "[$((Get-Date).TimeOfDay)] Found $($data.count) user profile item(s)"
foreach ($item in $data) {
Write-Verbose "[$((Get-Date).TimeOfDay)] Processing $($item.LocalPath)"
Try {
$files = Get-ChildItem -LiteralPath $item.LocalPath -Recurse -File -ErrorAction Stop
$FSize = If ($Files) {
Write-Verbose "[$((Get-Date).TimeOfDay)] Measuring $($files.count) files"
($files | Measure-Object -Property Length -Sum -ea stop).Sum
}
else {
0
}
}
Catch {
$FSize = 0
}
$itemPath = (Get-Item -LiteralPath $item.LocalPath)
#create the custom output
[PSCustomObject]@{
PSTypeName = 'UserProfileInfo'
Name = Split-Path $item.LocalPath -Leaf
LastUseTime = $item.LastUseTime
Path = $item.LocalPath
PathCreated = $itemPath.CreationTime
PathModified = $itemPath.LastWriteTime
FileSize = $FSize
Special = $item.Special
Loaded = $item.Loaded
Computername = $ENV:Computername
}
} #foreach Item
}
I'm filtering using the WMI LIKE
operator which lets me use wildcards.
"LocalPath LIKE '%$ProfileName'"
Note that the wildcard character is %
and not *
. The LIKE
operator is case-insensitive. I make it upper-case so that it stands out. This function works locally and meets most of the requirements.


Working Remotely
The Get-CimInstance
cmdlet can work remotely and will get most of the information. But querying the disk space for the profile needs to happen on the remote computer. I could write code using Get-CimInstance
to use a combination of directory and file classes, but that's going to be a lot of work. I already have Get-Item
and Measure-Object
commands that work. All I need to do is invoke them in a remote session. I can do this using Invoke-Command
.
Function Get-UserProfileInfo {
[cmdletbinding(DefaultParameterSetName = "computer")]
Param(
[Parameter(Position = 0, HelpMessage = 'The name of the profile to query')]
[ValidateNotNullOrEmpty()]
[string]$ProfileName,
[Parameter(ParameterSetName = "computer",HelpMessage = 'The name of a computer name to query')]
[ValidateNotNullOrEmpty()]
[string]$ComputerName = $env:computername,
[Parameter(ParameterSetName = "computer",HelpMessage = 'Enter an alternate credential for the computer')]
[ValidateNotNullOrEmpty()]
[PSCredential]$Credential,
[Parameter(ParameterSetName = "session",HelpMessage = 'Enter a PSSession object')]
[ValidateNotNullOrEmpty()]
[System.Management.Automation.Runspaces.PSSession]$Session
)
Write-Verbose "[$((Get-Date).TimeOfDay)] Starting $($MyInvocation.MyCommand.Name)"
#define a scriptblock that will be run remotely
$sb = {
Param($ProfileName)
Write-Host "[$((Get-Date).TimeOfDay) $($env:COMPUTERNAME)] Starting remote code execution" -ForegroundColor Cyan
$splat = @{
classname = 'Win32_UserProfile'
ComputerName = $ENV:Computername
}
If ($ProfileName) {
Write-Host "[$((Get-Date).TimeOfDay) $($env:COMPUTERNAME)] Filtering on profile: $ProfileName" -ForegroundColor Cyan
$splat.Add('Filter', "LocalPath LIKE '%$ProfileName'")
}
[object[]]$data = Get-CimInstance @splat
Write-Host "[$((Get-Date).TimeOfDay) $($env:COMPUTERNAME)] Found $($data.count) user profile item(s)" -ForegroundColor Cyan
foreach ($item in $data) {
Write-Host "[$((Get-Date).TimeOfDay) $($env:COMPUTERNAME)] Processing $($item.LocalPath)" -ForegroundColor Cyan
Try {
$files = Get-ChildItem -LiteralPath $item.LocalPath -Recurse -File -ErrorAction Stop
$FSize = If ($Files) {
Write-Host "[$((Get-Date).TimeOfDay) $($env:COMPUTERNAME)] Measuring $($files.count) files" -ForegroundColor Cyan
($files | Measure-Object -Property Length -Sum -ea stop).Sum
}
else {
0
}
}
Catch {
$FSize = 0
}
$itemPath = (Get-Item -LiteralPath $item.LocalPath)
#the output is a hashtable
[ordered]@{
Name = Split-Path $item.LocalPath -Leaf
LastUseTime = $item.LastUseTime
Path = $item.LocalPath
PathCreated = $itemPath.CreationTime
PathModified = $itemPath.LastWriteTime
FileSize = $FSize
Special = $item.Special
Loaded = $item.Loaded
Computername = $ENV:Computername
}
}
}
Try {
#parameters to splat to Invoke-Command
$icmSplat = @{
ScriptBlock = $sb
ErrorAction = 'Stop'
HideComputerName = $true
}
if ($ProfileName) {
$icmSplat.Add('ArgumentList', $ProfileName)
}
Write-Verbose "[$((Get-Date).TimeOfDay)] Detected parameter set $($PSCmdlet.ParameterSetName)"
Switch ($PSCmdlet.ParameterSetName) {
'computer' {
Write-Verbose "[$((Get-Date).TimeOfDay)] Using ComputerName $($ComputerName.toUpper())"
$icmSplat.Add('ComputerName', $ComputerName)
if ($Credential) {
Write-Verbose "[$((Get-Date).TimeOfDay)] Adding Credential"
$icmSplat.Add('Credential', $Credential)
}
}
'session' {
Write-Verbose "[$((Get-Date).TimeOfDay)] Using Session to $($Session.ComputerName.toUpper())"
$icmSplat.Add('Session', $Session)
}
} #switch
#invoke the script block
Write-Verbose "[$((Get-Date).TimeOfDay)] Invoking scriptblock on $($ComputerName.ToUpper())"
Invoke-Command @icmSplat | ForEach-Object {
Write-Verbose "[$((Get-Date).TimeOfDay)] Processing remote output locally"
#Insert the typename into the object hashtable
$_.Add('PSTypeName', 'UserProfileInfo')
#create an object
New-Object -TypeName PSObject -Property $_
}
}
Catch {
$_
}
Write-Verbose "[$((Get-Date).TimeOfDay)] Ending $($MyInvocation.MyCommand.Name)"
}
This version of the function uses parameter sets to let me connect to a remote machine with a computername/credential combination or a PSSession object. I can also filter on a profile name. The function will work locally or remotely. I put the code that I want to run locally inside a script block. The script block will write a hashtable of information to the local computer. This is where I'll insert the typename and turn it into a custom object.
Invoke-Command @icmSplat | ForEach-Object {
Write-Verbose "[$((Get-Date).TimeOfDay)] Processing remote output locally"
#Insert the typename into the object hashtable
$_.Add('PSTypeName', 'UserProfileInfo')
#create an object
New-Object -TypeName PSObject -Property $_
}
I am using Write-Host
commands in the script block so I can verify where the code is executing.
