Last active
March 22, 2023 06:45
-
-
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/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
alternative solution to Bonus #1 on this link