PowerGui.
So let's start!
A few weeks ago, I wrote a short "one shot script"... For the first time I decided not to include any logging function (I was, maybe, a bit too confident…). The punishment came straight away: after the first deployment I was not able to know what didn't work properly and it took me awhile to understand why.
So even if you know it, force yourself to do it. Logging is not an option.
The first time that I wrote a logger in PowerShell, it took me a long time. But since then I never stopped to improve it and even added options. I copy hereafter, a light version of it (using the log4net library), just to give you an overview.
function New-Logger
{
<#
.SYNOPSIS
This function creates a log4net logger instance already configured
.OUTPUTS
The log4net logger instance ready to be used
#>
[CmdletBinding()]
Param
(
[string]
# Path of the configuration file of log4net
$Configuration,
[Alias("Dll")]
[string]
# Log4net dll path
$log4netDllPath
)
Write-Verbose "[New-Logger] Logger initialization"
$log4netDllPath = Resolve-Path $log4netDllPath -ErrorAction SilentlyContinue -ErrorVariable Err
if ($Err)
{
throw "Log4net library cannot be found on the path $log4netDllPath"
}
else
{
Write-Verbose "[New-Logger] Log4net dll path is : '$log4netDllPath'"
[void][Reflection.Assembly]::LoadFrom($log4netDllPath) | Out-Null
# Log4net configuration loading
$log4netConfigFilePath = Resolve-Path $Configuration -ErrorAction SilentlyContinue -ErrorVariable Err
if ($Err)
{
throw "Log4Net configuration file $Configuration cannot be found"
}
else
{
Write-Verbose "[New-Logger] Log4net configuration file is '$log4netConfigFilePath' "
$FileInfo = New-Object System.IO.FileInfo($log4netConfigFilePath)
[log4net.Config.XmlConfigurator]::Configure($FileInfo)
$script:MyCommonLogger = [log4net.LogManager]::GetLogger("root")
Write-Verbose "[New-Logger] Logger is configured"
return $MyCommonLogger
}
}
}
And to use it:
$log = New-Logger -Configuration ./config/log4net.config -Dll ./lib/log4net.dll
$log.DebugFormat("Logger configuration file is : '{0}'", (Resolve-Path "./config/log4net.config"))
Ok ok… If you really DON’T want to log… But follow what’s going on during your script execution you can use the Write-Host / Write-Verbose… The cmdlet Write-Verbose "something" will write on the console when the switch parameter -Verbose is passed as argument at your script execution¹, or if you set $VerbosePreference to "Continue". For more details
How can I run a script, for testing purpose, without impacts? -> With the WhatIf parameter WhatIf option simulates the behavior and writes it to the console, but without doing anything. You can do dry run test using this parameter.
Definitely the first testing step... Trying it, means adopting it.
A basic one: to apply it on a cmdlet
New-Item -Path "C:\Program Files\example.txt" -ItemType File –WhatIf
To apply it into your script:
function Test-WhatIf
{
[CmdletBinding()]
Param
(
[switch]
$WhatIf
)
New-Item -Path "C:\Program Files\example.txt" -ItemType File -WhatIf:$WhatIf
}
Sometimes you have to make a complex system just to enable it
On the PowerShell v1, the only way to use common functions was to use the Cmdlet:
. .\myFunctions.ps1
Definitely not the best, but I did run into limitations when I wanted to know what was imported or not. On top of that, it was executing the script and importing it. Ok it works but it's not its primary role.
PowerShell v2 came with some new features: one of them was the module feature. A module is a script file containing functions and it provides you with a way to share your functions. The module file extension in PowerShell is “.psm1” file. Once imported, it can be managed with standard cmdlet (Get-Module, Remove-Module, Import-Module, New-Module), and Force the reimport.
Import-Module $MyNewModule.psm1 –Force
I use to add at the beginning of a module file: Write-Host "importing MyOwnModule" This helps to check if the module has been imported several times (as it shouldn't be) or just once...
Cf. http://www.simple-talk.com/sysadmin/powershell/an-introduction-to-powershell-modules/
I don't know how do you manage your path, but it took me time to figure out how to manage them in my scripts. In fact I started to reach some limits when I started to interact a lot between scripts: they use the same paths but declared inside ps1 scripts... To avoid this issue, I decided to start to export them in a psm1 file and set this as a Best practice. On top of that it helped me during my tests (just copy/paste a new file to set testing locations).
Set a scope to your variable: at least the "script" scope but most of the time I use the "global" scope.
2 way of setting variable scope
Set-Variable -Name MY_FOLDER_PATH -Value ".\MyFolder" -Scope Global
Or
$global:MY_FOLDER_PATH = ".\MyFolder"
I definitely prefer the first way of writing it... But it's really a personal point of view :)
It's important to be aware of variable naming convention (another best practice), so as not to get variable value overwritten problems caused by global scope. Another point: bad scope could be the source of unexpected exceptions: you add a new path (as a variable) and forget to add a scope to it… Believe me, it was dreadful to understand this the first time…
PowerShell sets your current execution folder to the one from which the script is called. If you always run your script manually from the same folder: you don't care. If you run it through a GUI (like PowerGui), you have to always set your default folder in your script location, from RunOnce or a BAT you have to set the execution folder, etc.
Once you know it, it's easy to deal with. But that's not a convenient way to use relative path. It would be much better if your script was independent from where you run it, wouldn't it?
I advise you to follow this rule: add at the beginning of the script a variable set by "$myInvocation.MyCommand.Path".
Set-Variable -Name SCRIPT_PATH -Value (Split-Path (Resolve-Path $myInvocation.MyCommand.Path)) -Scope local -ErrorAction SilentlyContinue
Then, instead of
Set-Variable -Name MY_OTHER_FOLDER_PATH -Value ".\..\..\MyOtherFolder" -Scope Global
use:
Set-Variable -Name MY_OTHER_FOLDER_PATH -Value "$SCRIPT_PATH\..\..\MyOtherFolder" -Scope Global
I started to work with PowerShell v1, when the error management was not easy. Options were to use :
trap: but I’ve never been able to make it work properly (have a look here to see how it was crapy before)
test after a cmdlet execution its error variable (which was giving you the chance to "catch" an error and throw it with a nice log message)
Resolve-Path -Path "./Test" -ErrorAction SilentlyContinue -ErrorVariable Err
if ($Err)
{
Write-Host "The path was wrong :("
}
Hopefully since PowerShell v2, the block try{}catch{}finally{} appears.
Always catch exceptions. No need to go as far as in a program and catch specific exceptions but it’s useful to catch them to be logged, and then apply a rollback if needed. You can add the function name in the exception message to help you when you read the message to determine where it comes from.
Lately I have discovered that the $_.InvocationInfo.PositionMessage property indicates where the error originates from.
try
{
. ".\myScripts.ps1" # this script contains an error!!!
}
catch [Exception]
{
Write-Host "$($_.Exception.ToString()). $($_.InvocationInfo.PositionMessage)"
}
finally
{
Write-Host "End of this block"
}
To discover this official manual pages, I needed to read a book² on PowerShell... And I have to admit it was pretty useful and interesting. Theses detailed pages describe how to write or use some cmdlets.
You can find a lot of information on internet about PowerShell (… too often it's about PowerShell v1). So when you don't know (or even worst: when you don't have or do have just a limited access to the web): you can use this local help.
Get-Help about_try_catch_finally
Ps: That's how I've just learned that you could catch several exception type:
catch [System.Net.WebException],[System.IO.IOException]
{
}
It can take some time to find help about a specific subject....
I do not write help description on each function (yes I know I should...), and I never had to use the Get-Help on a function that I did...
But writing comments is part of our job (for other people and even ourselves sometimes), and if we have to write them, it's better to do it by the book To get more help on this: about_Comment_Based_Help
<#
.SYNOPSIS
About your script
.OUTPUTS (with a S at the end....)
output returned by your script or your function
.PARAMETER MyParam
MyParam description
#>
Function Test-MyFunction
{
[CmdletBinding()]
param($MyParam)
Write-Output $MyParam
}
Then you can interrogate your script with
Get-Help Test-MyFunction -Detailed
If you make a spelling mistake writing your help description, the Get-Help cmdlet won't work on it... For example: if you forgot the 's' at '.OUTPUTS' the Get-Help function won't show anything.
Function naming conventions are not that important... Until the moment when you need to fix a bug and you have to reread your whole script... Trust me on this one ;-)
Use the Get-Verb cmdlet to get common verb to use During a module import, function names will be checked: you can deactivate warnings with the switch parameter named "-DisableNameChecking"
At the beginning I started to name my logger function:
By the way, I think the various naming conventions I used represents my own evolution in PowerShell....)
Almost as stupid to say as "follow the pattern Verb-Noun", I know... But that helps when you read a script to be able to know what variables are...
It is not important to follow the CamelCase pattern or any other: the point is to FOLLOW and KEEP the same pattern to help yourself (and the others who would have to read your code)
For instance, I have used to write:
In this post, I have tried to give you best practices that I could identify during my last projects. I hope they will be useful (maybe even used). If you find some other, feel free to share them here as well.
¹ To use -verbose on your script (or/and your functions) you have to add [CmdletBinding ()] in your script (see about_Functions_CmdletBindingAttribute: when you write functions, you can add the CmdletBinding attribute so that Windows PowerShell will bind the parameters of the function in the same way that it binds the parameters of compiled cmdlets)
² "Windows PowerShell 2.0 - Best Practices" from Ed Wilson