Skip to content

Instantly share code, notes, and snippets.

@RichardSlater
Last active December 14, 2015 09:50
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save RichardSlater/6135339f31b3ae661c7d to your computer and use it in GitHub Desktop.
Separate your concerns with PowerShell
function Merge-Tokens {
param(
[Parameter(ValueFromPipeline=$true,Position=0)] [string] $template,
[Parameter(Position=1)] [hashtable] $tokens
)
process {
return [regex]::Replace(
$template,
'\{(?<tokenName>\w+)\}', {
param($match)
$tokenName = $match.Groups['tokenName'].Value
return $tokens[$tokenName]
}
)
}
}
# SMTP configuration
$smtpServer = "smtp.my-preferred-eaas-provider.com"
$smtpUsername = "smtpUsername"
$smtpPassword = "smtpPassword"
# Initialize the data
$data = @{
account1_tb = "4.87";
account2_tb = "4.92";
total_tb = "9.79";
ipaddress = (Get-NetAdapter | Where-Object status -eq 'up' | Get-NetIPAddress -ea 0 | Foreach-Object { [string]$_.IPAddress }) -Join ", "
}
$textBody = Get-Content storage-report.template.txt | Out-String | Merge-Tokens -Tokens $data
$htmlBody = Get-Content storage-report.template.html | Out-String | Merge-Tokens -Tokens $data
$htmlContentType = New-Object System.Net.Mime.ContentType("text/html")
$htmlView = [System.Net.Mail.AlternateView]::CreateAlternateViewFromString($htmlBody, $htmlContentType)
# Creating a Mail object
$msg = New-Object Net.Mail.MailMessage
# Email structure
$msg.From = "postmark@richard-slater.co.uk"
$msg.ReplyTo = "postmark@richard-slater.co.uk"
Get-Content .\to.txt | ForEach-Object { $msg.To.Add($_) }
$msg.Subject = "Test Message"
$msg.Body = $textBody
$msg.AlternateViews.Add($htmlView)
# Creating SMTP server object
$smtp = New-Object Net.Mail.SmtpClient($smtpServer)
$smtp.Credentials = New-Object System.Net.NetworkCredential($smtpUsername, $smtpPassword)
# Sending email
$smtp.Send($msg)

Modern software is complex; often so complex that it is simply too much for one person to hold onto the big picture while digging in to the minute detail of a specific bug or feature. The "Separation of Concerns" paradigm has emerged as a practical and effective way of managing this complexity; a concise description of Separation of Concerns can be drawn from Wikipedia:

Separation of concerns is a design principle for separating a computer program into distinct sections, such that each section addresses a separate set of information that has an effect on the code of the computer program.

The philosophy encapsulated in separation of concerns can be applied to the scripts we write; allowing us to build modules, scripts and functions that can are resistant to the unintended side effects that are common when working in large teams. It doesn't seem as easy or as natural as it might in .NET or most other modern programming environments, but some simple techniques can help you to break down your scripts into smaller more maintainable pieces.

Extracting configuration from the script

Configuration changes frequently, it may even change between single executions of a script if you are executing one script against several different environments such as Development, Test/QA, UAT and Production. If you find yourself hard coding a connection string into a script consider extracting this configuration setting into an external file or environment variable.

Lets say we have a script that sends an e-mail:

$msg.From = "richard.slater@amido.co.uk"
$msg.To.Add("ops-guy@big-client.com")
$msg.Subject = "Status Report"
$msg.Body = "All Systems Go!"

Now the Technical Director wants to get the status report each time it is run, well that would require line 2 to be duplicated right? alternativly how about the following change:

$msg.From = "richard.slater@amido.co.uk"
Get-Content .\to.txt | ForEach-Object { $msg.To.Add($_) }
$msg.Subject = "Status Report"
$msg.Body = "All Systems Go!"

Subsequently to.txt would contain each e-mail address you wanted to send to on a line of it's own:

ops-guy@big-client.com
tech-director@big-client.com

There is now no reason to open the script to add, remove or change an email address. A simple unit test and gated build can validate the correctness of the contents of the file.

Separating form from function

The text of an e-mail changes infrequently; however when it needs to be altered the change may be made by someone who does not understand PowerShell. As in the previous example you dont need to construct the message entirely in PowerShell; if you place the static parts of the message in an external file this can readily be changed without affecting the script.

There is a templating technique for filling in the dynamic portions of the message as documented by Brice Lambson; I have chosen to swap out the dollar signs that Brice uses in favour of curley braces:

function Merge-Tokens {
    param(
        [Parameter(ValueFromPipeline=$true,Position=0)] [string] $template,
        [Parameter(Position=1)] [hashtable] $tokens
    )

    process {
        return [regex]::Replace(
            $template,
            '\{(?<tokenName>\w+)\}', {
                param($match)
                 
                $tokenName = $match.Groups['tokenName'].Value
                 
                return $tokens[$tokenName]
            }
        )   
    }
}

You can then subsequently include the template as a seperate text file:

Total quantity of used storage in blob storage: 

  Account 1: {account2_tb} TB
  Account 2: {account2_tb} TB

Total: {total_tb} TB

This is an automated message sent by the TeamCity server running on {ipaddress}, if you do not believe you should be receiving these e-mails please contact the Operations Team as soon as possible.

Your script can then merge the data with the tokens:

$data = @{
    account1_tb = "4.87";
    account2_tb = "4.92";
    total_tb = "9.79";
    ipaddress = (Get-NetAdapter | Where-Object status -eq 'up' | Get-NetIPAddress -ea 0 | Foreach-Object { [string]$_.IPAddress }) -Join ", "
}

$textBody = Get-Content storage-report.template.txt | Out-String | Merge-Tokens -Tokens $data

I have included the a working sample of all of the files used in this blog post in a gist, including functionality to send a HTML Alternative View using the same template technique

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>{subject}</title>
</head>
<body>
<p>{subject}</p>
<p>Total quantity of used storage in blob storage:</p>
<table cellspacing="0" style="border: 1px solid black; border-collapse: collapse;">
<tr>
<td style="border: 1px solid black; border-collapse: collapse; width: 25%;">Account 1</td><td style="border: 1px solid black; border-collapse: collapse; width: 25%;">{account1_tb} TB</td>
</tr>
<tr>
<td style="border: 1px solid black; border-collapse: collapse; width: 25%;">Account 2</td><td style="border: 1px solid black; border-collapse: collapse; width: 25%;">{account2_tb} TB</td>
</tr>
</table>
<p>Total: {total_tb} TB</p>
<p>This is an automated message sent by the TeamCity server running on {ipaddress}, if you do not believe you should be receiving these e-mails please contact the Operations Team as soon as possible.</p>
</body>
</html>
Total quantity of used storage in blob storage:
Account 1: {account2_tb} TB
Account 2: {account2_tb} TB
Total: {total_tb} TB
This is an automated message sent by the TeamCity server running on {ipaddress}, if you do not believe you should be receiving these e-mails please contact the Operations Team as soon as possible.
richard@richard-slater.co.uk
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment