Ask Jeff
January 2024
Here we are at the end of the month. Lots of content is planned for the year, and big changes are afoot. More on that later. Here are some PowerShell odds and ends.
Extending PSResources
I hope you have updated your PowerShell package management to the new Microsoft.PowerShell.PSResourceGet module. The older PowerShellGet module isn’t going away any time soon, but I encourage you to use the new module to find, install, and update PowerShell modules and scripts. I spent a lot of time poking around the module as I was working on my latest Pluralsight course on this very topic.
The new PSResourceGet commands are faster, but the output is a little different than what we go with Find-Module
.
PS C:\> $m = Find-PSResource psworkitem
PS C:\> $m | Select-Object *
PSEdition : Core
Age : 22.00:06:10.7950141
AdditionalMetadata : {[NormalizedVersion, 1.7.0]}
Author : Jeff Hicks
CompanyName : JDH Information Technology Solutions, Inc.
Copyright : (c) JDH Information Technology Solutions, Inc. All
rights reserved.
Dependencies : {mySQLite}
Description : A PowerShell 7 module for managing work and personal
tasks or to-do items. This module uses a SQLite
database to store task and category information. The
module is not a full-featured project management
solution, but should be fine for personal needs. The
module requires a 64-bit Windows platform.
IconUri :
Includes : Microsoft.PowerShell.PSResourceGet.UtilClasses.Re...
InstalledDate :
InstalledLocation :
IsPrerelease : False
LicenseUri : https://github.com/jdhitsolutions/PSWorkItem/blob...
Name : PSWorkItem
Prerelease :
ProjectUri : https://github.com/jdhitsolutions/PSWorkItem
PublishedDate : 1/7/2024 5:41:06 PM
ReleaseNotes :
Repository : PSGallery
RepositorySourceLocation : https://www.powershellgallery.com/api/v2
Tags : {database, sqlite, to-do, project-management…}
Type : Module
UpdatedDate :
Version : 1.7.0
Some of the information I’d like to see is buried. For example, if I want to know if a module supports PowerShell 7, I have to look for the PSEdition. This information is in the tags.
PS C:\> $m.tags
database
sqlite
to-do
project-management
tasks
PSModule
PSEdition_Core
PSFunction_Get-PSWorkItem
PSCommand_Get-PSWorkItem
...
It looks like the command creates a tag for each command and function in the module.
Based on the PSEdition tag, this module supports PowerShell 7. I don’t see PSEdition_Desktop
so that tells me this module is not supported on Windows PowerShell.
Here’s a code snippet to retrieve the edition value from the tags.
PS C:\> $m.tags.where({ $_ -match '^PSEdition_' }).ForEach({ $_.split('_')[1] })
Core
But I don’t want to type that all the time. Instead, I can extend the object type and define a script property.
Update-TypeData -TypeName Microsoft.PowerShell.PSResourceGet.UtilClasses.PSResourceInfo -MemberType ScriptProperty -MemberName PSEdition -Value { $this.Tags.Where({ $_ -match '^PSEdition_' }).ForEach({ $_.split('_')[1] }) } -Force
The code in the Value script block is similar to what you would use with a custom property in Select-Object
, but instead of using $_
to reference the object, use $this
. The change is immediate.
PS C:\> $m | select Name, Version, PSEdition
Name Version PSEdition
---- ------- ---------
PSWorkItem 1.7.0 Core
Now, I can use it for any query.
PS C:\> Find-PSResource -tag DNS | select Name, Version, PSEdition
Name Version PSEdition
---- ------- ---------
Update-AzDNSRecordMultiSub 1.0.0
Test-DnsZone 0.0.1
Test-DnsServerScavenging 1.0
Get-MXReport 1.4.1
Get-Mailprotection 1.12
Get-DnsConfiguration 1.0
YandexPdd 2.0.1
WinSecureDNSMgr 0.0.4 Core
Test-DNSRecord 1.0.1
Resolve-DNSNameOverHTTP 0.2
Resolve-DnsNameCrossPlatform 1.0.1 {Core, Desktop}
PSWinDocumentation.DNS 0.0.10 {Desktop, Core}
PsNetTools 0.7.8
PSDNSDumpsterAPI 0.0.4
PSComputerManagementZp 0.1.0 {Desktop, Core}
PSc.CloudDNS 0.1.0 Desktop
...
The edition isn’t required in the module manifest. Typically, if there is nothing defined, I would expect the module to work in Windows PowerShell. I don’t know about PowerShell 7 until I try.
This type extension only lasts for the duration of your PowerShell session. If I always wanted this custom property, I’d put the Update-TypeData
command in my PowerShell profile script.
Module Commands
I can do something similar with command information. The information is buried.
PS C:\> $m.includes.command
Get-PSWorkItem
Set-PSWorkItem
Remove-PSWorkItem
Initialize-PSWorkItemDatabase
Complete-PSWorkItem
Get-PSWorkItemCategory
Add-PSWorkItemCategory
...
How about adding a property that makes it easier to get this data?
Update-TypeData -TypeName Microsoft.PowerShell.PSResourceGet.UtilClasses.PSResourceInfo -MemberType ScriptProperty -MemberName Commands -Value { $this.Includes.command} -Force
PS C:\> $m.commands | Sort | Group {($_ -split "-")[1]}
Count Name Group
----- ---- -----
5 PSWorkItem {Complete-PSWorkItem, Get-PSWorkItem, New-PS…
2 PSWorkItemArchive {Get-PSWorkItemArchive, Remove-PSWorkItemAr…
4 PSWorkItemCategory {Add-PSWorkItemCategory, Get-PSWorkItemCatego…
1 PSWorkItemConsole {Open-PSWorkItemConsole}
1 PSWorkItemData {Get-PSWorkItemData}
3 PSWorkItemDatabase {Get-PSWorkItemDatabase, Initialize-PSWorkIt…
2 PSWorkItemPreference {Get-PSWorkItemPreference, Update-PSWorkItem…
1 PSWorkItemReport {Get-PSWorkItemReport}
Module Age
Let’s try another extension. I can see the published date. But how long ago was that? It is easy enough to calculate.
Update-TypeData -TypeName Microsoft.PowerShell.PSResourceGet.UtilClasses.PSResourceInfo -MemberType ScriptProperty -MemberName Age -Value { (Get-Date) - $this.PublishedDate } -Force
Normally, I would have used New-Timespan
.
Update-TypeData -TypeName Microsoft.PowerShell.PSResourceGet.UtilClasses.PSResourceInfo -MemberType ScriptProperty -MemberName Age -Value { (NewTimeSpan -start $this.PublishedDate -end (Get-Date))} -force
But this gave me inconsistent results. Subtracting dates always seems to work.
Find-PSResource -Tag activedirectory -Type Module -ov a |
Select-Object Name, Author, Version, PSEdition, Description,
PublishedDate, Age |
Sort-Object -Property Age |
Out-GridView
Adding Custom Methods
Updating an object type is not limited to defining custom properties. You can also define custom methods for an object. The output can include the project URI, which is typically the GitHub repository. Personally, I tend to avoid installing modules that don’t have a public repository I can first review.
I’ll add a method to open the link.
Update-TypeData -TypeName Microsoft.PowerShell.PSResourceGet.UtilClasses.PSResourceInfo -MemberType ScriptMethod -MemberName OpenRepository -Value {
If ($this.ProjectUri) {
Start-Process $this.ProjectUri.AbsoluteUri
}
else {
Write-Warning 'This resource does not have a public project repository'
}
} -Force
I saved my Active Directory search to a variable. I can easily open the repository.
PS C:\> $a[0].OpenRepository()
It is possible the link points to a location that no longer exists.
The type extension is very specific. I’d like to use code like this with Out-GridView
as an object picker.
$a | Where ProjectURI | Select-Object Name,Version,PSEdition,ProjectUri |
Out-GridView -Title "Open Project Repository" -OutputMode Multiple |
Foreach-Object {
$_.OpenRepository()
}
But this fails because when I use Select-Object
, PowerShell creates a new object based on the original.
Selected.Microsoft.PowerShell.PSResourceGet.UtilClasses.PSResourceInfo
No problem. I’ll add the type extension.
Update-TypeData -TypeName Selected.Microsoft.PowerShell.PSResourceGet.UtilClasses.PSResourceInfo -MemberType ScriptMethod -MemberName OpenRepository -Value {
If ($this.ProjectUri) {
Start-Process $this.ProjectUri.AbsoluteUri
}
else {
Write-Warning 'This resource does not have a public project repository'
}
} -Force
Now, I can pick multiple modules; each link will open in my default browser.
The method displays a warning if there is no link.
PS C:\> $a[4].OpenRepository()
WARNING: This resource does not have a public project repository
The code for a script method can be as complex as necessary, although there are practical limitations. I decided to also add an Install()
method to the PSResourceGet object. The idea is that this might simplify my actions and eliminate the need to call Install-PSResource
explicitly.
Update-TypeData -TypeName Microsoft.PowerShell.PSResourceGet.UtilClasses.PSResourceInfo -MemberType ScriptMethod -MemberName Install -Value {
#set your preferred installation parameters
Param(
[ValidateSet("CurrentUser", "AllUsers")]
[string]$Scope = 'CurrentUser'
)
$inParams = @{
InputObject = $This
Scope = $Scope
Quiet = $true
PassThru = $true
}
Install-PSResource @inParams
} -Force
The method invokes Install-PSResource
with my preferred parameters. The method is defined with a parameter, but it won’t show with Get-Member
.
PS C:\> $a | Get-Member -Name Install
TypeName: Microsoft.PowerShell.PSResourceGet.UtilClasses.PSResourceInfo
Name MemberType Definition
---- ---------- ----------
Install ScriptMethod System.Object Install();
However, you can pull apart the underlying PSObject to see the details.
PS C:\> $a[0].psobject.methods["Install"]
Script :
#set your preferred installation parameters
Param(
[ValidateSet("CurrentUser", "AllUsers")]
[string]$Scope = 'CurrentUser'
)
$inParams = @{
InputObject = $This
Scope = $Scope
Quiet = $true
PassThru = $true
}
Install-PSResource @inParams
OverloadDefinitions : {System.Object Install();}
MemberType : ScriptMethod
TypeNameOfValue : System.Object
Value : System.Object Install();
Name : Install
IsInstance : False
As long as I know how to use the method, I can now easily install the module from my saved data.
PS C:\> $a[0].install("allusers")
Name Version Prerelease Repository Description
---- ------- ---------- ---------- -----------
ZPki 0.1.9.4 PSGallery PKI and certificate management
Note that with this approach, there is no tab completion for method parameters or support for features like -WhatIf.
Let’s look at one more custom addition. The PSResource object includes the module (or script) author. This means I could find other modules written by the author. This requires scraping the author’s profile page on PowerShellGallery.com.
Update-TypeData -TypeName Microsoft.PowerShell.PSResourceGet.UtilClasses.PSResourceInfo -MemberType ScriptMethod -MemberName FindAuthorModules -Value {
$Author = $this.Author
$base = "https://www.powershellgallery.com"
function GetAuthorData {
[cmdletbinding()]
Param([string]$AuthorPage)
$data = Invoke-WebRequest "https://www.powershellgallery.com/$AuthorPage" -ErrorAction Stop
$data.links | Where {$_.TagName -eq "A" -AND $_.href -match "/packages\/\w+(?=/$)"} |
Foreach-Object {
$_.href -replace "/packages/|\/$",""
}
#get next page if found
$pg = $data.links.where({$_.href -match 'page=\d'})
if ($pg) {
GetAuthorData $pg.href
}
}
Try {
GetAuthorData "profiles/$($author -replace '\s','')" -ErrorAction Stop
}
Catch {
Write-Warning "Unable to find modules for $Author. $($_.Exception.Message)"
}
} -Force
The method uses a private nested function to scrape the web page. The PowerShell Gallery pages results so the code checks to see if there is another page of data, and retrieves it if found.
PS C:\> $m.author
Jeff Hicks
PS C:\> $m.FindAuthorModules()
WingetTools
PSScriptTools
PSReleaseTools
PSAutoLab
ISEScriptingGeek
DNSSuffix
PSTeachingTools
...
Or, you could use it like this:
$m.FindAuthorModules() | Find-PSResource |
Select-Object -Property Name,Version,Description,PSEdition,ProjectUri,Age
You could turn my helper function into a stand-alone tool. However, there is one major caveat. You can’t assume that the listed author is the person who published the module to the PowerShell Gallery. For example, I have modules I found with an author of “Ironman Software,” but the actual publisher and profiled user is Adam Driscoll.
My goal here is two-fold; I wanted to encourage you to use the new PSResourceGet module and demonstrate how you can add value to existing objects in PowerShell.
UpdateWingetPackage
Another set of PowerShell tools I encourage you to try is Microsoft.PowerShell.ConsoleGuiTools and Microsoft.Winget.Client. The former provides Out-ConsoleGridView
and the latter is a PowerShell module wrapper around winget. I use these commands to run a script that makes it easy to update packages. I thought I’d share it with you.
The script requires PowerShell 7 because I am using the -Parallel parameter with ForEach-Object
to run updates in parallel.
#requires -version 7.3
#requires -Module Microsoft.Winget.Client
#requires -module Microsoft.PowerShell.ConsoleGuiTools
#UpdateWingetPackages.ps1
[CmdletBinding()]
Param()
#17 Jan 2024 Moved exclusions to an external file
[string]$Exclude = (Get-Content $PSScriptRoot\WingetExclude.txt | Where {$_ -notmatch "^#" -AND $_ -match "\w+"}) -join "|"
#10 Jan 2024 invoke updates in parallel
$sb = {
Param($MyExclude)
Write-Progress "[$((Get-Date).TimeOfDay)] Checking for Winget package updates"
Get-WinGetPackage -Source Winget | Where {$_.IsUpdateAvailable -AND ($_.InstalledVersion -notmatch "unknown|\<") -AND ($_.Name -notMatch $myExclude)} |
Out-ConsoleGridView -Title "Select Winget packages to upgrade" -OutputMode Multiple |
Foreach-Object -Parallel {
$Name = $_.Name
Write-Host "[$((Get-Date).TimeOfDay)] Updating $($_.Name)" -ForegroundColor Green
Update-WinGetPackage -mode Silent -Name $_.Name |
Select-Object @{Name="Package";Expression={$Name}},RebootRequired,InstallerErrorCode,Status
}
}
Try {
#verify Winget module commands will run. There may be assembly conflicts
$ver = Get-WinGetVersion -ErrorAction stop
Invoke-Command -ScriptBlock $sb -ArgumentList $Exclude
}
Catch {
#write-warning $_.Exception.message
#run the task in a clean PowerShell session to avoid assembly conflicts
pwsh -NoLogo -NoProfile -command $sb -args $Exclude
}
Because I know there are packages that I don’t want Winget to update, I have an exclude file in the same directory as the script.
#wingetexclude.txt
#Winget packages to exclude from UpdateWingetPackages.ps1
#These files must be in the same directory
#Package names will be used in a regex pattern
MuseScore
Python
ESET
Discord
Camtasia
Dymo
Spotify
PowerShell
When I run the script, I get packages with updates displayed in a terminal user interface (TUI).
I can select the packages I want to upgrade and press ENTER.
Installations will run in parallel. The error is related to an ongoing problem with Winget, so I ignored it. The script writes a custom result object to the pipeline for successful upgrades.
Package RebootRequired InstallerErrorCode Status
------- -------------- ------------------ ------
Slack False 0 Ok
Platform Changes
Finally, I am planning a major change for this newsletter. For a variety of reasons, I am moving from Substack to Buttondown. This is the last article I intend to publish on Substack. You don’t have to do anything. I am hoping for a seamless migration. Archived articles will be moved to the new platform. Paid subscribers should see no change, as I will continue to use Stripe as the payment processor. Everyone should have the same access on the new platform as they do on Substack. Of course, if you were using the Substack app to read my content, that would go away.
If you have any questions or concerns about the migration, please let me know at behind@jdhitsolutions.com.
Summary
I’ll be back next week, hopefully on a new platform with new content.