Automatic Traditional Modules
In the last email, I demonstrated how to use the New-Module
command to create a dynamic module. The result was a temporary module that existed only in memory. Nothing was written to disk. This is useful for creating modules on the fly with targeted commands, but what if you want to create a traditional module that you can save to disk and reuse later? In other words, I want the flexibility of a dynamic module with targeted commands, but I want to save it to disk for later use. Or I may want to take advantage of other module features.
Missing New-Module Features
Modules you create with New-Module
are not versioned. There is no manifest. That means you can't define aliases, custom format and type files, or specify dependencies. PowerShell will also automatically import the module when you run a module command. But in the use case we've been using, I don't necessarily know the command names ahead of time. There may be a little compromise but let's see what we can do.
Creating a Dynamic Traditional Module
I am going to use the same commands I've been using in the past demonstrations. These are commands based on computer names from my Active Directory domain. The story is that I'm writing a set of PowerShell tools for the help desk staff who have minimal PowerShell experience. They are most likely to run a single command and look at the output. Don't forget the importance of knowing your audience.
I'm going to create a module with the same sort of structure I would normally use. My demonstration module will be called CompanyDynamic
. This means I'll also have a root module of CompanyDynamic.psm1
and a module manifest, CompanyDynamic.psd1
. I'll come back to the manifest in a moment.
In the root module file, I'll get my computer names and save them to a variable.
$DomainMembers = (Get-ADComputer -Filter *).Name
I'll eventually export the variable as part of the module. I will be creating a set of functions.
#define an array to hold the dynamically generated function names
$cmdList=@()
Since I am using a traditional module file, I won't be using the New-Module
technique I showed you. Instead, I'll use the concepts I originally demonstrated in previous articles. I'm going to dynamically create a custom function for each computer name, with the value hard-coded into the function. I mentioned this before that I know this is not a best practice, but I'm knowingly bending the rules to meet a business requirement.
Foreach ($item in $DomainMembers) {
#this version does not rely on PSDefaultParameterValues
Write-Host "Creating function: $item" -ForegroundColor Green
$cmdText = @"
[cmdletbinding()]
Param()
Write-Verbose "Starting `$(`$MyInvocation.MyCommand)"
[string]`$ComputerName = '$item'
Try {
Write-Verbose "Resolving DNS name for `$(`$ComputerName.ToUpper())"
`$r = Resolve-DnsName `$ComputerName -Type A -ErrorAction Stop
Write-Verbose "Getting AD computer object for `$(`$ComputerName.ToUpper())"
`$ad = Get-ADComputer -Identity `$ComputerName -Properties ManagedBy, Description,
Enabled,OperatingSystem,PasswordLastSet -ErrorAction Stop
Write-Verbose "Testing connectivity to `$(`$ComputerName.ToUpper())"
If (Test-Connection -ComputerName `$ComputerName -Count 2 -ErrorAction SilentlyContinue) {
`$Online = `$True
}
else {
`$Online = `$False
}
}
Catch {
Throw `$_
}
if (`$r -AND `$ad ) {
[PSCustomObject]@{
PSTypeName = 'PSServerInfo'
ComputerName = `$ComputerName.ToUpper()
OperatingSystem = `$ad.OperatingSystem
DNSHostName = `$r.Name.ToUpper()
DistinguishedName = `$ad.DistinguishedName
Description = `$ad.Description
ManagedBy = `$ad.ManagedBy
IPAddress = `$r.IPAddress
Enabled = `$ad.Enabled
PasswordLastSet = `$ad.PasswordLastSet
Online = `$Online
} #close custom object
}
Write-Verbose "Ending `$(`$MyInvocation.MyCommand)"
"@
$sb = [scriptblock]::Create($cmdText)
#create the function in the global scope
Set-Item -Path Function:\Global:$item -Value $sb
$cmdList+=$Item
}
The command text is using a double-quoted here-string so that I can easily expand variables. However this means I need to escape the $ sign within the code. When the module is imported, this code will run and will create a function based on each computer name. I am not creating files.
I'm then creating another set of functions also based on the computer names. These functions will include comment-based help.
foreach ($item in $DomainMembers) {
#Make the function name pretty like Get-Dom1
$ItemString = (Get-Culture).TextInfo.ToTitleCase($item.ToLower())
$funName = "Get-$ItemString"
$cmdNames+=$funName
$cmd = @"
<#
.SYNOPSIS
Retrieve system information for $($item.ToUpper())
.DESCRIPTION
This function will query $($item.ToUpper()) for operating system information and related information.
.EXAMPLE
PS C:\> $funName
ComputerName : $($item.ToUpper())
OS : Microsoft Windows Server 2019 Standard
InstallDate : 8/26/2024 12:16:42 PM
Age : 24.23:52:47.9897311
TotalMemory : 1186752
FreeMemory : 549212
LastBoot : 8/26/2024 12:29:52 PM
Uptime : 24.23:39:37.3126291
.LINK
Get-CimInstance
#>
[CmdletBinding()]
[OutputType("OSInfo")]
param ()
Write-Verbose "Starting `$(`$MyInvocation.MyCommand)"
`$splat = @{
ComputerName = '$item'
ClassName = 'Win32_OperatingSystem'
Property = 'Caption', 'TotalVisibleMemorySize', 'InstallDate', 'FreePhysicalMemory', 'LastBootUpTime', 'CSName'
ErrorAction = 'Stop'
}
Try {
Write-Verbose "Querying $($item.ToUpper()) for Operating System information"
`$os = Get-CimInstance @splat
[PSCustomObject]@{
PSTypeName = 'OSInfo'
ComputerName = `$os.CSName
OS = `$os.Caption
InstallDate = `$os.InstallDate
Age = (New-TimeSpan -Start `$os.InstallDate -End (Get-Date))
TotalMemory = $`os.TotalVisibleMemorySize
FreeMemory = `$os.FreePhysicalMemory
LastBoot = `$os.LastBootUpTime
Uptime = (New-TimeSpan -Start `$os.LastBootUpTime -End (Get-Date))
}
} #try
Catch {
Throw `$_
} #catch
Write-Verbose "Ending `$(`$MyInvocation.MyCommand)"
#close $funName
"@
Write-Host "Creating function: $FunName" -ForegroundColor Green
#create the function in the global scope
Set-Content -Path Function:\Global:$FunName -Value $([scriptblock]::Create($cmd))
$cmdList+=$FunName
} #foreach