Behind the PowerShell Pipeline logo

Behind the PowerShell Pipeline

Subscribe
Archives
October 4, 2024

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
Want to read the full issue?
GitHub Bluesky LinkedIn About Jeff
Powered by Buttondown, the easiest way to start and grow your newsletter.