Skip to content

Instantly share code, notes, and snippets.

@jdhitsolutions
Last active March 22, 2023 06:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jdhitsolutions/e82e86efad8ed1ca1f66612d87c6e409 to your computer and use it in GitHub Desktop.
Save jdhitsolutions/e82e86efad8ed1ca1f66612d87c6e409 to your computer and use it in GitHub Desktop.
Some suggested solutions to PowerShell puzzles originally posted at https://jdhitsolutions.com/blog/powershell/8128/powershell-puzzles-and-challenges/
# PowerShell Puzzles and Quick Challenges
Return "This is an interactive demo script file."
<#
The answers to these puzzles are not limited to one-line commands, although some might.
Most of these problems should be solved with no more than a lines of PowerShell code
that you would run interactively at a PowerShell prompt.
This is not a test of your scripting skills although you will find it easier to
work in VS Code or the PowerShell ISE.
#>
#region 1. How many stopped services are on your computer?
Get-Service | Group-Object status
Get-Service | Where-Object status -eq 'stopped' | Measure-Object
Get-Service | Where-Object {$_.status -eq 'stopped'} | Measure-Object
Get-Service | Where-Object {$psitem.status -eq 'stopped'} | Measure-Object
(Get-Service | Where-Object status -eq 'stopped').count
(Get-Service).where({ $_.status -eq 'stopped' }).count
#endregion
#region 2. list services set to autostart but are NOT running
#use Get-Service
Get-Service |
Where-Object { $_.status -ne 'running' -AND $_.StartType -eq 'automatic' } |
Select-Object Name, Status, StartType
#use CIM
Get-CimInstance -ClassName win32_service -Filter "state <> 'running' AND startmode = 'auto'"
#endregion
#region 3. List ONLY the property names of the Win32_BIOS WMI class
#use a cmdlet designed for this task
Get-CimClass -ClassName win32_bios | Select-Object -ExpandProperty CIMClassProperties | Select-Object Name
Get-CimClass -ClassName win32_bios | Select-Object -ExpandProperty CIMClassProperties | Select-Object -ExpandProperty Name
#use object notation
(Get-CimClass win32_bios).CimClassProperties.name | Sort-Object
#get the property names from an instance of the object
$b = Get-CimInstance win32_bios
$b.psobject.Properties | Select-Object Name,TypeNameofValue
#endregion
#region 4. List all loaded functions displaying the name, number of parameter sets, and total number of lines in the function
#an inline solution
Get-ChildItem -Path function: |
Select-Object Name, @{Name = "ParameterSetCount"; Expression = { $_.parametersets.count } },
@{Name = "Lines"; Expression = { ($_.Scriptblock | Measure-Object -Line).lines } }
#formatted
Get-ChildItem -Path function: | Sort-Object Source, Name |
Format-Table -GroupBy Source -Property Name,
@{Name = "ParameterSetCount"; Expression = { $_.parametersets.count } },
@{Name = "Lines"; Expression = { ($_.Scriptblock | Measure-Object -Line).lines } }
#sometimes multiple steps might be better
$functions = Get-ChildItem -Path function: | Where-Object { $_.CommandType -eq 'function'}
$results = foreach ($function in $functions) {
[PSCustomObject]@{
PSTypeName = "PSFunctionInfo"
Name = $function.name
ParameterSetCount = $function.parametersets.count
Lines = ($function.Scriptblock | Measure-Object -Line).lines
Source = $function.source
}
}
#now the results can be used and re-used
$results | Sort-Object Lines,Name -Descending | Select-Object -first 5
$results | Where-Object {$_.name -notmatch "^[A-Z]:"} |
Sort-Object Source,Name |
Format-Table -GroupBy Source -Property Name,ParameterSetCount,Lines
#your next step could be to create a custom format.ps1xml file for PSFunctionInfo objects.
#endregion
#region 5. Create a formatted report of Processes grouped by UserName. Skip processes with no user name
#using Get-Process
Get-Process -IncludeUserName | Where-Object { $_.username} |
Sort-Object Username, Name |
Format-Table -GroupBy Username -Property Handles, WS, CPU, ID, ProcessName
#using CIM
#this isn't a speedy expression so I'll just use the first 10 processes
Get-CimInstance -ClassName Win32_Process | Select-object -first 10 |
Add-Member -MemberType Scriptproperty -Name Owner -Value {
$user = Invoke-CimMethod -InputObject $this -MethodName GetOwner
if ($user.returnValue -eq 0) {
"$($user.Domain)\$($user.User)"
}
} -PassThru -Force -outvariable a | Where-Object Owner |
Sort-Object -Property Owner, Name |
Format-Table -GroupBy Owner -Property ProcessID, Name, HandleCount, WorkingSetSize, VirtualSize
#this might perform better
$procs = Get-CimInstance -ClassName Win32_Process
$modified = ForEach ($process in $procs) {
Try {
$user = Invoke-CimMethod -InputObject $process -MethodName GetOwner -ErrorAction Stop
$value = "$($user.Domain)\$($user.User)"
}
Catch {
$value = $null
}
Add-Member -InputObject $process -MemberType NoteProperty -Name Owner -Value $value -PassThru -Force
}
$sort = $modified | Where-Object Owner | Sort-Object -Property Owner, Name
$sort | Format-Table -GroupBy Owner -Property ProcessID, Name, HandleCount, WorkingSetSize, VirtualSize
#endregion
#region 6. Using your previous code, display the username, the number of processes, the total workingset size. Set no username to NONE.
$grouped = Get-Process -IncludeUserName | Group-Object -Property Username
foreach ($item in $grouped) {
if ($item.name) {
$Name = $item.name
}
else {
$Name = "NONE"
}
#my total WS value is in MB formatted to 2 decimal points
#computername is using .NET to make this cross-platform
[pscustomobject]@{
Computername = [System.Environment]::MachineName
Owner = $Name
Count = $item.Count
TotalWS = [math]::Round(($item.group | Measure-Object -Property WS -Sum).sum / 1MB, 2)
}
}
#endregion
#region 7. Create a report that shows files in %TEMP% by extension. Include Count,total size, % of total directory size
$files = Get-ChildItem -Path $env:temp -File -Recurse
$totalSize = ($files | Measure-Object -Property length -Sum).sum
$grouped = $files | Group-Object -Property extension
#I could have used ForEach-Object. Why is one approach better than the other?
$results = foreach ($item in $grouped) {
$Size = $item.Group | Measure-Object -Property length -Sum
$pctSize = ($size.sum / $totalSize) * 100
[PSCustomObject]@{
Extension = $item.Name
Count = $item.Count
Size = $Size.sum
PctTotal = [math]::round($pctSize, 4)
}
}
$results | Sort-Object -Property PctTotal -Descending
#endregion
#region 8. Find the total % of WS memory a process is using. Show top 10 processes,count,totalWS and PctUsedMemory
#exclude System and Idle processes
$computername = $env:COMPUTERNAME
#get the in use memory value
$os = Get-CimInstance win32_operatingsystem -Property TotalVisibleMemorySize, FreePhysicalMemory -ComputerName $computername
$inUseMemory = ($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) * 1KB
Get-CimInstance win32_process -ComputerName $computername -Filter "Name <>'system idle process'" |
Group-Object Name | ForEach-Object {
$stat = $_.Group | Measure-Object -Property WorkingSetSize -Sum
$_ | Select-Object -Property Name, Count, @{Name = "TotalWS"; Expression = { $stat.sum } },
@{Name = "PctUsedMemory"; Expression = { [math]::Round(($stat.sum / $InUseMemory) * 100, 2) } }
} | Sort-Object PctUsedMemory -Descending | Select-Object -First 10
#break it down by step
$grouped = Get-CimInstance win32_process -ComputerName $computername -Filter "Name <>'system idle process'" |
Group-Object Name
#this is a bit awkward and redundant
$grouped | Select-Object -property Name,Count,
@{Name = "TotalWS"; Expression = {($_.group | Measure-Object -Property WorkingSetSize -Sum).sum }},
@{Name = "PctUsedMemory"; Expression = { [math]::Round(($(($_.group | Measure-Object -Property WorkingSetSize -Sum).sum) / $InUseMemory) * 100, 2) } }
#a bit less redundant
$results = foreach ($item in $grouped) {
#only measure once
$stat = $item.Group | Measure-Object -Property WorkingSetSize -Sum
$Item | Select-Object -Property Count,
@{Name="Name";Expression = {$_.name}},
@{Name = "TotalWS"; Expression = { $stat.sum } },
@{Name = "PctUsedMemory"; Expression = { [math]::Round(($stat.sum / $InUseMemory) * 100, 2) } }
}
$results | Sort-Object PctUsedMemory -Descending | Select-Object -First 10
#a nicer alternative
$results = foreach ($item in $grouped) {
$stat = $item.Group | Measure-Object -Property WorkingSetSize -Sum
#create a custom object instead of relying on Select-Object
[pscustomobject]@{
PSTypeName = "processMemoryItem"
Name = $item.Name
Count = $item.Count
TotalWS = $stat.sum
PctUsedMemory = [math]::Round(($stat.sum / $InUseMemory) * 100, 2)
}
}
$results | Sort-Object PctUsedMemory -Descending | Select-Object -First 10
#endregion
#requires -version 5.1
<#
1. Create a directory called C:\DataFiles
2. Create 25 files of varying sizes between 1KB and 1MB with your own naming convention.
But don't use something as simple as Test-1. Files should have a .dat extension.
3. Modify the creation and last write times to "age" the files between 10 and 100 days.
#>
New-Item -Name DataFiles -Path C:\ -ItemType Directory
#use services to generate dummy files
$svc = Get-Service
1..25 | foreach-object {
#$size = Get-Random -Minimum 1KB -Maximum 1MB
$filename = [system.io.path]::ChangeExtension(([System.IO.Path]::GetRandomFileName()),"dat")
$filePath = Join-Path -Path C:\DataFiles -ChildPath $filename
$svc[0..($_+5)] | Out-File -FilePath $filepath
<#
#this is a bit more sophisticated way to create a dummy file of a given size
$size = Get-Random -Minimum 1kb -Maximum 1MB
$stream = New-Object System.IO.FileStream($filePath, [System.IO.FileMode]::CreateNew)
$stream.Seek($Size, [System.IO.SeekOrigin]::Begin) | Out-Null
$stream.WriteByte(0)
$Stream.Close()
#>
#get the file
$file = Get-Item -path $filepath
#$d = Get-Random -Minimum 10 -Maximum 100
#$age = (Get-Date).addDays(-$d)
#calculate an age in minutes to get truly random datetimes
$min = Get-Random -Minimum (60*24*10) -Maximum (60*24*100)
$age = (Get-Date).AddMinutes(-$min)
$file.CreationTime = $age
$file.CreationTimeUTC = $age.ToUniversalTime()
$file.LastWriteTime = $age
$file.LastWriteTimeUTC = $age.ToUniversalTime()
}
Get-ChildItem -Path C:\DataFiles
#requires -version 5.1
<#
Create a formatted HTML report that shows each module location from %PSModulePath%,
the number of modules in each location and their total size, and then a listing of
each module which shows the most recent version and the total size of all module files.
Extra bonus points if you can make it pretty with CSS formatting.
#>
Param([string]$FilePath = "D:\temp\modulereport.html")
$thisScript = Convert-Path $myinvocation.InvocationName
$reportVersion = "1.3"
#I want to format paths in proper case.
# There are other techniques described at https://serverfault.com/questions/431416/get-filename-in-the-case-stored-on-disk/964436#964436
# I am Using the Scripting.FileSystem COM object
# It works, so I'm good with it. This means my code will only run on Windows
#the path separator depends on the OS
$splitter = [system.io.path]::PathSeparator
if ($PSEdition -eq 'Desktop' -OR $isWindows) {
$fso = New-Object -com scripting.filesystemobject
$locations = ($env:PSModulePath -split $splitter).foreach({$fso.GetAbsolutePathName($_)})
} else {
$locations = ($env:PSModulePath -split $splitter)
}
#get all modules
$modules = Get-Module -ListAvailable
#group the modules on a custom property which puts them in the corresponding location
$grouped = $modules | Group-Object -Property {
foreach ($loc in $locations) {
if ($_.path -match [System.Text.RegularExpressions.Regex]::Escape($loc)) {
return $loc
}
}
}
#create a list to hold the HTML fragments
$frag = [System.Collections.Generic.list[string]]::New()
#add some metadata to the report. This code should work cross-platform
$meta = [pscustomobject]@{
Computername = [System.Environment]::MachineName
Username = "$($env:USERDOMAIN)\$($env:USERNAME)"
PSVersion = $PSVersionTable.PSVersion
PowerShellHost = $host.Name
}
#create the HTML fragments
$frag.Add(($meta | ConvertTo-Html -Fragment))
$frag.Add("<h2>Location Summary</h2>")
$summary = $grouped | Select-Object @{Name="Module Location";Expression={$_.Name}},Count | ConvertTo-Html -Fragment
$frag.Add($summary)
$frag.Add("<h2>Detail</h2>")
foreach ($item in $Grouped) {
$frag.Add("<h3>$($item.name)</h3>")
#save the output as XML so it can be parsed
[xml]$detail = $item.group | Select-object Name,Version,ModuleType,
@{Name="TotalKB";Expression = {
(Split-Path $_.path | Get-ChildItem -Recurse -Force | Measure-Object length -sum).Sum/1KB -as [int]
}} | ConvertTo-Html -Fragment
#insert the Size CSS class into the last <td> item
for ($i = 1; $i -lt $detail.table.tr.count; $i++) {
$class = $detail.CreateAttribute("class")
$class.value = "size"
[void]$detail.table.tr[$i].childnodes[3].Attributes.append($class)
}
$frag.add($detail.innerXML)
}
#my HTML style settings
$ReportTitle = "Module Summary Report"
$head = @"
<Title>$ReportTitle</Title>
<style>
h2 {
width:95%;
background-color:#7BA7C7;
font-family:Tahoma;
font-size:12pt;
}
body { background-color:#FFFFFF;
font-family:Tahoma;
font-size:10pt; }
td, th { border:1px solid black;
border-collapse:collapse; }
th { color:white;
background-color:black; }
table, tr, td, th { padding-left: 10px; margin: 0px }
tr:nth-child(odd) {background-color: lightgray}
table { width:95%;margin-left:5px; margin-bottom:20px;}
.footer {
font-size:10pt;
}
.footer tr:nth-child(odd) {background-color: white}
.footer td,tr {
border-collapse:collapse;
border:none;
}
.footer table {width:15%;}
td.size {
text-align: right;
padding-right: 25px;
}
</style>
<H1>$ReportTitle</H1>
"@
#a footer for the report. This could be styled with CSS
$post = @"
<table class='footer'>
<tr align = "right"><td>Report run: <i>$(Get-Date)</i></td></tr>
<tr align = "right"><td>Report version: <i>$ReportVersion</i></td></tr>
<tr align = "right"><td>Source: <i>$thisScript</i></td></tr>
</table>
"@
#create the report and save it to a file
ConvertTo-Html -Body $frag -Head $head -PostContent $post| Out-File -FilePath $filepath
Get-Item -path $FilePath
#open the report in the web browser
#Invoke-Item $filepath
@sdp-au
Copy link

sdp-au commented Mar 22, 2023

alternative solution to Bonus #1 on this link

$dir="c:\datafiles\"              
New-Item -ItemType Directory -Name DataFiles -Path c:\ -Force -ErrorAction Continue  # creating directory and continues execution if the directory exists

for ($i=0; $i -lt 25; $i++) {
   $name=[System.IO.Path]::GetRandomFileName()                # creating random filename
   $name=$dir+$name.Substring(1,7)+".dat"                     # taking first 7 characters and adding .dat extension
   $r=[int](Get-Random -Minimum 1 -Maximum 1024 )*1024        # converting to KB
   $file=[System.IO.File]::Create($name)                      # creating file 
   $file.SetLength($r)                                        # setting length in KB
   $file.Close()                                              # closing the handle
   $at=[int](Get-Random -Minimum 10 -Maximum 100)             # last write time (numeric)
   $ct=[int](Get-Random -Minimum 10 -Maximum 100)             # creation time (numeric)
   (Get-Item $name).LastAccessTime=$(get-date).AddDays(-$at)  # setting the lastaccesstime attribute
   (Get-Item $name).CreationTime=$(get-date).AddDays(-$ct)    # setting the creationtime attribute
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment