Advice for HTML System Report

Hi Guys,

I am trying to write a script that uses the attached script as a template. I am simply trying to write a function and add it to the existing template. I’ve been able to get a couple of functions to haphazardly work by a lot of trial and error and asking for a lot of help. For the next function I’d like to try to get some advice before I start.

The objective of the function is to return an object to be included in an HTML report. The template uses a hashtable to build the object before passing it back to the calling code.

What I am trying to accomplish is reporting the status of a couple of registry values. For example, I have 5 applications that can be installed in any combination. A user can have 1, 2 or all 5 installed. Each application can have debug logging enabled by setting a DWORD to 1 and the path to the log is defined by a String:

DWORD DebugLog = 1
String DebugLogPath = C:\users\appdata\roaming\myAppA\logs

I don’t want to report on the applications that are not installed so the scope is to only report if the application is installed. Up to this point I have been querying the Windows Uninstall registry hive for the application GUIDs then building the function based on that.

What I am asking is, what might be the best way to do this? Should I create a hashtable or an array and iterate through them then do something else, etc. etc. I really don’t know where to start.

The output only needs to be a list that would look something like:

Logging status:
myApp A: Enabled - C:\users\appdata\roaming\myAppA\logs
myApp B: Disabled
myApp C: Enabled - C:\users\appdata\roaming\myAppB\logs

For these applications debug logging is expensive and there is a noticeable lag when enabled so I want to audit their status.

Thanks in advance for any feedback.

Terry

I think maybe you’re letting yourself get overwhelmed by trying to do too many things at once. You can break your description down into several smaller tasks, each of which has a pretty straightforward answer in code:

[ul]
[li]
Check a list of 5 known applications one at a time. This is some sort of collection (possibly an array, but it doesn’t really matter) and a foreach loop:

$apps = 'App 1', 'App 2', 'App 3', 'App 4', 'App 5'

foreach ($app in $apps)
{
    # Do something with this $app
}

[/li]
[li]
Check to see if an app is installed. You already know how to do this by checking the registry; nothing new to see here. (The values in the array will probably contain the registry key name for each app):

$isInstalled = Test-Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\$appKey"

[/li]
[li]
Check to see if logging is enabled. This is another registry check, except instead of just doing a Test-Path, you need to extract a value. If you want to use the Registry PSProvider for this, then you use Get-ItemProperty:

$regValue = (Get-ItemProperty -Path "HKLM:\$appRegistryPath" -Name DebugLog).DebugLog
if ($regValue -eq 1)
{
    $loggingStatus = 'Enabled'
}
else
{
    $loggingStatus = 'Disabled'
}

[/li]
[li]
Getting the logging path. Another registry value fetch just like the last one:

$loggingPath = (Get-ItemProperty -Path "HKLM:\$appRegistryPath" -Name DebugLogPath).DebugLogPath

[/li]
[li]
Create a new PSObject with the properties you want. You know how to do this as well, by building a hashtable and passing it to New-Object (or casting to [pscustomobject], in PowerShell 3.0 or later):

$properties = @{
    Name = $appName
    LoggingStatus = $loggingStatus
    LogPath = $loggingPath
}

New-Object psobject -Property $properties

[/li]
[/ul]

Now, I used some placeholder variables there because I don’t know everything about where your registry keys are stored, but if you put it all together, you get something like this:

function Get-AppLogInfo
{
    $apps = @(
        New-Object psobject -Property @{
            InstallerGuid = '4a7f8aa5-8ba9-4c15-a079-8bc25b6b0e29'
            Name = 'App1'
            RegPath = 'HKLM:\Software\App1'
        }

        New-Object psobject -Property @{
            InstallerGuid = '4a7f8aa5-8ba1-4c15-a079-8bc25b6b0e29'
            Name = 'App2'
            RegPath = 'HKLM:\Software\App2'
        }

        New-Object psobject -Property @{
            InstallerGuid = '4a7f8aa5-8ba2-4c15-a079-8bc25b6b0e29'
            Name = 'App3'
            RegPath = 'HKLM:\Software\App3'
        }

        New-Object psobject -Property @{
            InstallerGuid = '4a7f8aa5-8ba3-4c15-a079-8bc25b6b0e29'
            Name = 'App4'
            RegPath = 'HKLM:\Software\App4'
        }

        New-Object psobject -Property @{
            InstallerGuid = '4a7f8aa5-8ba4-4c15-a079-8bc25b6b0e29'
            Name = 'App5'
            RegPath = 'HKLM:\Software\App5'
        }
    )

    # Enumerate the array

    foreach ($app in $apps)
    {
        # Only produce output if the app is installed
        $isInstalled = Test-Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\$($app.InstallerGuid)"
        if ($isInstalled)
        {
            # Set up default values for logging status / path
            $loggingStatus = 'Disabled'
            $loggingPath = ""

            # Check this app's logging status
            $regValue = (Get-ItemProperty -Path $app.RegPath -Name DebugLog).DebugLog
            if ($regValue -eq 1)
            {
                # Logging is enabled; also fetch the log path for this app, overwriting the default values
                $loggingStatus = 'Enabled'
                $loggingPath = (Get-ItemProperty -Path $app.RegPath -Name DebugLogPath).DebugLogPath
            }

            # Build the PSObject using a hashtable with the information we've found
            $properties = @{
                Name = $app.Name
                LoggingStatus = $loggingStatus
                LogPath = $loggingPath
            }

            New-Object psobject -Property $properties
        }
    }    
}

Now, that code is starting to get a bit long, at 60+ lines for a single function, with lots of nested loops and conditions. It helps to split some of the code out into its own function, giving that function a descriptive name. Overall, the code might get longer, but it’s broken up into much smaller bits that are very easy to understand on their own. If you name your functions and parameters well, you can figure out what a function does just by looking at the calls to it; it should do pretty much just what you expect. This is more of a software craftsmanship concern; you could keep the above example as-is, if it suits your needs. Many scripts out there don’t even look as organized as that, and if it works, you could be happy.

However, if you get to a point where you want to produce quality code works AND is easy to read, you could break that original code down into something like this:

function Get-AppLogInfo
{
    foreach ($app in Get-AppsToCheck)
    {
        if (Test-IsAppInstalled -AppInfo $app)
        {
            Get-LogInfoForSingleApp -AppInfo $app
        }
    }
}

function Get-AppsToCheck
{
    # This is hard-coded for now, but you could even move this data out
    # into a database, CSV file, or whatever.

    New-Object psobject -Property @{
        InstallerGuid = '4a7f8aa5-8ba9-4c15-a079-8bc25b6b0e29'
        Name = 'App1'
        RegPath = 'HKLM:\Software\App1'
    }

    New-Object psobject -Property @{
        InstallerGuid = '4a7f8aa5-8ba1-4c15-a079-8bc25b6b0e29'
        Name = 'App2'
        RegPath = 'HKLM:\Software\App2'
    }

    New-Object psobject -Property @{
        InstallerGuid = '4a7f8aa5-8ba2-4c15-a079-8bc25b6b0e29'
        Name = 'App3'
        RegPath = 'HKLM:\Software\App3'
    }

    New-Object psobject -Property @{
        InstallerGuid = '4a7f8aa5-8ba3-4c15-a079-8bc25b6b0e29'
        Name = 'App4'
        RegPath = 'HKLM:\Software\App4'
    }

    New-Object psobject -Property @{
        InstallerGuid = '4a7f8aa5-8ba4-4c15-a079-8bc25b6b0e29'
        Name = 'App5'
        RegPath = 'HKLM:\Software\App5'
    }
}

function Test-IsAppInstalled($AppInfo)
{
    return Test-Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\$($AppInfo.InstallerGuid)"
}

function Get-LogInfoForSingleApp($AppInfo)
{
    if (Test-AppLoggingIsEnabled -AppInfo $AppInfo)
    {
        $loggingStatus = 'Enabled'
        $loggingPath = Get-AppLogPath -AppInfo $AppInfo
    }
    else
    {
        $loggingStatus = 'Disabled'
        $loggingPath = ""
    }

    $properties = @{
        Name = $app.Name
        LoggingStatus = $loggingStatus
        LogPath = $loggingPath
    }

    New-Object psobject -Property $properties
}

function Test-AppLoggingIsEnabled($AppInfo)
{
    (Get-ItemProperty -Path $app.RegPath -Name DebugLog).DebugLog -eq 1
}

function Get-AppLogPath($AppInfo)
{
    return (Get-ItemProperty -Path $app.RegPath -Name DebugLogPath).DebugLogPath
}

Overall, it’s longer, but it’s much easier to understand when you’re looking at any individual function, none of which are longer than 20 lines (except for Get-AppsToCheck, which is really more of a hard-coded database than lines of actual code.) The main function, Get-AppLogInfo, makes the logic very clear; you almost don’t even have to look at the rest of the code to understand what’s going on. Writing code like this takes some practice, and you will almost never get it right on the first try. The process of taking some complex code and reorganizing it to be easier to understand and maintain is called refactoring.

Dave, thanks for the feedback. Especially the compare and contrast of styles. This helped me see things from a different perspective.

Thanks again.