This post is about my experience converting the CodePlex project, SQL Server Powershell Extensions (SQLPSX) Powershell V1 function libraries into PowerShell V2 Advanced functions within modules.
In order to provide context for people reading this blog post a quick timeline is needed:
- Powershell V1 was released in November 2006
- SQLPS, the SQL Server Powershell host that ships with SQL Server 2008, is based on Powershell V1
- Powershell V2 was released in October 2009
- Everything you write in Powershell V1 should work in V2
- SQLPSX is a CodePlex project I started for working with SQL Server and Powershell. The first release was July 2008 and it has frequently updated since. A Powershell V2 release was published on 12/31/2009
Modules
- Handling related related script files and separate function libraries is difficult -- usually solved by creating an initialization script and detailed instructions.
- Loading assemblies
- Appending format files
- Make functions private through by pattern matching the FunctionsToExport property. As an example in the SQLServer module I specify FunctionsToExport = '*-SQL*' -- This tell Powershell to only export function that match -SQL prefix. I have several helper functions that I don't want to export, so I simply use a different prefix or none at all to avoid exporting.
- Import assemblies automatically by making use of the RequiredAssemblies property
- Nest modules i.e. import child modules with NestedModules property
The manifest files themselves are really easy to create. After you've created a module (.psm1), run new-modulemanifest and enter the information when prompted.
Simplified Error Checking
[Parameter(Position=0, Mandatory=$true)] $sqlserver,
[ValidateSet("Data", "Log")]
[Parameter(Position=1, Mandatory=$true)] [string]$dirtype
)
param( [Parameter(Position=0, Mandatory=$true, ValueFromPipeline = $true)] [ValidateScript({$_.GetType().Namespace -like "Microsoft.SqlServer.Management.Smo*"})] $smo, [Parameter(Position=1, Mandatory=$false)] [Microsoft.SqlServer.Management.Smo.ScriptingOptions]$scriptingOptions=$(New-SqlScriptingOptions) )
Testing
OK, so this items really isn't about Powershell V2 rather it's a change in process for me. As part of the conversion I wanted to adopt a testing framework and perform more rigorous testing. I first heard of a Powershell based xUnit testing framework on the Powerscripting podcast episode 80 in which Jon and Hal interviewed Klaus Graefensteiner about his CodePlex project PSUnit. So, I decided to try PSUnit and I've been very happy with the results. Following the directions on the PSUnit site it is a cinch to setup. PSUnit integrates with Powershell ISE. A menu item is added to execute Unit tests:
It should be noted that although I'm using PSUnit to test Powershell functions this doesn't mean that's all its good for. In fact the purpose of the PSUnt is to perform full unit testing of your .NET applications. You can test just about anything (.NET, COM, etc). For my purposes I'm interested in testing my own Powershell functions. As a script developer the easiest thing you can do with PSUnit is to create a test function for each of your functions and verify the output object is the type you expected. Here's an example test function for Get-SqlServer:
function Test.Get-SqlServer([switch] $Category_GetSql) { #Arrange #Act $Actual = Get-SqlServer "$env:computername\sql2K8" Write-Debug $Actual #Assert Assert-That -ActualValue $Actual -Constraint {$ActualValue.GetType().Name -eq 'Server'} }
Process from Pipeline
Embracing the pipeline is part of writing Powershell scripts to be well, more Powershell-like. In Powershell V1 I adopted a style of writing functions created Keith Hill as described in his blog post titled "Writing CMDLETs in PowerShell". The post shows us how to write functions to accept both command argument and pipeline input. Powershell V2 makes creating a function to accept both command argument and pipeline even easier. As example let's look at a Powershell V1 function and the equivalent Powershell V2 function:
Powershell V1 function:
function Get-SqlScripter { param($smo, $scriptingOptions=$(Set-SqlScriptingOptions)) begin { function Select-SqlScripter ($smo, $scriptingOptions=$(Set-SqlScriptingOptions)) { $smo.Script($scriptingOptions) } #Select-SqlScripter } process { if ($_) { if ($_.GetType().Namespace -like "Microsoft.SqlServer.Management.Smo*") { Write-Verbose "Get-SqlScripter $($_.Name)" Select-SqlScripter $_ $scriptingOptions } else { throw 'Get-SqlScripter:Param `$smo must be an SMO object.' } } } end { if ($smo) { $smo | Get-SqlScripter -scriptingOptions $scriptingOptions } } } Powershell V2 function:
function Get-SqlScripter { param( [Parameter(Position=0, Mandatory=$true, ValueFromPipeline = $true)] [ValidateScript({$_.GetType().Namespace -like "Microsoft.SqlServer.Management.Smo*"})] $smo, [Parameter(Position=1, Mandatory=$false)] [Microsoft.SqlServer.Management.Smo.ScriptingOptions]$scriptingOptions=$(New-SqlScriptingOptions) ) process { $smo.Script($scriptingOptions) } }
Help
.SYNOPSIS
Scripts an SMO object.
.DESCRIPTION
The Get-SqlScripter function calls the script method for an SMO object(s).
.INPUTS
Microsoft.SqlServer.Management.Smo.*
You can pipe SMO objects to Get-SqlScripter
.OUTPUTS
System.String
Get-SqlScripter returns an array System.String.
.EXAMPLE
Get-SqlDatabase "Z002\sql2k8" "pubs" | Get-SqlTable | Get-SqlScripter
This command scripts out all user tables in the pubs database.
.EXAMPLE
Get-SqlDatabase "Z002\sql2k8" "pubs" | Get-SqlTable -name "authors" | Get-SqlScripter
This command scripts out the authors table.
.EXAMPLE
$scriptingOptions = New-SqlScriptingOptions
$scriptingOptions.Permissions = $true
$scriptingOptions.IncludeIfNotExists = $true
Get-SqlDatabase "Z002\sql2k8" "pubs" | Get-SqlTable | Get-SqlScripter -scriptingOptions $scriptingOptions
This command scripts out all users tables in the pubs database and passes a scriptingOptions.
.LINK
Get-SqlScripter
New-SqlScriptingOptions
#>
function Get-SqlScripter
new-object -property hashtable
Deletion = $false
Creation = $true
DisableReplicationDB = $false
EnableReplicationDB = $false
IncludeAgentProfiles = $false
}
new-object PSObject -property (&"$scriptRoot\replscriptopts.ps1") | add-member scriptproperty ScriptOptions ` { $scriptOptions = [Microsoft.SqlServer.Replication.ScriptOptions]::None $this | get-member -type NoteProperty | where {$this.($_.name)} | foreach {$scriptOptions = $scriptOptions -bor [Microsoft.SqlServer.Replication.ScriptOptions]::($_.name)} $scriptOptions } -passthru
Gotchas
- Cannot find type for custom attribute 'Parameter '. Please make sure the assembly containing this type is loaded. Used this post from Richard Siddaway to resolve
- Be careful with strongly typing parameters. For the most part it's a good thing to strongly type variables, but I've found a few cases where it is isn't. I have several functions where I add PSNoteProperties to a strongly type object. If I then pipe the output to be used by another function whiich is also strongly typed the noteproperties are striped away leaving just the original object. The solution is to not strongly type the parameter.
- The position binder is supposed to be optional, however if I specify a parameterset this seems to be required in order to use positional.
- I wasn't able to do anything more than simple pattern matching with FunctionsToExport in the module manifest. This might be OK, but being able to explicitly list functions to export would be nice. What I ended up doing here is being very careful about adopting a standard prefix within a module.
- By default all functions within a module are exported (this means they are available for use), however aliases are not. I spent a day wrestling with this issue and posted a StackOverFlow question. Although I agree aliases can sometime confuse things, not exporting alias by default I explicitly create within a module is counter-intuitive to the approach taken with functions. My thought is that if I didn't want my aliases exported why would I create in my module? I'm sure this was a well-intentioned design decision, but it's probably a little over thought.